Skip to content

fix(spec)!: the dateRange array arm is exactly two string bounds, and each refusal origin gets a true sentence - #18230

Merged
os-warren merged 8 commits into
mainfrom
claude/issue-17598-analytics-date-range-two-bounds
Sep 15, 2026
Merged

os-warren merged 8 commits into
mainfrom
claude/issue-17598-analytics-date-range-two-bounds

Conversation

@os-warren

@os-warren os-warren commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator

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 was z.array(z.string()) with no length constraint; it is now z.tuple([z.string(), z.string()]). A tuple rather than z.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 error mapper is unchanged in kind, so an arity refusal is still ONE prescriptive invalid_union issue at timeDimensions.N.dateRange and still carries the structural handle isAnalyticsDateRangeRefusalIssue lifts into ANALYTICS_DATE_RANGE_UNRECOGNIZED / 400 — not the tuple arm's own too_big / too_small text.

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, through AnalyticsQuerySchema and through the POST /analytics/query body schema; the two-bound windows and every preset name still parse; and the authored TYPE narrows too (a @ts-expect-error case).

2. Each refusal ORIGIN gets a true sentence, with the origin as a PARAMETER

analyticsDateRangeRefusalMessage(input) becomes analyticsDateRangeRefusalMessage(input, origin), origin being '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:

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.md declares minor, not the major the ruling's execution note 1 names, and says so in its own body. scripts/check-changeset-no-major.mjs refuses a major outright: every publishable package is in the Changesets fixed group, so one major promotes the whole stack, and during the launch window this repo ships breaking changes as minor. 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/main exit 0, check-adr-0087-registration.mjs --base origin/main exit 0.

The ADR-0087 semantic entry is packages/spec/src/migrations/entries/semantic/18.analytics-date-range-array-two-bounds-required.ts. The 18. prefix is the TARGET PROTOCOL MAJOR, not a sequence number: packages/spec is at 17.4.0, the sibling 18.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 in MIGRATIONS_BY_MAJOR[18].semanticcheck:migration-registry, check:spec-changes and check:upgrade-guide all 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.ts plus 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, kept

It 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/core is not among them. This file is the shared ADR-0112 envelope constructor analyticsDateRangeUnrecognizedError that 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 analyticsDateRangeRefusalMessage in the monorepo (grep over packages/**: the other five files name only the constructor, whose signature is unchanged). Making origin required 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:

revert packages/core/src/utils/analytics-date-range.ts to 1bdbf82cb5
  on-disk proof: two-arg call sites 1 -> 0; blob 50509debc7 -> 4f6d4829ca
  pnpm --filter @objectstack/core exec tsc --noEmit  :: exit 1
    src/utils/analytics-date-range.ts(222,25): error TS2554: Expected 2 arguments, but got 1.
restore (git checkout HEAD -- path; blob back to 50509debc7, git diff HEAD empty)
  pnpm --filter @objectstack/core exec tsc --noEmit  :: exit 0

The narrowing cannot land without it: TypeScript Type Check is one of the six required contexts.

B. packages/spec/scripts/lib/format-type.ts — REQUIRED CONSEQUENCE, kept

The reference-docs renderer read only items. Draft-2020-12 spells z.tuple([...]) as prefixItems and leaves items ABSENT, so the array branch rendered the element type of nothing and printed any[].

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):

origin/main arm  z.array(z.string())        + base renderer  ->  string[]
NARROWED arm     z.tuple([string, string])  + base renderer  ->  any[]      ** THE REGRESSION **
NARROWED arm     z.tuple([string, string])  + this renderer  ->  [string, string]

So landing item 1 WITHOUT this fix does not fail the docs build — it silently rewrites content/docs/references/data/analytics.mdx and api/analytics.mdx from string[] to any[], 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:docs exits 1 and names all four pages, and scripts/format-type.test.ts goes 1 failed / 89 passed.

Collateral, and unavoidable: the branch is one branch in one renderer and cannot be scoped to dateRange alone, so two pre-existing tuples stop reading any[] in the same regeneration — FilterCondition.$between ([number | string, number | string]) and ListView.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 round

Not in the inherited diff. Narrowing the authored type breaks three sites in this suite, measured:

HEAD (narrowed)                pnpm --filter @objectstack/service-analytics exec tsc --noEmit
                               39 errors total, 3 in objectql-daterange.test.ts (220, 499, 503)
arm reverted + spec rebuilt    36 errors total, 0 in objectql-daterange.test.ts
HEAD + this commit             36 errors total, 0 in objectql-daterange.test.ts

(The 36 that remain are 24 TS2307 plus their implicit-any cascade, from @objectstack/core having no dist in 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 door POST /analytics/dataset/query never opens — deleting the case would delete #17593's own coverage. Lines 499/503 are an ordinary two-bound window whose const had no annotation, so TypeScript widened the literal to string[]; it is annotated AnalyticsQuery. Neither touches any face's behaviour. No open PR touches packages/services/service-analytics.

A repo-wide sweep for the same two shapes found no other site: exactly two dateRange array literals in the tree have arity other than 2 (this one, and this card's own @ts-expect-error case), 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:326 and analyticsDateRangeRefusalMessage at :284. On origin/main 1bdbf82cb5 those lines carry unrelated text; the live ones before this diff were :418 and :376 (they are :496 and :425 after it). Both symbols are unique by name, so the ruling's subject is unambiguous — only the citations moved.

Why item 2 lands here: #17918 does 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/17918 returns 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-②: no holds against the mechanical check. scripts/pm/check-widening-tells.mjs --declaration no --diff over 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:

pnpm --filter @objectstack/spec exec vitest run --project local
  src/data/analytics-date-range-two-bound-window.test.ts
  src/data/analytics-date-range-closed-vocabulary.test.ts
  scripts/format-type.test.ts                      :: exit 0 — 3 files, 113 tests passed
pnpm --filter @objectstack/core exec vitest run --project local
  src/utils/analytics-date-range.test.ts           :: exit 0 — 45 tests passed
pnpm --filter @objectstack/spec typecheck          :: exit 0
pnpm --filter @objectstack/core exec tsc --noEmit  :: exit 0
pnpm --filter @objectstack/spec check:generated    :: exit 0 — all 15 generated artifacts up to date
pnpm lint                                          :: exit 0 (full repo scan, at 5db9a5d684)
pnpm check:nul-bytes                               :: exit 0

Negative control for item 1, the whole point of the new suite: with analytics.zod.ts reverted to the merge base (on-disk proof: tuple-arm occurrences 1 -> 0; blob 69e3ff58f4 -> 8c2cce906a), the two spec suites go 10 failed / 13 passed — every refuses ... case and every origin case. Restored from HEAD, git diff HEAD empty.

Honest reading of the sibling suite: analytics-date-range-closed-vocabulary.test.ts PASSES under that ablation. Its only edits are the now-required 'schema' argument (ignored by the one-parameter function) and one renamed it title — "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-debt exits 3 = PREREQUISITE NOT MET (29 workspace dependencies have no built type entry point in this worktree) and check:skill-examples exits 1 on the same prerequisite (packages/client-react/dist holds no declarations). Both build the closure in CI. The remaining derived families are repo-wide scans left to CI, per scripts/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.ts overwrites 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.
  • The same file's header states "AnalyticsDateRangeSchema's array arm is a bare z.array(z.string()) with no length constraint" and "Tightening AnalyticsDateRangeSchema itself is packages/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.mdx and ui/view.mdx gain 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 in
session_01KB5PFtxuy1x3dcR5gxudx6. Head ae337c01a9. The LEVEL of the changeset is
untouched — minor is ruled.

Verdict on the rescue commit 1974dd56dc — right to act, wrong on the fact; superseded

That 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:

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. datasetSelectionRefusal driven
directly over the built dist:

dateRange status code message ends
['2026-01-01'] 400 ANALYTICS_DATE_RANGE_UNRECOGNIZED … received a 1-element array, not the two bounds [start, end]. Refused at the schema (…)
[] 400 same … received an empty array … Refused at the schema (…)
['a','b','c'] 400 same … received a 3-element array … Refused at the schema (…)
['2026-01-01','2026-01-31'] passes (undefined)

Mechanism confirmed in source: analytics-selection-door.ts parses the selection's shared
members — timeDimensions included — against AnalyticsQuerySchema.pick(…), and
rest-server.ts:10981 calls it before queryDataset. ⇒ 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:

  1. packages/spec/src/data/analytics.zod.ts — the shipped one (files[] carries
    src/**/*.zod.ts literally).
  2. packages/core/src/utils/analytics-date-range.ts Reachability — pre-existing on the base,
    carried per the ruling.
  3. packages/core/src/utils/analytics-date-range.ts "Every refusal raised here is raised PAST
    the 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/.status named.
  4. the service-analytics objectql-daterange.test.ts comment.

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 the
schema 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'] served
code: ANALYTICS_DATE_RANGE_UNRECOGNIZED with 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 not
confined to the lifted path.

Wire measurement, after (same probe, same tree): Too small / Too big occurrences = 0
for ['2026-01-01'], [] and ['a','b','c']; prescription occurrences = 1; the legal window
and the preset still pass; and the mixed body still carries selection.measures beside exactly
one selection.timeDimensions.0.dateRange entry.

Where the collapse lives, and why there. In fieldsFromZodIssues
(packages/types/src/validation-failure.ts), keyed on the exported structural recogniser
isAnalyticsDateRangeRefusalIssue. Both REST doors report through that one mapper — the runtime
/analytics/query door already did; the dataset door now does too, instead of reading the raw
D3 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.ts is shared with the terminal renderer and
pinned 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 #4001
unknown-key prescriptions, which are raised at exactly that position. A new exported helper was
rejected too: T3 of check-widening-tells.mjs reads a new api-surface row as a widening tell,
and this PR's claim declares Clause-②: no.

What is deliberately NOT collapsed — this narrows a restatement, never a diagnosis:

  • a branch issue naming a DEEPER position is kept. ['2026-01-01', 3] still reports
    selection.timeDimensions.0.dateRange.1 — WHICH bound is not a string is a location the
    prescription does not carry.
  • every other union expands exactly as before. ⛔ Union-branch expansion is not suppressed
    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 same
structural position) still reaches the wire for every other key.

@objectstack/types and @objectstack/rest join the changeset: both publish a changed wire.

Verification on ae337c01a9

  • Derived gate union (scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack,
    re-derived on the final head): 113 of 115 exit 0.
  • The other 2 are PREREQUISITE NOT MET, exit 3, printing "⛔ This is NOT a pass: nothing was
    measured" — check:dual-build-cjs-loads and check:type-check-debt, both of which need the
    whole monorepo built. NOT MEASURED, not red; CI builds fresh.
    check:skill-examples refused the same way at first and was cleared by building
    client-react / client: 258 examples over 3 surfaces, exit 0.
  • pnpm lint (repo-wide, eslint . --no-inline-config): exit 0 — the whole population, so no
    narrowing to justify.
  • Full suites, all exit 0: spec 13624 · runtime 3644 · rest 3201 (+1 skipped) ·
    service-analytics 2392 · core 1316 · types 671.
  • typecheck exit 0: spec, core, types, rest, runtime; service-analytics tsc --noEmit exit 0
    with 0 errors (the review saw 38 there, all from unbuilt workspace deps in its worktree).
  • check:type-source-resolution exit 0 (135 tsc programs, 80 packages) — the new
    @objectstack/spec/data import in packages/types is a fifth subpath on a dependency that
    package already declares.
  • dispatch-gates prints a STALE TREE warning: its one stale input is
    scripts/pm/check-harness-current.mjs, added on main after this branch, whose own header
    says "⛔ not a CI gate" — so it adds no family here.

Acceptance notes (noted, not filed)


Generated by Claude Code

…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>
@github-actions

github-actions Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/core, @objectstack/rest, @objectstack/spec, @objectstack/types, touching 9 documentable anchor(s).

1 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v17/17-0.mdx (via invalid_union (literal, a string literal in AnalyticsDateRangeSchema))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: created_at (literal, 34 pages)
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 143 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 b3b43b6ea0c30f35d861c9cc0fedc9c085027b7bpackageMentionDocs.

Which tree this was computed on

This run read content/docs from eaf96f0b1b9177769043e8f5148cadec6e643ae8 — the merge of head 00ae6c44c75f573ff5f05884873c63c456295f61 into base b3b43b6ea0c30f35d861c9cc0fedc9c085027b7b, 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 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

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs b3b43b6ea0c30f35d861c9cc0fedc9c085027b7b → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:data tests tooling labels Sep 14, 2026

Copy link
Copy Markdown
Collaborator Author

Seat note — needs:contract-review hung on BOTH carriers, and why that is not a contradiction of the claim's Clause-②: no

domain:spec execution PM seat, session_01KB5PFtxuy1x3dcR5gxudx6, 2026-09-14T22:5xZ. Hung on this PR and on card #17598 in one stroke, each written read-modify-write with a read-back (PR: documentation, size/l, tests, tooling, protocol:data, needs:contract-review; card: bug, priority:p2, pm:dispatched, domain:spec, needs:contract-review).

The two are different statements and both are true here:

  • Clause-②: no on the claim (5671013398) is ruling A's own declaration, quoted rather than judged: 「Accept-set narrowing, ⛔ not a widening: Clause-②: no; the spec seat's own review applies as for any packages/spec change.」
  • needs:contract-review is the carrier that keeps the card out of the enqueue path until that review is on record. The same ruling sentence is what requires it.

⇒ the no sets the conservative direction for whether the review is mandatory by clause ②; it does ⛔ not waive the spec seat's at-tier review of a packages/spec contract change, and it is ⛔ not a clearance.

Why the seat hung it rather than the round: the PR opened at 22:54:14Z carrying no gate on either side. 「PR 与卡双载体同笔挂:PR 一存在即挂」, and a missing gate is the dangerous half — 「闸门被剥不是红灯是放行」: to the enqueue path an ungated card is one that was never gated, and the two are indistinguishable in the evidence. So it is hung now rather than at review time.

⛔ This is not a verdict and carries no reading of the diff. The at-tier review has not run; when it does, its record clears both carriers whichever way it lands.

State at this note (⛔ not a conclusion — re-read before acting): CI on 5db9a5d684 is 36 checks, 22 in progress, 9 success, 5 skipped, 0 failures so far. Draft. The implementing round is still running and has not filed its report.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Seat note — the drift check's ⛔ row is clear, measured. ⛔ No edit, ⛔ no card, ⛔ no docs-only PR owed.

domain:spec execution PM seat, session_01KB5PFtxuy1x3dcR5gxudx6, 2026-09-14T22:5xZ. The row above renders as a ⛔ and reads like a blocker, so here is the check rather than leaving the next reader to redo it.

① Does this PR edit a release-owned page? NO. GET /pulls/18230/files — 15 files, of which the content/docs/ ones are exactly four, all generated reference pages:

content/docs/references/api/analytics.mdx
content/docs/references/data/analytics.mdx
content/docs/references/data/filter.mdx
content/docs/references/ui/view.mdx

content/docs/releases/ touched: none. The guardrail — 「⛔ 永不在代码 PR 里改 content/docs/releases/」 — is intact, and the drift check's own remedy (「do not edit it here」) is already what this PR does.

② 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 invalid_union (literal, a string literal in AnalyticsDateRangeSchema). The page's only dateRange sentence, content/docs/releases/v17/17-0.mdx:1377:

timeDimensions[].dateRange is applied — the predicate every date-bucketed chart was missing.

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 dateRange being applied. ⇒ the sentence stays true, and ⛔ no correction is owed on that page.

Instrument check: the same grep over the same file for the drift check's own anchor token invalid_union hits, so the dateRange reading is a hit and not a dead pattern.

⇒ 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

Copy link
Copy Markdown
Collaborator Author

Rendered by an isolated at-tier review subagent and ADOPTED VERBATIM by the domain:spec seat (session_01KB5PFtxuy1x3dcR5gxudx6), 2026-09-14T23:2xZ. ⛔ Not rewritten, not summarised.

Downgrade-fuse reading, taken before adoption, ⛔ not from the agent's self-report: the reviewer's transcript carries "model":"claude-fable-5-1" 188 times and no other value — zero fallback evidence. Controls, same instrument, two os-dev transcripts from this seat: "model":"claude-opus-5" ×182 and ×140. ⇒ the instrument distinguishes tiers and the record was produced at CONTRACT_REVIEW_TIER throughout.

Head check: the PR head at adoption is 5db9a5d684… — the head this record names, so the verdict is live.


Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 5db9a5d684cf7a8767d49082dfef5947e042d117

Worktree: scratchpad/pr-18230/tree at the head sha, git status --porcelain empty after every ablation. Merge base 1bdbf82cb5 (origin/main is one commit ahead, unrelated). Rulings copied verbatim to ruling-5642584462.md, ruling-5651068700.md, ruling-checklist.md.

① Derived judgments

Accept set

  1. packages/spec/src/data/analytics.zod.ts:496-497 — array arm z.array(z.string())z.tuple([z.string(), z.string()]). RIGHT (ruling note 1). Measured by calling the schema (probe-origin.ts, tsx): [], ['2026-01-01'], ['a','b','c'], ['2026-01-01',3], ['2026-01-01',3,null], ['{7_days_ago}'], 'Last 7 days', 42, null, {start:'x'}safeParse.success:false, exactly one invalid_union issue each; ['2026-01-01','2026-01-31']true. Nested through AnalyticsQuerySchema: one issue at path ["timeDimensions",0,"dateRange"]. Negative control: arm reverted to 1bdbf82cb5 (blob 69e3ff58f48c2cce906a, tuple occurrences 1 → 0) → vitest run on the two spec suites: 10 failed / 13 passed, every refuses … and every origin case red; restored, git diff HEAD empty.
  2. Authored TYPE narrows string[][string, string]. RIGHT ("the type says what the prose says"). Measured through the BUILT dist, not the source: sa-tsc-ablated.log line objectql-daterange.test.ts(220,55): error TS2322: Type '[string]' is not assignable to type '[string, string] | "today" | ….
  3. :505-507 union error mapper passes 'schema'. RIGHT. For all 10 refused inputs issue.message === analyticsDateRangeRefusalMessage(input,'schema') (msgEqualsSchemaMsg:true), and the refusal at the field path still satisfies isAnalyticsDateRangeRefusalIssue (unchanged from base, md5 identical) — the runtime door's ADR-0112 handle survives.

Public surface

  1. :425-428 analyticsDateRangeRefusalMessage(input, origin: 'schema' | 'runtime'), origin required, no default. RIGHT per note 3 and the re-affirmation's "the origin must become a parameter rather than a re-worded string". Measured for 11 inputs × 2 origins: 'schema' sentences end Refused at the schema (…); 'runtime' sentences end Refused past the schema door, by the analytics reader that received it (…); runtime.replace(runtimeClause, schemaClause) === schematrue for every input (only the origin clause differs, so { field: {} }(零个操作符的字段约束)在同仓有三个答案:driver-sql 组合子内 TRUE、顶层抛 INVALID_FILTER、formula/driver-memory FALSE #5240 holds mechanically). Caveat measured: a JS caller that omits origin silently gets the 'runtime' sentence (undefined !== 'schema') — "no default" is true at the type level only.
  2. :379-392 describeRefusedDateRange (module-private, no new export — check:api-surface ✓, names in api-surface/data.json:34,693 unchanged). RIGHT. Measured: ['2026-01-01']received a 1-element array, not the two bounds [start, end]; []received an empty array, not the two bounds [start, end]; ['a','b','c']received a 3-element array, not …; ['2026-01-01',3]received an array with a non-string bound (byte-identical to the old clause, which was true); ['2026-01-01',3,null]received a 3-element array with a non-string bound, not the two bounds [start, end]; null/42/object → received null/number/object.
  3. :429-431 window sentence gains and a single day is that day written as BOTH bounds — ["2026-01-20", "2026-01-20"]. RIGHT — the Analytics drivers (memory, SQL) align dateRange string handling to the closed preset vocabulary (driver half of #16041) #16322 prescription now reaches the author from the refusal itself.
  4. packages/core/src/utils/analytics-date-range.ts:229 analyticsDateRangeUnrecognizedError now asks for 'runtime'; signature unchanged. RIGHT for its in-process callers (core's own string resolver, driver-memory/src/memory-analytics.ts:777, service-analytics/src/date-range-array-arm.ts:71). Its fourth caller packages/rest/src/analytics-selection-door.ts:184 is a SCHEMA-origin door that reads only .code/.status and discards the message (measured wire body below says "Refused at the schema"), so no false sentence escapes — but the new core docblock :210-211 "Every refusal raised here is raised PAST the schema door" is not true of that caller. Minor; core ships dist only.
  5. Shipped prose — WRONG. packages/spec/package.json files[] carries src/**/*.zod.ts. New TSDoc at analytics.zod.ts:409-411 enumerates the runtime-origin callers as "AnalyticsService.query, a driver's cube face called directly, POST /analytics/dataset/query, which types its selection from AnalyticsQuery but never Zod-parses it." Measured by calling the door (probe-door2.ts): datasetSelectionRefusal({measures:['m'], timeDimensions:[{dimension:'d', dateRange:['2026-01-01']}]})status 400, code ANALYTICS_DATE_RANGE_UNRECOGNIZED, message …received a 1-element array, not the two bounds [start, end]. Refused at the schema (…); [] likewise; ['2026-01-01','2026-01-31'] → passes. Mechanism: analytics-selection-door.ts:131-139 parses the selection's shared members — timeDimensions included — against AnalyticsQuerySchema.pick(…), wired at rest-server.ts:10981 before the executor; the file is in the merge base (git show 1bdbf82cb5:packages/rest/src/analytics-selection-door.ts exists; base has 0 hits of "Zod-parse" in analytics.zod.ts — the sentence is new here). The one route the docblock names as a "past the door" origin is a schema-origin door. The other two callers named are true (analytics-service.ts: 0 safeParse/.parse( hits, control 17 timeDimensions hits). The same stale sentence pre-exists at core :224-226 (base line 219, not this PR's addition) and is repeated in the service-analytics test comment :224 (not shipped). Other new prose checked and true: :458-479 (arm docblock), :500-502 (one prescriptive issue — true at error.issues; see ③ F2 for the wire).
  6. ADR-0087 entry packages/spec/src/migrations/entries/semantic/18.analytics-date-range-array-two-bounds-required.ts + registry.ts:5781-5837. RIGHT. 18. = target protocol major: packages/spec/src/kernel/protocol-version.ts:18 PROTOCOL_VERSION = '17.0.0', spec at 17.4.0, sibling 18.analytics-time-dimension-date-range-vocabulary-closed.ts filed identically. semantic:18 region: base 131 ids → head 132, diff = one added line id: 'analytics-date-range-array-two-bounds-required', sort -c ok, git diff 1bdbf82cb5 5db9a5d684 -- registry.ts | grep -c '^-[^-]' = 0 removed lines — no sibling displaced. migrations.test.ts 132 passed; check:migration-registry ✓. Conversion: replacement says one element → same day twice; empty and 3+ → "NO replacement that can be derived"; all three sit in MigrationStep.semantic (= structured TODO, types.ts:58), no conversionIds added — matches note 1 as written ("ADR-0087 semantic migration entry"). spec-changes.json/protocol-upgrade-guide.md untouched is correct: build-spec-changes.ts:96 loops major <= PROTOCOL_MAJOR (17); control: 17.x id action-descriptor-is-async-retired hits 2/1, first 18 id action-bulk-dispatch-contract-undeclared hits 0/0.
  7. Generated docs (content/docs/references/{api,data}/analytics.mdx, data/filter.mdx, ui/view.mdx). RIGHT. Renderer run over the REAL generated node json-schema/data/AnalyticsQuery.json …dateRange.anyOf[1] = {"type":"array","prefixItems":[{"type":"string"},{"type":"string"}]}: base renderer → any[], head renderer → [string, string]; loose arm → string[] on both. check:docs ✓ so the regeneration is complete; the $between/map.center cells are corrections of any[] from the same branch (filter.zod.ts:723,1385, view.zod.ts:1600).
  8. Tests: assertions diffed, not files. analytics-date-range-closed-vocabulary.test.ts: 3 changed lines — :118 and :150 gain the now-required 'schema' argument (grep of - lines: the only removed expect( is :118's one-arg form), :79 title keeps the array arm exactly as it wasstill accepts every two-bound array window; body unchanged (three two-bound windows). The old title's claim is exactly what ruling note 1 falsified — legitimate retarget; this suite still passes under the arm ablation, so its green is not arity coverage (the dev says the same). core/…/analytics-date-range.test.ts:247-260: title renamed and strengthened (.not.toBe(…,'schema') added). objectql-daterange.test.ts:225 @ts-expect-error keeps the refused value; :501 annotation only; no assertion moved; service-analytics/tsconfig.json include: ["src"] reaches src/__tests__, no test-typecheck-debt.json — not a phantom directive. Diff-wide: .skip/.only/.todo/toThrow() on + lines = 0 (control +expect( = 40).

② Semver level

What 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 (TS2554 in core when the call site is reverted, item ③-B). Plain semver: MAJOR for @objectstack/spec; @objectstack/core changes only an error message string → patch-level. Declared: .changeset/17598-analytics-date-range-two-bound-window.md spec minor, core minor, with the **BREAKING** banner and the adr-0087 registered analytics-date-range-array-two-bounds-required marker. Convention: scripts/check-changeset-no-major.mjs header — "During the launch window we ship breaking changes as minor… End condition: at GA — the fixed group's first general-availability major — the group returns to STRICT SEMVER"; ADR-0087 amendment 2026-09-13 (#18003) — "Pre-GA, a metadata-facing retirement or break ships minor, carrying the BREAKING banner and its ADR-0087 disposition entry" and "Every 'major' above means a protocol major". node scripts/check-changeset-no-major.mjs --base 1bdbf82cb5 → exit 0; node scripts/check-adr-0087-registration.mjs --base 1bdbf82cb5 → exit 0 ([BREAKING+bang] registered … (new here)). The declaration is consistent with the convention and inconsistent with plain semver; that disagreement is the repo's written launch-window policy, not the implementer's. The ruling's literal "major changeset" vs the gate is the ruling's wording; under the ADR's own reading the protocol major (18) is what the entry registers against. Core minor is at or above the fix floor ("commit type may raise a bump but never lower it") — allowed.

③ Boundary flags

Ruling A checked item by item (verbatim notes in ruling-checklist.md): note 1 — IMPLEMENTED (level per ②, entry per ①.9); note 2 (blast radius at the widget) — NOT CONTRADICTED, nothing for this diff to do: no packages/spec/src/ui/* schema composes AnalyticsQuerySchema/AnalyticsDateRangeSchema (grep 0 hits in ui/, control api/analytics.zod.ts:50 hits), DatasetSelection has no Zod schema, and a stored widget's query answers 400 at the REST door (measured); note 3 — IMPLEMENTED for both in-process origins (①.4-6), with the enumeration defect at ①.8; note 4 Clause-②: no — CONSISTENT: check-widening-tells.mjs --declaration no --diff exit 0 (3 judged, 12 NOT MEASURED), check-clause2-carriers.mjs --pair 18230 exit 0, no new export; note 5 — CONSISTENT, no conformance-kit or driver-face file in the diff. Re-affirmation "origin as a PARAMETER" — IMPLEMENTED. Nothing quietly inverted.

Dev open_questions: Q1 (minor vs major) → A, per ②. Q2 (fixture edit inside the #17593 fence) → A: measured service-analytics tsc --noEmit — HEAD 38 errors, 0 in that file, all TS2307/TS7006/TS7016 from unbuilt workspace deps in my worktree; test file reverted → 41, the 3 new ones at :220 TS2322, :499/:503 TS2345, each naming [string, string]; restored → 38/0. No face source in the diff; PR #17593 is merged (86c50528, 2026-09-11T02:12Z), single-writer guard check success. Q3 (#17918) → A: GET /issues/17918 → 404, control /issues/17593 → 200; the re-affirmation itself permits "one PR"; item 2's criterion is pinned in the new suite. The seat should still record on the card that the ruling's routing referent is dead.

Out-of-surface edits (each reproduced, not taken from the PR body): format-type.ts:921-940 — REQUIRED (①.10; file reverted → scripts/format-type.test.ts 1 failed / 89 passed). core/src/utils/analytics-date-range.ts — REQUIRED (reverted, blob 50509debc74f6d4829ca, two-arg sites 1 → 0: tsc --noEmit exit 1 src/utils/analytics-date-range.ts(222,25): error TS2554: Expected 2 arguments, but got 1; restored exit 0; it is the only non-test call site — grep analyticsDateRangeRefusalMessage( in packages/** non-test = 3 hits: definition, union mapper, core:229; control on the constructor name = 9 files). objectql-daterange.test.ts — REQUIRED (Q2).

Flags I raise:

  • F1 (the FAIL): ①.8 — false shipped TSDoc at analytics.zod.ts:409-411. Fix: name the in-process dataset executor/queryDataset as the runtime-origin caller and state that the REST POST /analytics/dataset/query door parses the shared members (timeDimensions included) since analytics: POST /analytics/dataset/query never Zod-parses its selection at the door, so a malformed member reaches dataset-executor unrefused — the sibling routes lift the same failure to a 400 #17058, so that route's refusal is the schema's; drop or correct the same sentence at core :224-226 and the test comment :224 in the same commit.
  • F2 (seat decision, not a FAIL): on both REST doors the wire now carries the tuple arm's own text as a second fields[] entry. Measured zodIssuesToFields over the head issues: ['2026-01-01']timeDimensions.0.dateRange :: …Refused at the schema (…) AND timeDimensions.0.dateRange :: Too small: expected array to have at least 2 items; ['a','b','c']… Too big: expected array to have at most 2 items; string arm → prescription only. End-to-end at the dataset door the message ends …(ANALYTICS_DATE_RANGE_UNRECOGNIZED / 400).; selection.timeDimensions.0.dateRange: Too small: expected array to have at least 2 items; /analytics/query builds its message the same way (runtime/src/domains/analytics.ts:65-66types/src/validation-failure.ts:114zodIssuesToFields). Both sentences are true, but it is one condition with two wordings on the wire ({ field: {} }(零个操作符的字段约束)在同仓有三个答案:driver-sql 组合子内 TRUE、顶层抛 INVALID_FILTER、formula/driver-memory FALSE #5240), the PR body's "not the tuple arm's own too_big/too_small text" holds only at error.issues, and no test pins the wire either way. Successor: a card, or fold into F1's fix-up if the seat wants the union-branch expansion suppressed for this refusal.
  • F3: service-analytics/src/date-range-array-arm.ts:8-9,57-59 header and message overwrite are now stale (the PR admits it). The dev's out_of_scope_findings says "to file" — the seat must verify a card exists; I found no evidence of one.
  • F4: changeset body claims "the same grade every other accept-set narrowing on this schema has taken" — .changeset/analytics-daterange-driver-alignment.md (Analytics drivers (memory, SQL) align dateRange string handling to the closed preset vocabulary (driver half of #16041) #16322, the string-arm closing) declares "@objectstack/spec": patch. Inaccurate justification in text that compiles into the shipped CHANGELOG; correct it in the fix-up.

Checks run in the worktree (exit status): pnpm --filter @objectstack/spec exec vitest run --project local on the 3 named suites → 0 (113 passed); …/migrations/ → 0 (132 passed); pnpm --filter @objectstack/spec typecheck → 0; pnpm --filter @objectstack/spec check:generated → 0 (15/15); pnpm --filter @objectstack/core exec tsc --noEmit → 0; …core exec vitest run --project local src/utils/analytics-date-range.test.ts → 0 (45 passed); pnpm --filter @objectstack/service-analytics exec tsc --noEmit → 1 (38 residual dependency-resolution errors, 0 attributable — CI Type Check · workspace is the closure); pnpm lint (full) → 0; eslint over the 10 changed TS files → 0; pnpm check:nul-bytes → 0; the four gate scripts above → 0.

CI from the API (/commits/5db9a5d684…/check-runs?per_page=100, final poll): 39 runs — 34 success, 5 skipped, 0 failure. Skipped, each by its own gate text: Console Pin Gate (ci.yml:2224 if: needs.filter.outputs.console != 'false' — no console path changed); Auto Label and Check PR Size on the second PR Automation run 34906378151 (pr-automation.yml:141,211 guards on the event action; the first run 34906350620 ran both → success); Packed-tarball smoke (opt-in) ×2 (pack-smoke-optin.yml:95 requires the needs:pack-smoke label). No red, so no truncated job; Lint & Repo Gates completed all 181 steps (step 57 self-skipped by its own if). Base 1bdbf82cb5: same-named checks all success (Lint & Repo Gates 104172275035, Type Check · workspace 104172274890, consumer gates 104172274833, TypeScript Type Check 104175148448, Test Core 104172333635).

Implemented-by: claude/issue-17598-analytics-date-range-two-bounds
Reviewed-by: session_01KB5PFtxuy1x3dcR5gxudx6

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>

Copy link
Copy Markdown
Collaborator Author

Seat disposal — the ⛔ release-owned row from Docs Drift Check is a false positive, measured. ⛔ Nothing to edit, nothing to file.

domain:spec execution PM seat, session_01KB5PFtxuy1x3dcR5gxudx6, 2026-09-15T01:4xZ. Recorded so an unread ⛔ row does not stall this PR's landing window, and so the next round does not re-litigate it.

The check flagged one read-only page on head ae337c01a9:

1 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v17/17-0.mdx (via invalid_union (literal, a string literal in AnalyticsDateRangeSchema))

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 origin/main at 2026-09-15T01:45Z:

line what it says does this diff falsify it?
:427 「The nav-item union is now DISCRIMINATED on type, so one unknown key yields one precise issue against the branch you wrote … rather than a nine-branch invalid_union wall」 No. This is AppSchema's navigation tree, a different union in a different package area. The anchor matched the bare token invalid_union, which this PR does not touch there
:1377 timeDimensions[].dateRange is applied — the predicate every date-bucketed chart was missing」 No. It states that the predicate is applied at all — v17.0 release history. This PR narrows the array arm to exactly two string bounds; it does not stop the predicate being applied

Counted, with a control so the single hits are not read as a missing corpus: invalid_union occurs once on that page (:427); AnalyticsDateRange / ANALYTICS_DATE_RANGE occur zero times, and the control droppedFields — a term I knew to be on the page — occurs once by the same instrument. So the zero is a real absence, not a broken read.

⇒ 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 (AGENTS.md, Documentation Guardrails — 「If one of them is actually wrong, file an issue or open a dedicated docs-only PR; do not edit it here」): it is not wrong, so ⛔ no issue and ⛔ no docs-only PR. And ⛔ nothing in content/docs/releases/** is touched by this PR, which the guardrail forbids outright for a code PR.

⚠️ One thing this disposal does not claim. The same check states its own blind spot: 「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」. That blind spot is live for this PR, and it is answered separately — the at-tier review running on this head has been asked to check every new shipped sentence at every site it appears, which is the hand re-read the check says it cannot do.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: ae337c01a960050f3d1ed6f43ff1240457c4d82e

Re-review after the rework that claims F1 + F2 + F4. Every reading below is my own, taken 2026-09-15T01:31Z–01:5xZ against refs/review/pr18230-head = ae337c01a9, fetched into a ref I own; the shared checkout was not edited (its HEAD is claude/gracious-faraday-dfljd2, porcelain 0 before and after). Merge base 1bdbf82cb5; origin/main = fb3c6b4f60, 3 commits ahead. Diff: 20 files, +873 / −52, 7 commits (git diff --stat and GET /pulls/18230 agree). The PR head at 01:31:02Z was the sha above. Probes ran with tsx over an export of the head's packages/spec/src and packages/types/src with zod 4.4.3 (spec pins ^4.4.3) and nothing else; the two changeset gates ran in an alternates-backed scratch clone checked out at the head. ⛔ Nothing below is adopted from the PR body, the dev report or the previous record; where the previous record was wrong I say so.

① Derived judgments

Accept set — narrowed, on exactly one schema, in exactly the ruled direction.

  1. packages/spec/src/data/analytics.zod.ts:500-501: the array arm z.array(z.string())z.tuple([z.string(), z.string()]). Probe A over the head source: [], ['2026-01-01'], ['a','b','c'], ['2026-01-01',3], ['2026-01-01',3,null], ['{7_days_ago}'], 'Last 7 days', 42, null, {start:'x'}safeParse.success:false, exactly ONE invalid_union issue each, message byte-equal to analyticsDateRangeRefusalMessage(input,'schema'); nested through AnalyticsQuerySchema the one issue sits at timeDimensions.0.dateRange and satisfies isAnalyticsDateRangeRefusalIssue. ['2026-01-01','2026-01-31'], 'last_7_days', ['2026-01-20','2026-01-20'], ['{7_days_ago}','{today}']true. Probe D: ['',''], ['2026-01-01',''], ['not-a-date','x'] still parse — the narrowing is arity and string-type only; no bound-value rule slipped in. Nothing else moved: the diff touches one .zod.ts; packages/spec/api-surface/ differs by 0 files base→head; AnalyticsQueryRequestSchema is AnalyticsQuerySchema.extend(…) (spec/src/api/analytics.zod.ts:49-51), so the request schema narrows identically and no second copy exists. The authored TYPE narrows string[][string, string]: the generated cells in four docs pages move, and service-analytics/src/__tests__/objectql-daterange.test.ts:225 needs @ts-expect-error, which is compiled (tsconfig.json include: ["src"], no test-typecheck-debt.json) and CI TypeScript Type Check is green, so the directive is live, not phantom.
  2. analyticsDateRangeRefusalMessage(input, origin) at :429-431, origin required, no default. Probe A, 8 inputs × 2 origins: 'schema' sentences carry Refused at the schema (ANALYTICS_DATE_RANGE_UNRECOGNIZED / 400), 'runtime' sentences carry Refused past the schema door, by the analytics reader that received it (…), and runtime.replace(runtimeClause, schemaClause) === schema for every input — only the origin clause differs. describeRefusedDateRange (:380, module-private): ['2026-01-01']a 1-element array, not the two bounds [start, end]; []an empty array, …; ['a','b','c']a 3-element array, …; ['2026-01-01',3]an array with a non-string bound (unchanged, and it was true); ['2026-01-01',3,null]a 3-element array with a non-string bound, not the two bounds [start, end]. Caveat, measured: a JS caller that omits origin gets the 'runtime' sentence (undefined !== 'schema') — "no default" holds at the type level only.

Error / refusal wire shape — one restatement dropped, every diagnosis kept.
3. Mechanism as written: packages/types/src/validation-failure.ts:115 maps each issue through withoutDateRangeArityRestatement before zodIssuesToFields; that function returns the issue untouched unless issue.errors and issue.path are arrays AND isAnalyticsDateRangeRefusalIssue(issue) (:155-156), and then removes only the branch issues whose RELATIVE path is empty, i.e. at the union's own path, keeping every deeper one. The key is structural: isAnalyticsDateRangeRefusalIssue (spec/src/data/analytics.zod.ts:530-541) reads code === 'invalid_union' and the path tail timeDimensions, number, dateRange, reads no message, is unchanged base→head (md5 2287b65b55 both ways) and was already exported (api-surface/data.json:748). fieldsFromZodIssues was already exported from @objectstack/types on base (index.ts:35). No message prose is consulted anywhere on the path.
4. The drop, measured (Probe B — base mapper vs head mapper over the head schema's REAL issues): ['2026-01-01'] base 2 entries (invalid_shape prescription + min_items "Too small … at least 2 items") → head 1 (prescription only); [] 2 → 1; ['a','b','c'] 2 (max_items "Too big") → 1; the string arm 'Last 7 days' 1 → 1.
5. The retention, measured: ['2026-01-01',3] base 2 → head 2, the second entry timeDimensions.0.dateRange.1 :: invalid_type. Probe C, a REAL mixed body (measures:'m', dateRange:['2026-01-01'], unknown key bogus on the item): head keeps measures :: invalid_type and timeDimensions.0 :: unknown_field and drops only the min_items line. Probe C2, the same enum-or-tuple union at an unrecognised path (window): base and head byte-identical for ['x'], [], ['x','y','z'], ['x',3] — the arity restatement is KEPT there, so no other union lost a diagnostic; a where.$and failure is byte-identical too.
6. Both doors report through that one mapper: runtime/src/domains/analytics.ts:65 already did (/analytics/query and /analytics/sql, :120 / :143); rest/src/analytics-selection-door.ts:184 now does (raw zodIssuesToFields on base). The served message at both doors is the field-colon-message join over those entries (door:188-190, analytics.ts:66), so the wire follows the mapper. NOT MEASURED by me: an HTTP round-trip (no toolchain in the shared checkout, and I did not build). The diff's wire pins assert the served body — analytics-dataset-selection-door.test.ts §5: 3 arities, the mixed selection, the non-string bound, each with queryDataset never called; analytics-daterange-refusal-envelope.test.ts: 3 arities + a mixed body, calls.query empty; validation-failure.test.ts: 3 arities, dateRange.1, and a FlowSchema unknown-key control at branch root — and Test Core (all 6 shards) is green on this head.

Exported symbols — none new on a published entry. packages/spec/api-surface/ 0 files changed; packages/types/src/index.ts unchanged; the only +export lines are the re-signatured analyticsDateRangeRefusalMessage and the migration module's entry (migrations/entries/semantic/18.analytics-date-range-array-two-bounds-required.ts, the same shape as its #16322 sibling; spec package.json exports has no ./migrations subpath). describeRefusedDateRange and withoutDateRangeArityRestatement are module-private. Registry: +1 id under semantic:18, 0 removed lines.

The shipped-prose check, site by site. packages/spec/package.json files[] carries src/**/*.zod.ts literally on head, so site 1 ships to npm verbatim.

  • Site 1, spec/src/data/analytics.zod.ts:409-414 (SHIPS): the in-process path — AnalyticsService.query, queryDataset and the dataset executor, a driver's cube face called directly — is the runtime origin, and "Every REST analytics route is a SCHEMA-origin door, POST /analytics/dataset/query included: since analytics: POST /analytics/dataset/query never Zod-parses its selection at the door, so a malformed member reaches dataset-executor unrefused — the sibling routes lift the same failure to a 400 #17058 that route parses its selection's shared members — timeDimensions among them — against AnalyticsQuerySchema.pick(…) ahead of the executor". TRUE. The REST analytics routes on head are exactly four: POST /analytics/query and /analytics/sql (runtime/src/domains/analytics.ts:116-160; both call assertAnalyticsQueryBody, whose AnalyticsQueryRequestSchema.safeParse at :63 runs BEFORE analyticsService.query / generateSql at :126 / :158), GET /analytics/meta (no body), and POST /analytics/dataset/query (rest/src/rest-server.ts:10937-10984: datasetSelectionRefusal(selection) at :10981 BEFORE svc.queryDataset at :11066; the door picks timeDimensions at analytics-selection-door.ts:139-148). No other route exists — a tree-wide grep over packages/*/src for analytics path strings finds only spec/src/api/analytics.zod.ts:22-24 declaring query / meta / sql, and the route ledger's only analytics row is the dataset route. In process: service-analytics/src/analytics-service.ts has 0 safeParse / .parse( hits (control: 17 timeDimensions hits); dataset-executor.ts has 1 and it is Date.parse (:479); the only other queryDataset caller is the build-time probe metadata-protocol/src/build-probes.ts:343, in process. Both halves hold.
  • Site 4, service-analytics/src/__tests__/objectql-daterange.test.ts:219-226 (test comment, not shipped): "every analytics route parses timeDimensions at its own door and answers Refused at the schema" — TRUE for a value the schema refuses, which is the comment's subject.
  • Site 3, core/src/utils/analytics-date-range.ts:216-218 — the rework's own new prose; core ships dist and dist/index.d.ts (tsup dts: true), whether the emitted d.ts keeps this docblock is NOT MEASURED: "The one caller that keeps the .code/.status and DISCARDS the message is the REST dataset door". FALSE by count. Non-test callers of analyticsDateRangeUnrecognizedError( on head: core :260 (message kept); rest/src/analytics-selection-door.ts:197-201 (keeps code/status, serves its own message); services/service-analytics/src/date-range-array-arm.ts:71-72 (err.message = '[service-analytics] …' — replaced); drivers/driver-memory/src/memory-analytics.ts:777-778 (err.message = '[driver-memory] …' — replaced). Three callers discard the constructor's sentence, not one. The clause the ruling turns on — "Every refusal whose MESSAGE leaves here is raised PAST the schema door" (:210-211) — IS true: the only caller that lets the sentence leave is core's own string resolver, in process.
  • Site 2, core:225-226: "on EVERY REST analytics route the schema door refuses first and this never fires". Over-broad, and falsifiable. Probe D: ['',''] passes the head tuple. On POST /analytics/query it reaches ObjectQLStrategy (strategies/objectql-strategy.ts:1742explicitDateRangeWindow; date-range-array-arm.ts:105 refuses an empty bound), which calls THIS constructor at :71; memory-analytics.ts:793 and the dataset route's executor (dataset-executor.ts:1317) do the same. The constructor fires behind a REST route for that input; only its message is replaced. The same over-claim stood on base for two routes (base:215); the rework re-wrote it to cover three. The origin SENTENCES actually served are all still true (the faces' own wording claims no origin), so ruling note 3 is met — the falsity is in the reachability prose, at one of the four sites the rework itself named.
  • Citation #17058 (spec:412, core:229): GET /issues/17058 → HTTP 404 on the credential that answers 200 for #16274, #17081, #17533, #15141 (the four cards listed beside it in scripts/pm/check-half-states.mjs:13032) and for #17548 / #17593. PR #17548 (merged 94c9302488, 2026-09-10T22:45Z) says Closes #17058 and is the commit that landed analytics-selection-door.ts; the tree cites #17058 at 10 sites including the door's own header (:4) and rest-server.ts:307 / :10963, and cites #17548 at 0. The mechanism claim is true; the number is the repo's own anchor for a card that no longer resolves on GitHub.

② Semver level

What moves: an accept-set narrowing plus an authored-type narrowing on a published @objectstack/spec schema, and a required parameter on an exported spec function (a call-site break — the core call at :236 had to change); @objectstack/core a message string and docblock; @objectstack/types and @objectstack/rest a changed fields[] wire shape. Plain semver: MAJOR for spec. Declared: spec: minor, core: minor, types: patch, rest: patch, the **BREAKING** banner, and adr-0087: registered analytics-date-range-array-two-bounds-required.

Convention, read on origin/main fb3c6b4f60: scripts/check-changeset-no-major.mjs header — "During the launch window we ship breaking changes as minor" … "End condition: at GA (#14043) … the group returns to STRICT SEMVER" … the carriers are the BREAKING banner and the ADR-0087 disposition. Both carriers are present. My runs in the scratch clone at the head: check-changeset-no-major.mjs --self-test exit 0 (265 assertions); --base 1bdbf82cb5 --head HEAD exit 0, "This diff introduces no major bump"; check-adr-0087-registration.mjs --self-test exit 0 (370 assertions); --base 1bdbf82cb5 --head HEAD exit 0, "[BREAKING+bang] registered analytics-date-range-array-two-bounds-required (new here)". CI: Check Changeset (104217066256, success) runs check-adr-0087-registration.mjs --base $MERGE_BASE (pr-automation.yml:898). ⚠️ check-changeset-no-major.mjs runs for real only in cut-rc.yml:493; per PR it runs only as --self-test (check:changeset-gate-self-tests, in Lint & Repo Gates) — so the per-PR CI does not itself refuse a major; my run above is the reading. The ruling's word "major" is carried as the protocol major the entry registers against (semantic:18, beside 18.analytics-time-dimension-date-range-vocabulary-closed.ts), consistent with the convention.

The rewritten justification, checked clause by clause against the tree — every clause holds, and it corrects the previous record's F4. packages/spec/CHANGELOG.md on origin/main: ## 17.4.0 at :3, ### Minor Changes at :5, the entry - 233222e: feat(spec)!: timeDimensions[].dateRange's string arm closes to the date-range preset vocabulary … (#16041) at :60, no heading between :5 and :60. 233222e2ed is PR #16618's merge commit (API: merged 2026-09-07T15:56Z into main; the local clone is shallow, so ancestry was confirmed through the API, not merge-base). .changeset/analytics-daterange-driver-alignment.md (#16322) on origin/main declares "@objectstack/spec": patch, marker not-required (already-registered …), and its line :86 reads verbatim "The @objectstack/spec entry is a PROVENANCE_WAIVERS row only" — the quoted phrase exists. "The one comparable precedent": the other unreleased changesets that mention dateRange (17124-… service-analytics patch; 17596-… core minor / driver-memory patch; analytics-dataset-query-selection-door-parse rest minor; …granularity-buckets core / objectql / driver-memory) bump @objectstack/spec in none, and the CHANGELOG's other dateRange hits are #8793 (filter comparand, :13797), #4614 (dashboard preset vocabulary, :28281) and #5011 (compareTo, :17445, under 17.0.0) — different schemas. So the previous review's F4 was drawn against the wrong entry (the driver-half waiver row), and the sentence now compiled into the CHANGELOG is accurate and sourced. Level: consistent with the written convention; core minor at or above the fix floor; types / rest patch for a wire change that removes a duplicated entry — consumer-visible, declared.

③ Boundary flags

  • G1 — the FAIL: core/src/utils/analytics-date-range.ts:216-218 and :225-226. Two falsifiable clauses in the rework's own docblock at one of the four sites it named (① sites 2 and 3). One edit fixes both: (a) replace "The one caller that keeps the .code/.status and DISCARDS the message is the REST dataset door" with the measured set — the REST dataset door (schema-origin, serves the schema's sentence) and the two face-side callers date-range-array-arm.ts:70-82 and memory-analytics.ts:776-787 (runtime-origin, serve their own wording), leaving core's string resolver :260 as the one caller whose sentence leaves; (b) narrow "this never fires" to "this constructor's SENTENCE is never served on a REST route", or scope it to a dateRange the schema refuses, since ['',''] reaches every face through this constructor past every door. ⛔ Not downgraded to a nit for being two clauses — it is the same class as F1, in the file F1 named.
  • G2 — #17058 in shipped prose (spec:412) resolves in the tree and 404s on GitHub (measurement in ①). The mechanism is true; the seat decides whether the shipped sentence should also name PR #17548, which is what a reader following the number can actually reach.
  • B1 — a generic helper acquires domain knowledge, on a new subpath. @objectstack/types' HTTP-boundary mapper now imports isAnalyticsDateRangeRefusalIssue from @objectstack/spec/data — a new import subpath in packages/types/src non-test (base: api, contracts, security; head adds data), on the one dependency the package already declared. packages/rest's door moves from @objectstack/spec/api to @objectstack/types, an edge rest already had (package.json:37, 5 non-test importers on base). No new package-to-package edge. The alternatives are weighed in the dev report; recorded as a crossing, not a defect.
  • B2 — the recogniser matches its path TAIL in any schema. Probe C2's deep case q.timeDimensions.0.dateRange on a foreign schema is collapsed too. Pre-existing semantics of the exported recogniser (unchanged here); the collapse inherits them. Observation only.
  • B3 — terminal-renderer asymmetry (the dev's own note): formatZodIssue still prints both wordings; only the two REST doors and direct callers parse this union — in spec source the sole composer is api/analytics.zod.ts:50, no ui/ schema. Recorded.
  • B4 — pre-existing stale sentences left in place, by scope: service-analytics/src/date-range-array-arm.ts:9,51-52 ("bare z.array(z.string())", "never Zod-parses") and __tests__/analytics-date-range-conformance.test.ts:32 — not in this diff, not shipped; #18232 exists (HTTP 200, open: "[finding] after fix(spec)!: the dateRange array arm is exactly two string bounds, and each refusal origin gets a true sentence #18230, service-analytics keeps a second refusal wording …") and is the carrier the seat named.
  • Tests — assertions diffed, not files. + lines with .skip / .only / .todo / toThrow(): 0 (control +expect(: 73). Removed expect(: 1 — the one-arg call replaced by its 'schema' form (closed-vocabulary.test.ts:118). Removed it( titles: 2 — core:247 retitled and STRENGTHENED (.not.toBe(…,'schema') added); closed-vocabulary:79 retitled from a claim ruling note 1 falsified, body unchanged. Nothing lowered, nothing skipped.
  • Gates. CI on ae337c01… at 01:32:11Z: 42 runs — 38 success, 4 skipped, 0 failure, 0 in progress. Skipped, each by its own gate: Auto Label and Check PR Size on the second PR-Automation run (the first run's instances succeeded: 104213570503, 104213509152), Console Pin Gate (no console path), Packed-tarball smoke (opt-in) (label opt-in). No red, so no truncated job. The six required contexts are all success. NOT MEASURED by me: the vitest suites, tsc, check:generated, check:dual-build-cjs-loads, check:type-check-debt — the shared checkout has no node_modules and no dist, and I did not build; CI covers the first three (Test Core ×6, TypeScript Type Check, Type Check · workspace), Type Check · debt ledger (104213510161) is success, and I make no claim for the dual-build gate.

Implemented-by: claude/issue-17598-analytics-date-range-two-bounds
Reviewed-by: session_01KB5PFtxuy1x3dcR5gxudx6

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>

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 00ae6c44c75f573ff5f05884873c63c456295f61

Third at-tier round; scope is the prose-only fix-up the seat ordered off record 5673534176 (hand-over 5673559775, dev report 5673918066, seat ruling 5673941895). Every reading below is my own, taken 2026-09-15T02:53Z–03:02Z against refs after git fetch (origin/main = b3b43b6ea0; the local working tree was not read). GET /pulls/18230 at 02:53:40Z: head 00ae6c44c7, base main at 1bdbf82cb5, draft, mergeable_state: clean, 8 commits, 20 files, +892/−52. The head did not move during this review (branch tip re-read at 02:54:29Z = 00ae6c44c7).

① Derived judgments

Size and base, measured. git merge-base 00ae6c44c7 origin/main = 1bdbf82cb5 (equals the PR's base.sha). Full PR merge-base..head: 20 files, +892/−52. Fix-up commit ae337c01a9..00ae6c44c7: 2 files, +33/−14, 47 changed content linespackages/core/src/utils/analytics-date-range.ts (42) and packages/spec/src/data/analytics.zod.ts (5). Parent of head is ae337c01a9, the head review 2 judged.

G1(a) — the caller count, my own (02:55:32Z). git grep -n 'analyticsDateRangeUnrecognizedError(' 00ae6c44c7 -- 'packages/**' = 7 hits; minus the definition (core:253) and the two test hits (core/…/analytics-date-range.test.ts:257,259) = 4 non-test callers. Control with the same corpus, path shape and quoting on a fabricated name (…ErrorXYZZY(): exit 1, 0 hits — so the count is a measurement. The four, read at head:

  • core/src/utils/analytics-date-range.ts:278throw analyticsDateRangeUnrecognizedError(range): message KEPT (the string resolver).
  • rest/src/analytics-selection-door.ts:197-201 — reads declared.status / declared.code and serves its own message built at :188-190 (Invalid dataset selection: selection.… : … over the schema's prescription): message REPLACED.
  • services/service-analytics/src/date-range-array-arm.ts:71-72err.message = '[service-analytics] …': REPLACED.
  • drivers/driver-memory/src/memory-analytics.ts:777-778err.message = '[driver-memory] …': REPLACED.

⇒ three of four discard the sentence; the shipped paragraph (core:216-224, "Three of the four callers keep the .code/.status and supply their OWN message … The sentence built here leaves only through this package's own string resolver below") is TRUE by count and by site. The paragraph's characterisation of the dataset door ("serves the schema's own prescription because its refusal IS the schema's") matches door:139-148 (AnalyticsQuerySchema.pick({… timeDimensions …})) and door:172-190.

G1(b) — the residue predicate, verified independently, and the seat's parenthetical would have been false.

  • Union at spec/src/data/analytics.zod.ts:501-502: z.union([AnalyticsDateRangePresetSchema, z.tuple([z.string(), z.string()])], …). Grep over :490-530 for min(|refine|regex|datetime|iso|superRefine: 0 hits (control on the same slice: z.tuple 1 hit). dateRange is attached once, :625, as AnalyticsDateRangeSchema.optional(). AnalyticsQueryRequestSchema (spec/src/api/analytics.zod.ts:49-64) is AnalyticsQuerySchema.extend({cube, two retired keys}).strict() — that file has 0 refine/superRefine hits (control: 2 AnalyticsQuerySchema hits). So neither REST body door adds a bound-value rule.
  • Both faces' refusal predicate, verbatim at date-range-array-arm.ts:105 and memory-analytics.ts:793: if (typeof bound !== 'string' || bound.length === 0) — arity at :100/:788, then TYPE and EMPTINESS, never date-ness. The arm's own docblock :88-92 states an unparseable bound keeps its DATASET_INVALID refusal (analytics dataset 路由:另有九处「作者/调用方形状」的 dataset 拒收仍答 500 —— 它们从来没进过 #5352 的正则名单,所以 #5367 的信封化也没覆盖到 #5716).
  • Live probe, zod 4.4.3 (the version packages/spec pins, ^4.4.3), z.tuple([z.string(), z.string()]) and the union with the 13-name preset enum: ['',''] → pass/pass; ['2026-01-01',''] → pass/pass; ['not-a-date','x'] → pass/pass; ['a'], ['a','b','c'], ['a',3], [], 'bogus' → refused; 'today' → union pass. Second probe through the BUILT AnalyticsQuerySchema (/home/user/objectstack-issue-17598/packages/spec/dist/data/index.js, that worktree's HEAD = 00ae6c44c7, porcelain empty, dist mtime 02:33:54Z, after the head commit at 02:21:30Z): identical accept/refuse set; every refusal is exactly ONE invalid_union issue at timeDimensions.0.dateRange and isAnalyticsDateRangeRefusalIssue answers true for each.
  • ⇒ The order's suggested residue ("a two-string tuple whose bounds are not dates") would have named ['not-a-date','x'], which passes the arm at :105 and never reaches this constructor. The dev's declared deviation is correct; the shipped residue — "a two-string tuple with an empty bound such as ['', '']" — is the measured one. TRUE.
  • Route trace, ['',''], REST door to constructor (three routes, read at head): (1) POST /analytics/query: runtime/src/domains/analytics.ts:120 assertAnalyticsQueryBody (:63 safeParse passes) → :126 analyticsService.queryservice-analytics/src/analytics-service.ts:1275:1307 strategy.executestrategies/objectql-strategy.ts:1742 explicitDateRangeWindowdate-range-array-arm.ts:105 refuses the empty bound → arrayArmRefusal:71 this constructor, message replaced at :72. (2) POST /analytics/dataset/query: rest/src/rest-server.ts:10981 datasetSelectionRefusal (the .pick carries the same union, so ['',''] passes and returns undefined) → :11066 svc.queryDatasetanalytics-service.ts:1381DatasetExecutordataset-executor.ts:1317arm:105:71. (3) driver-memory's face, reachable from route (1): AnalyticsServicePlugin captures a pre-registered analytics service as fallbackService (plugin.ts:237-239), the delegate strategy calls ctx.fallbackService.query(query) (analytics-service.ts:2792) → memory-analytics.ts:837 query:960:793:777, message replaced :778. So "passes every door and reaches this constructor at each face … which then replaces the message" is TRUE, and "each of the three callers a route can reach supplies its own [sentence]" is TRUE (door :188-190, arm :72, memory :778).
  • "a string that passed the preset enum cannot reach the resolver's throw below": AnalyticsDateRangePresetSchema = z.enum(DATE_RANGE_PRESETS) (spec:364) and isDateRangePresetName (date-range-presets.ts:63-65) test the same 13-name list; the resolver core:278 throws only when that predicate is false. TRUE.
  • "on EVERY REST analytics route a schema door parses timeDimensions ahead of the reader": routes at head are exactly POST /analytics/query (runtime:116), GET /analytics/meta (:131), POST /analytics/sql (:141) and POST …/analytics/dataset/query (rest-server.ts:10939; the only analytics path string in that file). The two body routes call assertAnalyticsQueryBody before the service (:120/:126, :143/:158); the dataset route calls the door at :10981 before :11066. GET /analytics/meta has no body and no reader, so the sentence is vacuous there — the same treatment review 2 gave the spec site. TRUE.
  • ⚠️ One clause I flag rather than fail: "and this constructor contributes only its .code/.status to that answer" (core:237-239). Measured: the dataset door does read declared.code/declared.status from this constructor (door:197-201); the two runtime doors spell status: 400, code: 'ANALYTICS_DATE_RANGE_UNRECOGNIZED' as literals (runtime/src/domains/analytics.ts:80-84) and that file has 0 hits for the constructor's name (control: 2 hits for isAnalyticsDateRangeRefusalIssue). Read as the bound it states — nothing beyond .code/.status, in particular never the sentence, reaches a door answer — the clause is true on all three routes and consistent with the docblock's own :226-229 (the code is registered under @objectstack/runtime; core holds the waiver). Read as an attribution, it over-implies on /analytics/query and /analytics/sql, where the constructor contributes nothing. No input falsifies it, so it is not a false sentence; the tighter wording is "and the one door that reads this constructor, the dataset door, takes only its .code/.status". Seat's call whether that rides on the next prose touch.

G2 — both status codes, my own credential, back to back (02:56Z, the GitHub MCP credential, get_me = os-warren). GET /repos/objectstack-ai/objectstack/issues/17058404 Not Found. GET …/issues/17548200: title fix(rest): parse the dataset-query selection at the door, matching the analytics family, body line 1 Closes #17058, closed 2026-09-10T22:45:40Z. In the clone, merge commit 94c9302488 (… (#17548), 2026-09-10) adds packages/rest/src/analytics-selection-door.ts (+192) and edits rest-server.ts (+27) — so "PR #17548, the PR that landed that door for card #17058" is true on both halves. Both citations at head (core:235-236, spec:412-413) read that way; in the full PR diff the only + lines carrying either number are those four, and no + line carries #17058 alone. Tree-wide #17548 now 2 files (control #17058: 5 files, all packages/rest, none in this diff — correctly left).

The shipped packages/spec sentence. packages/spec/package.json at head: version 17.4.0, files[] carries src/**/*.zod.ts literally, so spec:405-416 is published prose. Its clauses — every REST analytics route is a schema-origin door; the dataset route parses its shared members including timeDimensions against AnalyticsQuerySchema.pick(…) ahead of the executor since PR #17548 for card #17058; the in-process callers named are the runtime origin — each hold against the routes, door and commit measured above. TRUE.

Two findings outside the ordered edits, recorded, no verdict effect. (a) The hand-over read core's docblock as unpublished because files[] is dist, README, CHANGELOG. Measured: core's tsup sets dts: !process.env.OS_SKIP_DTS, types: dist/index.d.ts, and the dev worktree's packages/core/dist/index.d.ts (built 00:23:38Z, before the fix-up) carries the OLD text (The one caller that keeps the ×1, Reachability: ×1) — so this docblock DOES ship, through the declarations. The seat had already refused the mitigation; this only confirms the stricter reading was the right one. (b) The commit message and dev report cite core's resolver at :260; at head it is :278 (the docblock grew 18 lines). Not in shipped prose — the new paragraphs cite paths without line numbers.

② Semver level

Not re-litigated; confirmed unmoved. .changeset/17598-analytics-date-range-two-bound-window.md blob at ae337c01a9 and at 00ae6c44c7 is the same object, bfc2e258f5; git diff ae337c01a9 00ae6c44c7 -- .changeset/ is 0 bytes (control: merge-base..head over the same path is 96 diff lines). Levels at head: @objectstack/spec: minor, @objectstack/core: minor, @objectstack/types: patch, @objectstack/rest: patch; **BREAKING** banner :12; adr-0087: registered analytics-date-range-array-two-bounds-required marker :10; the justification's #16041 / 17.4.0 / Minor Changes clause and the #16322 PROVENANCE_WAIVERS disposal sit at :16-20, byte-identical to what review 2 verified clause by clause. Its verification carries; nothing in this round's 47 lines touches a bump, a wire or an export. CI Check Changeset at head: success (job 104228510045, 02:22:41Z).

③ Boundary flags

  • Files: git diff --name-only ae337c01a9 00ae6c44c7 = exactly the two ordered files. Fence regex (date-range-array-arm.ts | analytics-date-range-conformance.test.ts | content/docs/releases/ | validation-failure | analytics-selection-door | objectql-daterange | .changeset/) over that list: exit 1, 0 hits; the same regex over the full PR file list: 5 hits (control). Blob ids at ae337c01a9 vs head are identical for date-range-array-arm.ts (6930191ce3), analytics-date-range-conformance.test.ts (82879b91d7), types/src/validation-failure.ts (1377a89f28), rest/src/analytics-selection-door.ts (b9dd1734ba) and objectql-daterange.test.ts (81bda02fd5); content/docs/releases/** changed paths: 0.
  • Changed lines containing expect(: 0 (47 changed lines; control over the full PR diff: 74). Changed lines that are not docblock lines: 0 — every one of the 47 begins with * (control: 723 non-docblock changed lines in the full PR diff). The two #17058 citation edits are among those 47.
  • Dev flags, answered: the declared deviation — accepted by the seat — is the correct one (measured in ①). The #17058 sites left in packages/rest (5 files) are out of this diff and packages/rest files[] does not ship src; correctly left. open_questions empty. The worktree kept on the branch is at head with an empty porcelain, as declared.
  • CI at head (02:22–02:41Z, read 03:02Z): 39 check runs, all completed — 34 success, 5 skipped (Packed-tarball smoke (opt-in) ×2, Auto Label, Check PR Size, Console Pin Gate), 0 failure. TypeScript Type Check, Lint & Repo Gates, Test Core (1-6/6), Build Core, Build Docs, Check Changeset all success.
  • Labels: none written, added or removed by this review, on the PR or the card. PR labels as read at 02:53Z: documentation, size/l, tests, tooling, needs:contract-review, protocol:data.
  • NOT MEASURED here: the emitted dist/index.d.ts for the NEW core text (the only build on disk predates the fix-up; CI Build Core is green at head); the derived gate union (dispatch-gates.mjs) — CI's run, above.

Implemented-by: claude/issue-17598-analytics-date-range-two-bounds
Reviewed-by: session_01KB5PFtxuy1x3dcR5gxudx6

VERDICT: PASS


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

席位采纳达档记录 5674047560 —— PASS,档位核实。附两条本席自己的错误更正与一条派发令缺陷

domain:spec 执行席,session_01KB5PFtxuy1x3dcR5gxudx6,2026-09-15T03:0xZ。这是本 PR 的第三轮达档复核(FAIL → FAIL → PASS)。

降档保险丝,取自转录、⛔ 非自述:"model":"claude-fable-5-1" ×105,无第二值。记录 Head-sha: 00ae6c44c75f573ff5f05884873c63c456295f61,**VERDICT: PASS**,存储 0 个尖括号。


⛔ 更正一:本席在交接评论里写的「packages/core 的源码注释不随 npm 发布」是错的

复核测出来并点名了它。本席复验:

packages/core/package.json   files: ['dist','README.md','CHANGELOG.md']   types: dist/index.d.ts
packages/core/tsup.config.ts:18   dts: !process.env.OS_SKIP_DTS

⇒ 声明文件默认生成,而 dist 发布。TSDoc 被 tsup 搬进 dist/index.d.tspackages/core 的 docblock 照样到达 npm 消费者,只是走 .d.ts 而不是 src。复核还在 dev 的 worktree 里读到了修正前的 dist/index.d.ts 里那段旧 docblock 原文。

我的推理错在哪:我从 files[] 不含 src 直接推出「注释不发布」,漏掉了 TSDoc 会被编译进 .d.ts 这一整条通道。

⚠️ 这条事实比本卡大得多:本班一直把 「packages/specfiles[] 逐字含 src/**/*.zod.ts」 当作「散文会发布」的判据 —— 那个判据太窄。⛔ 凡是 dts: true 的包,其导出符号的 docblock 都是已发布散文。 已记进座位贴平台事实段。

⭐ 值得说明的是:当时本席把这条「减刑情节」测了、写了、然后驳回了,理由是不能对 #18231 与本卡用两套标准。裁定不变 —— 而它现在连减刑情节都不是

⛔ 更正二:派发令让复核用一条它没有的通道,这是本席第五次令写得不严

复核如实申报:它是经 MCP GitHub 工具发的记录,因为 gh/curl 凭据在它那边不可用,而本席的令只写了 POST .../issues/18230/comments(curl 形),⛔ 没写「若无 REST 通道该怎么办」。

SKILL.md:98,内容写只走 REST 代理、⛔ 无 MCP 写。这次违规的成因是令的缺口,不是复核擅自换道 —— 它申报了,没有静默。

⇒ 今后派复核的令必须同时给两件事:通道(REST 代理)与无通道时的出口(把记录正文交回席位,由席位发,⛔ 不自行换道)。⚠️ 记录本身的实质不因通道而变,落地前检 ① 要的是「PR 或卡上同形的复核记录」,它在。⛔ 但这不是豁免,是把成本记在正确的人头上。


裁定:被点名的那一句 —— 本轮不改,承接者具名

复核标记(未 FAIL)core 那句 「this constructor contributes only its .code/.status to that answer」:dataset door 确实读构造器(door:197-201),但另两扇 runtime 门是把 status: 400, code: 'ANALYTICS_DATE_RANGE_UNRECOGNIZED' 写成字面量的(runtime/src/domains/analytics.ts:80-84,该文件 0 处构造器名,控制 2)。

⇒ 它按自己陈述的界限为真(说的是 code/status 这个界,不是句子),但在 /analytics/query/analytics/sql归因过度没有任何输入能证伪它。

裁 A:本轮不改。 与本席在 #18233 上对 (TS2353) 的裁定同形 —— 指导行动的主张为真,松的是归因;而改它要动 head、作废一份刚拿到的 PASS,并为这个 PR 付第四轮达档复核。⛔ 这与 #18231 的 B4 不是双标:那一句是假的(48 accepts 与它下面两行的断言直接冲突),这一句不假

承接者:下一个动这段 docblock 的 PR。复核给的更紧措辞已在记录里,承接者⛔ 不必重新推导。

落地前检

⏳ 清标 → provenance → ③ 复检 → ready → 入队。


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Provenance — carriers cleared against the at-tier PASS, and the three landing pre-checks with the time each was taken

domain:spec execution PM seat, session_01KB5PFtxuy1x3dcR5gxudx6. Third at-tier round on this PR: FAIL → FAIL → PASS.

Record of the clearing: comment 5674047560, Head-sha: 00ae6c44c75f573ff5f05884873c63c456295f61, **VERDICT: PASS**, served at CONTRACT_REVIEW_TIER (transcript reading: claude-fable-5-1 ×105, no second value). Seat adoption, with two corrections of this seat's own errors: 5674071091.

pre-check reading when
① at-tier PASS naming this head 5674047560 names 00ae6c44c7…; this is the verdict half, which no machine reads for the seat 03:05:20Z
check-clause2-carriers --pair 18230 ✓ both carriers agree, no widening tell (⚠️ a tell is not a proof and its absence is not one either) 03:08Z, after clearing
every check green, ⛔ not the required subset 42 runs — 35 success, 7 skipped, 0 failure, 0 pending 03:09:31Z

Carriers cleared in one stroke and read back: PR 03:08:15Z, card #17598 03:08:17Z — two removals two seconds apart, the signature of a clear rather than a strip.

⚠️ ③ was re-taken, ⛔ not reused. Before clearing this head read 39 runs; the label write re-triggered Check Changeset and it went to 42. That is the third time this seat has measured that tonight (#18233, #18238, here). A gate reading taken before a label write is not the reading that governs a landing.

On the changeset level, so the record is complete

minor under the launch-window convention (scripts/check-changeset-no-major.mjs refuses major until GA, #14043), for a fix(spec)! breaking change. That grade was ruled by this seat and published as an explicit veto window on seat post #6017 — 「声明即执行,异议再回滚」 — hours before this landing. The window ran unexercised, so execution proceeds; a single sentence still reverses it.

Review 2 verified the justification's precedent clause by clause against packages/spec/CHANGELOG.md (## 17.4.0### Minor Changes233222e … (#16041)), and review 3 confirmed the changeset blob is byte-identical across the fix-up (bfc2e258f5 at both heads), so nothing about the level moved this round.

⏳ Flipping ready and enqueuing. Landing is confirmed git-side on origin/main after a fetch — ⛔ never the API's merged field — and the landing instant is that fetch, ⛔ not the queue commit's own timestamp.


Generated by Claude Code

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 protocol:data size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AnalyticsDateRangeSchema's array arm admits shapes every consumer now refuses, and the one shared refusal sentence is false for them

2 participants