feat(spec): ship a per-release section in spec-changes.json, verified against both tarballs - #18889
Conversation
`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>
📓 Docs Drift CheckThis PR changes 2 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 142 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 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
|
…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>
…r-release-spec-changes
Contract reviewServed-tier: 100/100 ⭐ 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 judgments1. 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
⭐ 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. 3. Generation really is publish-time only. Established by sweeping every workspace 4. The card's figures reproduced, and the one that did NOT is checked. 17.3.0 → 17.4.0: ② Semver levelminor on ③ Boundary flags — PASS, and three of these are worth the next reader's time
⭐ The
|
…r-release-spec-changes
…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>
✅ 达档合约复核 PASS —— 合并增量,点名落地 head
|
| 断言 | 复核的取法 |
|---|---|
| 合并没吞任何一侧 | 主侧 39 路径 / 分支侧 18 路径,comm -12 交集为空(亮对照:两个重叠表命中 b)⇒ 冲突结构上不可能。57 个路径的 blob 逐个对上,0 处不符 |
| 没有手工解决 | git diff-tree -c 951f75c1f7a 空。⭐ 亮对照是在合成仓里造的 —— 因为本仓 squash 合并,origin/main 最近 40 个合并同样给 0,真仓的 0 证明不了这条 |
| 两条 MIXED 免路由路径 | dropped-refinements.baseline.json 与 migrations/registry.ts 在 base / branch / main / merge / head 五个点字节相同,两侧都没动 |
| 生成物无漂移 | 本地真 build 后 check: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 语境错:
- 更正按语被写进了 code fence 里面(围栏 68→73,按语在 72)—— 它会以等宽字面量渲染,读起来像是闸门自己打印的一部分;
- 更正把围栏内那行改成了
**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 = 4f1724d33b9 → 1737a24b510f1086af658c1351ad53ec85e1be7e, 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-sidegit diff --name-only 89c6ec52b56 951f75c1f7a= 18 paths.comm -12intersection = EMPTY (lit control: two overlapping lists hitb). ⇒ 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=916e598cb16andpackages/spec/src/migrations/registry.ts=09ff2453556at 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:generatedrun locally at the head after a realpnpm --filter @objectstack/spec build(both underos-verify-lock.sh, VERDICT command-exit 0, held 175s / 75s, no queue-timeout): 「✓ All 15 generated artifacts are up to date」 includingcheck:spec-changes,check:migration-registry,check:api-surface,check:export-origins. This is stronger than the PASS, which tookcheck:generatedfrom CI. - The devalue remedy really arrived through the base:
git merge-base --is-ancestor 5e0a1b9e023 951f75c1f7aexit 0 (lit:89c6ec52b56exit 0; dark: the newer main tip873e0e8e270exit 1); head'spnpm-workspace.yamlcarries'devalue@<6.0.0': '^5.9.2'and the lockfile resolvesdevalue@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, removed — no 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 Guard — all 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
- Clause ② does not move.
Clause-②: yes (widening)stands for the PR's own additive published surface (optionalreleasesection,specReleaseChangeskey, 7 new root exports) — 「扩大公开面」, ⛔ not theSKILL.md:515pull-back case. The delta itself adds nothing to that:check-widening-tells --declaration noover the docs commit alone (951f75c1f7a..head) = 0 tells, exit 0; over the whole delta (4f1724d33b9..head) the single tell ispackages/spec/src/ui/view.zod.ts:1777(styleonListMapConfigSchema), which is main's feat(spec): declarestyleonListMapConfigSchemaand pointobject-map.mapback 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 ownreleaseschema and exports. - The PR now edits the committed
spec-changes.json(19 files, not the 18 the PASS counted). Only the$commentvalue 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. - 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) : {}andcomposeSpecChanges(MIGRATION_SUPPORT_FLOOR, PROTOCOL_MAJOR, surfaceDiff)are identical at merge-base (lines 91/100) and head (189/198), andPREV_SURFACEis set whenever--previous-packageis. At merge-base the lane already ran the generator with--previous-surfacepost-publish (release.yml:920), so the Release asset already carried this. What is new is that--prepareruns beforechangeset publishand writes into the tree that is packed: in my packed head artifactaggregate.added= 386 entries labelledsince: 17andaggregate.removed= 302 labelledremovedIn: 17, name-for-name equal torelease.added/removed, underfrom: 10, to: 17, whileperMajor[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;sinceis 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 gatedreleasesection precedes it and the docs route a consumer to it; and the previous tarball carriedaggregate.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 reachesnode_modules, and the$commenthonesty fix says nothing about the aggregate slice where it could have in one sentence. Remedy candidates for that card: omit the surface diff fromaggregateat publish time (keep it only inrelease), or state the slice's true bounds in data. - 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. - PR body drift the seat owns: 「374 added」 → 386;
302 removedabsent from the body's verification line. Nothing else in the body, the PASS record or the merge round's report5727978261was 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 Dependenciessuccess) 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/cliunit/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 CITest Core (1..6/6)andType Check · workspacesuccess at the head.- The repo-wide gate farm locally (the 125 UNRUN families the round declared) —
Lint & Repo Gatessuccess at the head is CI's reading, not mine; locally I rancheck:generated(15), the release-gate self-test (15 batteries),check-widening-tellsandcheck-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 (
873e0e8e270is 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/(worktreewtat the head,prev/= unpacked 17.3.0,packed/= my pack, logs); the worktree is registered in/home/user/objectstack/.gitand can be removed withgit worktree remove.
Implemented-by: claude/issue-17080-per-release-spec-changes
Reviewed-by: session_01LvwGppdonww4zGLWZo5rho
VERDICT: PASS
Generated by Claude Code
…r-release-spec-changes
…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
Contract reviewServed-tier: Merge-delta review only. Everything the PASS at ① Derived judgments — each claim of the round, re-measured1. Merge was mechanical; no hand resolution; the MIXED paths arrived from main untouched — TRUE, but the round's "clean" needs one correction of instrument.
2. 3. Regeneration touched
4 / 5. 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' 7. Both merge sides survived — TRUE. 17/17 shards head vs main tip (control: 0 at ⭐ The dispute — the ROUND is right, the seat's dispatch was wrong for this window. ② Clause ② for the deltaThe delta neither widens nor narrows; the PR's standing ③ 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, Boundary flags — none blocks
NOT MEASURED
Implemented-by: VERDICT: PASS — landing head Generated by Claude Code |
✅ 增量复核 PASS —— 落地 head
|
| 前提 | 读数 |
|---|---|
| ① 点名落地 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
Contract reviewServed-tier: Merge-delta review only. Everything the PASS at ① Derived judgments — each claim of the round, re-measured1. Merge was mechanical; no hand resolution; the MIXED paths arrived from main untouched — TRUE, with one correction of instrument.
2. 3. Regeneration touched
4. 5. 6. Release gate at this head — TRUE, re-derived from real artifacts, ⛔ not transcribed. 7. Both merge sides survived — TRUE. 17/17 shards head vs main tip (control: 0 at ⭐ The dispute — the ROUND is right; the seat's dispatch was wrong for this window. ② Clause ② for the deltaThe delta neither widens nor narrows; the standing ③ 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, Boundary flags — none blocks
NOT MEASURED
Implemented-by: VERDICT: PASS — landing head Generated by Claude Code |
📌 复核记录升级 —— 同一裁断、更强的证据;并更正本席自己上一条记录里的一句话
裁断没变:PASS,仍点名落地 head ⛔ 本席上一条记录(
|
| 此前 | 现在 |
|---|---|
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
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-surfacerow convention (
ENTRY: NAME (KIND)— entry point, export name, export kind — the spellingbuild-spec-changes.tsflattens to):spec-changes.json→aggregate.added/.removedprotocolVersionperMajor[16→17][REMOVED]prescriptions injson-schema/**Every figure the card reports reproduced. The shipped manifest answered
0 / 0over a releasethat moved 276 exports, and a consumer reading semver sees a minor.
What lands
1 · A per-release section, shipped in the tarball.
spec-changes.jsongainsrelease—fromVersion→toVersionat package-version resolution, withadded/removed(the exportsthat arrived and left, each named) and
converted/migrated(the ADR-0087 D2/D3 entries firstregistered in that release). Generated at publish time only, from the previously published
tarball: the committed copy stays the deterministic registry projection and
check:spec-changesis 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.mjsdeliberately does not import the generator'sflattening, 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 --jsonreads the same data. New keyspecReleaseChanges—fromVersion,toVersion, the four counts and the file it read. It is a sibling ofprotocolVersionGap,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
specVersionGapis spelledprotocolVersionGapin this tree — itwas 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, changesetcarrying the migration-free declaration, and
content/docs/upgrading.mdxgains What theinstalled 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
releaseis indistinguishable from "this release changed nothing", which isthe 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.
specReleaseChangesisnullin exactly those cases.Verification
Real artifacts, not fixtures —
npm packof published 17.3.0,pnpm packof this tree:domain:specseat — this line read 「374 added」 when the PR opened.origin/mainhas since moved the export surface, and 386 is the number measured at the current head1737a24b510from a realnpm packof the published 17.3.0, independently confirmed by the at-tier review at the previous head.os-dev.md:56reserves 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.addedin the packedmanifest (mutation proven on disk: sha256
bc21927e…→3bbff365…), restored undertrap … EXIT INT TERM, hash verified equal after: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:generated— all 16 artifacts up to date aftergen:api-surface+gen:export-origins+gen:api-surface-declarations(7 new declarations onthe root entry, 0 removed).
domain:specseat: this read 「all 15」 when the PR opened. The count movedbecause feat(spec): pin every export by its .d.ts declaration text, and retire the 27 signature hashes #18971 (
d8b12fca97c) registeredcheck:api-surface-declarationsas a new generatedartifact family. The gate now prints 「Checking 16 generated artifacts」. ⛔ The 15 was not wrong
when written — the base moved under it.
os-dev.md:56reserves this body to the PR-open write, sothe 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 modulefor packages this worktree had not built).pnpm check:entry-guard— exit 0 (265 files, 205 exporters inert on import). This one caught areal defect in the new gate: it exports
verifyReleaseand dispatched at the top level, soimporting it would have run it. Fixed with
isEntrypoint.Gate families, derived by
scripts/pm/dispatch-gates.mjsfrom 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, whoseown 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-changesandcheck:skill-examplesdeserve a word each: the first is green, which is the determinismhalf 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
release.converted. Between 17.3.0 and 17.4.0perMajor[16→17].convertedwent 58 → 57:field-required-notnull-explicitleft the publishedchain. 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.
claim to close it. Of the 65 new
[REMOVED]prescriptions between 17.3.0 and 17.4.0, exactlyone names 17.4.0 as the retiring release.
release.removedanswers "which exports left in thisrelease" 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.
toMajor: 17" holds insideperMajor[16→17]; theaggregate carries
toMajorvalues 11, 13, 14, 15 and 17. The substance — no resolution finerthan a major — reproduced.
Generated by Claude Code
Generated by Claude Code