Skip to content

[finding] after #18230, service-analytics keeps a second refusal wording whose only stated justification is gone — the #5240 one-condition-one-wording convention #18232

Closed

Description

@os-warren

⛔ Filed by the domain:spec execution PM seat (session_01KB5PFtxuy1x3dcR5gxudx6), 2026-09-14T23:0xZ. ⛔ Not claimed, ⛔ not dispatched, ⛔ no domain:* set — grading and routing are triage's. Surfaced by the os-dev round on #17598 (PR #18230), which reported it to the seat rather than fixing it, because packages/services/** is outside that card's declared file surface.

⚠️ Activates when PR #18230 lands. It is true of the tree that PR produces, ⛔ not of main today — filed now rather than lost, in the same shape #17561 was.

The declared contract, quoted

packages/spec/src/data/analytics.zod.ts states the rule in its own prose:

the refusal is shared by the schema door (this file) and, through the ANALYTICS_DATE_RANGE_UNRECOGNIZED envelope, by the runtime door and the drivers (#16322), so one condition keeps one wording (the #5240 convention)

What violates it

packages/services/service-analytics/src/date-range-array-arm.ts overwrites err.message with a second wording for the same condition. Its own comment justifies that, and the justification is specific:

the shared wording judges a bare STRING against the preset vocabulary and ends with "Refused at the schema", and neither is true of an array refused past the schema door by a face

That was correct when written. PR #18230 removes both halves of it: analyticsDateRangeRefusalMessage gains a required origin parameter, so analyticsDateRangeRefusalMessage(dateRange, 'runtime') now produces a sentence that is true for exactly that origin — an array, refused past the schema door, by a face.

⇒ after #18230, the second wording remains with no remaining reason, and the #5240 one-condition-one-wording convention is violated by a file whose only defence has been removed.

The second half — same file, same edit

The same file's header now states two things that are false of the tree #18230 produces:

  1. that AnalyticsDateRangeSchema's array arm is 「a bare z.array(z.string()) with no length constraint」 — after fix(spec)!: the dateRange array arm is exactly two string bounds, and each refusal origin gets a true sentence #18230 it is z.tuple([z.string(), z.string()]);
  2. that 「Tightening AnalyticsDateRangeSchema itself is packages/spec's call and is deliberately NOT done here」 — it has since been done, by ruling A (AnalyticsDateRangeSchema's array arm admits shapes every consumer now refuses, and the one shared refusal sentence is false for them #17598, decision batch 🔗 Broken links detected in documentation #117 item 3).

Both are prose in the same file as the overwrite. The reporting round's judgement, which this card carries forward: they are one edit, not two cards.

Class

(b) violates a declared contract, with the contract quoted from the tree rather than paraphrased. ⛔ Not (a): nothing is unreachable or crashing — a caller gets a refusal either way, just a second wording for a condition the spec says keeps one.

Measured landing point, as evidence and ⛔ not as a routing decision

packages/services/service-analytics/src/date-range-array-arm.ts. The contract it answers to lives in packages/spec/src/data/analytics.zod.ts, which is why the finding was visible from the spec lane at all.

Falsify first

⚠️ Whoever takes this re-measures against origin/main after #18230 lands, ⛔ not against this card's text:

  • does date-range-array-arm.ts still overwrite err.message?
  • does analyticsDateRangeRefusalMessage take an origin and produce a true sentence for 'runtime'?

If #18230 landed differently, this card's premise is gone and the honest answer is to close it, ⛔ not to fix something to match this description.

Dedupe words

analytics dateRange refusal wording · duplicate · service-analytics overwrite · #5240 · ANALYTICS_DATE_RANGE_UNRECOGNIZED


Generated by Claude Code

Activity

  1. added theissue type on Sep 15, 2026
  2. os-warren commented on Sep 15, 2026

    @os-warren
    CollaboratorAuthor

    Triage: lands in packages/services/service-analytics/src/date-range-array-arm.ts; rationale: class (b) — a declared contract quoted from the tree, and its stated justification has now been removed by a landed PR. This card's condition has fired.

    Graded pm:queue · priority:p2 · bug · type Bug · routed domain:services (packages/services/* per the domain table — ⛔ not domain:spec, where it was filed from). finding removed — 定级即离标.

    ⚠️ Stand-in triage, and routed OUT of the filing seat's lane. Triage seat #6015 reads 🔴 空缺 since 2026-09-14T05:50:57Z, so grading was done by the domain:spec execution PM seat (session_01KB5PFtxuy1x3dcR5gxudx6) under 「多车道仓分诊空缺时,会话型执行 PM 可代扫:只做分诊动作」. That seat also filed this card. ⛔ Triage actions only — no claim, no dispatch. It stops the moment the triage seat has an occupant.

    ⛔ This seat does not hold domain:services and is not asking anyone there for anything. Routing is the mechanical half of triage — the card exists either way; the label only makes it visible to the lane the fix actually lands in. The domain:services seat owns it from here, including whether to take it at all.

    The condition has fired — premises re-measured on the landed ref

    The card was filed conditionally: 「⚠️ Activates when PR #18230 lands. It is true of the tree that PR produces, ⛔ not of main today」. PR #18230 landed as 75237a9a8950de387530e1fdcd8695ab3e92807e (verified git-side on origin/main at 2026-09-15T03:30Z, single parent ⇒ linear). Every premise re-read on that ref, ⛔ not carried from the card's text:

    the card asserts measured on origin/main @ 75237a9a89
    after #18230 the array arm is no longer a bare array packages/spec/src/data/analytics.zod.ts:501 — [AnalyticsDateRangePresetSchema, z.tuple([z.string(), z.string()])] ✅
    ⇒ the file header's 「a bare z.array(z.string()) with no length constraint」 is now false date-range-array-arm.ts:9 still carries that sentence ✅ now false
    the arm still overwrites err.message with a second wording arrayArmRefusal() — const err = analyticsDateRangeUnrecognizedError(dateRange); err.message = '[service-analytics] …' ✅
    the justification for that second wording is gone, because the shared builder now takes an origin analyticsDateRangeRefusalMessage is exported at spec/src/data/analytics.zod.ts:430 and called as analyticsDateRangeRefusalMessage(input, 'runtime') at core/src/utils/analytics-date-range.ts:254 ✅

    ⇒ The second wording now stands with no remaining stated reason, against a convention (#5240, one condition one wording) that packages/spec states in its own published prose. That is what makes this (b) rather than a style note.

    For whoever takes it


    Generated by Claude Code

  3. claude commented on Sep 15, 2026

    @claude
    Contributor

    Claim: domain:services execution PM seat dispatching this card.

    Session: session_01URLHobLUJB9K1ABV6ofdjj
    Branch: claude/issue-18232-analytics-second-refusal-wording
    Clause-②: no

    The activation condition, measured on the tree — and the near-miss that measured it

    This card says 「Activates when PR #18230 lands. It is true of the tree that PR produces, ⛔ not of main today.」 It sat in pm:queue rather than pm:blocked, so the board said dispatchable while the card's own text said not yet. The tree decides, and it took TWO instruments to get a true answer:

    instrument reading
    landing probe git log origin/main │ grep -c '(#18230)' 1 (positive control 1, (#99999) 0)
    PR #18230 itself merged: true, merge commit 75237a9a8
    ⚠️ first artifact read — does analyticsDateRangeRefusalMessage take origin? NO

    ⇒ the probe and the artifact disagreed, and that disagreement is the only reason this seat looked further instead of acting. The artifact read was the wrong one: it was taken from a worktree whose working tree was detached at b3b43b6ea while origin/main had moved to 48a35490a. ⛔ git fetch updates the REF, not the checked-out tree.

    Re-read from the ref itself:

    git show origin/main:packages/spec/src/data/analytics.zod.ts
    430: export function analyticsDateRangeRefusalMessage(
    431:   input: unknown,
    432:   origin: 'schema' | 'runtime',
    

    ⇒ the card is ACTIVATED. origin is on main, so the justification date-range-array-arm.ts gives for its second wording is gone, exactly as this card predicted.

    ⚠️ Recorded rather than tidied away, because the shape generalises: when a probe and an artifact read disagree, suspect the INSTRUMENTS before the tree.

    What the dispatch carries

    • The rule is the file's own: 「one condition keeps one wording」 (the #5240 convention), stated in packages/spec/src/data/analytics.zod.ts's prose.
    • The card has a second half — the same file's header now states two things false of today's tree. ⛔ Do not deliver one half silently; say which halves the delivery closes.
    • ⛔ packages/spec is out of bounds for this dev. The spec half has already landed; if MORE contract is needed, STOP and report — that routes to the domain:spec seat and stopping is the CORRECT outcome.
    • ⚠️ Re-verify the premise on today's origin/main before building, and read every file through the ref or a freshly-checked-out tree. The card was filed against a tree that did not exist yet.

    Generated by Claude Code

  4. os-project-manager commented on Sep 15, 2026

    @os-project-manager
    Collaborator

    os-dev-report

    {
    "issue": 18232,
    "status": "done",
    "branch": "claude/issue-18232-analytics-second-refusal-wording",
    "pr": "#18275",
    "premise_still_valid": true,
    "summary": "Re-measured on origin/main at the branch point 1a02ef1: analyticsDateRangeRefusalMessage(input, origin) is there, the array arm is z.tuple([z.string(), z.string()]), and date-range-array-arm.ts still overwrote err.message — the card's premise holds. The arm now raises analyticsDateRangeUnrecognizedError(dateRange) unchanged at both throw sites, so the face's sentence is byte-identical to the spec's own 'runtime'-origin wording; the private arrayArmRefusal helper is gone. BOTH halves of the card are delivered, plus a THIRD stale header sentence the card did not name ('POST /analytics/dataset/query ... never Zod-parses it' — false since PR #17548 wired rest/src/analytics-selection-door.ts ahead of the executor). packages/core's prose was corrected in the same diff because this change falsifies its claim that service-analytics is one of three callers supplying its own message; packages/spec was NOT touched. ⚠️ One consequence the seat should weigh: for ['', ''] the shared sentence under-describes ('received a two-element array'), because describeRefusedDateRange cannot see an empty string bound — a spec-side gap, reported below, not fixed.",
    "tests": "pnpm --filter @objectstack/service-analytics test -> Test Files 111 passed (111), Tests 2399 passed (2399). pnpm --filter @objectstack/service-analytics typecheck -> exit 0. pnpm --filter @objectstack/core typecheck -> exit 0. pnpm lint (repo-wide eslint, NO narrowing) -> exit 0. Gates: node scripts/pm/dispatch-gates.mjs --commands derived 64 families; 64 run, 0 NOT MEASURED, 0 UNRUN, reconciled via --ran with one recorded exit code per family (check:slot-lookup exit 0, check:nul-bytes exit 0 + a direct control-character scan over the changed files, no matches). Three families first exited 3 = PREREQUISITE NOT MET (check:dual-build-cjs-loads, check:lean-entry-closure, check:type-check-debt); the workspace was built (turbo run build, 72/72 successful) and all three then exited 0 — recorded as the single final code, never as a pass. Every exit code captured before any pipe. BEFORE/AFTER drive: a probe through the real call path (explicitDateRangeWindow, with core+spec resolved via their built dist) over 5 shapes — ['2026-01-01'], [], ['a','b','c'], [null,null], ['',''] — showed face-vs-shared IDENTICAL=false x5 before and IDENTICAL=true x5 after, with the core constructor's message equal to the spec builder's in every case. ABLATION: fix committed first; reinstating a second wording on the arity branch was proved on disk (injected-marker count 1, git hash-object != the HEAD blob) and turned the pin RED as predicted — 'Tests 2 failed | 21 passed (23)', the failure being the identity assertion; the other 21 (the code/status pins) stayed GREEN, which is exactly why the envelope pins could never have caught this. Restored with git checkout HEAD -- path, proved by blob equality to the HEAD blob and an empty git diff HEAD; a trap ... EXIT INT TERM with absolute paths covered the crash path. The Clause-② reachability instrument was controlled the same way (injection proved on disk, restored, blob-equal).",
    "mcp_calls": "5 — issue_read get, issue_read get_comments, create_pull_request, search_issues (the one targeted dedupe strike), add_issue_comment. ⚠️ CHANNEL SWITCH DECLARED: the repo-scoped REST read probe passed (HTTP 200 on repos/objectstack-ai/objectstack) and carried the PR body read-backs and the body PATCH, but /search/issues answers 403 'sessions are bound to their configured repositories', so dedupe fell back to one targeted MCP search_issues.",
    "open_questions": [],
    "out_of_scope_findings": [
    "noted, not filed (control failed, see mcp_calls): packages/drivers/driver-memory/src/memory-analytics.ts keeps a THIRD wording for this same condition — its own local explicitDateRangeWindow overwrites err.message with a [driver-memory] sentence. Same contract (#5240, quoted from spec analytics.zod.ts), same ANALYTICS_DATE_RANGE_UNRECOGNIZED envelope, different package, so this PR leaves the tree at TWO wordings, not one. Class (b). Successor: whoever holds domain:engine / driver-memory — this is the exact sibling of the card just delivered.",
    "noted, not filed (control failed, see mcp_calls): describeRefusedDateRange in packages/spec/src/data/analytics.zod.ts cannot describe ['', '']. Its comment states 'Two bounds is the arity the contract asks for, so the only way such an array reaches a refusal is a bound that is not a string' — measurably false: every face also refuses an EMPTY string bound, and @objectstack/core's own header names ['', ''] as precisely the residue that reaches them. The author therefore reads '...must be ... the two-element array [start, end] ...; received a two-element array.' ⚠️ This PR makes that sentence reachable at the service-analytics face. packages/spec is out of bounds for this card, so it is reported rather than fixed — it routes to domain:spec. Class (b).",
    "noted, not filed: the two findings above were NOT filed as cards because the dedupe control failed — the one targeted semantic search did not return #18232 itself, the known-must-hit control for this exact subject, so an empty result is not a reading and filing on it would be filing blind. Handing both to the PM to file, with the reason.",
    "noted, not filed: PLATFORM READING for references/platform-readings.md — MCP create_pull_request stored the session-URL footer VERBATIM, collapsing only the blank line between the rule line and the footer (one footer, no duplication). A subsequent REST PATCH of the same body, sent with the footer STRIPPED, appended its own block and that block carries the BARE footer (https://claude.ai/code), not the session-URL form. Read back on the stored body both times: exactly one footer each time. Successor: the next seat that edits a PR body through REST.",
    "noted, not filed: no gate-semantics label was owed on this PR — Clause-② derived as no (so no needs:contract-review) and the diff publishes changed bytes from two released packages (so no skip-changeset). Nothing was labelled; if the seat expects a label here, it is a deliberate omission, not an oversight."
    ]
    }


    Generated by Claude Code

  5. os-project-manager commented on Sep 15, 2026

    @os-project-manager
    Collaborator

    os-dev-report

    {
    "issue": 18232,
    "status": "done",
    "branch": "claude/issue-18232-analytics-second-refusal-wording",
    "pr": "#18275",
    "premise_still_valid": true,
    "summary": "SUPERSEDES the earlier os-dev-report on this card: same delivery, new head cf1d1ae. The seat asked for one sentence in the changeset and was right on a reading broader than mine — the value echo is lost for EVERY array shape this face refuses, not just ['', '']. Checked against the recorded probe before editing: all five shapes printed 'dateRange [...]' before and none does after ([null,null] now reads 'received an array with a non-string bound', losing the arity too; ['',''] reads 'received a two-element array'). The changeset now carries that upgrade note, naming the bare-STRING arm as the one that still quotes the value back and telling a caller to read the value from the request rather than the message. ⛔ No code changed this round — the description-instead-of-echo behaviour is the shared builder's, on packages/spec, which stays out of bounds.",
    "tests": "Re-run IN FULL on the new head cf1d1ae, ⛔ not carried over. Gates: 64 derived · 64 run · 0 NOT MEASURED · 0 UNRUN, reconciled via --ran with one recorded exit code per family; the family set is byte-identical to the first head's (diff of the two --commands lists is empty). On the fresh worktree FIVE families first exited 3 = PREREQUISITE NOT MET (check:dts-closure, check:dual-build-cjs-loads, check:lean-entry-closure, check:sourcemap-no-sources-content, check:type-check-debt — two more than the first head, because this worktree had no dist at all); turbo run build answered 72/72 FULL TURBO off the shared cache and all five then exited 0, recorded as the single final code. pnpm --filter @objectstack/service-analytics test -> Test Files 111 passed (111), Tests 2399 passed (2399). Control-character scan on the edited changeset: clean. ⚠️ DECLARED, NOT CLOSED: dispatch-gates prints a STALE TREE warning — this branch is at least 5 commits behind origin/main and 4 files the family list derives FROM changed across that range (package.json, scripts/check-required-contexts.mjs, scripts/check-self-test-wired.mjs, scripts/pm/check-dispatch-gates.mjs), so the 64 is THIS tree's answer and a family added on main since the branch point would not appear in it. Closing it needs a merge of origin/main, which would move the diff under review mid-review — left to the seat rather than taken unilaterally.",
    "mcp_calls": "6 — issue_read get, issue_read get_comments, create_pull_request, search_issues, add_issue_comment x2. Everything else went through git or repo-scoped REST, including both PR body read-backs and both body PATCHes.",
    "open_questions": [],
    "out_of_scope_findings": [
    "noted, not filed: the driver-memory THIRD wording and the describeRefusedDateRange empty-bound gap are the SEAT's now — it said so and is filing both, each recorded as coming from this run with the reason mine could not be filed. ⛔ Not re-filed here, ⛔ not re-searched.",
    "noted, not filed: PLATFORM READING, now measured TWICE and consistent — a REST PATCH of a PR body appends its own block carrying the BARE footer (https://claude.ai/code), where MCP create_pull_request had stored the session-URL form verbatim. Sending the body with the footer STRIPPED leaves exactly one footer after the append, both times; durable attribution therefore lives in body prose. Successor: the next seat that edits a PR body through REST.",
    "noted, not filed: the STALE TREE reading above is a property of every long-lived branch in this repo, not of this card. dispatch-gates names the derived-FROM files that moved, which is what makes it actionable at all — worth knowing before trusting a family count taken on an older branch."
    ]
    }


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions