fix(service-analytics): raise the ONE shared dateRange refusal wording - #18275
Conversation
`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>
Claude-Session: https://claude.ai/code/session_01URLHobLUJB9K1ABV6ofdjj Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check2 anchor(s) derived from 2 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 30 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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 |
…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>
Fixes #18232
Clause-②: no
packages/services/service-analytics/src/date-range-array-arm.tsoverwroteerr.messagewith a SECOND wording forANALYTICS_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:analyticsDateRangeRefusalMessagedescribes a non-string by what is WRONG with it (describeRefusedDateRange) and takesoriginas 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/mainat the branch point1a02ef17d, ⛔ 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 onmain(specanalytics.zod.ts), the array arm isz.tuple([z.string(), z.string()]), and the overwrite was still there.Which halves this delivery covers
z.array(z.string())with no length constraint"z.tuple, ruling A on #17598, landed by #18230)AnalyticsDateRangeSchema… is deliberately NOT done here"POST /analytics/dataset/query… never Zod-parses it, so the schema door is BEHIND these faces"timeDimensionsthroughAnalyticsQuerySchema.pick(…)inrest/src/analytics-selection-door.ts, wired ahead of the executor inrest-server.tsdriver-memoryDriven, 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/coreand@objectstack/specresolved through their builtdist:Before —
['2026-01-01'], two different sentences:After — one wording, byte-for-byte, on all five shapes:
The envelope never moved:
code+statushave always come from the one shared constructor, and the cross-package conformance kit judges exactly those two (analytics-date-range-conformance.tsjudgeRefusal), so it is unaffected.Tests
packages/services/service-analytics/src/__tests__/date-range-array-arm-arity.test.tspinned the message with fourtoContains 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:analyticsDateRangeRefusalMessage(range, 'runtime');[service-analytics]prefix as negative controls.Ablation (fix committed first; mutation proved on disk by marker count and
git hash-objectvs the HEAD blob; restored bygit checkout HEAD -- path, proved by blob equality and an emptygit diff HEAD). Predicted direction RED, observed RED: reinstating a second wording on the arity branch alone fails 2 of 23 —⭐ The other 21 stayed green, which is the point: the
code/statuspins cannot see a second wording, and did not.Gates — denominator
Re-run in full on the CURRENT head
cf1d1ae49after 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--rancarrying 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 testre-run on this head: 111 files / 2399 tests passed.pnpm check:slot-lookupexit 0.pnpm check:nul-bytesexit 0, plus a direct control-character scan over the changed files (no matches). Exit codes captured before any pipe.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 onmainsince the branch point would not appear in it. Closing that needs a merge oforigin/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
exportand ⛔ not fromdist/index.js. The published entry isexports→./dist/index.{js,cjs}(+ types) withfiles: [dist, README.md, CHANGELOG.md], i.e. the single barrelsrc/index.ts; the derivation walks that barrel's re-export list transitively and collects every symbol reachable from it, atorigin/mainand at HEAD.@objectstack/service-analytics: 56 → 56, sets identical.@objectstack/core: 287 → 287, sets identical.explicitDateRangeWindowis internal, so no published symbol moved.AnalyticsServiceFOUND;analyticsDateRangeUnrecognizedErrorFOUND (core).arrayArmRefusal(module-private) NOT reachable at either ref; a nonexistent name NOT reachable.exportlines" is not an answer: an injectedexport functionin 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.)throwexpressions. The@objectstack/corediff is comment-only — every added/removed line begins with*, checked mechanically.code/status/messageare unchanged in shape; only themessageVALUE moves, and it moves ONTO the declared contract, which is the narrowing direction.scripts/pm/check-widening-tells.mjs --declaration noreads NOT MEASURED on this diff (no declared surface covers these four files) — recorded as such, ⛔ not as a pass.Changeset
patchfor both packages, and it now carries the upgrade note the envelope paragraph did not: for an ARRAYdateRangethe 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 readsreceived an array with a non-string bound;['', ''], where the description carries least, readsreceived a two-element array). A bare STRINGdateRangeis 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.tsbytes move andskip-changesetwould 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/issuesanswers403 — 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.packages/drivers/driver-memory/src/memory-analytics.tskeeps a THIRD wording for this same condition — its own localexplicitDateRangeWindowoverwriteserr.messagewith[driver-memory] dateRange …. Same contract ({ field: {} }(零个操作符的字段约束)在同仓有三个答案:driver-sql 组合子内 TRUE、顶层抛 INVALID_FILTER、formula/driver-memory FALSE #5240, quoted fromanalytics.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.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."service-analyticsface (before it, the face said "has a bound that is not a date string (string)", which is self-contradictory in its own way).packages/specis 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 namedservice-analyticsas one of three, which this change falsifies. Carried here rather than left for a later reader.Generated by Claude Code in session
session_01URLHobLUJB9K1ABV6ofdjj; branchclaude/issue-18232-analytics-second-refusal-wording.Generated by Claude Code