Skip to content

fix(service-analytics): raise the ONE shared dateRange refusal wording - #18275

Merged
claude[bot] merged 3 commits into
mainfrom
claude/issue-18232-analytics-second-refusal-wording
Sep 15, 2026
Merged

claude[bot] merged 3 commits into
mainfrom
claude/issue-18232-analytics-second-refusal-wording

Conversation

@os-project-manager

@os-project-manager os-project-manager commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator

Fixes #18232

Clause-②: no

packages/services/service-analytics/src/date-range-array-arm.ts overwrote err.message with a SECOND wording for ANALYTICS_DATE_RANGE_UNRECOGNIZED. Its own comment justified that on two specific grounds — the shared sentence judged a bare STRING against the preset vocabulary, and it ended with "Refused at the schema". PR #18230 removed both: analyticsDateRangeRefusalMessage describes a non-string by what is WRONG with it (describeRefusedDateRange) and takes origin as a required parameter. With the grounds gone the second wording is what the #5240 convention exists to prevent, so the arm now raises the shared sentence unchanged.

Measured against origin/main at the branch point 1a02ef17d, ⛔ not against the card's text — the card was filed against a tree that did not exist yet. The premise holds: analyticsDateRangeRefusalMessage(input, origin: 'schema' | 'runtime') is on main (spec analytics.zod.ts), the array arm is z.tuple([z.string(), z.string()]), and the overwrite was still there.

Which halves this delivery covers

half of the card status
the second wording for one condition DONE
header sentence ① — the array arm is "a bare z.array(z.string()) with no length constraint" DONE (now z.tuple, ruling A on #17598, landed by #18230)
header sentence ② — "Tightening AnalyticsDateRangeSchema … is deliberately NOT done here" DONE (it has been done, upstream)
header sentence ③, ⚠️ not named by the card — "POST /analytics/dataset/query … never Zod-parses it, so the schema door is BEHIND these faces" DONE — measured false in the same paragraph: since PR #17548 that route parses timeDimensions through AnalyticsQuerySchema.pick(…) in rest/src/analytics-selection-door.ts, wired ahead of the executor in rest-server.ts
the SAME defect in driver-memory NOT DONE — different package, see Acceptance notes

Driven, not concluded from shape

Both wordings a caller actually receives for the same condition, through the real call path (explicitDateRangeWindow, the one reading all four faces in this package call), with @objectstack/core and @objectstack/spec resolved through their built dist:

Before['2026-01-01'], two different sentences:

[A] face:   [service-analytics] dateRange ["2026-01-01"] is a 1-element array, not a window. An explicit
            window is the TWO-element array [start, end] … Refused (ANALYTICS_DATE_RANGE_UNRECOGNIZED / 400)
            rather than guessed: this package's four analytics faces read an odd-sized array three …
[B] shared: dateRange must be a date-range preset name (today, …) or an explicit window is the two-element
            array [start, end] … ; received a 1-element array, not the two bounds [start, end].
            Refused past the schema door, by the analytics reader that received it (…/ 400).
IDENTICAL(A,B)? false        (same for [], ['a','b','c'], [null,null], ['',''] — 5 shapes, 5 disagreements)

After — one wording, byte-for-byte, on all five shapes:

IDENTICAL(A,B)? true    ×5        [C] core constructor message === [B]? true  ×5

The envelope never moved: code + status have always come from the one shared constructor, and the cross-package conformance kit judges exactly those two (analytics-date-range-conformance.ts judgeRefusal), so it is unaffected.

Tests

packages/services/service-analytics/src/__tests__/date-range-array-arm-arity.test.ts pinned the message with four toContains on literals of the wording being removed. That pin is satisfied by ANY private wording that quotes the contract — which is what stood here. Replaced with the stronger pin:

  • identity, across all 4 faces × 4 shapes: the message IS analyticsDateRangeRefusalMessage(range, 'runtime');
  • the clauses identity alone cannot vouch for (identity tracks the builder wherever it goes): what is wrong with what arrived, the two-bound contract, the single-day spelling, and the RUNTIME origin — with the schema-origin sentence and the [service-analytics] prefix as negative controls.
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

Ablation (fix committed first; mutation proved on disk by marker count and git hash-object vs the HEAD blob; restored by git checkout HEAD -- path, proved by blob equality and an empty git diff HEAD). Predicted direction RED, observed RED: reinstating a second wording on the arity branch alone fails 2 of 23 —

AssertionError: ObjectQLStrategy.dateRangeBounds answered one element — the card's shape with a wording
                of its own: expected '[service-analytics] ABLATION second w…' to be 'dateRange must be a…'
 Tests  2 failed | 21 passed (23)

⭐ The other 21 stayed green, which is the point: the code/status pins cannot see a second wording, and did not.

Gates — denominator

Re-run in full on the CURRENT head cf1d1ae49 after the changeset note below, ⛔ not carried over from the earlier head. node scripts/pm/dispatch-gates.mjs --commands (derived in-process from the merge-base change set, ⛔ not from a hand-written diff): 64 derived · 64 run · 0 NOT MEASURED · 0 UNRUN, reconciled with --ran carrying one recorded exit code per family. On the fresh worktree FIVE 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 — they read built output); the workspace was built (turbo run build, 72/72, FULL TURBO off the shared cache) and all five then exited 0. Recorded as the single final code per family, ⛔ never as a pass. pnpm --filter @objectstack/service-analytics test re-run on this head: 111 files / 2399 tests passed. pnpm check:slot-lookup exit 0. pnpm check:nul-bytes exit 0, plus a direct control-character scan over the changed files (no matches). Exit codes captured before any pipe.

⚠️ Declared, ⛔ not closed: that derivation prints a STALE TREE warning — this branch is at least 5 commits behind origin/main, and 4 of the files the family list is derived 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 that needs a merge of origin/main, which would move the diff under review mid-review — left to the seat deliberately rather than taken unilaterally.

Clause ② — derived as no, with controls

⛔ Not from the word export and ⛔ not from dist/index.js. The published entry is exports./dist/index.{js,cjs} (+ types) with files: [dist, README.md, CHANGELOG.md], i.e. the single barrel src/index.ts; the derivation walks that barrel's re-export list transitively and collects every symbol reachable from it, at origin/main and at HEAD.

  • @objectstack/service-analytics: 56 → 56, sets identical. @objectstack/core: 287 → 287, sets identical.
  • The changed file is not reachable from the barrel at allexplicitDateRangeWindow is internal, so no published symbol moved.
  • Positive controls: AnalyticsService FOUND; analyticsDateRangeUnrecognizedError FOUND (core).
  • Negative controls: arrayArmRefusal (module-private) NOT reachable at either ref; a nonexistent name NOT reachable.
  • Sensitivity control — because "zero added export lines" is not an answer: an injected export function in the barrel moves the count 56 → 57 and is listed as added, while the same injection in the unreachable file is invisible. The instrument can see a new export; it is not seeing one here. (Injection proved on disk, restored, blob-equal.)
  • No new member on an already-exported class: the only non-comment change deletes a module-private function and replaces two throw expressions. The @objectstack/core diff is comment-only — every added/removed line begins with *, checked mechanically.
  • The payload gains no key: code/status/message are unchanged in shape; only the message VALUE moves, and it moves ONTO the declared contract, which is the narrowing direction.

scripts/pm/check-widening-tells.mjs --declaration no reads NOT MEASURED on this diff (no declared surface covers these four files) — recorded as such, ⛔ not as a pass.

Changeset

patch for both packages, and it now carries the upgrade note the envelope paragraph did not: for an ARRAY dateRange the shared sentence DESCRIBES the shape where the removed one echoed the value, so a log line that used to carry the offending array no longer does — for EVERY array shape this face refuses, measured on all five ([null, null] now reads received an array with a non-string bound; ['', ''], where the description carries least, reads received a two-element array). A bare STRING dateRange is still quoted back. @objectstack/core's edit is comment-only, but that JSDoc is published: grep -c "Three of the four callers" packages/core/dist/index.d.ts = 1 (positive control: the symbol itself, 3), so the shipped .d.ts bytes move and skip-changeset would be wrong.

Acceptance notes

Reported to the dispatching seat for filing, ⛔ neither filed nor fixed here. ⚠️ Deliberately not filed: repo-scoped REST is the read channel on this session (/search/issues answers 403 — sessions are bound to their configured repositories), so dedupe fell back to one targeted semantic search, and that search did not return #18232 itself — the known-must-hit control for this exact subject. A dedupe whose control misses is not a reading, and filing on it would be filing blind.

  1. packages/drivers/driver-memory/src/memory-analytics.ts keeps a THIRD wording for this same condition — its own local explicitDateRangeWindow overwrites err.message with [driver-memory] dateRange … . Same contract ({ field: {} }(零个操作符的字段约束)在同仓有三个答案:driver-sql 组合子内 TRUE、顶层抛 INVALID_FILTER、formula/driver-memory FALSE #5240, quoted from analytics.zod.ts), same envelope, different package. Out of this card's file surface; fixing it would add another package's test surface to this PR.
  2. describeRefusedDateRange (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 exactly the residue that reaches these faces. The sentence an author gets is therefore "…must be … the two-element array [start, end] …; received a two-element array." ⚠️ This PR makes that sentence visible at the service-analytics face (before it, the face said "has a bound that is not a date string (string)", which is self-contradictory in its own way). packages/spec is out of bounds for this card, so this is reported, ⛔ not fixed.

Noted, not filed: @objectstack/core's prose was updated in this PR to stay true about which callers still supply their own message — it named service-analytics as one of three, which this change falsifies. Carried here rather than left for a later reader.

Generated by Claude Code in session session_01URLHobLUJB9K1ABV6ofdjj; branch claude/issue-18232-analytics-second-refusal-wording.


Generated by Claude Code

`date-range-array-arm.ts` overwrote `err.message` with a second wording for
`ANALYTICS_DATE_RANGE_UNRECOGNIZED`, on two stated grounds: the shared sentence
judged a bare STRING against the preset vocabulary, and it ended with "Refused
at the schema". Both grounds are gone — `analyticsDateRangeRefusalMessage` now
describes a non-string by what is wrong with it and takes the origin as a
required parameter — so the arm raises the shared sentence unchanged, which is
what the one-condition-one-wording convention asks for.

Also corrects three sentences the same file's header carried that the tightened
schema arm and the REST dataset door had made false, and keeps `@objectstack/core`'s
prose true about which callers still supply their own message.

Claude-Session: https://claude.ai/code/session_01URLHobLUJB9K1ABV6ofdjj
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 2 changed package(s); no hand-written page names any of them. ⚠️ 1 changed file(s) yielded no anchor (packages/core/src/utils/analytics-date-range.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/core/src/utils/analytics-date-range.ts) — pages documenting those are invisible to this run
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 30 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json e07eecfa7db5684b291e659f80629a6bb47dc1d3packageMentionDocs.

Which tree this was computed on

This run read content/docs from 6b25ddc5b00d99e9ac0f11754f89114254631a28 — the merge of head cf1d1ae493ed7b0c4ca6900e5e24c6c695f805f0 into base e07eecfa7db5684b291e659f80629a6bb47dc1d3, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 6b25ddc5b00d99e9ac0f11754f89114254631a28 && git checkout 6b25ddc5b00d99e9ac0f11754f89114254631a28
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e07eecfa7db5684b291e659f80629a6bb47dc1d3 cf1d1ae493ed7b0c4ca6900e5e24c6c695f805f0 && git checkout -B drift-repro e07eecfa7db5684b291e659f80629a6bb47dc1d3 && git merge --no-ff cf1d1ae493ed7b0c4ca6900e5e24c6c695f805f0

node scripts/docs-audit/affected-docs.mjs --json e07eecfa7db5684b291e659f80629a6bb47dc1d3

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

…ange

The envelope note covered classification; it did not cover the one thing a
caller reading a 400 in a log will notice. The shared sentence describes a
non-string by its shape, so every array shape this face refuses stops carrying
the value that was sent — not just the degenerate ones.

Claude-Session: https://claude.ai/code/session_01URLHobLUJB9K1ABV6ofdjj
Co-authored-by: Claude <noreply@anthropic.com>
@claude
claude Bot marked this pull request as ready for review September 15, 2026 07:33
@claude
claude Bot enabled auto-merge September 15, 2026 07:33
@claude
claude Bot added this pull request to the merge queue Sep 15, 2026
Merged via the queue into main with commit fe0ae5c Sep 15, 2026
43 checks passed
@claude
claude Bot deleted the claude/issue-18232-analytics-second-refusal-wording branch September 15, 2026 07:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants