Skip to content

feat(spec): ship a per-release section in spec-changes.json, verified against both tarballs - #18889

Merged
os-litant merged 10 commits into
mainfrom
claude/issue-17080-per-release-spec-changes
Sep 18, 2026
Merged

os-litant merged 10 commits into
mainfrom
claude/issue-17080-per-release-spec-changes

Conversation

@os-litant

@os-litant os-litant commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator

Fixes #17080

Clause-②: yes (widening)

Ruled A (issue comment 5643392133, decision batch #119 item 2, maintainer 「同意」 2026-09-12):
minors ship real, machine-readable change data, with a correctness gate. All four execution
clauses land here.

The defect, re-measured from the published tarballs

npm pack @objectstack/spec@17.3.0 @objectstack/spec@17.4.0, then the repo's own export-surface
row convention (ENTRY: NAME (KIND) — entry point, export name, export kind — the spelling
build-spec-changes.ts flattens to):

17.3.0 17.4.0
export-surface rows 5259 5433
true delta 225 added, 51 removed
spec-changes.jsonaggregate.added / .removed 0 / 0 0 / 0
protocolVersion 17.0.0 17.0.0
perMajor[16→17] converted 58, migrated 77 converted 57, migrated 77
[REMOVED] prescriptions in json-schema/** 156 221

Every figure the card reports reproduced. The shipped manifest answered 0 / 0 over a release
that moved 276 exports, and a consumer reading semver sees a minor.

What lands

1 · A per-release section, shipped in the tarball. spec-changes.json gains release
fromVersiontoVersion at package-version resolution, with added / removed (the exports
that arrived and left, each named) and converted / migrated (the ADR-0087 D2/D3 entries first
registered in that release). Generated at publish time only, from the previously published
tarball: the committed copy stays the deterministic registry projection and check:spec-changes
is green against it, which is how the determinism concern is answered rather than traded away.

2 · The correctness gate, as acceptance. Before anything reaches npm, the release lane packs
the artifact it is about to publish and recomputes the delta from the two tarballs, with its
own reader — scripts/check-release-spec-changes.mjs deliberately does not import the generator's
flattening, because an instrument that shares the code it audits reports agreement with itself.
A mismatch fails the release, naming the disagreeing exports and the direction of each
disagreement (claimed-but-not-real, real-but-unclaimed, per array) plus the command that
regenerates the section. A held release arrives with its own diagnosis.

3 · os validate --json reads the same data. New key specReleaseChangesfromVersion,
toVersion, the four counts and the file it read. It is a sibling of protocolVersionGap,
not a widening of it: that key means "the platform on disk is outside the range you declared",
and a consumer gating CI on it must not start failing because an ordinary minor shipped exports.
One key, one question. (Note specVersionGap is spelled protocolVersionGap in this tree — it
was renamed by #14261, which is still an unreleased changeset; the ruling's clause 3 names the
old spelling.)

4 · Published payload change. Clause-②: yes (widening), contract-review carrier, changeset
carrying the migration-free declaration, and content/docs/upgrading.mdx gains What the
installed artifact tells you, without a second worktree
.

Absence is not zero

The section is omitted, loudly, when the previous tarball ships no export snapshot or no
manifest — an empty release is indistinguishable from "this release changed nothing", which is
the misreading the whole section exists to end. The gate derives the same condition from the same
artifacts, accepts that absence, and refuses a section that is present when it could not have
been computed. specReleaseChanges is null in exactly those cases.

Verification

Real artifacts, not fixtures — npm pack of published 17.3.0, pnpm pack of this tree:

generate --previous-package (the unpacked published 17.3.0)  ->  pack  ->  gate
✓ release 17.3.0 → 17.4.0 verified against both tarballs: 386 added, 302 removed, 0 converted, 0 migrated.

⚠️ Corrected by the domain:spec seat — this line read 「374 added」 when the PR opened. origin/main has since moved the export surface, and 386 is the number measured at the current head 1737a24b510 from a real npm pack of the published 17.3.0, independently confirmed by the at-tier review at the previous head. os-dev.md:56 reserves this body to the PR-open write, so the round named the number and the seat writes it.

Ablation, on the real artifact. One export dropped from release.added in the packed
manifest (mutation proven on disk: sha256 bc21927e…3bbff365…), restored under
trap … EXIT INT TERM, hash verified equal after:

exit 1
✗ the per-release section of spec-changes.json disagrees with the two tarballs (ADR-0087 D4).
  A wrong change file is worse than none — a consumer gates its upgrade on this data.

  release.added: 1 export(s) the two tarballs show as added and the section OMITS:
        ./ai: BUILD_PROGRESS_FRAME_TYPE (const)
  Regenerate with: pnpm --filter @objectstack/spec exec tsx scripts/build-spec-changes.ts
  --previous-package PREV_PACKAGE_DIR      [the real line spells the placeholder in angle brackets]

Gates and tests, exit codes captured by redirect:

  • pnpm check:release-spec-changes — 15 batteries (roster + floor + verdict handshake), exit 0.
    The real run needs two published tarballs, so the self-test is what a PR can run; it drives the
    same verifyRelease() the release lane calls.

  • pnpm --filter @objectstack/spec check:generatedall 16 artifacts up to date after
    gen:api-surface + gen:export-origins + gen:api-surface-declarations (7 new declarations on
    the root entry, 0 removed).

    ⚠️ Corrected by the domain:spec seat: this read 「all 15」 when the PR opened. The count moved
    because feat(spec): pin every export by its .d.ts declaration text, and retire the 27 signature hashes #18971 (d8b12fca97c) registered check:api-surface-declarations as a new generated
    artifact family. The gate now prints 「Checking 16 generated artifacts」. ⛔ The 15 was not wrong
    when written — the base moved under it. os-dev.md:56 reserves this body to the PR-open write, so
    the round named the number and the seat writes it.

  • pnpm --filter @objectstack/spec exec vitest run --project local src/migrations/migrations.test.ts
    — 137 passed.

  • pnpm --filter @objectstack/cli exec vitest run --project unit src/utils/spec-release-changes.test.ts
    — 6 passed.

  • pnpm --filter @objectstack/spec typecheck — exit 0. pnpm --filter @objectstack/cli typecheck
    — exit 0 (after building the runtime closure the CLI's test project reads; its first run reported
    only TS2307 Cannot find module for packages this worktree had not built).

  • pnpm check:entry-guard — exit 0 (265 files, 205 exporters inert on import). This one caught a
    real defect in the new gate: it exports verifyRelease and dispatched at the top level, so
    importing it would have run it. Fixed with isEntrypoint.

  • Gate families, derived by scripts/pm/dispatch-gates.mjs from the diff and reconciled with
    --ran: 153 derived, 147 run (146 exit 0), 4 NOT MEASURED (check:i18n,
    check:i18n-coverage, check:i18n-walk-parity, check:dual-build-cjs-loads — each exits 3,
    PREREQUISITE NOT MET, wanting a full repo build), 2 unrun (check:pm-dispatch-gates, whose
    own header forbids running it in an agent container's foreground; check:type-check-debt,
    repo-wide tsc, killed by this runner's timeout). check:spec-changes and
    check:skill-examples deserve a word each: the first is green, which is the determinism
    half of clause 1; the second exits 1 saying "packages/client-react/dist holds no .d.ts — the
    package is not built"
    , so it is NOT MEASURED, not a finding. None of the six touches this
    diff's subject, and CI builds fresh.

Acceptance notes

  • A withdrawn conversion is not reported by release.converted. Between 17.3.0 and 17.4.0
    perMajor[16→17].converted went 58 → 57: field-required-notnull-explicit left the published
    chain. The registry records that as a deliberate withdrawal (⛔ WITHDRAWN — there is deliberately NO field-required-notnull-explicit), so this is documented behaviour, not drift.
    The section reports entries a release added, which is the four arrays ADR-0087 D4 names;
    an id that disappeared is visible only by comparing two published manifests, and the code says
    so rather than implying otherwise.
  • The tombstone trap the card names is not closed by this change, and this change does not
    claim to close it.
    Of the 65 new [REMOVED] prescriptions between 17.3.0 and 17.4.0, exactly
    one names 17.4.0 as the retiring release. release.removed answers "which exports left in this
    release" exactly; it says nothing about which prescriptions were written in it, because the
    prescription text carries no retirement version in data. A consumer still cannot tell
    「retired in X」 from 「prescription written in Y」 from the tombstones alone.
  • The card's "every conversion entry carries toMajor: 17" holds inside perMajor[16→17]; the
    aggregate carries toMajor values 11, 13, 14, 15 and 17. The substance — no resolution finer
    than a major — reproduced.

Generated by Claude Code


Generated by Claude Code

`spec-changes.json` is keyed to the protocol major while this repo's launch
window ships breaking changes in minors, so a consumer crossing 17.3.0 -> 17.4.0
reads `added: 0, removed: 0` over a delta of 225 added and 51 removed exports.

Adds the `release` section (from -> to at package-version resolution), generated
at publish time only so the committed copy stays the deterministic registry
projection, plus the correctness gate that recomputes the delta from the two
tarballs and fails the release on a mismatch, naming the disagreeing exports.

Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho
Co-authored-by: Claude <noreply@anthropic.com>
`protocolVersionGap` answers the major-resolution question and is null for an
app on `^17` running spec 17.4.0 — correct, and silent about a release that
narrowed accept-sets under the launch-window convention. `specReleaseChanges`
reads the installed artifact's own per-release section (ADR-0087 D4) so a CI job
can ask what moved in the release it has. A sibling key, not a widening of the
one above: one key, one question.

Adds the consumer docs section, the changeset, and the unit tests for both the
fold and the reader.

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

Seven new declarations reach the root entry — the per-release schema, its
surface-entry schema, the fold and their types. No export is removed or
narrowed.

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

The gate exports `verifyRelease` so its self-test drives the same function the
release lane calls; a `scripts/**` module that exports a binding and dispatches
at the top level runs inside whoever imports it (`pnpm check:entry-guard`).

Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl ci/cd dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation tests tooling labels Sep 18, 2026
@github-actions

github-actions Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cli, @objectstack/spec, touching 20 documentable anchor(s). ⚠️ 4 changed file(s) yielded no anchor (packages/spec/api-surface-declarations/root.txt, packages/spec/api-surface/root.json, packages/spec/export-origins/root.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 SpecReleaseChangesSchema; a field of interface ReleaseSection; a field of interface SpecReleaseChanges), toVersion (symbol, a field of const object SpecReleaseChangesSchema; a field of interface ReleaseSection; a field of interface SpecReleaseChanges))

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
  • 4 changed file(s) yielded no anchor (packages/spec/api-surface-declarations/root.txt, packages/spec/api-surface/root.json, packages/spec/export-origins/root.json, …) — pages documenting those are invisible to this run
  • 1 anchor(s) matched too much of the corpus to be a work list: os validate (command, 49 pages)
  • 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.

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 2767af8e8354511f9c82ce402b147ad12c512819packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 2767af8e8354511f9c82ce402b147ad12c512819

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

…ssify the release-delta reader

Two CI reds at the pushed head, both this branch's.

`scripts/check-release-spec-changes.mjs` seeded its self-test tree from
`fs.realpathSync('/tmp')`. The exposed-scratch-dir sweep in
`scripts/pm/dispatch-gates.mjs` resolves every `mkdtempSync`/`mkdirSync` base
statically to prove no gate writes its scratch tree into the repo, and reports a
base it cannot read as UNRESOLVED rather than assuming it is fine — so the sweep
reddened on a call it does not model. `tmpdir()` is the spelling every other
gate in `scripts/` uses, it is what the sweep reads as outside the tree, and it
honours TMPDIR/RUNNER_TEMP where the literal did not.

`packages/cli/test/validate-build-gate-parity.test.ts` holds a CLOSED roster:
every bare identifier `compile.ts` or `validate.ts` calls must land in exactly
one of its three ledgers. `readSpecReleaseChanges` arrived in `validate.ts`
unclassified. It goes in NOT_A_GATE, with the reason: it reads the installed
`@objectstack/spec`'s own published per-release delta and takes nothing from the
stack, so it reaches no verdict and can refuse nothing. The row states the
distinction from `checkProtocolVersionGap` next to it, which does judge the
input and is therefore a shared gate.

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

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: 100/100 CONTRACT_REVIEW_TIER
Head-sha: 4f1724d33b9d0537248bfc7f726b03745c8fe1a3

⭐ Tier verified by the seat from the reviewing round's harness-stamped per-message served-model field: 100 of 100 at tier, 0 off-tier. The round correctly declined to count its own.

① Derived judgments

1. The correctness gate fails DIAGNOSABLY — reproduced on real artifacts, ⛔ not taken on report. This was the seat's added constraint (ruling clause 2 requires the gate; the seat required that a mismatch name what disagrees and in which direction, because a gate that wedges a release without saying why is worse than the defect it guards).

The round unpacked the published 17.3.0 and 17.4.0 tarballs, generated, packed, and ran the gate green (「release 17.3.0 → 17.4.0 verified against both tarballs: 386 added, 302 removed, 0 converted, 0 migrated」), then broke it five ways, each with a sha256 proof of the mutation on disk and a restore proven byte-equal:

ablation the gate said
drop one export from release.added 「1 export(s) the two tarballs show as added and the section OMITS: ./ai: BUILD_PROGRESS_FRAME_TYPE (const)
invent an export in added 「the section CLAIMS but the two tarballs do not show as added: ./ai: Ghost (const)
drop one from removed named, 「OMITS」
wrong fromVersion 「generated against a different release」
delete the section 「ships no release section, but one is owed」

The dev's own reported ablation reproduced verbatim — same export, same direction wording. Lit control: the tarballs as published (17.4.0 ships no section) → exit 1 with the 「one is owed」 diagnosis. Dark controls: no args → exit 2 + usage; a nonexistent path → exit 2 「nothing was checked」. ⇒ not a bare non-zero anywhere.

2. Ships in the tarball AND the committed copy stays deterministic — both hold, which is the pair clause 1 buys. files[] lists it, the pack carried it, and the committed copy has zero release keys. Lit: with the section in the tree check:spec-changes → exit 1 「stale」; dark: after restore → exit 0. ⇒ the committed copy cannot carry the section without going red.

3. Generation really is publish-time only. Established by sweeping every workspace package.json for a build/prepack/prepublishOnly that invokes the generator (none), reading turbo's build outputs (only dist/**, json-schema/**), confirming neither release-lane script contains git checkout|stash|clean|reset|restore, and finding that @changesets/cli passes --no-git-checks, so the dirty tree packs.

4. The card's figures reproduced, and the one that did NOT is checked. 17.3.0 → 17.4.0: perMajor[16→17].converted 58→57, [REMOVED] prescriptions 156→221, export rows 5259→5433, true delta 225 added / 51 removed, no release key in either. The dev's own caveat holds: 「every conversion entry carries toMajor 17」 is true only inside perMajor[16→17] — the aggregate carries {11,13,14,15,17} — and the substance (nothing finer than a major) stands.

② Semver level

minor on @objectstack/spec and @objectstack/cli, as declared. Purely additive: an optional section, a new --json key, 7 new root exports; nothing renamed, retired or reshaped.

③ Boundary flags — PASS, and three of these are worth the next reader's time

  1. ⚠️ Withdrawals are invisible in the shipped artifact. The section carries ADR-0087 D4's four arrays, and the boundary 「an id that disappeared is deliberately NOT reported」 lives in a JSDoc and this PR's body — ⛔ not in the shipped spec-changes.json $comment and ⛔ not on the consumer docs page. Measured consequence for the very window this card is about: a consumer reads release.converted: [] as 「no conversion change」 while field-required-notnull-explicit actually left the chain.
  2. ⚠️ Two copies of the delta ship and only one is gated. At publish time aggregate.added / aggregate.removed are also filled with the same surfaces, under a from: 10, to: 17 record, while perMajor[16→17].added/removed stay 0/0. The gate verifies release.* only ⇒ a consumer reading aggregate.added reads a one-release slice labelled at major resolution.
  3. 「Previous」 is the last version published on npm, prereleases included. Harmless today (order == semver order across all 166 versions), but ADR-0087 D6 requires an RC cycle before every major, so the next major's section will read 18.0.0-rc.N → 18.0.0, ⛔ not 17.x → 18.0.0.
  4. The repair lane's backfill step runs --prepare + --attach with no --verify — Release page only, never the tarball.
  5. Clause ② is right and so is the arm: additive published surface ⇒ 「扩大公开面」 per SKILL.md:476, ⛔ not the :515 pull-back case. Clause-②: yes (widening) is line-anchored in both the PR body and the changeset; yes takes at least minor ✓; one arm; no governed surface in the 18-file list.
  6. The PR body's 「374 added」 is stale against this head (386 after the origin/main merge moved the surface). The seat owns that line and will correct it.

⭐ The specVersionGap question the seat left open — what the measurement supports, ⛔ not a ruling

Ruling clause 3 names specVersionGap, which has no live referent: #14261 renamed it protocolVersionGap and that changeset is still unconsumed in the tree, while published @objectstack/cli@17.4.0 has 2 hits for the old name and 0 for the new. The dev added a sibling, specReleaseChanges, rather than widening either.

The existing key's assertion is pinned twice — its header (「fires precisely when boot would refuse the app」) and the rename changeset's migration snippet, which tells consumers to gate on if (payload.protocolVersionGap). ⇒ making it non-null on a minor delta would make a key documented as 「boot would refuse」 fire for every app boot accepts, and would flip the boolean gate consumers were told to write.

The sibling reads the same release section (clause 3's operative words), is null when absent and never zeros. ⇒ the measurement supports the sibling. The naming question — the ruling says one thing and the tree has neither — is the seat's to settle, and is recorded here rather than decided by the round.

NOT MEASURED

The release lane itself (no seat may trigger release.yml); its local half was reproduced instead. The built dist/*.d.ts — whether the JSDoc boundary note reaches a consumer's declarations. check:generated / check:api-surface / check:entry-guard / check:adr-0087-registration locally (need dist or a full closure) — taken from CI's green at this head. Whether Validate Package Dependencies is branch-protection-required. The repair lane's checkout ref. objectstack-ai/ats — out of scope, not reached. Full local sweeps — CI's; only the two targeted suites ran here.

⚠️ Landing blocker, and it corrects an action the seat took

Validate Package Dependencies is still red at this head — for a reason outside this PR: OSV flagged devalue@5.9.0 on main itself. That is fixed and merged (5e0a1b9e023, 07:36:02Z).

The seat re-ran the failed job at 08:0xZ expecting the new base to clear it. That was wrong, and the round measured why: attempt 2 reuses the original merge ref, so its log still shows devalue 5.9.0 from a lockfile predating the fix. ⇒ a re-run cannot clear this; only a new commit merging origin/main will. A merge round is dispatched.

⚠️ That merge will MOVE THE HEAD, so this PASS will no longer name the landing head and a merge-delta review is owed before landing. ⭐ Lesson recorded: when the base has moved, merge main BEFORE dispatching the at-tier review, ⛔ not after.

Implemented-by: claude/issue-17080-per-release-spec-changes
Reviewed-by: session_01LvwGppdonww4zGLWZo5rho

VERDICT: PASS


Generated by Claude Code

…rted

The per-release section carries ADR-0087 D4's four arrays, and the boundary
they draw — an id that disappeared from the published chain is deliberately
NOT reported — lived only in `composeReleaseChanges`'s JSDoc and the PR body.
A consumer reads `release.converted: []` as "no conversion change" while an id
really did leave the chain between the two releases.

Say it where a consumer reads it: the shipped manifest's `$comment` and the
upgrading page's "what it does not tell you" callout, with the two-manifest
comparison that does answer the question. Documentation of an existing
boundary — no fifth array, no shape change, and the generator and the release
gate are untouched.

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

Copy link
Copy Markdown
Collaborator Author

✅ 达档合约复核 PASS —— 合并增量,点名落地 head 1737a24b510f1086af658c1351ad53ec85e1be7e

domain:spec 席位,session_01LvwGppdonww4zGLWZo5rho,2026-09-18T09:44Z。

本轮是合并增量复核:4f1724d33b9 上的 PASS 记录 5727299294 判过的一律不重开,范围只有合并提交 951f75c1f7a 加一笔 docs。⭐ 先并 main 再派复核,⛔ 不是先复核再并 —— 否则 PASS 不再点名落地 head,要再买一次复核。

档位核验(本席自取,⛔ 不采信复核自述)

复核轮 JSONL 抄本里逐条消息的 harness 盖章 message.model:claude-fable-5-1 89 / 89,off-tier 0,抄本 222 行。CONTRACT_REVIEW_TIER 即该档 ⇒ 达档。⛔ 未用派发时的 model 参数(那是请求,不是服务读数)。

复核自己去证的几条(摘,全文见下)

断言 复核的取法
合并没吞任何一侧 主侧 39 路径 / 分支侧 18 路径,comm -12 交集为空(亮对照:两个重叠表命中 b)⇒ 冲突结构上不可能。57 个路径的 blob 逐个对上,0 处不符
没有手工解决 git diff-tree -c 951f75c1f7a 。⭐ 亮对照是在合成仓里造的 —— 因为本仓 squash 合并,origin/main 最近 40 个合并同样给 0,真仓的 0 证明不了这条
两条 MIXED 免路由路径 dropped-refinements.baseline.jsonmigrations/registry.ts 在 base / branch / main / merge / head 五个点字节相同,两侧都没动
生成物无漂移 本地真 buildcheck:generated —— 15 个产物全部最新。⭐ 这比 PASS 强:PASS 那条取自 CI
OSV 解药确实由 base 到达 git merge-base --is-ancestor 5e0a1b9e023 951f75c1f7a exit 0(亮 89c6ec52b56 exit 0;暗 873e0e8e270 exit 1);head 锁文件解析 devalue@5.9.2
386 这个数 重取,⛔ 不转抄 —— 真 npm pack 拉published 17.3.0,head 树 pnpm pack,闸门对两个 tarball:exit 0,386 added, 302 removed。每次跑都 trap … EXIT INT TERM 还原,前后 hash 相等,git status --porcelain
head 上的 CI 43 行 = 39 success + 4 skipped,0 失败 0 在跑,每行 head_sha 都等于落地 head。必需上下文从 ruleset 自己读(12119582,strict=false):七条全 success

⭐ 复核还把撤回配方拿真产物跑了:published 17.3.0 对 head 清单,conversions 68 → 67,撤回的恰是 field-required-notnull-explicit;migrations 87 → 87。⇒ 文档里那条配方真的能回答它声称能回答的问题,不是一句安慰话。

⛔ 本席不采信复核的一条,并已当场证伪

复核 ③.5 写「正文尚未带 302 removed」。。取正文实测:302 在正文中出现 1 次,就在验证行上,原文 ✓ release 17.3.0 → 17.4.0 verified against both tarballs: 386 added, 302 removed, 0 converted, 0 migrated. ⇒ 该条作废;同一条的前半(374 → 386 是本席的更正且更正正确)复核测对了,保留。

⭐ 复核没看见、本席自己查出并已修的一个缺陷

本席上一次写那条更正时犯了 markdown 语境错:

  1. 更正按语被写进了 code fence 里面(围栏 68→73,按语在 72)—— 它会以等宽字面量渲染,读起来像是闸门自己打印的一部分;
  2. 更正把围栏内那行改成了 **386** added闸门从来没打印过星号。 围栏是逐字引用语境,往里塞 ** 就是把工具输出的引文改掉了

两条都已修:围栏内那行还原成闸门逐字打印的样子,按语移到围栏外面当散文。守卫脚本核过:围栏数 4 且成对、Clause-②: 行首仍恰好 1 条、302 removed 仍恰好 1 次、未新增尖括号。⇒ 这与本席此前踩过的 引用时间戳的 WAS 记号落进反引号那次是同一族错(引用语境里的标记不是标记)。

⭐ 顺带测到一条此前明说未测的事:正文重复 PATCH 不累积页脚 —— 本次写回字节数 8782,与本地算出的完全相等,平台没有再追加那 58 字节。此前只能说「只 PATCH 过一次,故未测」,现在测了。

③.3 已另立卡,⛔ 不挡本卡

复核点名 aggregate 切片在 major 分辨率上标注误导、且本 PR 把它推进了 npm tarball 的可达面(AGENTS.md 规则 4「机器可读面不得说谎」)。本席已立 #18978,并同意它不挡:裁决第 1 条要的就是发布期清单进 tarball 而这正是它的形状;since 在其自述分辨率上为真;上一版 tarball 带的是 aggregate.added: [],严格更差,正是本卡要终结的读数。


以下为达档复核记录原文,本席逐字采纳,⛔ 未编辑一字

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 1737a24b510f1086af658c1351ad53ec85e1be7e

Merge-delta review only: everything the PASS record 5727299294 judged at 4f1724d33b9 is closed and not reopened here. Scope = 4f1724d33b91737a24b510f1086af658c1351ad53ec85e1be7e, i.e. merge commit 951f75c1f7a (parents exactly 4f1724d33b9 and 89c6ec52b56, git rev-list --parents) plus one docs commit. Clone deepened by 200 before any negative was trusted; merge-base 625db0e8531 is 653 commits deep and is NOT in .git/shallow (lit: the first shallow entry IS listed there).

① Derived judgments

1. The merge dropped neither side — measured per path, both directions, with hitting controls.

  • Main-side git diff --name-only 4f1724d33b9 951f75c1f7a = 39 paths; branch-side git diff --name-only 89c6ec52b56 951f75c1f7a = 18 paths. comm -12 intersection = EMPTY (lit control: two overlapping lists hit b). ⇒ no path was touched on both sides, so a conflict was structurally impossible — 「zero conflicts」 confirmed from the tree, not the report.
  • Every one of the 39 main-side paths carries main's blob in the merge and every one of the 18 branch-side paths carries the branch's blob: 0 mismatches of 57 (lit control: the first main-side path vs the branch parent reads ABSENT ≠ merge blob).
  • git diff-tree -c 951f75c1f7a = EMPTY, i.e. no path differs from all parents — no hand resolution happened anywhere. Lit control in a synthetic repo: a hand-resolved conflict yields 1 combined-diff path; dark control: a clean two-sided merge yields 0. (Note: origin/main's last 40 merges yield 0 too — the repo squash-merges — which is why the synthetic control was needed.)
  • The two MIXED, hand-resolve-only paths carry the base bytes unchanged on all five points: packages/spec/dropped-refinements.baseline.json = 916e598cb16 and packages/spec/src/migrations/registry.ts = 09ff2453556 at base, branch, main, merge and head. Neither side moved them; nothing was resolved and nothing needed to be.
  • Generated drift: pnpm --filter @objectstack/spec check:generated run locally at the head after a real pnpm --filter @objectstack/spec build (both under os-verify-lock.sh, VERDICT command-exit 0, held 175s / 75s, no queue-timeout): 「✓ All 15 generated artifacts are up to date」 including check:spec-changes, check:migration-registry, check:api-surface, check:export-origins. This is stronger than the PASS, which took check:generated from CI.
  • The devalue remedy really arrived through the base: git merge-base --is-ancestor 5e0a1b9e023 951f75c1f7a exit 0 (lit: 89c6ec52b56 exit 0; dark: the newer main tip 873e0e8e270 exit 1); head's pnpm-workspace.yaml carries 'devalue@<6.0.0': '^5.9.2' and the lockfile resolves devalue@5.9.2.

2. Gate and generator untouched — confirmed with a hitting control. git diff 4f1724d33b9 1737a24b510 -- scripts/check-release-spec-changes.mjs packages/spec/src/migrations/spec-changes.ts packages/cli/src/utils/spec-release-changes.ts scripts/release-spec-changes.sh .github/workflows/release.yml = 0 bytes (lit: the same instrument against 89c6ec52b56 on the gate file alone = 690 lines). origin/main moved none of those paths either (git diff --stat 625db0e8531 89c6ec52b56 over them + build-spec-changes.ts = empty; lit: main did move scripts/check-adr-0087-registration.mjs, 342+/21−). The only generator change is packages/spec/scripts/build-spec-changes.ts 6+/1−, and I read the hunk: every changed line is inside the $comment: string-literal concatenation ('tool derive from this same data. ' +'withdrawn"; a withdrawal is visible only by comparing two published manifests.'). check-release-spec-changes.mjs --self-test: 15 batteries pass, exit 0. SpecReleaseChangesSchema at head names exactly fromVersion, toVersion, added, converted, migrated, removedno fifth array, and the generated section's keys read back as exactly those six.

3. The honesty fix — read as a consumer, it does prevent the misreading, not merely mention it. The shipped $comment now says, verbatim: 「converted: [] means "this release registered none", never "none was withdrawn"; a withdrawal is visible only by comparing two published manifests.」 That is the exact sentence a reader of release.converted: [] needs, in the first key of the file they open. The upgrading page's warn callout repeats it and adds the working recipe — compare .aggregate.converted[].conversionId / .aggregate.migrated[].migrationId across the two installed manifests. I ran that recipe on the real published 17.3.0 tarball against the head's committed manifest: conversions 68 → 67, withdrawn = exactly field-required-notnull-explicit, new = none; migrations 87 → 87, none either way (lit: the same reader finds 68 on the previous manifest). So the recipe answers the very question the PASS flagged. Two limits, stated not hidden: the release object carries no $comment of its own, so a machine that jqs straight to .release never sees the text — no comment can stop that, and the docs page is the second carrier; and the text covers withdrawals only (see ③.3).

4. The numbers at the new head — re-taken, not transcribed. npm pack @objectstack/spec@17.3.0 (real registry, version read back 17.3.0, no release key, aggregate.added/removed 0/0), unpacked, then at the head worktree tsx scripts/build-spec-changes.ts --previous-package …: release = { fromVersion: '17.3.0', toVersion: '17.4.0', added: 386, removed: 302, converted: 0, migrated: 0 }. Then pnpm pack of the head tree and the gate against both unpacked tarballs: exit 0, 「✓ release 17.3.0 → 17.4.0 verified against both tarballs: 386 added, 302 removed, 0 converted, 0 migrated.」 Lit control: the gate with the published 17.3.0 artifact as --published → exit 1, 「ships no release section, but one is owed」. Dark control: --check on the committed copy → exit 0 「up to date」. Every generator run restored packages/spec/spec-changes.json under trap … EXIT INT TERM, hash proven equal before/after (9dbc98682bfc…), git status --porcelain empty. ⇒ 386 is right; the PR body's 「374 added」 is stale and the seat's correction to 386 is correct. The body also does not yet carry 302 removed.

5. CI at the head, pinned to the sha. GET /commits/1737a24b510…/check-runs: 43 rows, 39 success + 4 skipped, 0 failures, 0 in progress, every row's head_sha = 1737a24b510f1086af658c1351ad53ec85e1be7e. (My earlier PR-level read at ~09:30Z showed 36 = 34 + 2; the 7 extra rows are second runs of the pull_request-edit-triggered jobs after the seat's body edit — Auto Label, Check PR Size, Check Changeset, the three claim/single-writer guards, Part-of — all success or skipped.) The four skipped: Auto Label (2nd run), Check PR Size (2nd run), Console Pin Gate, Packed-tarball smoke (opt-in). Required contexts read from the ruleset itself, GET /rules/branches/main → ruleset 12119582, strict=false: exactly the seven the round names — Lint & Repo Gates, TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guardall success. Validate Package Dependencies is success (id 105535698035) and is NOT a required context.

6. Carrier state. node scripts/pm/check-clause2-carriers.mjs --pair 18889 (read-only, 4 GETs): exit 0, declaration Clause-②: yes readable via correction 5725116531, both carriers agree, pair.1.head-sha = the head above. Governed-surface hits in the PR's 19-file list: 0 (lit: a list containing AGENTS.md hits 1). The PR's file list from the API matches the local 89c6ec52b56..head list exactly (19 = the branch's 18 + packages/spec/spec-changes.json, which joined in the docs commit).

② Semver level

minor on @objectstack/spec and @objectstack/cli, unchanged by the delta. The changeset .changeset/17080-per-release-spec-changes.md is byte-identical 4f1724d33b9..head (0-byte diff) and still declares both minor with Clause-②: yes (widening) line-anchored. The docs commit adds no key, no export and no schema change: the committed spec-changes.json changes only in its $comment string value, and check:spec-changes is green against it (deterministic, dark control exit 0). Main's side of the merge carries its own already-landed changesets (18102, 18406, client session envelope) — those are main's declarations, not this PR's.

③ Boundary flags

  1. Clause ② does not move. Clause-②: yes (widening) stands for the PR's own additive published surface (optional release section, specReleaseChanges key, 7 new root exports) — 「扩大公开面」, ⛔ not the SKILL.md:515 pull-back case. The delta itself adds nothing to that: check-widening-tells --declaration no over the docs commit alone (951f75c1f7a..head) = 0 tells, exit 0; over the whole delta (4f1724d33b9..head) the single tell is packages/spec/src/ui/view.zod.ts:1777 (style on ListMapConfigSchema), which is main's feat(spec): declare style on ListMapConfigSchema and point object-map.map back at it #18904 arriving through the base (1aa5026e30a, in the 39 main-side paths) — main's side is not this PR widening anything. Lit: the whole PR (89c6ec52b56..head) shows 14 tells, all the PR's own release schema and exports.
  2. The PR now edits the committed spec-changes.json (19 files, not the 18 the PASS counted). Only the $comment value moved; the generator and the committed copy agree; no shape change. Recorded so the next reader is not surprised by the extra path; not a defect.
  3. Honesty gap 2(b) — I agree it is a reach change, not a content change, and I agree it does not block; it must be filed. Measured: the aggregate line surfaceDiff = PREV_SURFACE ? diffSurfaces(PREV_SURFACE) : {} and composeSpecChanges(MIGRATION_SUPPORT_FLOOR, PROTOCOL_MAJOR, surfaceDiff) are identical at merge-base (lines 91/100) and head (189/198), and PREV_SURFACE is set whenever --previous-package is. At merge-base the lane already ran the generator with --previous-surface post-publish (release.yml:920), so the Release asset already carried this. What is new is that --prepare runs before changeset publish and writes into the tree that is packed: in my packed head artifact aggregate.added = 386 entries labelled since: 17 and aggregate.removed = 302 labelled removedIn: 17, name-for-name equal to release.added/removed, under from: 10, to: 17, while perMajor[16→17] stays 0/0. Why not blocking: ruling clause 1 orders the publish-time manifest shipped in the tarball, and this IS its shape; since is documented as 「The protocol major that added it」 (schema line 38), so the label is true at its stated resolution — what misleads is the completeness the 10→17 record implies, a pre-existing D4 design; the gated release section precedes it and the docs route a consumer to it; and the previous tarball carried aggregate.added: [], the strictly worse reading this card exists to end. Why it must be filed: AGENTS.md rule 4 「Machine-readable surfaces must not lie」 now reaches node_modules, and the $comment honesty fix says nothing about the aggregate slice where it could have in one sentence. Remedy candidates for that card: omit the surface diff from aggregate at publish time (keep it only in release), or state the slice's true bounds in data.
  4. The withdrawal recipe is verified true on real artifacts (68 → 67, field-required-notnull-explicit; migrations 87 → 87) — evidence for this PR's change, no card.
  5. PR body drift the seat owns: 「374 added」 → 386; 302 removed absent from the body's verification line. Nothing else in the body, the PASS record or the merge round's report 5727978261 was falsified by my measurements — every figure I re-took (39 files, 18 branch-side, empty conflict set, empty gate diff, 6+/1− inside $comment, 15 artifacts up to date, 386/302/0/0, 7 required contexts green, Validate Package Dependencies success) reproduced.

NOT MEASURED

  • The release lane itself (release.yml --prepare--verify → publish) — no seat may trigger it; its local half was reproduced (generate → pack → gate) instead.
  • packages/cli unit/integration suites and typecheck at the new head, locally — the delta's branch-side touches no cli file (docs page, generator $comment, committed manifest), so my method owed none; taken from CI Test Core (1..6/6) and Type Check · workspace success at the head.
  • The repo-wide gate farm locally (the 125 UNRUN families the round declared) — Lint & Repo Gates success at the head is CI's reading, not mine; locally I ran check:generated (15), the release-gate self-test (15 batteries), check-widening-tells and check-clause2-carriers.
  • GitHub Actions job logs — not read; every CI reading is from the check-runs API pinned to the sha and the ruleset API.
  • Behaviour of the merge-queue build against a main that has since moved (873e0e8e270 is already ahead of the PR base; ruleset strict=false) — the queue's own build is what measures that.
  • objectstack-ai/ats — out of scope, not reached.
  • Scratch left at /tmp/claude-0/-home-user/d31c56ec-952d-5a16-945f-6e430b9a6d34/scratchpad/pr-18889/ (worktree wt at the head, prev/ = unpacked 17.3.0, packed/ = my pack, logs); the worktree is registered in /home/user/objectstack/.git and can be removed with git worktree remove.

Implemented-by: claude/issue-17080-per-release-spec-changes
Reviewed-by: session_01LvwGppdonww4zGLWZo5rho

VERDICT: PASS


Generated by Claude Code

@os-litant
os-litant marked this pull request as ready for review September 18, 2026 09:45
@os-litant
os-litant enabled auto-merge September 18, 2026 09:45
@os-litant
os-litant added this pull request to the merge queue Sep 18, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 18, 2026
…w exports

`main` grew `packages/spec/api-surface-declarations/**` after this branch was
enqueued, so the queue build's `check:api-surface-declarations` saw this PR's
7 root exports as undeclared text. Regenerated with the remedy the gate prints;
no hand edit to a shard.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 2733f3f2495146867eba94dff1d0fce6b7dc14df

Merge-delta review only. Everything the PASS at 4f1724d33b9 (record 5727299294) and the merge-delta PASS at 1737a24b510 (record 5728233285) judged is closed and not reopened. Scope = 1737a24b5102733f3f2495146867eba94dff1d0fce6b7dc14df: merge commit 09ae986603f (parents exactly 1737a24b510f1086af658c1351ad53ec85e1be7e + f347c793e16322a4befc77651d1ab8760bf36874, git log --format=%p) plus regeneration commit 2733f3f2495. Clone deepened by 200 before any negative was trusted (.git/shallow 12 → 11 entries); merge-base(1737a24b510, f347c793e16) = 89c6ec52b56 before and after deepening; the window 89c6ec52b56..f347c793e16 is 10 commits, none in .git/shallow, and 89c6ec52b56 is an ancestor of both parents (merge-base --is-ancestor exit 0 both). Readings taken 2026-09-18T10:58Z–11:12Z against the fetched refs, never the shared checkout's working tree.

① Derived judgments — each claim of the round, re-measured

1. Merge was mechanical; no hand resolution; the MIXED paths arrived from main untouched — TRUE, but the round's "clean" needs one correction of instrument.

  • git merge-tree --write-tree 1737a24b510 f347c793e16 in the driver-registered clone = tree 56b011c172d459197a4d1fbfa736b3b8ed201dc3; the committed merge tree 09ae986603f^{tree} = the same hash; git diff --stat between them = EMPTY. The same probe from a bare --shared clone with NO merge.os-regen.driver registered (PROBE.git, per AGENTS.md §11) = exit 0, the same tree 56b011c172d…. ⇒ what was committed is byte-for-byte what git produces unattended, with and without the driver.
  • ⚠️ Unlike the previous merge-delta, the path sets are NOT disjoint: branch-side 89c6ec52b56..1737a24b510 = 19 paths, main-side 89c6ec52b56..f347c793e16 = 81 paths, comm -12 = .github/workflows/lint.yml and package.json (lit control: overlapping lists hit b). git diff-tree -c 09ae986603f therefore lists exactly those two, so the prior record's "combined diff empty" instrument would have flagged this round. The correct reading: both files equal a mechanical 3-way git merge-file of (branch, base, main) byte-for-byte (cmp clean, merge-file exit 0 = no conflict hunks), the PR's hunks survive (lint.yml:956 run: pnpm check:release-spec-changes; root package.json:74 the script) and main's hunks survive (main's removal of check:type-source-resolution — 0 hits at head, control 1 hit at 1737a24b510; main's 33+/31− lint.yml rewrite present). Two-sided automatic text merge, not hand resolution.
  • The two MIXED, hand-resolve-only paths: packages/spec/dropped-refinements.baseline.json = 916e598cb16 at base AND branch, 6622c962124 at main tip AND merge AND head; packages/spec/src/migrations/registry.ts = 09ff2453556 at base AND branch, b2d7a0d58d1 at main tip AND merge AND head. The branch never touched either (git diff --name-only 89c6ec52b56 1737a24b510 over both = EMPTY; control: that diff lists 19 paths); both carry main's blob unchanged. ✓
  • os-regen-pending: the driver runs only when BOTH sides change a routed path. The only both-sides paths are lint.yml and package.json, and git check-attr merge on them reads unspecified (control: api-surface-declarations/root.txt and spec-changes.json read os-regen). ⇒ the driver never ran in this merge, so no deferral could have been recorded. The marker itself lives in the round's $GIT_DIR and is NOT MEASURED; the structural argument is the reading.

2. .gitattributes read from the merged tree — TRUE. Row 147 at head is verbatim packages/spec/api-surface-declarations/** merge=os-regen; the only api-surface rows at head are 146 (api-surface/**) and 147; the api-surface-signatures.json row is gone (control: at 1737a24b510 it is row 150). Row 147 is identical on f347c793e16 and on current main 2767af8e8.

3. Regeneration touched root.txt only, +53/−2, no hand edit — TRUE as far as the tree can show; the "no hand edit" half rests on CI's gate at this sha, ⛔ not on my own regeneration (see NOT MEASURED).

  • git diff --numstat 09ae986603f 2733f3f2495 = exactly 53 2 packages/spec/api-surface-declarations/root.txt; GitHub's file row agrees (additions:53, deletions:2). No other path in the regen commit.
  • The 2 removed lines are the header counts # exported names: 212 / # declarations: 213219 / 220; the 51 other added lines are 7 // ── NAME (kind) ── blocks whose headers are byte-equal to the 7 rows the PR adds to api-surface/root.json (PreviousReleaseRegistries (interface), ReleaseSurfaceDiff (interface), SpecReleaseChanges (type), SpecReleaseChangesSchema (const), SpecReleaseSurface (type), SpecReleaseSurfaceSchema (const), composeReleaseChanges (function)); each is present exactly once in head's root.txt as a declaration header. root.json at head holds 220 rows; the header's 219 names / 220 declarations is the one dual-kind name this entry already carried (212/213 before, +7 both).
  • The queue's own gate output for the merge-group run 35331317491 (job Type Check · consumer gates, 09:55:02Z, queue branch gh-readonly-queue/main/pr-18889-f347c793e16…) names exactly these 7 as + added, 「declaration text changed: 0 removed, 7 added, 0 reshaped」, remedy pnpm --filter @objectstack/spec gen:api-surface-declarations — the regen diff is exactly that remedy's shape, nothing more.
  • 17 shards at head; git ls-tree head vs f347c793e16 differs in ONE blob only (root.txt 7ebf656f6da6c83181913b); the other 16 shards are main's blobs untouched. No .txt outside root.txt moved.
  • At head, CI's Type Check · consumer gates (the job that runs check:api-surface-declarations, a regenerate-and-compare against a fresh build) concluded success (id 105569853327, 10:49:26Z) on 2733f3f2495. That is the instrument that distinguishes "regenerated" from "hand-typed to look regenerated"; it is CI's reading pinned to the sha, not mine.

4 / 5. check:generated → 16, check:api-surface-declarations exit 0 (17 entry points, 5343 declarations) — NOT MEASURED by me (lock queue, see below). From the tree: check-generated.ts at head registers check:api-surface-declarations / gen:api-surface-declarations / api-surface-declarations/ (lines 121–123) and its GATED row count equals current main's (19 check: keys both). From CI at head: Lint & Repo Gates success 11:06:15Z, TypeScript Type Check success 10:54:38Z. The numbers 16 / 5343 are the round's; I did not reproduce them.

6. Release gate re-derived at this head, 386/302/0/0 unchanged — NOT MEASURED live; DERIVED to be unmovable. The gate reads only the two tarballs' api-surface/ rows, spec-changes.json and package.json version. git diff --stat 1737a24b510 2733f3f2495 -- packages/spec/api-surface/ packages/spec/export-origins/ = EMPTY (control: the same instrument over 89c6ec52b56..head = 2 files, +14), and the delta's only branch-side change is root.txt, which the gate never reads; the gate and generator scripts are unchanged in the delta. The prior record measured 386/302/0/0 on real tarballs at 1737a24b510 on these identical inputs. My own real run (npm pack of published 17.3.0 completed: prev version: 17.3.0, npm-pack-exit=0, shasum 1ca896ffbc05…) is queued behind the lock and had not reached the generator/pack/gate step when this report was forced; see NOT MEASURED for where its logs land.

7. Both merge sides survived — TRUE. 17/17 shards head vs main tip (control: 0 at 1737a24b510); api-surface-signatures.json absent at head and at f347c793e16, PRESENT at 1737a24b510 (control); packages/spec/src/shared/refinement-projection.ts head vs main tip = 0-byte diff (control vs 1737a24b510: 159 insertions); the 7 exports are 7/7 in api-surface/root.json and 7/7 in api-surface-declarations/root.txt; the PR's lint.yml step and package.json script survive main's rewrites of both files (item 1); main's 3 added lines in packages/spec/package.json (gen:/check:api-surface-declarations) are at head lines 266–267. Delta path set 1737a24b510..head (81 paths) equals main's window path set exactly (comm both directions empty); the PR's changeset .changeset/17080-per-release-spec-changes.md is 0-byte-different across the delta (control vs base: +48).

⭐ The dispute — the ROUND is right, the seat's dispatch was wrong for this window. git diff --stat 89c6ec52b56 f347c793e16 -- packages/spec/api-surface/ packages/spec/export-origins/ = EMPTY (exit 0, 0 lines). Two hitting controls on the same instrument: over packages/spec/ in the same window = 37 files (main moved api-surface-declarations/*.txt ×17, deleted api-surface-signatures.json, build-api-surface.ts 98±, registry.ts 47±, …); over the same two paths in the PR's own window 89c6ec52b56..1737a24b510 = 2 files, +14. An independent second instrument agrees: the queue build's own check:api-surface on the merge onto f347c793e16 printed 「@objectstack/spec public API surface unchanged ✓」 at 09:54:59Z, three seconds before the declarations gate failed. What main moved in this window is the declaration-TEXT family and the retired signatures file — not the export listing the 386/302 delta is computed from. The seat's 「main has moved the export surface since」 was true of the PREVIOUS window (625db0e8531..89c6ec52b56, 374 → 386) and was carried forward one window too far; the 386/302 counts were never at risk from this merge.

② Clause ② for the delta

The delta neither widens nor narrows; the PR's standing Clause-②: yes (widening) is unchanged and still the correct and sufficient declaration. The regenerated shard records the .d.ts text of 7 exports the PR had already added and the two prior records had already judged as 「扩大公开面」; it adds no export, no key, no closed-set member, no registration. api-surface-declarations IS in packages/spec files[] (published bytes), so the delta is the published artifact catching up to a surface already declared — SKILL.md:515's pull-back case does not apply (nothing is pulled back), and its 「卡面复述仍是条款②」 clause is why this stays under the existing yes rather than converting to no. Mechanically: check-widening-tells --declaration no over the delta alone = exit 0 with root.txt reported NOT MEASURED — by the #16045 ruling the declaration-text family is a declared non-surface for the tell gate (its own comment lines 355–365), so that exit 0 is evidence about no surface at all; the lit control over the whole PR vs main tip (f347c793e16..head) = exit 4, 14 tells (7 api-surface/root.json rows, migrations/index.ts:39–40, spec-changes.ts ×5), all the PR's own. Semver: minor on @objectstack/spec and @objectstack/cli, changeset byte-identical across the delta, Clause-②: yes (widening) line-anchored in changeset and PR body; check-clause2-carriers --pair 18889 exit 0 (PR body DECLARED yes (arm: widening), governing claim 5725112711).

③ Does the delta change any conclusion of the two prior PASSes? No.

Every artifact those records judged is either byte-identical across the delta (gate script, generator, spec-changes.ts, spec-release-changes.ts, release.yml, changeset, api-surface/, export-origins/, committed spec-changes.jsongit diff 1737a24b510 head over each = 0 bytes) or is main's own landed work arriving through the base. The one new artifact (root.txt) is the generated description of a surface both records already accepted.

Boundary flags — none blocks

  1. Path-set overlap on lint.yml / package.json (item 1): auto text-merge, proven equal to git merge-file and to driver-less merge-tree; both sides' hunks present. The prior record's diff-tree -c instrument is not sufficient on its own for a merge with overlapping paths — merge-tree equality is.
  2. Governed surface: check-governed-merges.mjs --test over the 20 effective paths (GitHub get_files list = local f347c793e16..head list, IDENTICAL) = exit 0 「NOT governed」; lit control with AGENTS.md = exit 3. Governed Surface Queue Guard success at head.
  3. CI at head, pinned to the sha (read 11:12Z): 47 check runs, 40 success + 7 skipped (Auto Label ×2, Check PR Size ×2, Packed-tarball smoke ×2, Console Pin Gate), 0 failure, 0 in progress; all seven required contexts success (Lint & Repo Gates 11:06:15Z, TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard). Validate Package Dependencies success. Ruleset required-set not re-read this round (taken from record 5728233285).
  4. Main has moved again: f347c793e16..2767af8e835 = 1 commit, 3 files (a plugin-hono-server fix + its changeset), none under packages/spec, none merge=os-regen; driver-less merge-tree of head onto 2767af8e835 = exit 0 clean. The queue will text-merge; the failure mode this round repaired (a new generated family landing on main) cannot recur from that commit. It CAN recur from any later main commit that registers another artifact family — the queue build is the only instrument for that.
  5. Regen commit trailer reads Co-Authored-By: Claude <noreply@anthropic.com> (harness form) + Claude-Session: — model-free; AGENTS.md exempts the harness-written trailer. Reporting only.
  6. PR body: still carries the seat's corrected 386 added, 302 removed line and 「all 16」 correction; the docs-drift bot recomputed on c38608877cc (head merged onto 2767af8e835) — informational.

NOT MEASURED

  • My own live regeneration, check:api-surface-declarations, check:generated and the real generate→pack→gate run at this head. They are scripted and running as a detached job (run.sh/inner.sh) that was still at lock queue position 1 (holder: another round's @objectstack/lint... build; 395s of budget left) when this report was forced out. npm pack of published 17.3.0 completed (exit 0, version 17.3.0). When it finishes, the readings land at /tmp/claude-0/-home-user/d31c56ec-952d-5a16-945f-6e430b9a6d34/scratchpad/pr-18889/: inner.log (exit codes), status-after-gen.txt + diff-after-gen.txt (EMPTY = regeneration is a no-op = no hand edit, my own instrument), check-decl.log, check-generated.log, gen-release.log (release: {added,removed,converted,migrated}), gate.log (gate-exit=), gate-ctrl.log (lit control, expect exit 1 「owed」), gate-dark.log (--check after restore, expect 0), restore.log (hash before/after + git status --porcelain), run.log ends with DONE. If the lock times out the wrapper exits 99 and lock-run.log says so; nothing ran unlocked.
  • The queue build itself and the release lane (no seat may trigger either).
  • The os-regen-pending marker in the round's own $GIT_DIR (structurally impossible here, item 1).
  • Full local suites/typecheck at head — CI's readings above, pinned to the sha.
  • The ruleset's required-context list — taken from the prior record, not re-read.
  • Scratch left behind, for the seat to remove once the job ends: worktree /home/user/objectstack-pr18889-cr (detached at head, git worktree remove --force), owned refs refs/pm-review/pr-18889-head and refs/pm-review/main-now in /home/user/objectstack/.git, bare probe …/scratchpad/pr-18889/PROBE.git, and the scratchpad logs/tarballs.

Implemented-by: claude/issue-17080-per-release-spec-changes
Reviewed-by: session_01LvwGppdonww4zGLWZo5rho

VERDICT: PASS — landing head 2733f3f2495146867eba94dff1d0fce6b7dc14df; the delta is exactly the gate's printed remedy and nothing else, both merge sides are byte-verified, Clause ② is unchanged at yes (widening), and the seat's 「main moved the export surface」 premise is falsified for this window (the round was right).


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

✅ 增量复核 PASS —— 落地 head 2733f3f2495146867eba94dff1d0fce6b7dc14df

domain:spec 席位,session_01LvwGppdonww4zGLWZo5rho,2026-09-18T11:14Z。

范围只有 1737a24b510 → 2733f3f2495:合并提交 09ae986603f 加重生成提交 2733f3f2495。前两条 PASS(5727299294 / 5728233285)判过的一律不重开。

档位核验(本席自取)

复核轮抄本逐条消息的 harness 盖章 message.model:claude-fable-5-1 99 / 99,off-tier 0。⇒ 达档。

⭐ 这一轮推翻了本席,本席照单认下 —— 而且它把本席错在哪指出来了

本席在派发令里写了「main 已经动了导出面」。假。 实测 git diff --stat 89c6ec52b56 f347c793e16 -- packages/spec/api-surface/ packages/spec/export-origins/ = ,两个命中对照:同窗口扫 packages/spec/ = 37 个文件;同两条路径扫本 PR 自己的窗口 = 2 个文件 +14。第二件独立仪器同向:队列构建自己的 check:api-surface 在 09:54:59Z 打印「public API surface unchanged ✓」——比声明闸红掉早 3 秒

⭐ 它还指出本席错在哪一步:那句话对上一个窗口(625db0e8531..89c6ec52b56,374 → 386)是真的,本席把它往前多带了一个窗口。⇒ 这不是笔误,是把一条过期的真话当成当前读数。386/302 从这次合并起就没有过风险。

⭐ 一处仪器升级,值得下一个人知道

这次合并的两侧路径集并不互斥 —— comm -12 命中 .github/workflows/lint.ymlpackage.json。⇒ 上一条记录用的 git diff-tree -c 为空来证「无手工解决」的仪器,在这种合并上会误报

本轮换了更强的:git merge-tree --write-tree 产出的树与已提交的合并树哈希逐字相等,并且在一个没有注册 merge.os-regen.driver 的裸 --shared 探针克隆里跑出同一个树。⇒ 提交的就是 git 无人干预时产出的东西,有无驱动都一样。两个重叠文件各自等于 git merge-file 的三方自动合并,两侧的改动都在。

diff-tree -c 为空只在路径集互斥时才够用;不互斥时要用 merge-tree 等值。

落地三前提,逐条

前提 读数
① 点名落地 head 的达档 PASS 本记录,点名 2733f3f2495
check-clause2-carriers --pair 18889 见下方本席重取
③ 受管面 check-governed-merges.mjs --test 覆盖 20 条有效路径 = exit 0「NOT governed」;亮对照加 AGENTS.md = exit 3

head 上 CI:47 个 check-run = 40 success + 7 skipped,0 失败 0 在跑,七条必需上下文全 success

条款②:不动,仍是 yes (widening)

重生成的分片只是把 PR 早已新增、且前两条记录已判过的 7 个导出的 .d.ts 文本记下来;不新增导出、不新增键、不新增闭集成员。⇒ 已发布产物追上一个已声明的面,⛔ 不是 SKILL.md:515 的拉回情形。

⛔ 本席没有回避的一条:那句「没手改分片」不是复核自测的

复核明说它自己的重生成还卡在锁队列里,这一半取自 CI 在该 sha 上的 Type Check · consumer gates(它本身就是「重新生成再比对」)。本席接受这条取法并写在明处:那正是能分辨「真重生成」与「手打得像重生成」的仪器,而它在 2733f3f2495 上是 success。⛔ 本席不把它说成复核亲测。


记录原文另贴一条,⛔ 原因写在明处

达档复核记录逐字发在紧接本评论之后的那一条。之所以拆成两条:该记录正文里带它自己读到的裸时间戳(它在自己那次取数时读到的时刻),与本评论的 {{NOW}} 同处一体会被 post-stamped 判为 MIXED拒写——那条拒绝是对的。

⛔ 本席不会为了让工具放行去改复核记录一个字:协议是逐字采纳或整条作废,没有第三种。⇒ 拆条,本席这条带自己的时钟,记录那条原样落地。


Generated by Claude Code

@os-litant
os-litant added this pull request to the merge queue Sep 18, 2026

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 2733f3f2495146867eba94dff1d0fce6b7dc14df

Merge-delta review only. Everything the PASS at 4f1724d33b9 (record 5727299294) and the merge-delta PASS at 1737a24b510 (record 5728233285) judged is closed and not reopened. Scope = 1737a24b5102733f3f2495146867eba94dff1d0fce6b7dc14df: merge commit 09ae986603f (parents exactly 1737a24b510f1086af658c1351ad53ec85e1be7e + f347c793e16322a4befc77651d1ab8760bf36874, git log --format=%p) plus regeneration commit 2733f3f2495. Clone deepened by 200 before any negative was trusted (.git/shallow 12 → 11); merge-base(1737a24b510, f347c793e16) = 89c6ec52b56 before and after deepening; the window 89c6ec52b56..f347c793e16 is 10 commits, none in .git/shallow; 89c6ec52b56 is an ancestor of both parents. Readings taken 2026-09-18T10:58Z–11:17Z against fetched refs, never the shared checkout's working tree. Heavy runs went through scripts/pm/os-verify-lock.sh (VERDICT command-exit 0, held 294s, waited 288s; nothing ran unlocked except the gate scripts the lock's own status text excludes).

① Derived judgments — each claim of the round, re-measured

1. Merge was mechanical; no hand resolution; the MIXED paths arrived from main untouched — TRUE, with one correction of instrument.

  • git merge-tree --write-tree 1737a24b510 f347c793e16 in the driver-registered clone = tree 56b011c172d459197a4d1fbfa736b3b8ed201dc3 = the committed merge tree 09ae986603f^{tree}; diff between them EMPTY. The same probe from a bare --shared clone with NO merge.os-regen.driver (AGENTS.md §11 method) = exit 0, same tree. ⇒ what was committed is byte-for-byte what git produces unattended, with and without the driver.
  • ⚠️ Unlike the previous merge-delta, the path sets are NOT disjoint: branch-side 89c6ec52b56..1737a24b510 = 19 paths, main-side 89c6ec52b56..f347c793e16 = 81 paths, comm -12 = .github/workflows/lint.yml and package.json (lit control hits b). git diff-tree -c 09ae986603f lists exactly those two, so the prior record's "combined diff empty" instrument would have flagged this round. Correct reading: both files equal a mechanical 3-way git merge-file (branch, base, main) byte-for-byte (cmp clean, exit 0 = no conflict hunks); the PR's hunks survive (lint.yml:956 run: pnpm check:release-spec-changes; root package.json:74) and main's survive (main's removal of check:type-source-resolution: 0 hits at head, control 1 at 1737a24b510; main's 33+/31− lint.yml rewrite present). Two-sided automatic text merge, not hand resolution.
  • MIXED paths: packages/spec/dropped-refinements.baseline.json = 916e598cb16 at base AND branch, 6622c962124 at main tip AND merge AND head; packages/spec/src/migrations/registry.ts = 09ff2453556 at base AND branch, b2d7a0d58d1 at main tip AND merge AND head. Branch never touched either (git diff --name-only 89c6ec52b56 1737a24b510 over both = EMPTY; control: 19 paths). ✓
  • os-regen-pending: the driver runs only when BOTH sides change a routed path; the only both-sides paths read merge: unspecified under git check-attr (control: root.txt and spec-changes.json read os-regen). ⇒ the driver never ran, so no deferral could exist. The marker in the round's $GIT_DIR itself is NOT MEASURED; the structural argument is the reading.

2. .gitattributes read from the merged tree — TRUE. Row 147 at head verbatim packages/spec/api-surface-declarations/** merge=os-regen; only api-surface rows at head are 146 and 147; the api-surface-signatures.json row is gone (control: row 150 at 1737a24b510). Row 147 identical on f347c793e16 and current main 2767af8e8.

3. Regeneration touched root.txt only, +53/−2, no hand edit — TRUE, proven by my own regeneration.

  • git diff --numstat 09ae986603f 2733f3f2495 = exactly 53 2 packages/spec/api-surface-declarations/root.txt; GitHub's file row agrees (additions:53, deletions:2). The 2 removed lines are the header counts 212/213219/220; the 51 other added lines are 7 // ── NAME (kind) ── blocks byte-equal to the 7 rows the PR adds to api-surface/root.json (PreviousReleaseRegistries (interface), ReleaseSurfaceDiff (interface), SpecReleaseChanges (type), SpecReleaseChangesSchema (const), SpecReleaseSurface (type), SpecReleaseSurfaceSchema (const), composeReleaseChanges (function)), each present once in head's root.txt; root.json holds 220 rows.
  • No hand edit, my instrument: in a detached worktree at the head, real pnpm --filter @objectstack/spec build (exit 0) then gen:api-surface-declarations (exit 0, 「Wrote api-surface-declarations/ (17 entry points, 5343 declarations, 12.08 MiB)」) → git status --porcelain 0 lines, git diff --stat 0 lines. Regenerating all 17 shards from a fresh build is a byte-for-byte no-op on the committed tree — a hand-typed shard cannot survive that.
  • The queue's own gate output on merge-group run 35331317491 (job Type Check · consumer gates, 09:55:02Z, branch gh-readonly-queue/main/pr-18889-f347c793e16…) names exactly these 7 as + added, 「0 removed, 7 added, 0 reshaped」, remedy gen:api-surface-declarations — the regen diff is exactly that remedy's shape. 17 shards at head; ls-tree head vs f347c793e16 differs in ONE blob (root.txt 7ebf656f6da6c83181913b).

4. check:generated — TRUE, re-measured: 「Checking 16 generated artifacts」 … 「✓ All 16 generated artifacts are up to date.」 exit 0, at head after the real build. check-generated.ts at head registers the declarations family (lines 121–123); its GATED row count equals current main's.

5. check:api-surface-declarations — TRUE, re-measured: 「@objectstack/spec declaration text unchanged ✓ (17 entry points, 5343 declarations)」 exit 0.

6. Release gate at this head — TRUE, re-derived from real artifacts, ⛔ not transcribed. npm pack @objectstack/spec@17.3.0 from the real registry (exit 0, version read back 17.3.0, shasum 1ca896ffbc05…), unpacked; at the head worktree tsx scripts/build-spec-changes.ts --previous-package … exit 0 → release = {fromVersion 17.3.0, toVersion 17.4.0, added 386, removed 302, converted 0, migrated 0}; pnpm pack of the head tree (exit 0), unpacked; node scripts/check-release-spec-changes.mjs --previous … --published …exit 0, 「✓ release 17.3.0 → 17.4.0 verified against both tarballs: 386 added, 302 removed, 0 converted, 0 migrated.」 Lit control: the published 17.3.0 as --published → exit 1 「ships no release section, but one is owed」. Dark control: --check on the restored committed copy → exit 0 「up to date」. spec-changes.json mutated (f17b29ec56e…) and restored under trap … EXIT INT TERM: hash before = after = 9dbc98682bfc131a8a18f41e971b27c86f99e8fa, git status --porcelain 0 lines. Also derivable: git diff --stat 1737a24b510 head -- packages/spec/api-surface/ packages/spec/export-origins/ = EMPTY (control over 89c6ec52b56..head = 2 files, +14), so the gate's inputs did not move.

7. Both merge sides survived — TRUE. 17/17 shards head vs main tip (control: 0 at 1737a24b510); api-surface-signatures.json absent at head and f347c793e16, PRESENT at 1737a24b510 (control); src/shared/refinement-projection.ts head vs main tip = 0-byte diff (control vs 1737a24b510: +159); 7/7 exports in root.json and 7/7 in root.txt; PR's lint.yml step and package.json script survive main's rewrites (item 1); main's 3 added packages/spec/package.json lines present (266–267). Delta path set 1737a24b510..head (81) equals main's window set exactly (comm both directions empty); the PR's changeset is 0-byte-different across the delta (control vs base: +48).

⭐ The dispute — the ROUND is right; the seat's dispatch was wrong for this window. git diff --stat 89c6ec52b56 f347c793e16 -- packages/spec/api-surface/ packages/spec/export-origins/ = EMPTY (exit 0, 0 lines). Hitting controls on the same instrument: over packages/spec/ in the same window = 37 files (main moved the 17 api-surface-declarations/*.txt, deleted api-surface-signatures.json, build-api-surface.ts 98±, registry.ts 47±, …); over the same two paths in the PR's own window 89c6ec52b56..1737a24b510 = 2 files, +14. Independent second instrument: the queue build's own check:api-surface on the merge onto f347c793e16 printed 「public API surface unchanged ✓」 at 09:54:59Z, three seconds before the declarations gate failed. What main moved here is the declaration-TEXT family and the retired signatures file, not the export listing the 386/302 delta is computed from — and my real re-run (item 6) confirms 386/302 unmoved. The seat's 「main has moved the export surface since」 was true of the PREVIOUS window (374 → 386) and was carried one window too far.

② Clause ② for the delta

The delta neither widens nor narrows; the standing Clause-②: yes (widening) is unchanged and remains the correct declaration. The regenerated shard records the .d.ts text of 7 exports the PR had already added and both prior records had already judged as 「扩大公开面」; it adds no export, key, closed-set member or registration. api-surface-declarations IS in packages/spec files[] (published bytes), so the delta is the published artifact catching up to a surface already declared — SKILL.md:515's pull-back case does not apply (nothing is pulled back), and its 「卡面复述仍是条款②」 clause is why this stays under the existing yes rather than converting to no. Mechanically: check-widening-tells --declaration no over the delta alone = exit 0 with root.txt reported NOT MEASURED (by the #16045 ruling the declaration-text family is a declared non-surface for the tell gate, its own lines 355–365), so that exit 0 is evidence about no surface at all; lit control over the whole PR vs main tip = exit 4, 14 tells, all the PR's own. Semver: minor on @objectstack/spec and @objectstack/cli, changeset byte-identical across the delta, Clause-②: yes (widening) line-anchored in changeset and PR body; check-clause2-carriers --pair 18889 exit 0.

③ Does the delta change any conclusion of the two prior PASSes? No.

Every artifact those records judged is byte-identical across the delta (gate script, generator, spec-changes.ts, spec-release-changes.ts, release.yml, changeset, api-surface/, export-origins/, committed spec-changes.json — 0-byte diffs 1737a24b510..head) or is main's own landed work arriving through the base. The one new artifact (root.txt) is the generated description of a surface both records already accepted, and regenerating it is a no-op.

Boundary flags — none blocks

  1. Path-set overlap on lint.yml / package.json (item 1): auto text-merge, proven equal to git merge-file and to driver-less merge-tree; both sides' hunks present. The prior record's diff-tree -c instrument is not sufficient alone on a merge with overlapping paths — merge-tree equality is.
  2. Governed surface: check-governed-merges.mjs --test over the 20 effective paths (GitHub get_files = local f347c793e16..head list, IDENTICAL) = exit 0 「NOT governed」; lit control with AGENTS.md = exit 3. Governed Surface Queue Guard success at head.
  3. CI at head, pinned to the sha (read 11:12Z): 47 check runs, 40 success + 7 skipped (Auto Label ×2, Check PR Size ×2, Packed-tarball smoke ×2, Console Pin Gate), 0 failure, 0 in progress; all seven required contexts success (Lint & Repo Gates 11:06:15Z, TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard). Validate Package Dependencies success.
  4. Main has moved again: f347c793e16..2767af8e835 = 1 commit, 3 files (plugin-hono-server fix + changeset), none under packages/spec, none merge=os-regen; driver-less merge-tree of head onto 2767af8e835 = exit 0 clean. The failure this round repaired cannot recur from that commit; it CAN recur from any later main commit registering another artifact family — the queue build is the only instrument for that.
  5. Regen commit trailer Co-Authored-By: Claude <noreply@anthropic.com> + Claude-Session: — model-free; harness form, exempt per AGENTS.md. Reporting only.
  6. PR body carries the seat's corrected 386 added, 302 removed and 「all 16」 lines; both now reproduced by my own run.

NOT MEASURED

  • The queue build itself and the release lane (no seat may trigger either); its local half (generate → pack → gate) was reproduced instead.
  • The os-regen-pending marker in the round's own $GIT_DIR (structurally impossible here, item 1).
  • Full local test suites / typecheck at head — CI's readings above, pinned to the sha; locally I ran the spec build, the two generators, check:api-surface-declarations, check:generated (16), the release gate with lit/dark controls, check-widening-tells, check-governed-merges --test, check-clause2-carriers.
  • The ruleset's required-context list — taken from record 5728233285, not re-read.
  • Scratch left for the seat to remove: worktree /home/user/objectstack-pr18889-cr (detached at head; git worktree remove --force), owned refs refs/pm-review/pr-18889-head and refs/pm-review/main-now in /home/user/objectstack/.git, and /tmp/claude-0/-home-user/d31c56ec-952d-5a16-945f-6e430b9a6d34/scratchpad/pr-18889/ (PROBE.git, prev/ = unpacked 17.3.0, packed/ = my pack, all logs with exit codes captured before any pipe).

Implemented-by: claude/issue-17080-per-release-spec-changes
Reviewed-by: session_01LvwGppdonww4zGLWZo5rho

VERDICT: PASS — landing head 2733f3f2495146867eba94dff1d0fce6b7dc14df; the delta is exactly the gate's printed remedy and regenerates as a byte-for-byte no-op, both merge sides are byte-verified, check:generated 16/16 and the release gate 386/302/0/0 reproduce on real artifacts at this head, Clause ② is unchanged at yes (widening), and the seat's 「main moved the export surface」 premise is falsified for this window (the round was right).


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

📌 复核记录升级 —— 同一裁断、更强的证据;并更正本席自己上一条记录里的一句话

domain:spec 席位,session_01LvwGppdonww4zGLWZo5rho,2026-09-18T11:19Z。升级后的记录原文见紧接本评论之前的那一条(5729240400)。

裁断没变:PASS,仍点名落地 head 2733f3f2495146867eba94dff1d0fce6b7dc14df。档位重取:claude-fable-5-1 105 / 105,off-tier 0。

⛔ 本席上一条记录(5729188327)里有一句现在不成立,当场更正

本席当时写:

那句「没手改分片」不是复核自测的……这一半取自 CI 在该 sha 上的 Type Check · consumer gates

那句话现在是过期的。 复核的重活当时卡在验证锁队列里;锁拿到后它自己跑了,并把三条此前列在 NOT MEASURED 里的读数补成了亲测:

此前 现在
check:generated = 16 —— 取自施工轮 亲测:「Checking 16 generated artifacts」「✓ All 16 generated artifacts are up to date」exit 0
check:api-surface-declarations —— 取自施工轮 亲测:「declaration text unchanged ✓ (17 entry points, 5343 declarations)」exit 0
发布闸门 386/302 —— 推导为不可动 亲测,真产物:真 npm pack 拉 published 17.3.0(shasum 1ca896ffbc05…)+ pnpm pack 本树 ⇒ exit 0「386 added, 302 removed, 0 converted, 0 migrated」。亮对照:拿 published 17.3.0 当 --published ⇒ exit 1「ships no release section, but one is owed」;暗对照:还原后 --check ⇒ exit 0

最要紧的一条:「没手改分片」现在是复核自己的仪器证的。 它在 head 的独立工作树里做了一次真构建,再跑 gen:api-surface-declarations 重新生成全部 17 个分片git status --porcelain 0 行git diff --stat 0 行

重生成是对已提交树的逐字空操作。手打出来的分片活不过这一关。 这比 CI 那条更强,因为它是在本席能复核的工具下、带前后哈希与 porcelain 证据跑出来的。

spec-changes.json 的变异/还原仍在 trap … EXIT INT TERM 下,前后哈希相等(9dbc98682bfc…),porcelain 空。

不变的部分

条款② 仍是 yes (widening),delta 既不放宽也不收窄;两侧合并逐字存活;⛔ 前两条 PASS 的任何结论都没被这次 delta 改动。

⭐ 以及那条方法学升级仍然成立,值得下一个人记住:本次合并两侧路径集并不互斥,所以 git diff-tree -c 为空不足以证「无手工解决」;要用 merge-tree --write-tree 的树哈希等值,并在一个没注册驱动的裸克隆里复现。

本席的收尾

PR 已重新入队 —— added_to_merge_queue 事件,2026-09-18T11:16:13Z(第二次;第一次在 10:07:20Z 被那道新闸踢出)。入队前本席按自己的教训先看了 main 的新落地:#18984#18994 都不碰声明分片、产物注册表、api-surface/export-origins/ ⇒ 上次那个失败模式不会从这两笔再来。

复核点名要本席清理的残留(独立工作树、两个 refs/pm-review/*、探针裸库与 scratch),本席清。


Generated by Claude Code

Merged via the queue into main with commit 8b48903 Sep 18, 2026
51 checks passed
@os-litant
os-litant deleted the claude/issue-17080-per-release-spec-changes branch September 18, 2026 11:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A consumer upgrading across a MINOR has no machine-readable answer to "what changed" — spec-changes.json is major-shaped and ships added/removed empty

2 participants