Skip to content

feat(spec): author DatasetSelectionSchema and parse the whole selection at the analytics dataset door - #19638

Merged
os-warren merged 8 commits into
mainfrom
claude/issue-17551-dataset-selection-schema
Sep 22, 2026
Merged

os-warren merged 8 commits into
mainfrom
claude/issue-17551-dataset-selection-schema

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes #17551

Clause-②: yes

Ruling-ref: 5754491527 — decision batch #204 item 3, letter A, maintainer 「204 同意」 2026-09-21T02:08Z. Governing text: docs/NORTH-STAR.md 〈优先级〉4 「错的必须被响亮拒绝并给处方,永不静默落库」. ⛔ This PR executes that ruling; it does not reopen it. Option C (a second declaration in packages/rest) was refused by name and is not taken.

What was wrong

DatasetSelection was a TypeScript interface with no Zod schema anywhere in the repo. PR #17548 (card #17058) doored POST /api/v1/analytics/dataset/query, but only over the seven members the selection shares with AnalyticsQuery — the other four (runtimeFilter, dateGranularity, compareTo, totals) were declared in TypeScript, published in the api-surface, and enforced by nothing on the wire. #17550 is the measured consequence: compareTo: { kind: 'nonsense' } returned a previous-period comparison under an ordinary 200.

The placement, and why

packages/spec/src/api/analytics.zod.ts. The ruling left the choice between that file and contracts/analytics-service.ts to this seat, ONE place. Three readings decided it:

  1. DatasetSelection's own published text calls it 「the wire shape a preview/query endpoint posts」. api/analytics.zod.ts is 「the HTTP interface for the Semantic Layer」 and already holds AnalyticsQueryRequestSchema, the sibling routes' request body. One file now holds the analytics family's request-body declarations.
  2. contracts/analytics-service.ts is type-only for this service — every one of its analytics imports is import type. A Zod value there would put zod on the import path of anything that imports @objectstack/spec/contracts for types alone.
  3. ⭐ Only in the zod tier can the seven shared members be taken by reference off AnalyticsQuerySchema.shape. The door used to carry that agreement as a hand-written array — a standing claim that two declarations matched. Taking the declarations themselves makes it structural: there is no second copy left to drift, and a member that leaves AnalyticsQuery fails the build rather than becoming a private dialect.

@objectstack/spec/contracts re-exports the DatasetSelection / DatasetCompareTo types from the schema instead of declaring interfaces beside it — the move AnalyticsQuery already made in that same file (#4538), taken here before a mirror could drift rather than after. ⛔ There is no second declaration anywhere.

The door's parse point

packages/rest/src/rest-server.tsdatasetSelectionRefusal(selection), unchanged in position: after the route's own selection.measures check and before queryDataset is called. What changed is what it parses — the whole selection against DatasetSelectionSchema, where it used to parse a projection of seven members. The envelope is unchanged (400 VALIDATION_FAILED + details.fields[], or the family's ANALYTICS_DATE_RANGE_UNRECOGNIZED when every issue is that condition), and the route still forwards the caller's object to the service by identity, never a parse output.

⚠️ One live consequence recorded at the code: the shared mapper's root rename was inert under the projection and is live now. A .strict() parse puts an unrecognized-keys issue at the ROOT, which the mapper spells (body) — true for the sibling routes, false here — so the root is re-spelled selection.

The refusal messages, and their remedies

condition what the caller is told
compareTo.kind outside the closed pair what arrived, the two windows the executor implements ('previousPeriod' / 'previousYear', each described), 「Name one of those, or drop compareTo」, where it was refused, and that an unrecognised spelling used to answer a comparison under a 200
compareTo.offset (retired in #5011) { offset: '1y' } is exactly previousYear; for any other duration, state the window on the timeDimensions[] entry and compare with previousPeriod
compareTo: 'previousPeriod' (the bare-string form) write compareTo: { kind: 'previousPeriod' }; when to add dimension
an unknown key on the selection the surface named, the key echoed, and the canonical spelling (whereruntimeFilter, orderByorder, granularitydateGranularity, valuesmeasures)
cube / dataset / datasetName / previewDrafts the wrong-layer pointer: they live one level up, beside selection
totals: { dimensions: […] } dimensions is how a total is reported back; ask for it as totals: { groupings: [[…], []] }
dateGranularity outside the vocabulary the closed enum
runtimeFilter ⛔ deliberately no dataset-only sentence. It carries the canonical FilterCondition, so its refusals are that vocabulary's own — byte-identical to what the sibling body's where answers. A second wording here would be exactly the defect the compareTo.kind builder avoids.

datasetCompareKindRefusalMessage(input, origin) is ONE builder for ONE condition, on the analyticsDateRangeRefusalMessage pattern: the schema door raises it with 'schema', and @objectstack/service-analytics' shiftRange — a published export reachable in-process by a caller that never posted a body — raises the same sentence with 'runtime'. The two differ only in the clause that says where the refusal happened, which is the one clause no input can supply. The executor's own refusal is not made redundant and is not removed.

#17550 as a refusal test

packages/rest/src/analytics-dataset-selection-door.test.ts §6 drives that card's own specimen through the real route: 400, selection.compareTo.kind in details.fields[], the message carrying "nonsense", both legal kinds and 「drop compareTo」, the ApiErrorSchema envelope check, queryDataset never called, a CONTROL that the same selection with a declared kind still answers 200 with the caller's object by identity, and a pin that the door's sentence is the builder's.

⚠️ One correction the ruling's execution list deserves, stated rather than silently absorbed: the ruling says #17550 「closes as covered when this lands」. #17550 was already closed (PR #17570, 2026-09-11) — shiftRange gained an exhaustive switch, ten days before the ruling was written. So this PR does not close it; it closes the half that fix could not reach, at the door, for the route it names. Nothing else in the execution list is affected.

Generated artefacts

Regenerated with the repo's own tooling, ⛔ never by hand: pnpm --filter @objectstack/spec build then check:generated --fix, which proved 5 of 15 stale and regenerated exactly those — api-surface/, export-origins/, declaration-map/, content/docs/references/**, the strictness-ledger counts — then re-checked all five green. authorable-surface/ and json-schema.manifest/ were written by the build's own gen:schema. Three hand-maintained ledgers moved with them and each is a decision, not a regeneration: dropped-refinements.baseline.json gains api/DatasetSelection → runtimeFilter.lazy (the same FilterCondition projection gap api/AnalyticsQueryRequest → where.lazy already records) with its two header counts, and type-alias-convention.pin.test.ts gains three isomorphic pins with its count.

Acceptance notes

  • packages/spec/src/ui/dashboard.zod.ts's widget compareTo and the new DatasetCompareToSchema declare the same two members with different prescriptions, deliberately: the widget's point at neighbouring widget keys (options.dateGranularity, the widget's own filter) that do not exist on a wire selection, and it carries authoring-time tombstones the wire never had. They share the vocabulary by construction — kind's refusal is the one builder — and nothing is filed.
  • packages/core/src/utils/analytics-date-range.ts, packages/services/service-analytics/src/date-range-array-arm.ts and packages/spec/src/data/analytics.zod.ts each carried a sentence asserting the dataset route parses 「the selection's shared members against AnalyticsQuerySchema.pick(…)」. Each conclusion still holds and only the mechanism sentence was stale; all three are corrected in place. Comment-only, no publish surface.
  • SELECTION_MEMBERS_SHARED_WITH_ANALYTICS_QUERY is deleted from packages/rest. It was module-local with one consumer (its own test), and the claim it encoded is now pinned at the schema by identity.

Validation

Every command's exit code was captured before any pipe.

command exit reading
pnpm --filter @objectstack/spec typecheck 0 tsc --noEmit + scripts + test layer
pnpm --filter @objectstack/spec build 0 under the shared verify lock
pnpm --filter @objectstack/spec check:generated 0 after --fix 5 of 15 proved stale, regenerated, re-checked green
pnpm --filter @objectstack/spec test 0 510 passed / 1 skipped (511 files), 14930 tests
pnpm --filter @objectstack/rest test 0 194 files, 3265 passed / 1 skipped
pnpm --filter @objectstack/service-analytics test 0 113 files / 2411 tests
pnpm --filter @objectstack/core test 0 51 files / 1321 tests
pnpm --filter @objectstack/{spec,rest,service-analytics,core} typecheck 0 four packages, one run
pnpm check:spec-parsed-alias 0 787 pinned isomorphic

The gate family was derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack against the real change set rather than from a list, and the derived commands were run and reconciled with --ran.


Generated by Claude Code

…ntracts

Ruled on #17551 (letter A): the four undoored members become refusable.

Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx
Co-authored-by: Claude <noreply@anthropic.com>
Measured against the sibling route's own schema: identical posture on all
seven spellings, so the dataset route is no longer the looser of the two.

Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation protocol:data tests tooling labels Sep 22, 2026
@github-actions

github-actions Bot commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/core, @objectstack/rest, @objectstack/service-analytics, @objectstack/spec, touching 29 documentable anchor(s). ⚠️ 12 changed file(s) yielded no anchor (packages/core/src/utils/analytics-date-range.ts, packages/rest/src/rest-server.ts, packages/services/service-analytics/src/date-range-array-arm.ts, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/error-catalog.mdx (via dateGranularity (symbol, a field of interface DatasetSelection, an authorable key of api/DatasetSelection), dateGranularity (literal, a string literal in DatasetSelectionSchema))
  • content/docs/data-modeling/analytics.mdx (via dateGranularity (symbol, a field of interface DatasetSelection, an authorable key of api/DatasetSelection), runtimeFilter (symbol, a field of interface DatasetSelection, an authorable key of api/DatasetSelection), dateGranularity (literal, a string literal in DatasetSelectionSchema), runtimeFilter (literal, a string literal in DatasetSelectionSchema))
  • content/docs/data-modeling/queries.mdx (via dateGranularity (symbol, a field of interface DatasetSelection, an authorable key of api/DatasetSelection), dateGranularity (literal, a string literal in DatasetSelectionSchema))
  • content/docs/protocol/objectql/query-syntax.mdx (via dateGranularity (symbol, a field of interface DatasetSelection, an authorable key of api/DatasetSelection), dateGranularity (literal, a string literal in DatasetSelectionSchema))
  • content/docs/ui/dashboards.mdx (via compareTo (symbol, a field of interface DatasetSelection, an authorable key of api/DatasetSelection), dateGranularity (symbol, a field of interface DatasetSelection, an authorable key of api/DatasetSelection), runtimeFilter (symbol, a field of interface DatasetSelection, an authorable key of api/DatasetSelection), dateGranularity (literal, a string literal in DatasetSelectionSchema), previousPeriod (literal, a string literal in COMPARE_TO_OFFSET_ON_THE_WIRE_RETIRED; a string literal in COMPARE_TO_STRING_ON_THE_WIRE_RETIRED; a string literal in DatasetCompareTo; a string literal in DatasetCompareToSchema; a string literal in datasetCompareKindRefusalMessage), previousYear (literal, a string literal in COMPARE_TO_OFFSET_ON_THE_WIRE_RETIRED; a string literal in COMPARE_TO_STRING_ON_THE_WIRE_RETIRED; a string literal in DatasetCompareTo; a string literal in DatasetCompareToSchema; a string literal in datasetCompareKindRefusalMessage), runtimeFilter (literal, a string literal in DatasetSelectionSchema))
  • content/docs/ui/reports.mdx (via runtimeFilter (symbol, a field of interface DatasetSelection, an authorable key of api/DatasetSelection), runtimeFilter (literal, a string literal in DatasetSelectionSchema))

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

  • content/docs/releases/v15.mdx (via runtimeFilter (symbol, a field of interface DatasetSelection, an authorable key of api/DatasetSelection), runtimeFilter (literal, a string literal in DatasetSelectionSchema))
  • content/docs/releases/v16.mdx (via dateGranularity (symbol, a field of interface DatasetSelection, an authorable key of api/DatasetSelection), dateGranularity (literal, a string literal in DatasetSelectionSchema), queryDataset (sdk, the bare tail of client method analytics.queryDataset, bound to POST /api/v1/analytics/dataset/query))
  • content/docs/releases/v17/17-0.mdx (via dateGranularity (symbol, a field of interface DatasetSelection, an authorable key of api/DatasetSelection), timeDimensions (symbol, a field of interface DatasetSelection, an authorable key of api/DatasetSelection), dateGranularity (literal, a string literal in DatasetSelectionSchema), timeDimensions (literal, a string literal in SELECTION_MEMBERS_SHARED_WITH_ANALYTICS_QUERY; a string literal in a comment on a changed line; a string literal in timeDimensions))
  • content/docs/releases/v17/17-1.mdx (via runtimeFilter (symbol, a field of interface DatasetSelection, an authorable key of api/DatasetSelection), runtimeFilter (literal, a string literal in DatasetSelectionSchema))
  • content/docs/releases/v17/17-3.mdx (via analytics.queryDataset (sdk, the route ledger binds it to POST /api/v1/analytics/dataset/query), queryDataset (sdk, the bare tail of client method analytics.queryDataset, bound to POST /api/v1/analytics/dataset/query))
  • content/docs/releases/v17/17-4.mdx (via timeDimensions (symbol, a field of interface DatasetSelection, an authorable key of api/DatasetSelection), timeDimensions (literal, a string literal in SELECTION_MEMBERS_SHARED_WITH_ANALYTICS_QUERY; a string literal in a comment on a changed line; a string literal in timeDimensions))
  • content/docs/releases/v9.mdx (via runtimeFilter (symbol, a field of interface DatasetSelection, an authorable key of api/DatasetSelection), runtimeFilter (literal, a string literal in DatasetSelectionSchema), queryDataset (sdk, the bare tail of client method analytics.queryDataset, bound to POST /api/v1/analytics/dataset/query))

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
  • 12 changed file(s) yielded no anchor (packages/core/src/utils/analytics-date-range.ts, packages/rest/src/rest-server.ts, packages/services/service-analytics/src/date-range-array-arm.ts, …) — pages documenting those are invisible to this run
  • 6 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 142 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 1912237481125d0d6223ae9326bff621d60327a5packageMentionDocs.

Which tree this was computed on

This run read content/docs from a7171b6f8f84de3024f24ac91daebdb536cf0003 — the merge of head 9b7fcc36e44569454070aa0cfccaaf6434eaf1a7 into base 1912237481125d0d6223ae9326bff621d60327a5, 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 a7171b6f8f84de3024f24ac91daebdb536cf0003 && git checkout a7171b6f8f84de3024f24ac91daebdb536cf0003
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 1912237481125d0d6223ae9326bff621d60327a5 9b7fcc36e44569454070aa0cfccaaf6434eaf1a7 && git checkout -B drift-repro 1912237481125d0d6223ae9326bff621d60327a5 && git merge --no-ff 9b7fcc36e44569454070aa0cfccaaf6434eaf1a7

node scripts/docs-audit/affected-docs.mjs --json 1912237481125d0d6223ae9326bff621d60327a5

⚠️ 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 1912237481125d0d6223ae9326bff621d60327a5 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…ed window

`check:test-source-alias` — the CONTROL cases added for #17551 reach
`@objectstack/spec/api` through a dynamic import inside a test body, and this
package resolves that specifier through `dist/`.

Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx
Co-authored-by: Claude <noreply@anthropic.com>
`check:issue-citations` — the card that round closed is no longer on this
board (`allocated-but-absent`; deleted vs transferred NOT MEASURED), so every
site this change touches names PR #17548 instead, and the door's header says
so in prose.

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

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: 205/205 CONTRACT_REVIEW_TIER
Head-sha: 9b7fcc36e44569454070aa0cfccaaf6434eaf1a7

Stamp control measured by the adopting seat over the reviewer's transcript: all 205 assistant rows carry the model CONTRACT_REVIEW_TIER names, read from origin/main at 2026-09-22T04:03Z. ⚠️ The constant's line moved :12142:12265 between two reads this session while its VALUE held — which is why the rule is re-read every time rather than recalled. Total, non-zero, no fallback row. Text below is the reviewer's, adopted.

① Derived judgments

Detached worktrees cut from the head sha and merge-base 744a0a3f1d. The shared checkout was never read or written; a stale scratchpad worktree at 8dcd6a42ae — PR #19600's head — was removed before use so nothing was read off the wrong tree. pnpm install exit 0, so the review executed rather than inferred; every exit code captured before any pipe.

2. "By reference" — VERIFIED in the strongest available form: object identity. const shared = AnalyticsQuerySchema.shape, then a probe asserting DatasetSelectionSchema.shape[m] === AnalyticsQuerySchema.shape[m] for each of dimensions / measures / timeDimensions / order / limit / offset / timezone — all seven print SAME-REFERENCE. There is no second copy to drift; a member leaving AnalyticsQuery fails the build here. The roster is exactly eleven, and the seven resolve to declarations matching the retired interface member-for-member. The PR's central argument stands.

3. No second declaration — zero, with a firing control. Target regex over both repo trees returns 6 hits, all in packages/spec/src/api/analytics.zod.ts, all of them THE declaration. Positive control, same command and scope: FilterCondition|AnalyticsQuery fires in two other files — known targets outside the zero's file, proving the scope reaches past it. objectui at the pin carries two local DatasetTotals interfaces, but they are the response shape, the opposite direction. Zero declare module '@objectstack/spec…' augmentations in either repo, control firing 5 hits.

4. Zod-on-the-import-path — VERIFIED, the reason is not hollow. All nine imports in contracts/analytics-service.ts are import type, and both re-exports are export type, which TypeScript erases. No zod value reaches that entrypoint.

5. Single builder — genuinely ONE sentence. datasetCompareKindRefusalMessage(input, origin) is a single template whose only variable part is refusedAt, one clause naming where the refusal happened. shiftRange's default arm calls it with 'runtime'; the enum's error calls it with 'schema'. ⛔ Not two look-alikes. No third wording: one production hit repo-wide, control on drop compareTo firing 6 hits across three packages including dataset-executor.ts:703 — proving the scope reaches the executor file, where a different refusal lives. There is deliberately no runtimeFilter sentence, which is correct: a second wording there is the #5240 defect.

6. What newly refuses — measured, and it is a pull-back, not a narrowing past published text. The old door was reconstructed exactly (project the seven, parse against AnalyticsQuerySchema.pick) and both run against 38 specimens. Fifteen moved PASS→FAIL: unknown key; where/cube/datasetName at the wrong layer; three structurally-malformed runtimeFilter spellings; dateGranularity: 'decade'; compareTo.kind nonsense, the retired {offset} arm, the bare-string form, an extra key on compareTo; an extra key on totals; totals: {dimensions}; and null on each of the four newly-doored members. Every one was already refused by the published TypeScript interfacenull included, since ?: means T | undefined, and null was already refused on the other seven (limit: null FAIL→FAIL).
The sibling control is real and was reproduced. runtimeFilter and the sibling's where are literally the same FilterConditionSchema object. Measured spelling for spelling: the three structural spellings refuse on both; the four semantic ones pass both and are answered deeper. Extended by the reviewer — null and the authored array form also refuse on both. ⇒ the dataset route is now exactly as strict as its sibling, ⛔ not stricter.

7. Cross-repo — re-measured at the PIN; nothing breaks. ⚠️ The report measured at objectui @98178b2; .objectui-sha pins 87af769e9a…, unchanged by this PR. The pin is an ancestor of 98178b2, so the dev measured a newer tree than the one the repo pins. Re-measured at the pin: DatasetWidget.tsx:595, DatasetReportRenderer.tsx:405, DashboardFilterBar.tsx:306, DatasetPreview.tsx:123, data-objectstack/src/index.ts:6290. All use conditional spreads, so an absent member is omitted, never null — which is what makes the null narrowing unreachable from them. The riskiest site, DashboardRenderer.tsx:687 forwarding compareTo: widget.compareTo, is safe because the widget's compareTo is a strictObject with an identical member and accept set.

8. #17550 as a refusal test — VERIFIED as claimed. §6 drives the card's own specimen through the real route: 400, VALIDATION_FAILED, selection.compareTo.kind in details.fields[], both legal kinds, queryDataset never called, and a CONTROL that a declared kind still answers 200 with the identity claim pinned.

9. Generated artefacts — fresh, and measured against a dist the reviewer built. pnpm --filter @objectstack/spec build exit 0 produced zero tracked-file drift. check:generated exit 0, "All 15 generated artifacts are up to date" — and because the build ran first, this satisfies the tool's own caveat that a stamp hashes inputs, not emitted files. Hand-maintained ledgers reconcile: strictness api/ 451→454 = the three new strictObjects; dropped-refinements 204→205 schemas / 560→561 sites = the one new row, whose precedent was confirmed already present. check:spec-parsed-alias exit 0, 787 pinned isomorphic.

Tests run first-hand: spec dataset-selection.test.ts 20/20; rest door + envelope 64/64 (after building rest's dependency closure — the first two failures were unresolved workspace deps, ⛔ not assertions); service-analytics dataset-compare-kind-refusal 10/10; type-alias-convention.pin 3/3. All exit 0.

10. CI at this head — by job conclusion, 35 runs, 35 distinct names. 33 success, 2 skipped, zero failure, zero cancelled, zero pending, no exit-3. Both aggregators are backed by their member lanes (Test Core (1..6/6), Dogfood Regression Gate (1..3/3)) — all success, so ⛔ no aggregate stands in for an unmeasured member. Both skips rostered and their gate conditions match this PR.
The three rounds' fixes introduced nothing. Clocked-window: a module-scope import so the transform is paid at collection; check:test-source-alias exit 0 (74 packages scanned). Citation: the remedy cites PR #17548, which resolves, and states in prose that the card number does not — ⛔ it did not guess a replacement. #17058 independently confirmed 404. check-issue-citations exit 0 with a live board probe. Eight #17058 mentions survive in test files the fix did not touch; the gate is diff-scoped to citations a change adds, so this is historical record, ⛔ not a dangling new citation.

② Semver level

Changeset: @objectstack/spec minor, @objectstack/rest patch, @objectstack/service-analytics patch. The level is correct.

What grew. The api entrypoint gains seven published names, recorded across api-surface/, export-origins/, declaration-map/ and json-schema.manifest/. A purely additive widening takes at least minor. On contracts, nothing is removed: both names remain, export-origins repoints them to the zod source, and api-surface records a kind flip interfacetype. That flip breaks only a consumer who declaration-merges the name; both repos searched for module augmentation, zero found, control firing 5 hits. The deleted SELECTION_MEMBERS_SHARED_WITH_ANALYTICS_QUERY was never published, so its removal is module-local.

Which side Check Changeset can fail on. (a) any major bump — the launch-window guard refuses one outright; and (b) the #16055 level axis: a PR declaring clause ② while every package whose published source it moves grades patch. Here clause ② is yes and spec is minor, so the axis is satisfied and its green is a real reading on that axis.

Which side it cannot. ⛔ It CANNOT fail on which package received the widening — in the script's own words, "Clause ② is declared ONCE, FOR THE PR". And it cannot fail on the narrowing at all: during the launch window the bump level is explicitly not the carrier for breaking-ness, and check-adr-0087-registration fires only on a changeset that self-declares breaking, so an undeclared narrowing is unreachable by it. ⇒ this job's green is evidence that the declaration and the levels agree, and no evidence whatever that the accept-set narrowing was correctly graded. That judgement is the reviewer's, ⛔ not the gate's.

Making it. The narrowing is real on the wire — fifteen measured specimens — but every one was already refused by the published interface, and each is refused identically by the sibling route. The repo's standard for a breaking narrowing is a narrowing past published text; this is a pull-back onto it. The direct precedent settles it: PR #17548 performed the same act on the same route one round earlier, graded Clause-②: no, concluding "nothing this door refuses is admitted by that text". The reviewer reached that finding independently. No BREAKING banner or ADR-0087 disposition is owed.

One inconsistency, immaterial to what ships: that predecessor graded @objectstack/rest minor for the door-narrowing act; this PR grades it patch. @objectstack/rest widens no public surface here, so patch is defensible on the written rule — and under lockstep the group takes the highest bump, so the release is minor either way.

③ Boundary flags

  • objectui measurement sha. The report measured the five call sites at @98178b2, ⛔ not at the pin. The pin is an ancestor, so a newer tree than the repo pins was measured. Re-measured at the pin: no call site breaks. Outcome unaffected; the stated radius was the wrong sha.
  • No CI lane measured the objectui consumers. Console Pin Gate skipped — correctly and rostered, because .objectui-sha did not move. That is the lane that would have built the pinned console against this change, so the cross-repo claim rests on manual measurement; the reviewer supplied its own at the pin.
  • @objectstack/rest: patch vs the predecessor's minor for the same class of act. Flagged for consistency, ⛔ not a defect.
  • content/docs/references/index.mdx contention (this PR, feat(spec)!: split the translation bundle type — settings is a platform group, not a per-app one (#15178) #19600, fix(spec,objectql): declare the inert-JSON artifact and registry-record package body stages, and stop the record under-reporting functions #19373). ⭐ The reviewer reaches the seat's outcome by a firmer route and explicitly declines to rely on "the other two are stalled" — that is a fact about today that can change without notice. The file is machine-generated (its own header says DO NOT EDIT), this PR's entire delta is three integers (1531→1534, API 441→444 — exactly the three new schemas), and check:generated is a required lane that forces whoever lands second to rebase and regenerate. The conflict is mechanical and self-healing whether or not the other PRs move.
  • Root re-spelling is a string match on the mapper's literal (body). Correct today and measured, but it couples this door to the mapper's spelling of the root rather than to a shared constant. A seam worth knowing, ⛔ not a defect.
  • Narrowings declared rather than banked. The targeted suites were run (20 + 64 + 10 + 3, all exit 0), ⛔ not the report's full-package runs (510 / 194 / 113 / 51 files); those figures are not independently reproduced here. pnpm lint and the four-package typecheck were not run. check:react-declaration-parity is not covered by the 15/15 check:generated reading.

Implemented-by: claude/issue-17551-dataset-selection-schema
Reviewed-by: session_01UDXER3sdqfeVYpEWZs5mZx

VERDICT: PASS


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Carrier stripped on BOTH carriers, citing the record — the four landing preconditions, read

Seat domain:spec#2, session_01UDXER3sdqfeVYpEWZs5mZx, 2026-09-22T04:30Z. needs:contract-review removed from card #17551 and PR #19638 in two four-step writes, each read back.

The record it is stripped against: PR comment 5771223547## Contract review, Served-tier: 205/205 CONTRACT_REVIEW_TIER, naming head 9b7fcc36e44569454070aa0cfccaaf6434eaf1a7, VERDICT: PASS. ⛔ 「清标缺引记录即半态」 — so it is cited here rather than assumed.

precondition reading
① at-tier PASS on record comment 5771223547, 205/205, PASS, names this head
check-clause2-carriers --pair 19638 run after the strip, below
③ CI green by job conclusion, latest run per check NAME 35 runs / 35 distinct names — 33 success, 2 skipped, 0 failure, 0 cancelled, 0 pending. Both skips rostered. ⛔ No aggregate stands in for a member: Test Core and Dogfood Regression Gate are green and all 6 + 3 member lanes are independently green
④ carrier stripped on both carriers, citing the record this act

Governed-surface reading, derived not recalled: check-governed-merges.mjs --pr 19638 against the final file list — "0 of 24 path(s) hit the register (6 surfaces)", ✅ NOT governed — ordinary queue landing applies. Size 1591 changed lines (+1314/−277) ≤ 5000, under the human-merge threshold. ⚠️ Re-run on the final list on purpose: the register has grown several times in two days and an earlier reading is recall.

⚠️ Why ④ is listed as a precondition at all. It is not in the regime's original three. This seat added it after enqueuing four PRs at 2026-09-22T00:35Z with the carrier still hung — the merge queue refused two of them at exit code 6. Following ①②③ literally still lets a carrier-bearing PR into the queue, because --pair EXIT 0 is a consistency statement about the two carriers, ⛔ not a release.

Proceeding to ready and auto-merge. ⛔ Nothing here approves the PR and ⛔ nothing merges it by hand — the queue does that.


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/xl tests tooling

Projects

None yet

2 participants