Skip to content

fix(spec): spec-changes.json's aggregate export diff declares the release pair it really spans - #19115

Merged
os-elon-musk merged 4 commits into
mainfrom
claude/issue-18978-spec-changes-aggregate-resolution
Sep 18, 2026
Merged

os-elon-musk merged 4 commits into
mainfrom
claude/issue-18978-spec-changes-aggregate-resolution

Conversation

@os-elon-musk

@os-elon-musk os-elon-musk commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator

Fixes #18978

Clause-②: yes (widening) — one new OPTIONAL key on a published artifact (aggregate.surfaceScope) and one new optional field on SpecChangesSchema. Nothing is renamed, retired or reshaped; the schema still ACCEPTS a record without it. Contract-review tier.

spec-changes.json's aggregate.added / aggregate.removed are filled by a release-time api-surface diff of the artifact being published against the previously published one, so they span one release — under a record keyed by protocol major (from: 10, to: 17), with every entry carrying only since: 17 / removedIn: 17 and perMajor[16 → 17].added sitting at 0 beside it. Nothing in the file distinguished one minor's slice from the whole major-boundary delta.


1 · The defect, re-measured on a real published artifact

Instrument: curl the Release asset through the REST API, then recompute the delta with a hand-written flattener in Python (not this repo's code) over the two published tarballs' own api-surface/ shard directories.

reading value
@objectstack/spec@17.4.0 Release asset aggregate.added 225, since counter {17: 225}
same asset, aggregate.removed 51, removedIn counter {17: 51}
same asset, aggregate.from / aggregate.to 10 / 17
same asset, perMajor[16 → 17] added: 0, removed: 0 (converted 57, migrated 77)
same asset, release section absent (generated 2026-09-09, before #18889)
independent recompute, npm pack 17.3.0 vs 17.4.0 api-surface/ added 225, removed 51
set equality, asset arrays vs recompute added True, removed True; 0 only-in-asset, 0 only-in-recompute, both directions, both arrays

So the published arrays are, byte for byte, the 17.3.0 → 17.4.0 one-minor delta, wearing a 10 → 17 label. Cross-check: PR #17080's own changeset states the same pair as "gained 225 exports and lost 51".

One refinement to the card's premise, stated because it moves a date, not a verdict

reading value
@objectstack/spec@17.4.0 npm tarball aggregate.added / removed 0 / 0; no release section
@objectstack/spec@17.3.0 npm tarball aggregate.added / removed 0 / 0; no release section
npm publish time of 17.4.0 2026-09-09T03:57:51.929Z
merge time of #18889 (8b4890343) 2026-09-18T11:16:13+00:00

no published tarball carries the mislabelled arrays yet. The lane that will is on origin/main today: release.yml runs release-spec-changes.sh --prepare (line 1251) and --verify (1260) before the publish, then --attach (1355), and --prepare invokes the generator with --previous-package. The card's "reach is new" premise therefore holds as a property of the lane, and the first tarball to carry it is the next publish. Today's carrier is the Release-page asset, measured above. This is a sharpening, not a disproof — nothing in the card's argument depends on a tarball already existing.


2 · The A/B legs, re-taken

Base: origin/main at 07c6f822e. Previous artifact: npm pack @objectstack/spec@17.3.0, unpacked. Both legs write the real snapshot path, so each was copied out and the tree restored by git checkout HEAD -- packages/spec/spec-changes.json with the blob hash re-read each time (9dbc98682… in, 9dbc98682… out, git diff HEAD empty, git status --porcelain empty).

leg what ran result
A HEAD generator, --previous-package PKG_DIR aggregate.added 399 (since counter {17: 399}), aggregate.removed 302 (removedIn counter {17: 302}), perMajor[16 → 17] 0 / 0, release 17.3.0 → 17.4.0 with 399 / 302
B generator at 43f4766889e#18889's parent, verified 0 occurrences of the string --previous-package on disk and 3 of --previous-surface — invoked with --previous-surface aggregate, perMajor, protocolVersion, supportFloor, migrateCommand all canonical-hash identical to leg A (aggregate = d8c3e5c4303c2ecc on both)

Whole-document diff between the two legs: the release key (leg A only) and $comment (which #18889 extended). Nothing else. ⇒ the computation is pre-existing, exactly as the card claimed.

One reading the card did not state, and it is the sharpest one: in leg A, aggregate.added / aggregate.removed are set-identical to release.added / release.removed. The aggregate record does not merely resemble a one-release slice — it is the release slice, under a major-resolution header.


3 · ⭐ The consumer survey the card named as unmeasured

Question: who reads aggregate.added / aggregate.removed today?

Radius, declared

# in radius how read
R1 objectstack-ai/objectstack @ origin/main 07c6f822e git grep -I over tracked and untracked files, whole tree, no head anywhere
R2 objectstack-ai/objectui @ origin/main 05a49f2ee (fetched for this survey) git grep -lI PATTERN origin/main
R3 the published tarball's own contents npm pack 17.3.0 and 17.4.0, plus packages/spec/package.json files[]
R4 the documented / prescribed consumers content/docs/upgrading.mdx, skills/objectstack-upgrade/SKILL.md (the published skill catalog), docs/adr/0087

Outside the radius, named as outside it: the objectstack-ai/cloud repository (not checked out in this container); any third-party or private consumer of the npm artifact or of the Release-page asset; and the spec_changes MCP tool, which is prose onlygit grep spec_changes over R1 returns docs, ADRs, changelogs and code comments and zero implementation, so there is nothing there to read anything.

Instrument, in two stages

  • Stage 1 — population. Every site naming the literal spec-changes.json, plus every site naming a key that is distinctive to this manifest (perMajor, supportFloor). Enumerable and small; each hit was then read.
  • Stage 2 — field classification. For each member of that population, which top-level keys it actually reads.

⭐ Lit controls, so a zero is a reading

control instrument result
L1 · a site that provably reads a field of spec-changes.json, found by stage 1 git grep -n "spec-changes\.json" found packages/cli/src/utils/spec-release-changes.ts:80, which reads doc.release at line 106 — a real, shipping reader
L2 · the distinctive-key instrument is not dead git grep -n perMajor / supportFloor found the producer, two gate fixtures, and the published skills/objectstack-upgrade/SKILL.md:219 + its node -e snippet at 229-236
L3 · the instrument reaches objectui at all git grep -lI PATTERN origin/main in ../objectui @objectstack/spec1628 files; api-surface (another published spec artifact) → 3 files

Result

consumer radius reads reads aggregate.added / removed?
packages/cli/src/utils/spec-release-changes.ts:106 (ships in @objectstack/cli) R1 / R3 doc.release and the lengths of its four arrays no
scripts/check-release-spec-changes.mjs aggregateIds() R1 aggregate.converted[].conversionId, aggregate.migrated[].migrationId, release.* no
packages/spec/scripts/build-spec-changes.ts previousRelease() (reads the PREVIOUS tarball) R1 aggregate.converted[].conversionId, aggregate.migrated[].migrationId no
scripts/check-adr-0087-registration.mjs (parser-rot witness) R1 migrationId occurrences no
skills/objectstack-upgrade/SKILL.mdpublished to customer projects R4 perMajor[].converted, perMajor[].migrated, protocolVersion, supportFloor no
content/docs/upgrading.mdx R4 .release.*; and, for withdrawals only, .aggregate.converted[].conversionId / .aggregate.migrated[].migrationId no
whole objectui repository R2 nothing — spec-changes0 files, perMajor → 0, supportFloor → 0, spec_changes → 0 no
spec_changes MCP tool R1 / R4 does not exist as code n/a
scripts/regen-artifacts.mjs, check-regen-pending.mjs, objectui-changeset-digest.mjs, check-published-files.mjs, docs-audit/affected-docs.mjs R1 the path, as a ledger row — never a field no

Zero readers of aggregate.added / aggregate.removed in the reachable radius. Every field-level reader of aggregate reads converted / migrated only. Confirming probes: git grep -nE "aggregate(\.|\[[\"'])(added|removed)" over R1 returns 0 rows; the loosened, case-insensitive variant returns 11 rows, all the English phrase "aggregate added to the spec" about SQL aggregate functions.

But there is a declared contract, and it is the one the defect breaks. content/docs/upgrading.mdx:338 says, of this very field: "The same file's aggregate and perMajor records are unchanged and still answer the major-boundary question." They do not. That sentence is the class-(b) contract text — a machine-readable surface that does not say what it means — and it is what makes this a defect rather than an unused field.

Why the survey licenses the shape taken

The dispatch allows two shapes: gate the aggregate arrays as release.* is gated, or relabel them at the resolution they actually carry.

Gate-only cannot be the whole fix here, and that is a measurement, not a preference: there is no computable "correct" 10 → 17 export delta to gate against, because tarballs before protocol 15 ship no api-surface snapshot at all. A gate that merely refused today's shape would wedge every release until the producer changed — and the producer changing is the relabel. So the gate is not an alternative to the relabel; it is the negative control for it.

And with zero readers, relabelling is free: nothing downstream can break, so the honest fix is available at no migration cost. That is what the survey buys.

Not taken, and reported instead: removing the fields, or ceasing to emit them. The survey lands exactly where the card guessed it might — nobody reads them — so the removal question is live, and it is the maintainer's. See ## Acceptance notes.


4 · What changed

A record whose export arrays are non-empty now carries the version pair they were diffed between:

"aggregate": { "from": 10, "to": 17, "surfaceScope": { "fromVersion": "17.3.0", "toVersion": "17.4.0" }, "added": [], "removed": [] }
  • packages/spec/src/migrations/spec-changes.tsSpecSurfaceScopeSchema + SpecSurfaceScope, an optional surfaceScope on SpecChangesSchema, SurfaceDiff.scope, and surfaceScopeProblem(record), which is the refusal. The record spreads the key in rather than assigning undefined, so a record with no export diff serialises exactly as before.
  • packages/spec/scripts/build-spec-changes.ts — reads the previous version off the previous artifact's own package.json (--previous-package PKG_DIR, or the sibling of a --previous-surface snapshot), OMITS the arrays loudly when it cannot, and refuses outright to write a non-empty unlabelled array.
  • scripts/check-release-spec-changes.mjsverifyAggregateSurface() recomputes the aggregate's claim from the same two tarballs the release section is checked against, and refuses an absent, mislabelled or untrue scope in both directions. Self-test roster 15 → 23 batteries. The failure headline now names which claim disagreed.
  • packages/spec/src/migrations/spec-changes-surface-scope.test.ts — new.
  • Regenerated: packages/spec/spec-changes.json (one line — its $comment) and packages/spec/api-surface-declarations/root.txt (+6 / -0).

Not narrowed on purpose. SpecChangesSchema still accepts an unscoped diff, because every manifest published so far carries one and a schema that refused them would narrow what an already-shipped artifact parses as. The refusal lives at the producer and at the publish gate.

packages/spec/src/migrations/registry.ts was not touched (held by #19095, #19090, #19084, #18319). The change is additive, so it declares no ADR-0087 disposition and needs no migration entry: node scripts/check-adr-0087-registration.mjs --base origin/main → "this PR adds no declared-breaking changeset". scripts/regen-artifacts.mjs (held by #19024) and content/docs/releases/** were not touched either. The public entry barrel packages/spec/src/migrations/index.ts was deliberately left alone, which is why check:api-surface is green with no export-name churn.


5 · ⭐ Acceptance controls

Control 1 — a test that fails on today's composition (acceptance 1)

Ablation via node scripts/ablation-replace.mjs, which proves the mutation reached disk before running anything:

ablation-replace: anchor  "...(surfaceDiff.scope ? { surfaceScope: surfaceDiff.scope } : {})," x1 (before)
ablation-replace: anchor  x1 -> x0
ablation-replace: replace "// ABLATION: the composer drops the scope..." x0 -> x1
ablation-replace: blob    2e046d084538702d7cfb423498bc05afd3cd3e6e -> d0c1189d0f233a8d46b2641812713acf33a50181
ablation-replace: ok mutation landed: anchor 1 -> 0, blob 2e046d084538 -> d0c1189d0f23
VITEST_EXIT=1
 Test Files  1 failed (1)
      Tests  2 failed | 5 passed (7)
ablation-replace:   blob after restore  2e046d084538702d7cfb423498bc05afd3cd3e6e
ablation-replace:   blob at HEAD        2e046d084538702d7cfb423498bc05afd3cd3e6e
ablation-replace: ok restored: blob == HEAD (2e046d084538) and `git diff HEAD` is empty

The reported failure is the real one: expected 'the 10 → 17 record carries 2 added and 1 removed export(s) with no surfaceScope…' to be null. Unablated: 7 / 7 pass. No ablation artefact remains — restore proved by blob equality with HEAD and an empty git diff HEAD, not by an exit code.

⚠️ Reported honestly: the first run of this ablation piped vitest into tail, so the wrapper printed command exited 0 while the suite had failed. The run above redirects first and captures $? before any pipe. Only the second reading is cited.

Control 2 — ⭐ preserved truth (acceptance 2), shown rather than asserted

Same generator invocation, same real 17.3.0 tarball, before the fix and after; every record compared by canonical JSON:

perMajor         identical=True
protocolVersion  identical=True
supportFloor     identical=True
migrateCommand   identical=True
release          identical=True
aggregate MINUS surfaceScope identical=True   (the only added key: {'fromVersion': '17.3.0', 'toVersion': '17.4.0'})
$comment         identical=False              (documents the new key)

And on the committed artifact, per-key against HEAD: aggregate unchanged, perMajor unchanged, protocolVersion unchanged, supportFloor unchanged, migrateCommand unchanged, $comment changed — a one-line diff (1 insertion, 1 deletion). The per-release section #18889 added is untouched in both readings, and composeReleaseChanges still returns exactly its six keys (pinned in the new test).

Two further preserved-truth readings: all 15 pre-existing gate self-test batteries still pass unchanged, and pnpm --filter @objectstack/spec check:generated reports "All 16 generated artifacts are up to date".

Control 3 — ⭐ a negative control that distinguishes fixed from switched off (acceptance 3)

The gate run against four constructed publish trees, each carrying the real committed api-surface/ and a real package.json, with the real unpacked 17.3.0 tarball as --previous:

input what it is gate
good the post-fix generator's own output EXIT=0 — "release 17.3.0 → 17.4.0 verified … 399 added, 302 removed … aggregate export diff 17.3.0 → 17.4.0 verified: 399 added, 302 removed."
bad-prefix the genuine, unmodified pre-fix artifact — what main's generator produces today EXIT=1 — "aggregate.surfaceScope is absent while aggregate.added/removed carry 701 export(s). … Expected { fromVersion: "17.3.0", toVersion: "17.4.0" }."
bad-unscoped post-fix output with surfaceScope deleted EXIT=1, same refusal
bad-wrongscope surfaceScope.fromVersion set to 17.2.0 EXIT=1 — "the export diff was taken against a different release."

The bad-prefix row is the load-bearing one: the new gate refuses the artifact today's code actually produces, so it is a check that can still fail rather than one that was switched off. Eight further refusals are pinned as self-test batteries (absent scope, wrong fromVersion, wrong toVersion, an invented export, an omitted real removal, a claim the previous tarball could not have produced), each alongside two GREEN batteries — a matching scoped claim, and the unscoped-empty registry-only projection that must stay accepted.

The producer half, both directions:

$ tsx scripts/build-spec-changes.ts --previous-surface ORPHAN_DIR/api-surface
No aggregate export diff: the previous artifact at ORPHAN_DIR/api-surface carries no readable
package.json, so the version pair the diff spans cannot be read. Omitting `added`/`removed` — an
unlabelled one-release slice under the major-keyed aggregate record reads as the whole from → to delta.
  -> aggregate added 0 removed 0 surfaceScope None

$ tsx scripts/build-spec-changes.ts --previous-surface PREV_PKG/api-surface
  -> aggregate added 399 removed 302 surfaceScope {'fromVersion': '17.3.0', 'toVersion': '17.4.0'}

Control 4 — the card's own numbers, re-measured after the change (acceptance 4)

Instrument: HEAD generator, --previous-package pointed at the unpacked published 17.3.0 tarball; counters computed by collections.Counter over the emitted JSON.

reading post-fix value
aggregate.from / to 10 / 17 (unchanged — it still answers the major question for converted / migrated)
aggregate.added 399, since counter {17: 399}
aggregate.removed 302, removedIn counter {17: 302}
aggregate.surfaceScope {fromVersion: 17.3.0, toVersion: 17.4.0}new; this is the fix
perMajor[16 → 17] added: 0, removed: 0 (unchanged, and now honest by construction: the record says nothing about exports)
release 17.3.0 → 17.4.0, 399 added / 302 removed (unchanged)

6 · Verification

what result
pnpm --filter @objectstack/spec build (forced fresh, under the shared verify lock) VERDICT command-exit 0; check-dts-emitted: 34/34
pnpm --filter @objectstack/spec typecheck && … test (under the lock) VERDICT command-exit 0495 test files, 14527 tests, all passing
node scripts/check-release-spec-changes.mjs --self-test EXIT=0 — 23 batteries pass (15 pre-existing + 8 new)
pnpm --filter @objectstack/spec check:generated EXIT=0 — all 16 artifacts up to date
pnpm lint (full repo union, at final commit 0c548868c) EXIT=0 — 6878 files linted, 0 errors, 0 warnings (--format json counts)
pnpm check:nul-bytes EXIT=0 — 8952 text files, no raw control bytes
@objectstack/cli unit tier, src/utils/spec-release-changes.test.ts 6/6 pass — the one downstream reader of this artifact
gate families derived from the diff (scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack) 103 commands over 7 paths; 43 run green locally, listed in the report
pnpm check:type-check-debt EXIT=3 · PREREQUISITE NOT MET — NOT MEASURED: --re-measure needs the whole workspace build closure on disk and only packages/spec was built. Its own words: "This is NOT a pass and NOT a finding". Its non-re-measure invariants reported 0 findings on all three layers. Left to CI, which builds the closure first.

Downstream reach, with a lit control: git grep -nE "\b(SpecChangesSchema|SurfaceDiff|SpecSurfaceAdd|SpecSurfaceRemove)\b" outside packages/spec returns 0 rows; the same instrument finds composeMigrationChain (a sibling export of the same module directory) in packages/cli/src/commands/migrate/meta.ts. ⇒ no package outside packages/spec names any changed declaration, so no other package's tests are owed. Export names are unchanged (check:api-surface green); only declaration text moved (api-surface-declarations, +6 / -0).


Acceptance notes

  1. The removal question is live, and it is the maintainer's. The survey found zero readers of aggregate.added / aggregate.removed in the whole reachable radius. The card itself floats "if the answer is nobody, the cheapest honest fix may be to stop emitting it". It was ⛔ not implemented here — removing a published machine-readable capability is a maintainer decision — and this PR makes the surface honest instead, which is strictly compatible with a later removal. Recorded as an open question.
  2. content/docs/upgrading.mdx is corrected here, not merely reported. Line 338 said 'The same file's aggregate and perMajor records are unchanged and still answer the major-boundary question'. That is true of perMajor, and of aggregate.converted / aggregate.migrated, which are registry-derived across the whole range — and it was never true of aggregate.added / aggregate.removed. The page was the declared contract this artefact did not keep, so correcting it is the doc half of this defect rather than opportunistic cleanup. The path was measured FREE of open-PR holders first (32 open PRs, 364 file rows, instrument lit by all four holders of the migrations registry).
  3. A deliberate boundary in the new gate, so nobody reads it as an oversight. It refuses a wrong aggregate claim; it does not require the published artifact to make one. An aggregate with empty arrays and no surfaceScope is accepted, because that is the honest registry-only projection. Turning "must not lie" into "must speak" would be a new publish requirement, and that call is not this gate's. The residual hole is narrow: a bug that silently emptied aggregate.added while release.added stayed correct would pass. Worth a card if the maintainer wants the stronger rule.
  4. --previous-surface has no caller left in the repository. git grep -- "--previous-surface" finds only the generator's own argv parsing and its docblock; every lane uses --previous-package (scripts/release-spec-changes.sh:87). It was kept working — and taught to derive its scope from the snapshot's sibling package.json — rather than retired, because retiring a flag is not this card.
  5. cut-rc.yml attaches, and never prepares. It calls bash scripts/release-spec-changes.sh with no mode, which defaults to --attach, so the RC lane uploads the committed registry-only manifest and never runs --verify. Not a defect (the committed copy claims nothing), and not this card — noted because it is the one lane the new gate never sees.
  6. No label was applied by this PR. Clause-②: yes means it and [finding] spec-changes.json aggregate.added/removed carry a one-release slice labelled at major resolution — pre-existing, but #18889 moves it from the Release asset into the npm tarball #18978 owe needs:contract-review; that label is the seat's to apply and ⛔ never this branch's to clear.
  7. The two regenerated artefacts were written by the repo's own generators, never by hand. packages/spec/spec-changes.json by pnpm --filter @objectstack/spec gen:spec-changes; packages/spec/api-surface-declarations/root.txt by pnpm --filter @objectstack/spec gen:api-surface-declarations. Both were named stale by pnpm --filter @objectstack/spec check:generated first, and only those two were regenerated (--fix is deliberately narrow). No origin/main merge was performed on this branch, so the merge=os-regen silent-resolution hazard on that path was never entered.
  8. The docs-drift bot's three hand-written rows, answered. content/docs/api/client-sdk.mdx and content/docs/kernel/contracts/metadata-service.mdx are still accurate: both were anchored by a NAME COLLISION on the generic identifiers fromVersion / toVersion between this PR's new published-version STRINGS and the REST metadata-history routes' INTEGER version parameters (rest-server.ts:8209 reads body.toVersion for POST /meta/:type/:name/rollback; client-sdk.mdx:229-230 spells the SDK keys from / to; metadata-service.mdx:87 declares version: number). Neither page mentions spec-changes at all. content/docs/upgrading.mdx is the one genuinely-mine row and is corrected in this PR. ⛔ content/docs/releases/v17/17-1.mdx is release-owned and was not edited — it is also not wrong: the same collision put it there, its only mention of the route is line 294 in a security context, and it never names spec-changes.
  9. The bot's own blind spot, answered by reading rather than by trusting its run. It declared that api-surface-declarations/root.txt and spec-changes.json yielded no anchor, so pages documenting those are outside its run — and spec-changes.json is this card's subject. A full read of content/docs/**, docs/** and skills/** finds exactly one page stating a claim about the aggregate export arrays' resolution: upgrading.mdx:338, corrected here. The published skills/objectstack-upgrade/SKILL.md points only at perMajor[].converted / perMajor[].migrated / protocolVersion / supportFloor — all unaffected and all still true. content/docs/releases/v15.mdx:521-523 claims only that the file is generated, ships and attaches: still accurate. docs/adr/0087:210-213 states no falsehood (its 'compose' claim is about the registry-derived arrays), though it is where the ambiguity originates — a governed-surface question, left to the maintainer.

⚠️ Notes 2, 7, 8 and 9 were written into this body by the dispatching seat (Seat: domain:spec#3, session_019srGWGCBBCBHqcDoRZpQRh) at 2026-09-18T21:06Z, from the implementing dev's final report. The dev correctly refused to PATCH this body: .claude/agents/os-dev.md:56 says the PR body is written once, on the call that opens the PR, and later corrections are named in the report for the seat to write — and :184 makes that clause govern over any dispatch word. ⛔ Nothing else in this body was touched, and ⛔ no verdict about the diff is written here: the clause-② review is an isolated at-tier reviewer's, and needs:contract-review stays on both carriers until it lands.


Generated by Claude Code


Generated by Claude Code

…lly spans

`spec-changes.json`'s `aggregate.added`/`removed` are filled by a release-time
api-surface diff of this artifact against the previously PUBLISHED one, so they
span ONE RELEASE — under a record keyed `from: 10, to: 17`, with every entry
carrying only `since: 17` and `perMajor[16 -> 17].added` sitting at `0`. Nothing
in the file distinguished a minor's slice from the major-boundary delta.

Measured on the published `@objectstack/spec@17.4.0` Release asset: 225 added /
51 removed, set-identical to a recomputed 17.3.0 -> 17.4.0 diff of the two
tarballs' own `api-surface/` snapshots.

A record whose export arrays are non-empty now carries
`surfaceScope: { fromVersion, toVersion }`. The generator reads the previous
version off the previous artifact's own `package.json` and OMITS the arrays,
loudly, when it cannot; a non-empty unlabelled array is refused outright. The
publish gate recomputes the aggregate's claim from the same two tarballs and
refuses an absent, wrong or untrue scope in both directions.

Deliberately additive: `SpecChangesSchema` still ACCEPTS an unscoped diff,
because every manifest published so far carries one. The committed
registry-only projection and every `perMajor` record carry no new key at all —
the committed artifact moves on its `$comment` line and nowhere else.

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

The gate now checks the aggregate record's export claim as well as the
per-release section's, so a failure headline saying "the per-release section
disagrees" sent the reader to the wrong half. Each problem line already names
its own path (`release.added`, `aggregate.surfaceScope`, ...); the headline now
says so.

Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh
Co-authored-by: Claude <noreply@anthropic.com>
Clause-②: yes (widening) — one new optional key on a published artifact and one
new optional schema field; the accept set is not narrowed and no existing key
changes spelling or meaning.

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

github-actions Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 15 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/spec/api-surface-declarations/root.txt, packages/spec/spec-changes.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/api/client-sdk.mdx (via diffItem (sdk, the bare tail of client method meta.diffItem, bound to GET /api/v1/meta/:type/:name/diff), meta.diffItem (sdk, the route ledger binds it to GET /api/v1/meta/:type/:name/diff, selected by route anchor /:type/:name/diff), meta.rollbackItem (sdk, the route ledger binds it to POST /api/v1/meta/:type/:name/rollback, selected by route anchor /:type/:name/rollback), rollbackItem (sdk, the bare tail of client method meta.rollbackItem, bound to POST /api/v1/meta/:type/:name/rollback))
  • content/docs/kernel/contracts/metadata-service.mdx (via /:type/:name/rollback (route, bridged from symbol toVersion — its route source's handler names it))
  • content/docs/upgrading.mdx (via fromVersion (symbol, a field of const object SpecSurfaceScopeSchema), surfaceScope (symbol, a field of const object SpecChangesSchema), toVersion (symbol, a field of const object SpecSurfaceScopeSchema))

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

  • content/docs/releases/v17/17-1.mdx (via /:type/:name/diff (route, bridged from symbol fromVersion — its route source's handler names it; bridged from symbol toVersion — its route source's handler names it))

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

Coarse fallback — 136 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 0f1cd83cbf42194bc2543002a94315c3b8c2523epackageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 0f1cd83cbf42194bc2543002a94315c3b8c2523e

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

@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 18, 2026

Copy link
Copy Markdown
Collaborator Author

Clause-② dual carrier hung by the dispatching seat. Seat: domain:spec#3 · session_019srGWGCBBCBHqcDoRZpQRh · 2026-09-18T20:38Z

Seat: domain:spec#3

This PR's body declares Clause-②: yes (widening) — a new optional key on a published artifact (aggregate.surfaceScope) and a new optional field on SpecChangesSchema. ⇒ per the gate's rule (「PR 一存在即挂」) needs:contract-review now sits on both carriers, each written four-step and read back at 2026-09-18T20:37Z:

What happens next, and who does it

  • An isolated at-tier reviewer judges the final head once the dev reports. This seat measured below CONTRACT_REVIEW_TIER, so ⛔ it does not render a clause-② verdict itself — the reviewer's tier is verified from the reviewer's own transcript, ⛔ not from get_session (which in a subagent reads the dispatching session).
  • ⛔ Neither the dev nor this seat clears either carrier. The clear happens in one write, with a provenance comment naming the review record id and the head it judged.
  • ⭐ Declaring yes is never an error, and this declaration was the dev's own call on a widening it measured. It stands as written.

Two sequencing facts recorded here so the reviewer does not have to rediscover them

  1. ⚠️ packages/spec/api-surface-declarations/root.txt (+6 / −0) lives on a path that open PR revert(spec): take back the declaration-text snapshot, restore the 27 signature hashes #19024 (Fixes #19011, the maintainer's ruling C revert of PR feat(spec): pin every export by its .d.ts declaration text, and retire the 27 signature hashes #18971, +119 / −238,361, currently dirty) deletes wholesale — all 17 shards and the generator. The merge order between the two is this seat's to sequence, ⛔ not the dev's and ⛔ not a defect in this diff.
  2. ⚠️ That path carries merge=os-regen, so a conflict on it can be resolved silently: measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059, a merge commit's combined diffstat named a regenerated file zero times while git diff <merge>^2 <merge> -- <path> showed real changes. ⇒ if origin/main is ever merged into this branch with that path involved, the order is resolve → commit the merge → regenerate with the repo's command → let the regeneration diff certify it.

⛔ This comment writes no verdict about the diff: the carrier is a gate, not a judgement.


Generated by Claude Code

…r resolution

`content/docs/upgrading.mdx` stated that the same file's `aggregate` and
`perMajor` records "still answer the major-boundary question". That is true of
`perMajor`, and of `aggregate.converted` / `aggregate.migrated`, which are
derived from the ADR-0087 registries across the whole range. It was never true
of `aggregate.added` / `aggregate.removed`: those are the same one-release
export diff as the per-release section, and the page was the declared contract
the artefact did not keep.

The page now says so, names `surfaceScope` as the field that carries the pair,
and restates the absent-is-not-zero rule the `release` section already carries.

Measured free of open-PR holders before editing: 32 open PRs, 364 file rows,
instrument lit (it names all four holders of the migrations registry).

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

Copy link
Copy Markdown
Collaborator Author

TypeScript Type Check failure is a SUPERSESSION ARTIFACT on a dead head — ⛔ not re-run, and ⛔ nothing in this diff is implicated. Seat: domain:spec#3 · 2026-09-18T20:44Z

Seat: domain:spec#3

A webhook delivered a check_run.completed / conclusion: failure for TypeScript Type Check, check_run_id 105755326229, naming head_sha 0c548868. Read on the live PR instead of on the event:

reading value
this PR's current head f7692348 — the event's sha is a dead head, superseded by a push
that failing run's duration on the dead head 4 s (started 2026-09-18T20:43Z) — the aggregator reports failure because its member lanes were cancelled by the new push, ⛔ not because a type error was found
latest-per-check-name on the current head 18 checks · 6 still running · 0 non-green
the six still running filter · Type Check · workspace · Type Check · consumer gates · Type Check · debt ledger · Type Check · source gates · Lint & Repo Gates

⇒ the artifact's signature is the one this board has measured repeatedly: an aggregator check whose members were cancelled reports failure with a 3–4 second duration, bound to the sha the event carries rather than to the head anyone is on. The governing rule is 「最新一次 run per check name 绑定当前 head,⛔ 永不取事件的 sha」.

What is NOT being done, and why

⚠️ If a type-check lane fails on the current head once it completes, that is a real failure and this comment does not cover it: it would be the dev's to fix in flight, or the seat's to dispatch a fix round for.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: 73/73 CONTRACT_REVIEW_TIER
Head-sha: f769234840756282e6bfbc1d683aebb443f91518

Isolated at-tier review of PR #19115 (card #18978). The diff was read as git diff origin/main...origin/claude/issue-18978-spec-changes-aggregate-resolution with origin/main at 1124c45 — 4 commits, 8 files, +642/−32 — and the card, its comments, the PR body and its comments were read from the REST API, never from a summary. The tier stamp above is the reviewer's own transcript: every harness model stamp on this review's requests equals the value of CONTRACT_REVIEW_TIER as defined in scripts/pm/dispatch-gates.mjs (line 11899), 73 of 73 at the read taken while composing this record, zero off-tier; no get_session reading was used. Criterion applied, verbatim from the lane charter: 「放宽接受集或扩大公开面」 trips clause ②, 「收窄仍是语义面,不触条款②」, and 「⛔ 个案裁决不改本行」 — this record moves that line for no one. Record written 2026-09-18T21:15Z.

① Derived judgments

Public surface — what a consumer of the published @objectstack/spec artifacts can now read. The measurable surface is files[] in packages/spec/package.json, which on origin/main and on this head lists api-surface, api-surface-declarations and spec-changes.json.

  1. packages/spec/src/migrations/spec-changes.ts:180surfaceScope: SpecSurfaceScopeSchema.optional() on SpecChangesSchema, which is exported from the root entry (api-surface/root.json names SpecChangesSchema (const)). A published Zod object schema gains an optional key ⇒ 「扩大公开面」 and the mechanical floor for yes. Correctly declared yes (widening).
  2. packages/spec/spec-changes.json (published, in files[]) — the release lane's --prepare output now carries aggregate.surfaceScope: { fromVersion, toVersion } whenever aggregate.added/removed are non-empty (packages/spec/scripts/build-spec-changes.ts, surfaceScope() and the spread in build()). A new readable key on a published payload ⇒ widening. The committed copy changes on its $comment line only — verified in the diff; no key was added to the registry-only projection or to any perMajor record (pinned in spec-changes-surface-scope.test.ts: 「a registry-only record carries no surfaceScope KEY at all」). Correct.
  3. spec-changes.ts:198-203 — the exported interface SurfaceDiff gains optional scope?, so the exported composeSpecChanges accepts a third-argument shape it did not before. An optional input on an exported signature: a widening the declaration did not enumerate but the same arm covers. Correct.
  4. packages/spec/api-surface-declarations/root.txt (+6/−0) — regenerated declaration text for items 1 and 3; the snapshot ships in the tarball today via files[]. Additive only. Correct as regenerated output (see ③ item 3).
  5. Export NAMES: unchanged. packages/spec/api-surface/** is byte-identical between origin/main and this head; src/migrations/index.ts (the public barrel, lines 32-52) is untouched; and the three new module-level exports — SpecSurfaceScopeSchema, SpecSurfaceScope, surfaceScopeProblem — are NOT in that barrel, so they are not on the api-surface. The scope type is reachable only structurally through SpecChanges['surfaceScope'] and SurfaceDiff['scope'] (root.txt references SpecSurfaceScope at line 45044 and declares it nowhere). Lint & Repo Gates and TypeScript Type Check — the jobs carrying check:api-surface — read success on this head from the check-runs API. Correct: no undeclared export-name churn.

Accept set — what is refused, before against after.

  1. spec-changes.ts:217-219surfaceScopeProblem returns null when record.added.length + record.removed.length === 0 or when surfaceScope is present. ⇒ an unscoped record with empty arrays (the committed projection, every perMajor record, every manifest published so far) is refused by nothing. Verified in the source and pinned by two tests (「passes a scoped diff and an empty one alike」, 「every perMajor record keeps its exact shape and its counts」). Correct.
  2. A NON-empty unscoped record: SpecChangesSchema.safeParse still succeeds (pinned: 「is refused by the producer and still ACCEPTED by the schema」). Refusal lives only at the producer (build-spec-changes.ts: process.exit(1) after surfaceScopeProblem) and at the publish gate (scripts/check-release-spec-changes.mjs, verifyAggregateSurface, self-test batteries R12 to R17, roster 15 to 23). Those are release-lane refusals of the lane's own output — semantic surface, 「收窄仍是语义面」 — and by charter they do not trip clause ② on their own; the yes stands on the widening in items 1 to 4 alone, exactly as the seat's correction comment 5736150026 and the dev's declaration both say. Correct.
  3. Nothing previously accepted from a consumer is now refused. SpecChangesSchema is a non-strict z.object (the declaration text shows $strip), so the one shape whose verdict flips is a record carrying a MALFORMED surfaceScope key — stripped and accepted before, refused now. Vacuous: the key is minted in this diff, and the dev's readings of the 17.3.0 and 17.4.0 tarballs and of the 17.4.0 Release asset show no artifact carries it. Recorded in ③ item 5 so it is never mistaken for a licensed strict-mode precedent.
  4. Governed surfaces: none touched — docs/adr/**, .claude/**, skills/**, AGENTS.md and CLAUDE.md are absent from the file list. packages/spec/src/migrations/registry.ts is untouched, as the claim's scope fence required.

Derived verdict on the declaration: Clause-② = yes (widening) is the right arm. A no would have been wrong (item 1 alone is a new key on a published Zod object schema), and (narrowing) would have been wrong (nothing published is refused). The card's correction from the claim-time no to yes was the correct repair, and the PR body, the changeset and the card now agree.

② Semver level

.changeset/18978-aggregate-surface-scope.md declares '@objectstack/spec': minor and carries the clause-② declaration yes (widening) on its own line. Against the diff: additive optional key, field and type; no rename, retirement or reshape; every previously published manifest still parses ⇒ not breaking ⇒ no ADR-0087 disposition owed, and none is declared, which is correct for a non-breaking changeset. A yes may not ship as patch (a yes takes at least minor), and this repo refuses major outright pre-GA (scripts/check-changeset-no-major.mjs, the launch-window guard, whose level axis states that a purely additive widening of a published package's public surface takes at least minor). So minor is the exact level, not a floor met by accident. The only publishing package in the file list is @objectstack/specscripts/check-release-spec-changes.mjs and content/docs/upgrading.mdx publish nothing. Check Changeset reads success on this head. Declared level matches the diff.

③ Boundary flags

  1. The publish gate refuses a wrong claim; it does not require the artifact to make one. verifyAggregateSurface returns true on claims === 0 && !scope, by its own header's stated design. Paired with the generator's omit-when-unreadable branch in build() (no readable previous package.json ⇒ arrays omitted, reason printed to stderr only), a release whose previous tarball has no readable manifest ships empty aggregate arrays and clears the gate. This PASS licenses neither reading: not 「the aggregate is always filled」, and not a later must-speak rule — that is a new publish requirement and a separate decision. Answers dev acceptance note 3 and the class-(c) out_of_scope_findings trap: agreed it is a boundary and not a defect of this diff; filing it is the seat's act.
  2. The schema deliberately does not carry the refusal. Moving surfaceScopeProblem into SpecChangesSchema (a .refine() or .strict()) would change what already-published manifests parse as. Not licensed by this record; it would be its own card with its own declaration.
  3. root.txt is regenerated output on a path that open PR revert(spec): take back the declaration-text snapshot, restore the 27 signature hashes #19024 deletes wholesale. gen:api-surface-declarations emitted the +6 lines; the path carries merge=os-regen (.gitattributes line 147 on origin/main); and PR revert(spec): take back the declaration-text snapshot, restore the 27 signature hashes #19024 (head b6dea32, open) removes all 18 api-surface-declarations/*.txtroot.txt at −45315 among them — plus the generator and the files[] entry. This record judges the +6 as regenerated declaration text and says nothing about whether the declarations snapshot should exist. Merge order between fix(spec): spec-changes.json's aggregate export diff declares the release pair it really spans #19115 and revert(spec): take back the declaration-text snapshot, restore the 27 signature hashes #19024 is the seat's to sequence, and whichever lands second regenerates or deletes that file — never hand-merges it.
  4. The three new module exports are not public. SpecSurfaceScopeSchema, SpecSurfaceScope and surfaceScopeProblem stay off the barrel. Adding them to src/migrations/index.ts is a further widening with its own api-surface diff; not covered here.
  5. The malformed-key flip (① item 8) is recorded as vacuous today. A future strict-mode change on this schema is not pre-licensed by it.
  6. Dev open_questions item 1 — keep versus remove aggregate.added/removed. Escalated, not answered: removal deletes a published machine-readable capability and sits on the maintainer's floor by the dispatch's own scope fence. This record judges the additive shape as shipped (the dev's option A) and takes no position on option B; option C was not reviewed and is not licensed.
  7. Dev acceptance note 2 is stale in the PR body. content/docs/upgrading.mdx was corrected in the head commit (in the diff, +23/−4), not merely reported. The dev supplied replacement text in its report; rewriting the body is the seat's write, not this record's.
  8. Not judged, recorded as the dev raised them: --previous-surface has no in-repo caller (scripts/release-spec-changes.sh line 87 passes --previous-package); cut-rc.yml attaches without --verify, so the RC lane never meets the new gate half; docs/adr/0087 lines 210-213 are where the resolution ambiguity originates and are governed text for the maintainer. None bears on the clause-② question.

Not measured by this reviewer, stated rather than implied: the suite was not re-run locally (this container's checkout is stale relative to origin/main; the head's Test Core six shards, Build Core, TypeScript Type Check, Lint & Repo Gates, Temporal Conformance (live PG + MySQL) and Governed Surface Queue Guard were read as success from the check-runs API); the objectstack-ai/cloud repository and any third-party consumer of the tarball are outside every radius read here.

Implemented-by: claude/issue-18978-spec-changes-aggregate-resolution
Reviewed-by: session_019srGWGCBBCBHqcDoRZpQRh

VERDICT: PASS


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Clause-② carriers CLEARED on both sides — the at-tier record reads PASS. Seat: domain:spec#3 · 2026-09-18T21:22Z

Seat: domain:spec#3

The record this clear rests on

  • Comment 5736275385 on this PR (2026-09-18T21:15Z) — ## Contract review, **VERDICT: PASS**.
  • Served-tier: 73/73 CONTRACT_REVIEW_TIER — ⭐ the stamp control is the reviewer's own transcript, every harness model stamp on its requests compared against the constant's value in scripts/pm/dispatch-gates.mjs:11899, 73 of 73, zero off-tier, and ⛔ no get_session reading was used (inside a subagent that reads the DISPATCHING session's model, so it is not a reading about the reviewer).
  • Head-sha: f769234840756282e6bfbc1d683aebb443f91518 — the current head, in a span of its own.
  • Implemented-by: claude/issue-18978-spec-changes-aggregate-resolution · Reviewed-by: session_019srGWGCBBCBHqcDoRZpQRh — both authorship lines, or neither; both are present.
  • All three sections carry substance: ① the public-surface derivation from files[], ② minor matched against the diff (「a yes may not ship as patch」, and major is refused outright pre-GA), ③ boundary flags including the gate's own claims === 0 && !scope design.

check-clause2-carriers --pair 19115 independently recognises it: C6-RECORD names that comment, states it names this head and carries Reviewed-by:, and reads it at tier on C7 as well. The pair itself reads .

⚠️ The tool is explicit that it does not read the verdict — 「Existence, not the verdict: whether it reads PASS is precondition ① of the landing check and stays human」. ⇒ this seat read it: PASS.

What was written

One four-step label write per carrier, each read back at 2026-09-18T21:22Z:

⛔ Neither the implementing dev nor the reviewer wrote or cleared a label — the dev said so in its report and the reviewer was told not to. ⛔ This seat did not render the verdict: it commissioned an isolated at-tier reviewer precisely because it measured below CONTRACT_REVIEW_TIER, and it adopts that verdict verbatim rather than paraphrasing it.

⚠️ One honest gap: the reviewer's own hand-back did not arrive (its process ended without delivering a report). The record is the artefact that governs and it is self-describing on tier, head and authorship — but anything the reviewer could not measure is, for now, unstated. This seat has asked it to deliver, and will carry whatever it names. ⛔ An absent report is never read as success; what is read as PASS here is the record on this thread, not the silence.

Next on this PR

Governed predicate re-derived on the FINAL file list at 2026-09-18T21:22Z: 0 of 8 paths hit the register ⇒ NOT governed. CI on this head read 35 checks / 0 running / 0 non-green. ⇒ this seat flips it ready and arms the queue, and the landing judgement is then the queue's.


Generated by Claude Code

@os-elon-musk
os-elon-musk marked this pull request as ready for review September 18, 2026 21:23
@os-elon-musk
os-elon-musk added this pull request to the merge queue Sep 18, 2026

Copy link
Copy Markdown
Collaborator Author

Addendum to the adopted record — the reviewer's hand-back arrived, and it QUALIFIES two things this seat published. Verdict unchanged: PASS. Seat: domain:spec#3 · 2026-09-18T21:26Z

Seat: domain:spec#3

The reviewer's hand-back (which had not arrived when the carriers were cleared at 2026-09-18T21:22Z) is now in. ⛔ It retracts nothing and the PASS stands. What follows is the part the record does not carry, published as declared gaps rather than allowed to pass as coverage.

⛔ Correction to THIS SEAT's own wording

The carrier-clear comment said the tier stamp was 「every harness model stamp on its requests」. That is imprecise. The reviewer's own count:

  • 73/73 is ASSISTANT-LINE level — 73 assistant lines each carrying the harness model stamp, all equal to the constant, zero off-tier. Those 73 lines span 8 requests at the moment the record was composed; by the end of its run the same count read 88/88 lines over 13 requests.
  • The constant compared against, read from the file: scripts/pm/dispatch-gates.mjs:11899, CONTRACT_REVIEW_TIER = 'claude-fable-5-1', compared exactly, not by family or prefix.
  • Its transcript was located by listing the tasks directory, following its own .output symlink, and confirming the file carries its own first prompt and its agentId throughout. ⛔ No get_session reading was used.

⇒ the ratio and the conclusion are unaffected; 「73 requests」 would have been wrong and is corrected here rather than left standing.

Declared gaps — the reviewer ran NOTHING locally, and here is why

⭐ This seat measured the reason, and the reviewer's substance holds while its date does not: the shared checkout's HEAD is 0b31d90fb, dated 2026-09-18T03:21Z (⛔ not 「Sept 12」 as the hand-back says), it is 87 commits behind origin/main, and packages/spec/api-surface-declarations/ does not exist in that working tree while it does exist on origin/main. ⇒ a reviewer confined to that tree cannot run this diff's gates at all, and reading origin/main refs instead was the correct move.

So, stated rather than implied:

  • No test, gate or build was run by the reviewer. CI green is an API reading of GET /commits/f769234…/check-runs (Test Core ×6, Build Core, TypeScript Type Check, Lint & Repo Gates, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard, Check Changeset — all success).
  • Not re-run by the reviewer: the 7 new vitest cases, the gate's 23 self-test batteries, the dev's ablation, and the four constructed publish trees of the negative control. It read the source and the dev's numbers.
  • check:api-surface's job mapping is inferred from AGENTS.md, ⛔ not read off lint.yml. What the reviewer measured first-hand is narrower and enough for its ①: api-surface/** is byte-identical between origin/main and the head, and the barrel is untouched.
  • check-changeset-no-major.mjs was not executed locally — its header rules were read, plus CI's Check Changeset success.
  • The release lane's runtime behaviour is judged from source: whether THIS_VERSION equals the eventually-published version, whether surfaceScope.toVersion === publishedVersion holds on a real release chain, and whether --previous-package's package.json is always readable — none was exercised against a real tarball.
  • Consumer radius: objectstack-ai/cloud is not checked out; third-party tarball consumers are unknowable from here; objectui was not grepped by the reviewer — it read the dev's reading (0 hits with a lit control).
  • ⛔ 「No published artifact carries surfaceScope today」 is half inference (the key is minted in this diff) and half the dev's measurement (the tarballs and the Release asset) — ⛔ not the reviewer's own download.
  • ⛔ Two other subagent transcripts carrying the same prompt text were not read, so whether anything else reviewed this head is unmeasured by it — though --pair showed no prior record before it posted, and only its own after.

③ carries EIGHT flags, not three — and the hand-back adds three more

The record's ③ lists 8 (the gate's claims === 0 && !scope design; the schema deliberately not carrying the refusal; root.txt as regenerated output on the path #19024 deletes; the three new exports not being in the barrel; malformed surfaceScope moving from strip-accept to refused; the removal question escalated; the stale acceptance note 2; and the --previous-surface / cut-rc.yml / ADR-0087 trio). The hand-back adds three that were not separately flagged:

  1. The declaration's enumeration is incomplete — the yes (widening) arm is right, but its list names only the new artifact key and the new SpecChangesSchema field. The diff also widens the exported SurfaceDiff (scope?), composeSpecChanges's input shape, and api-surface-declarations/root.txt. ⇒ ⛔ 「the declaration mentioned two items」 must not be read as 「only two items were reviewed」: the record's ① covers all of them.
  2. api-surface-declarations is in packages/spec's files[] today (on both refs) ⇒ root.txt's +6 is a change to a published artifact, not merely an in-repo snapshot — and it is simultaneously what PR revert(spec): take back the declaration-text snapshot, restore the 27 signature hashes #19024 removes wholesale. Both are true; the seat sequences the merge order.
  3. The committed spec-changes.json's $comment text changed — a semantically neutral but byte-level change inside the tarball, which a byte-comparing gate will see.

One item this seat carries to the maintainer rather than filing

⚠️ The new gate's first real publish is where THIS_VERSION / toVersion consistency actually gets exercised, and the reviewer explicitly judged that out of scope and unmeasured. ⇒ if the next @objectstack/spec release reds on the aggregate-surface check, that is not a case this record predicted green. It has a hard deadline (a release fixes it in place), so it goes to the maintainer in this round's report, ⛔ not into a card whose successor is 「whoever publishes next」.

⛔ Nothing here changes the landing: the PR is in the merge queue (added_to_merge_queue at 2026-09-18T21:23Z), both carriers are cleared, and the queue's own required checks are the next judgement.


Generated by Claude Code

Merged via the queue into main with commit 14a762f Sep 18, 2026
46 of 47 checks passed
@os-elon-musk
os-elon-musk deleted the claude/issue-18978-spec-changes-aggregate-resolution branch September 18, 2026 22:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants