fix(spec)!: the dateRange array arm is exactly two string bounds, and each refusal origin gets a true sentence - #18230
Conversation
…each refusal origin gets a true sentence `AnalyticsDateRangeSchema`'s array arm was `z.array(z.string())` with no length constraint, while the refusal sentence in the same file said "the two-element array [start, end]" and #16322's shipped migration table told an author to write a single day as `['2026-01-20', '2026-01-20']`. So `['2026-01-01']`, `[]` and `[a, b, c]` passed the contract door and were refused by every reader behind it (#17593 aligned all four analytics faces on "not exactly two bounds is a refusal"). Only the TYPE was weaker than the prose beside it. The arm is now `z.tuple([z.string(), z.string()])`, so the type states the arity to the author's compiler before any parse runs. `analyticsDateRangeRefusalMessage` takes the refusal ORIGIN as a required parameter. "Refused at the schema" is the one clause no input can supply, and it was asserted unconditionally — false for every refusal raised past the schema door, which is why `service-analytics` had to overwrite the message rather than reuse it. The `received …` clause now names the arity and the bad bound separately instead of calling every refused array "an array with a non-string bound", which is false when every bound is a string. Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <noreply@anthropic.com>
… changeset for the dateRange arity narrowing The one-element window converts to the same day written twice, the shape the shipped #16322 migration table already prescribes. The empty array and three or more bounds get no conversion and the structured TODO: an empty array names no window at all, and a 3+ array names no pair, so deriving either would be the platform inventing the window the author meant. Graded `minor` under the launch-window convention for breaking changes (`scripts/check-changeset-no-major.mjs`), with the BREAKING banner and the ADR-0087 disposition carrying the breaking-ness, as the sibling accept-set narrowings on this schema did. Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <noreply@anthropic.com>
…s, never as `any[]` Draft-2020-12 spells `z.tuple([...])` as `prefixItems` and leaves `items` absent, so `format-type.ts`'s array branch rendered the element type of nothing at all. Every tuple in the spec printed `any[]` — a published cell strictly weaker than the schema beside it, which carried both element types all along. Without this, narrowing the dateRange array arm to a two-bound window would have REGRESSED a published line from `string[]` to `any[]`: the card's whole point is a declared surface that says what the contract enforces, so shipping the narrowing with that cell would have been the same defect one layer out. Three published pages gain precision as a side effect and none loses any: `ListView.map.center` becomes `[number, number]`, `$between` becomes `[number | string, number | string]`, and the `FilterArray` variants print their positions. Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <noreply@anthropic.com>
…er where the arity rule now lives The array arm of `timeDimensions[].dateRange` is a `z.tuple([z.string(), z.string()])`, so two authored shapes in this suite stopped compiling — and both are consequences of that narrowing, not of anything this suite means: - the deliberate one-element window keeps its value and gains a `@ts-expect-error`, because the refusal it pins is the FACE's, raised past a schema door `POST /analytics/dataset/query` never opens; - the cross-object window is annotated `AnalyticsQuery`, because an un-annotated `const` widens `['2026-01-01', '2026-01-31']` to `string[]`. Measured: `pnpm --filter @objectstack/service-analytics exec tsc --noEmit` reports 3 errors in this file with the narrowed arm and 0 with the arm reverted; after this commit it reports 0, with the package's other error count unchanged. Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 4 package(s): ⛔ 1 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 143 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 eaf96f0b1b9177769043e8f5148cadec6e643ae8 && git checkout eaf96f0b1b9177769043e8f5148cadec6e643ae8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b3b43b6ea0c30f35d861c9cc0fedc9c085027b7b 00ae6c44c75f573ff5f05884873c63c456295f61 && git checkout -B drift-repro b3b43b6ea0c30f35d861c9cc0fedc9c085027b7b && git merge --no-ff 00ae6c44c75f573ff5f05884873c63c456295f61
node scripts/docs-audit/affected-docs.mjs --json b3b43b6ea0c30f35d861c9cc0fedc9c085027b7b
|
Seat note —
|
Seat note — the drift check's ⛔ row is clear, measured. ⛔ No edit, ⛔ no card, ⛔ no docs-only PR owed.
① Does this PR edit a release-owned page? NO.
② Is the flagged page nevertheless now FALSE? That is the part worth checking, because the check lists a page when a code change touches something it names — and a page that this diff falsifies would owe a card even though the PR must not edit it. The anchor is
That is a claim about the predicate being applied, ⛔ not about which shapes the arm accepts. This PR narrows the array arm's arity (exactly two string bounds) and re-words the refusal per origin; it does not stop Instrument check: the same grep over the same file for the drift check's own anchor token ⇒ Row closed as advisory, satisfied. ⛔ Nothing here is a verdict on the diff; the at-tier clause-② review has not run. Generated by Claude Code |
|
Rendered by an isolated at-tier review subagent and ADOPTED VERBATIM by the Downgrade-fuse reading, taken before adoption, ⛔ not from the agent's self-report: the reviewer's transcript carries Head check: the PR head at adoption is Contract reviewServed-tier: Worktree: ① Derived judgmentsAccept set
Public surface
② Semver levelWhat moved: accept-set narrowing on a published schema; authored type narrowing (compile breaks measured in a consumer, item 2); a required parameter added to an exported function ( ③ Boundary flagsRuling A checked item by item (verbatim notes in Dev Out-of-surface edits (each reproduced, not taken from the PR body): Flags I raise:
Checks run in the worktree (exit status): CI from the API ( Implemented-by: VERDICT: FAIL Generated by Claude Code |
…rktree INCOMPLETE AND UNREVIEWED. This edit was uncommitted in the dispatch worktree when the container restarted and killed the fix-up round; the seat committed it so the work is not lost, and has NOT reviewed it. Observed state only: 1 modified path, .changeset/17598-analytics-date- range-two-bound-window.md, on top of 5db9a5d. It addresses one of the three fix-up items (the inaccurate justification sentence). The other two items are NOT in this commit. The continuing round diffs this rather than trusting it. Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <noreply@anthropic.com>
…ller, and the arity refusal keeps one wording on the wire F1 — `packages/spec/package.json` ships `src/**/*.zod.ts` literally, so the TSDoc at `analytics.zod.ts` reaches npm verbatim. It listed `POST /analytics/dataset/query` as a runtime-origin caller "which types its selection from `AnalyticsQuery` but never Zod-parses it". Measured on this tree, that route answers `400 ANALYTICS_DATE_RANGE_UNRECOGNIZED` with a message ending `Refused at the schema (…)`: since #17058 its door parses the selection's shared members — `timeDimensions` included — against `AnalyticsQuerySchema.pick(…)` ahead of the executor. The sentence now names `queryDataset` / the dataset executor reached IN PROCESS as the runtime origin and states that every REST analytics route is a schema-origin door. Same correction at the core echo (which pre-exists on the base) and at the `service-analytics` test comment. F2 — on both REST doors the tuple arm's own `Too small: expected array to have >=2 items` rode along as a second `fields[]` entry beside the prescription: one condition, two wordings (#5240), introduced by this card's narrowing (on the base a 1-element array was not refused by the schema at all). The branch issues that land at the union's OWN path are now dropped in `fieldsFromZodIssues`, the one mapper both doors share, keyed on `isAnalyticsDateRangeRefusalIssue`. Branch issues naming a DEEPER position (`dateRange.1`, the non-string bound) are kept — this narrows a restatement, never a diagnosis — and no other union is touched. Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <noreply@anthropic.com>
…ne, and the wire half is declared F4 — the justification claimed `minor` was "the same grade every other accept-set narrowing on this schema has taken", which sourced nothing. The rescue commit replaced it with an attribution to #16322; measured, that is not the string-arm closing either. #16322 is the DRIVER half of #16041, and its own changeset describes its `"@objectstack/spec": patch` entry as "a `PROVENANCE_WAIVERS` row only" — not an accept-set narrowing. The accept-set narrowing on this schema is #16041, released as `@objectstack/spec` minor (`packages/spec/CHANGELOG.md` 17.4.0, under Minor Changes). The sentence now names that precedent, its grade and where to read it, and disposes of the neighbouring entry so the next reader does not re-litigate it from the same mis-reading. ⛔ The LEVEL is untouched — `minor` is ruled. `@objectstack/types` and `@objectstack/rest` join the changeset: both publish a changed wire (one `fields[]` entry for an arity refusal instead of two), and the body now states that client-visible effect and its two edges. `analytics.zod.ts`'s "a single prescriptive issue rather than the tuple arm's own too_big/too_small text" is true at `error.issues` and was being read as a claim about the wire, where the ADR-0114 union expansion had made it false. The comment now says which half it covers and where the other half is enforced. Also hardens the new filter: an issue carrying an `errors` array but no `path` now returns untouched instead of reaching a recogniser that reads `path.length`. Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <noreply@anthropic.com>
Seat disposal — the ⛔ release-owned row from Docs Drift Check is a false positive, measured. ⛔ Nothing to edit, nothing to file.
The check flagged one read-only page on head
The check invites exactly this reply — 「a wrong row is reportable rather than merely annoying」 — so here is the reading. Both candidate carriers on that page, read on
Counted, with a control so the single hits are not read as a missing corpus: ⇒ The page names neither the schema nor the error code this PR changes. The row is an anchor collision on a generic zod issue-code token. Disposal, per the guardrail's own two branches ( Generated by Claude Code |
Contract reviewServed-tier: Re-review after the rework that claims F1 + F2 + F4. Every reading below is my own, taken 2026-09-15T01:31Z–01:5xZ against ① Derived judgmentsAccept set — narrowed, on exactly one schema, in exactly the ruled direction.
Error / refusal wire shape — one restatement dropped, every diagnosis kept. Exported symbols — none new on a published entry. The shipped-prose check, site by site.
② Semver levelWhat moves: an accept-set narrowing plus an authored-type narrowing on a published Convention, read on The rewritten justification, checked clause by clause against the tree — every clause holds, and it corrects the previous record's F4. ③ Boundary flags
Implemented-by: VERDICT: FAIL Generated by Claude Code |
…real residue The rework's own docblock in `core/src/utils/analytics-date-range.ts` carried two falsifiable clauses, and both citations it added pointed at a card number a reader cannot open. Prose only — no behaviour, no assertion, no bump change. - `:216-218` said "the one caller that ... DISCARDS the message is the REST dataset door". Measured on this head: four non-test callers of `analyticsDateRangeUnrecognizedError(`, of which THREE replace the message — the dataset door (`rest/src/analytics-selection-door.ts:197`) and the two face-side array arms (`service-analytics/src/date-range-array-arm.ts:71`, `driver-memory/src/memory-analytics.ts:777`). Only core's own string resolver (`:260`) lets the sentence leave. The paragraph now names the set. - `:225-226` said "on EVERY REST analytics route the schema door refuses first and this never fires". The array arm is `z.tuple([z.string(), z.string()])` with bare bounds — no `.min(1)`, no format refinement (spec `data/analytics.zod.ts:501`) — so `['','']` passes the union and reaches this constructor at each face past every door; only its message is replaced. The paragraph now states what the door does refuse and names the residue it cannot. - Both `#17058` citations this diff added are now `PR #17548, the PR that landed that door for card #17058`. `GET /issues/17058` answers 404 on the credential that answers 200 for `17548`; the `analytics.zod.ts` site ships to npm (`packages/spec` `files[]` carries `src/**/*.zod.ts`), so it was published prose pointing at a dead link. Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Third at-tier round; scope is the prose-only fix-up the seat ordered off record ① Derived judgmentsSize and base, measured. G1(a) — the caller count, my own (02:55:32Z).
⇒ three of four discard the sentence; the shipped paragraph ( G1(b) — the residue predicate, verified independently, and the seat's parenthetical would have been false.
G2 — both status codes, my own credential, back to back (02:56Z, the GitHub MCP credential, The shipped Two findings outside the ordered edits, recorded, no verdict effect. (a) The hand-over read core's docblock as unpublished because ② Semver levelNot re-litigated; confirmed unmoved. ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位采纳达档记录
|
Provenance — carriers cleared against the at-tier PASS, and the three landing pre-checks with the time each was taken
Record of the clearing: comment
Carriers cleared in one stroke and read back: PR
On the changeset level, so the record is complete
Review 2 verified the justification's precedent clause by clause against ⏳ Flipping ready and enqueuing. Landing is confirmed git-side on Generated by Claude Code |
Fixes #17598
Ruling A (comment 5642584462, decision batch #117 item 3, 2026-09-12; re-affirmed 5651068700 on 2026-09-13). Both items of the card land here — see "Why item 2 lands here" below.
1. The array arm refuses anything that is not exactly two string bounds
AnalyticsDateRangeSchema's array arm wasz.array(z.string())with no length constraint; it is nowz.tuple([z.string(), z.string()]). A tuple rather thanz.array(z.string()).length(2)on purpose: the arity is then stated to the author's compiler before any parse runs, which is the ruling's "the type says what the prose says".The union's
errormapper is unchanged in kind, so an arity refusal is still ONE prescriptiveinvalid_unionissue attimeDimensions.N.dateRangeand still carries the structural handleisAnalyticsDateRangeRefusalIssuelifts intoANALYTICS_DATE_RANGE_UNRECOGNIZED/ 400 — not the tuple arm's owntoo_big/too_smalltext.Pinned in
packages/spec/src/data/analytics-date-range-two-bound-window.test.ts:[],['2026-01-01'],['2026-01-01','2026-01-31','2026-02-28']and['{7_days_ago}']are each refused with one issue at the field's own path, throughAnalyticsQuerySchemaand through thePOST /analytics/querybody schema; the two-bound windows and every preset name still parse; and the authored TYPE narrows too (a@ts-expect-errorcase).2. Each refusal ORIGIN gets a true sentence, with the origin as a PARAMETER
analyticsDateRangeRefusalMessage(input)becomesanalyticsDateRangeRefusalMessage(input, origin),originbeing'schema'or'runtime', required, with deliberately no default — a defaulted origin makes the same false assertion silently for every caller who does not think about it.Two clauses were false for an array refused past the schema door:
'runtime'answers "Refused past the schema door, by the analytics reader that received it", and the two sentences are otherwise byte-identical (pinned by a replace-and-compare assertion, so the{ field: {} }(零个操作符的字段约束)在同仓有三个答案:driver-sql 组合子内 TRUE、顶层抛 INVALID_FILTER、formula/driver-memory FALSE #5240 one-condition-one-wording convention is mechanical rather than asserted).describeRefusedDateRangenames the ARITY and the bad bound separately and claims neither when it is not true:['2026-01-01']now reads "received a 1-element array, not the two bounds [start, end]", while['2026-01-01', 3]keeps exactly the description it had, because that one was true.The ruling's acceptance criterion — the wording is true BOTH BEFORE AND AFTER the arm narrows — is itself a test:
['2026-01-01']is described correctly at both origins, and answering item 1 did not falsify item 2's wording.3. Changeset and ADR-0087 entry
.changeset/17598-analytics-date-range-two-bound-window.mddeclaresminor, not themajorthe ruling's execution note 1 names, and says so in its own body.scripts/check-changeset-no-major.mjsrefuses amajoroutright: every publishable package is in the Changesetsfixedgroup, so onemajorpromotes the whole stack, and during the launch window this repo ships breaking changes asminor. The information carriers the guard names in its place are both present — the BREAKING banner and the ADR-0087 disposition marker. Measured:check-changeset-no-major.mjs --base origin/mainexit 0,check-adr-0087-registration.mjs --base origin/mainexit 0.The ADR-0087 semantic entry is
packages/spec/src/migrations/entries/semantic/18.analytics-date-range-array-two-bounds-required.ts. The18.prefix is the TARGET PROTOCOL MAJOR, not a sequence number:packages/specis at17.4.0, the sibling18.analytics-time-dimension-date-range-vocabulary-closed.ts(#16322, the same schema's string arm) is filed the same way, and the generated table lands it inMIGRATIONS_BY_MAJOR[18].semantic—check:migration-registry,check:spec-changesandcheck:upgrade-guideall green, so the projections agree.Conversions, per the ruling: one element becomes the same day twice; the empty array and 3+ bounds get the structured TODO and no conversion, because there is nothing to derive a pair FROM. No D2 conversion and no stored-metadata rewrite — rewriting
['2026-01-01']at load would be the platform deciding silently that the author meant one day rather than a window whose end they forgot.Scope: the two out-of-declaration edits, measured and then decided
The claim declared
packages/spec/src/data/analytics.zod.tsplus its tests, one ADR-0087 entry and a changeset. Two edits reach outside it. Both were MEASURED before being kept, by ablation with on-disk mutation proof and a restore proof.A.
packages/core/src/utils/analytics-date-range.ts— REQUIRED CONSEQUENCE, keptIt is not one of the four analytics faces PR #17593 landed. Measured from that PR's own file list: all 8 files are under
packages/services/service-analytics/plus its changeset;packages/coreis not among them. This file is the shared ADR-0112 envelope constructoranalyticsDateRangeUnrecognizedErrorthat those faces — and driver-memory, and the REST selection door — all call, and it predates #17593.It is the only non-test call site of
analyticsDateRangeRefusalMessagein the monorepo (grep overpackages/**: the other five files name only the constructor, whose signature is unchanged). Makingoriginrequired is item 2 of the ruling, so this call site MUST state the origin it alone knows.Ablation, one file reverted to the merge base, nothing else:
The narrowing cannot land without it:
TypeScript Type Checkis one of the six required contexts.B.
packages/spec/scripts/lib/format-type.ts— REQUIRED CONSEQUENCE, keptThe reference-docs renderer read only
items. Draft-2020-12 spellsz.tuple([...])asprefixItemsand leavesitemsABSENT, so the array branch rendered the element type of nothing and printedany[].Measured directly, both renderer versions run over the REAL generated schema for this field (
packages/spec/json-schema/data/AnalyticsQuery.json,properties.timeDimensions.items.properties.dateRange):So landing item 1 WITHOUT this fix does not fail the docs build — it silently rewrites
content/docs/references/data/analytics.mdxandapi/analytics.mdxfromstring[]toany[], i.e. this card's narrowing would make the published reference page say the two bounds may be anything. A regenerated page that is wrong is the criterion, and this is it. Confirmed the other way too: with the branch reverted,check:docsexits 1 and names all four pages, andscripts/format-type.test.tsgoes 1 failed / 89 passed.Collateral, and unavoidable: the branch is one branch in one renderer and cannot be scoped to
dateRangealone, so two pre-existing tuples stop readingany[]in the same regeneration —FilterCondition.$between([number | string, number | string]) andListView.map.center([number, number]). Both are corrections in the same direction, on generated pages, with no source change behind them. They are called out here rather than filed, because there is no PR in which they could land separately.C.
packages/services/service-analytics/src/__tests__/objectql-daterange.test.ts— a THIRD required consequence, found this roundNot in the inherited diff. Narrowing the authored type breaks three sites in this suite, measured:
(The 36 that remain are 24 TS2307 plus their implicit-any cascade, from
@objectstack/corehaving nodistin this worktree. They are identical in all three runs and CI builds the closure.)Line 220 is a deliberate one-element window: the value is KEPT and gains
@ts-expect-error, because the refusal this test pins is the FACE's, raised past a doorPOST /analytics/dataset/querynever opens — deleting the case would delete #17593's own coverage. Lines 499/503 are an ordinary two-bound window whoseconsthad no annotation, so TypeScript widened the literal tostring[]; it is annotatedAnalyticsQuery. Neither touches any face's behaviour. No open PR touchespackages/services/service-analytics.A repo-wide sweep for the same two shapes found no other site: exactly two
dateRangearray literals in the tree have arity other than 2 (this one, and this card's own@ts-expect-errorcase), and every remaining literal sits in a directly contextually-typed position.Premise checks
Two line numbers in the ruling and the card are stale. The ruling cites the array arm at
analytics.zod.ts:326andanalyticsDateRangeRefusalMessageat:284. Onorigin/main1bdbf82cb5those lines carry unrelated text; the live ones before this diff were:418and:376(they are:496and:425after it). Both symbols are unique by name, so the ruling's subject is unambiguous — only the citations moved.Why item 2 lands here:
#17918does not exist. The 2026-09-13 re-affirmation routes item 2 to a split card,#17918, and permits "one PR or #17918 first".GET /repos/objectstack-ai/objectstack/issues/17918returns HTTP 404 on a credential that answers 200 for #17593, #17596, #17124 and #16322 on the same call shape, so the 404 is that card's own and not a permissions artefact. Item 2 therefore has no other carrier, and the ruling's own permission to land both in one PR is what is being used here. This PR does not name that number in any closing keyword.Clause-②: noholds against the mechanical check.scripts/pm/check-widening-tells.mjs --declaration no --diffover this branch's full diff: exit 0, no widening tell. 3 files judged against a declared surface (the schema source, the migrations registry and the new entry), 11 NOT MEASURED — the ADR-0087 registration did not fire T4. The declaration stands as the ruling wrote it and is not flipped.Verification
Control, at the tip:
Negative control for item 1, the whole point of the new suite: with
analytics.zod.tsreverted to the merge base (on-disk proof: tuple-arm occurrences 1 -> 0; blob 69e3ff58f4 -> 8c2cce906a), the two spec suites go 10 failed / 13 passed — everyrefuses ...case and every origin case. Restored fromHEAD,git diff HEADempty.Honest reading of the sibling suite:
analytics-date-range-closed-vocabulary.test.tsPASSES under that ablation. Its only edits are the now-required'schema'argument (ignored by the one-parameter function) and one renamedittitle — "keeps the array arm exactly as it was" became "still accepts every two-bound array window", because the old title is false after this change. No assertion in it was weakened, deleted, loosened or skipped; its three windows all had two bounds already, and the diff is 3 changed lines.60 gate families run in total, 58 exit 0. Two did not measure anything and are recorded as such, not as passes:
check:type-check-debtexits 3 =PREREQUISITE NOT MET(29 workspace dependencies have no built type entry point in this worktree) andcheck:skill-examplesexits 1 on the same prerequisite (packages/client-react/distholds no declarations). Both build the closure in CI. The remaining derived families are repo-wide scans left to CI, perscripts/pm/dispatch-gates.mjs --ran(115 derived, 30 recorded in the first reconciliation pass, 30 more run after it).Acceptance notes
Noted, not filed — each names who would meet it:
packages/services/service-analytics/src/date-range-array-arm.tsoverwrites the shared refusal sentence and documents why: "the shared wording ... ends with 'Refused at the schema', and neither is true of an array refused past the schema door by a face". After this PR that justification is gone —analyticsDateRangeRefusalMessage(dateRange, 'runtime')now says something true — so the repo keeps one condition with two wordings for no remaining reason. That file is fenced to PR fix(analytics): a dateRange array that is not a two-bound window is refused once, instead of meaning three different things #17593's landing for this round. Successor: whoever next touches that file, or a follow-up that finishes the{ field: {} }(零个操作符的字段约束)在同仓有三个答案:driver-sql 组合子内 TRUE、顶层抛 INVALID_FILTER、formula/driver-memory FALSE #5240 convergence item 2 opened the door for.AnalyticsDateRangeSchema's array arm is a barez.array(z.string())with no length constraint" and "TighteningAnalyticsDateRangeSchemaitself ispackages/spec's call and is deliberately NOT done here". Both sentences are true of the tree this PR starts from and false of the tree it produces. Same fence, same successor.content/docs/references/data/filter.mdxandui/view.mdxgain corrected tuple cells with no source change behind them, as set out in scope decision B. Successor: none needed — they are generated, and the regeneration is in this PR.Fix-up round 2 — F1 + F2 + F4
Scope from the at-tier review record (PR comment
5672172206) and the seat's rulings(card comment
5672187836): F1 + F2 + F4, nothing else. Run insession_01KB5PFtxuy1x3dcR5gxudx6. Headae337c01a9. The LEVEL of the changeset isuntouched —
minoris ruled.Verdict on the rescue commit
1974dd56dc— right to act, wrong on the fact; supersededThat commit (seat-made, marked INCOMPLETE AND UNREVIEWED) rewrote the justification to read
"the string-arm closing on this same schema (#16322) declared
"@objectstack/spec": patch".It was right that the original sentence ("the same grade every other accept-set narrowing on
this schema has taken") sourced nothing. Its replacement attribution does not hold, measured
on this tree:
.changeset/analytics-daterange-driver-alignment.md(Analytics drivers (memory, SQL) aligndateRangestring handling to the closed preset vocabulary (driver half of #16041) #16322) is still in.changeset/,unreleased, and describes its own spec entry as "a
PROVENANCE_WAIVERSrow only" — thedriver half of
driver-memoryanalytics silently accepts an unparseabledateRangeand matches EVERY row — and the platform's own documented spelling'Last 7 days'is one of them #16041, not an accept-set narrowing at the schema.driver-memoryanalytics silently accepts an unparseabledateRangeand matches EVERY row — and the platform's own documented spelling'Last 7 days'is one of them #16041, and it shipped"@objectstack/spec": minor—packages/spec/CHANGELOG.md17.4.0, commit233222e, under### Minor Changes(heading at :5, entry at :60, no intervening heading).So the review's F4 comparison was drawn against the wrong entry, and repeating it would have
compiled a second inaccurate justification into the shipped CHANGELOG — the exact defect F4
names. The sentence now names #16041, its grade and where to read it, and disposes of #16322
explicitly so the next reader does not re-litigate it from the same mis-reading. The ruling is
executed (the justification is corrected), not reopened (the level is not touched).
F1 — a sentence that ships to npm was false
Door measurement taken here, not adopted from the review.
datasetSelectionRefusaldrivendirectly over the built dist:
dateRange['2026-01-01']ANALYTICS_DATE_RANGE_UNRECOGNIZED… received a 1-element array, not the two bounds [start, end]. Refused at the schema (…)[]… received an empty array … Refused at the schema (…)['a','b','c']… received a 3-element array … Refused at the schema (…)['2026-01-01','2026-01-31']undefined)Mechanism confirmed in source:
analytics-selection-door.tsparses the selection's sharedmembers —
timeDimensionsincluded — againstAnalyticsQuerySchema.pick(…), andrest-server.ts:10981calls it beforequeryDataset. ⇒ the one route the TSDoc named as a"past the door" origin is a schema-origin door.
Corrected at four sites, all naming the in-process
AnalyticsService.query/queryDataset/dataset-executor path as the runtime origin and stating that every REST analytics route is a
schema-origin door:
packages/spec/src/data/analytics.zod.ts— the shipped one (files[]carriessrc/**/*.zod.tsliterally).packages/core/src/utils/analytics-date-range.tsReachability — pre-existing on the base,carried per the ruling.
packages/core/src/utils/analytics-date-range.ts"Every refusal raised here is raised PASTthe schema door" — this PR's own new prose, which the review flagged as untrue of the REST
dataset door. Narrowed to refusals whose MESSAGE leaves the constructor, with the one caller
that keeps only
.code/.statusnamed.service-analyticsobjectql-daterange.test.tscomment.F2 — one condition, two wordings on the wire
Premise check first (falsifiable, and it holds): on the base the arm was
z.array(z.string())with no length constraint, so a 1-element array was not refused by theschema at all. No schema refusal, no second entry ⇒ this PR introduces the second wording, and
it is this PR's to collapse.
Wire measurement, before: on the dataset door,
['2026-01-01']servedcode: ANALYTICS_DATE_RANGE_UNRECOGNIZEDwith a message ending… Refused at the schema (ANALYTICS_DATE_RANGE_UNRECOGNIZED / 400).; selection.timeDimensions.0.dateRange: Too small: expected array to have >=2 items— the prescription, then zod's restatement of the very arity it prescribes. A body wrong in
more than the dateRange carried the same pair as two
fields[]entries, so the defect was notconfined to the lifted path.
Wire measurement, after (same probe, same tree):
Too small/Too bigoccurrences = 0for
['2026-01-01'],[]and['a','b','c']; prescription occurrences = 1; the legal windowand the preset still pass; and the mixed body still carries
selection.measuresbeside exactlyone
selection.timeDimensions.0.dateRangeentry.Where the collapse lives, and why there. In
fieldsFromZodIssues(
packages/types/src/validation-failure.ts), keyed on the exported structural recogniserisAnalyticsDateRangeRefusalIssue. Both REST doors report through that one mapper — the runtime/analytics/querydoor already did; the dataset door now does too, instead of reading the rawD3
zodIssuesToFields— so the rule has ONE implementation. The alternatives were each worse:a copy at both doors is the two-copies-of-one-policy shape #8124/#8318 exist to end; the
selection policy in
shared/union-branch-policy.tsis shared with the terminal renderer andpinned against it by a parity test, so a rule added there would have to be schema-agnostic; and
a schema-agnostic rule ("drop branch issues at the union's own path") silences the
#4001unknown-key prescriptions, which are raised at exactly that position. A new exported helper was
rejected too: T3 of
check-widening-tells.mjsreads a newapi-surfacerow as a widening tell,and this PR's claim declares
Clause-②: no.What is deliberately NOT collapsed — this narrows a restatement, never a diagnosis:
['2026-01-01', 3]still reportsselection.timeDimensions.0.dateRange.1— WHICH bound is not a string is a location theprescription does not carry.
wholesale, so the stop condition in the ruling was not reached.
Pinned on the wire, in both directions (the review recorded that nothing pinned it either
way): 4 cases at the dataset door, 4 at
/analytics/query, 5 in the shared mapper's own suite —including a CONTROL that a branch issue at its own branch root (
unrecognized_keys, the samestructural position) still reaches the wire for every other key.
@objectstack/typesand@objectstack/restjoin the changeset: both publish a changed wire.Verification on
ae337c01a9scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack,re-derived on the final head): 113 of 115 exit 0.
PREREQUISITE NOT MET, exit 3, printing "⛔ This is NOT a pass: nothing wasmeasured" —
check:dual-build-cjs-loadsandcheck:type-check-debt, both of which need thewhole monorepo built. NOT MEASURED, not red; CI builds fresh.
check:skill-examplesrefused the same way at first and was cleared by buildingclient-react/client: 258 examples over 3 surfaces, exit 0.pnpm lint(repo-wide,eslint . --no-inline-config): exit 0 — the whole population, so nonarrowing to justify.
service-analytics 2392 · core 1316 · types 671.
typecheckexit 0: spec, core, types, rest, runtime;service-analytics tsc --noEmitexit 0with 0 errors (the review saw 38 there, all from unbuilt workspace deps in its worktree).
check:type-source-resolutionexit 0 (135 tsc programs, 80 packages) — the new@objectstack/spec/dataimport inpackages/typesis a fifth subpath on a dependency thatpackage already declares.
dispatch-gatesprints a STALE TREE warning: its one stale input isscripts/pm/check-harness-current.mjs, added onmainafter this branch, whose own headersays "⛔ not a CI gate" — so it adds no family here.
Acceptance notes (noted, not filed)
packages/services/service-analytics/src/__tests__/analytics-date-range-conformance.test.ts:32carries the same "which types its selection from
AnalyticsQueryand NEVER Zod-parses it"sentence about
POST /analytics/dataset/query. It is pre-existing (not in this diff) and theruling scoped this round to F1 + F2 + F4 naming three sites, so it is left alone. Not shipped
prose — a test comment. Carrier: the next PR in this family, or [finding] after #18230, service-analytics keeps a second refusal wording whose only stated justification is gone — the
#5240one-condition-one-wording convention #18232.Generated by Claude Code