feat(spec): pin every export by its .d.ts declaration text, and retire the 27 signature hashes - #18971
Conversation
Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh Co-authored-by: Claude <noreply@anthropic.com>
…egistries Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check
What this run could not see
Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
|
Contract reviewServed-tier: 88/88 Tier was verified from the reviewing subagent's OWN transcript, not from its self-report: 88 per-turn model stamps read, all 88 equal to the constant above, zero stamps of any other value. A subagent cannot self-attest, because its ① Derived judgmentsThe ruling (
② Semver level
③ Boundary flags
Carrier clearBoth carriers are cleared on the strength of this record, in the protocol's order — record first, then the strip, so the gate is never open-with-nothing-behind-it: PR #18971 and card #16045. The reviewer's per-item verdicts, for audit:
Reviewer-declared instrument failures, carried rather than hidden: the Actions job-logs endpoint returned 403 through the proxy, so the new gate's CI result was bound through the job's CI on this head at 2026-09-18T0924Z: 31 success, 5 skipped, 0 failures, all converged, every run bound to this head. The earlier Implemented-by: VERDICT: PASS Generated by Claude Code |
…/main The declaration-text pins main brought in #18971 move with this branch's two facts: `source` becomes required wherever the predicate contract composes, and `cel` / `expression` now declare the `EvaluatedExpression` they always emitted. Exactly 8 distinct changed lines across all six files; the line count is those two facts repeated at every composing site. Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh Co-authored-by: Claude <noreply@anthropic.com>
|
Maintainer ruling C — this PR is being reverted (skills seat, Recorded verbatim on #16045 (the card this PR closed): the maintainer read this PR's size and review trail with the skills seat and chose, of three options, 「C」 — revert, and make consumer compilation against Generated by Claude Code |
…napshot Resolves 13 modify/delete conflicts under packages/spec/api-surface-declarations/. Every conflict has the same shape: this branch deletes the file (no stage 2), main regenerated it (stage 3). Retiring that directory is the revert's whole purpose, so each conflict resolves to the delete. All 17 shards are gone from the merged tree -- the 4 main did not touch auto-resolved to delete already. The one other overlapping path, scripts/pm/dispatch-gates.mjs, auto-merged: main's hunk sits about 1600 lines from the reverted one. Verified on the merged tree rather than assumed: - no code, script, workflow, gitattributes or package.json entry references api-surface-declarations in any spelling; the only three mentions left are historical prose in .changeset release notes (17108, 18991, 19085), reported separately and deliberately not edited here. - of the 31 paths the reverted commit touched, none still carries a line that commit added; the four that differ from its parent are later, unrelated work main landed (lint.yml keeps #18889's step; check-published-files, dispatch-gates and regen-artifacts carry post-revert commits). - api-surface-signatures.json is back with its 27 hashes and, built from these merged sources, check:api-surface reports the public API surface and factory signatures unchanged -- so the restored pin is correct, not merely present. - check:generated reports all 15 artifacts up to date; main's count is 16, and 16 is what #18971 made it when it registered check:api-surface-declarations. Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-Authored-By: Claude <noreply@anthropic.com>
Fixes #16045
Clause-②: yes (widening)
Ruled at
5560224701(director batch #60, 2026-09-06, maintainer verbatim 「同意」), re-affirmed by triage at5724532096: option A, a readable declaration-text snapshot, ⛔ not a hash. The card body's three mutually exclusive routes predate that ruling and were not re-litigated here.@objectstack/specpinned its public surface on one axis.api-surface/records each export asname (kind), and a signature change, a renamed interface field and a dropped union member move none of those rows. The only shape pin wasapi-surface-signatures.json: 27 rows, and reference-level even there. This addsapi-surface-declarations/, the declaration text the packed build actually emits for every export of every published entry point, and retires the 27 hashes it subsumes.The counts, re-derived on this head before the first generation
The ruling asks for this by name; the card's own numbers were self-declared unverified and 12 days old.
b33898f5d)exportsmap — those whoserequire.typesends in.d.ts. Adding or removing one such subpath.exportsmap entries./openapi.jsonand./package.json— asset subpaths with no declaration at all, filtered out by the same.d.tstestbuild-api-surface.tshas always applied. ⇒ premise 1 resolved: 17 is right and the map did not grow; 19 counts two things that were never entry points.name (kind)rows summed over the 17api-surface/shards. +27 since the card. Ratio unmoved: 27/5336 = 0.51%, so the headline 99.5% stands.api-surface-signatures.json. Bright control: the first value really is asha256:string, so this counts signature entries and not empty objects.Premise 3 also holds: all 17 packed
.d.tsfiles exist and resolve through the map (3,215,437 bytes for the root entry down to 13,081 for./integration). No entry point lacks a packed declaration, so the gap the dispatch reserved for itself did not open.What the artefact costs — premise 4, which nobody had costed
dist, so about +5.8% of tarballsystem.txt, 3,592,701 bytes / 73,283 linesEnvironmentArtifactSchema21,868,ObjectStackDefinitionSchemaandObjectStackSchema21,851,ChangeSetSchema20,395)Stated plainly, as the dispatch asks, and ⛔ not as a veto: the packed
.d.tsis a tsup dts rollup, so a Zod schema's declaration is its fully expanded structural type. That expansion is exactly what makes an inner field rename visible — and it is also why a single schema can produce a 21,000-line diff. The ruling's stated reason for choosing text over a hash is that the contract-review seat reads the diff; that reasoning holds per declaration and is worth a second look at the top twenty. One reading, for whoever wants it: 31% of declarations hold 97.7% of the bytes, so nothing cheap is available by trimming the tail.Both instruments, measured on one tree at one commit
The card's thesis is that the old pin cannot fail on a shape change. Not argued — ablated, with the mutation proven on disk by blob hash and the mutation proven to have reached
dist/before any verdict was read.A. the source-level control — a renamed interface field, the card's own class.
JobRunOutcome.reason?renamed todegradationReason?inpackages/spec/src/contracts/job-service.ts(blob363443e2tod4b1520c), spec rebuilt,ablation-dist-preflightexit 0 confirming the marker reached the built artefact:Restore leg: blob back to
363443e2, rebuilt,ablation-dist-preflight --absentexit 0 (marker gone from all 214 built files),git diff HEADclean, gate back to exit 0.B. the gate can fail on its own artefact. One field renamed inside
qa.txtby hand (blob3f5efb04to5b86fec2, injected occurrences 1, deleted text 0): exit 1, attributed toTestSuiteSchema (const), failure text naming the regenerate command. Restored to the HEAD blob,git diff HEADempty: exit 0.The retirement, and the coverage proof the ruling demands
All 27 signature names resolve to a declaration block in
api-surface-declarations/root.txt, 0 missing — enumerated fromdefineActionthroughdefineWebhook, each as(function).One honest qualification, because the subsumption is not uniform. For those 27 factory declarations the text is
declare function defineAction(config: z.input of ActionSchema): ActionParsed;— a type reference, exactly as blind to an inner-key narrowing astypeToStringwas. What is gained is not sharper text on the 27; it is the 5309 other declarations, includingActionSchemaitself, whose own expanded block is where such a narrowing shows up. So the retirement is a strict superset of pinned declarations, not an equal trade. Nothing published read the retired file — it was never in this package'sfiles[].Where it lands, and why there
packages/spec/scripts/build-api-surface-declarations.ts, beside the eight sibling artefact generators, reading the same input through the samecollectEntrieslogic. The ruling says "one generator script underscripts/"; this reads that as the directory the whole family lives in, because the artefact reads the built dist and only the lane that builds spec can run its gate.packages/spec/api-surface-declarations/ENTRY.txt, a sibling directory ofapi-surface/. Not inside it:listShardNamesthrows on any file in that directory that is not aNAME.jsonshard, soapi-surface/is closed by construction. No existingapi-surface/*.jsonis regenerated by this PR (check:api-surfacegreen throughout), which keeps it clear of PR feat(spec)!: a structured region body refuses a pause-capable node and an 'end' node #18688 and PR feat(spec)!: manifest.id enforces the reverse-domain rule its registry face already had #18319.check:api-surface-declarations, a step in lint.yml'sType Check · consumer gateslane after the two build steps, withcheck:api-surfaceand the other dist-reading gates. No new required context — a step in an existing lane. Registered in thecheck:generatedledger, inREGEN_ARTIFACTS, and in.gitattributesasmerge=os-regen.5715457322is answered by the layout rather than by an assumption — andcheck:merge-driver, which reconciles.gitattributesagainstREGEN_ARTIFACTSin both directions, is green over the swap.check:published-filesrefuses afiles[]entry that carries none; the registered line says what a consumer does with it — read two published tarballs and see which declared shape moved between releases, the questionapi-surfacecannot answer. If 1.02 MiB of tarball is judged too much, one line offiles[]removes it without touching anything else.Three registries had to learn about the new gate, each because it discovered the gate on its own rather than because a list named it:
check:published-files— demanded the reason above.scripts/pm/dispatch-gates.mjs— its live manifest edge gave the new gate a population before anything listed it, which is the eighth member of a class whose seventh was recorded the same way. Declared asCLASS_EIGHTH, with a case asserting the edge really reaches it.scripts/pm/check-widening-tells.mjs—PUBLISHED_SURFACESis derived fromREGEN_ARTIFACTS, so retiring the signatures row dropped it off that surface and reddened two self-test cases. Both are retargeted to state the retirement as a counterfactual (the surface follows the table, not a literal); ⛔ the new artefact is not added to that surface, because the ruling assigns "is a snapshot diff a Clause-② signal" to the skills seat by name and out of this card's scope. Both directions are now pinned, so the boundary is declared rather than forgotten. 483 cases pass, up from 481.Verification
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsfrom the merge base, 120 commands, every exit code redirected to a file and read back. All 120 green. Four returned exit 3 PREREQUISITE NOT MET on first pass (check:doc-formula-expressions,check:dual-build-cjs-loads,check:lean-entry-closure,check:type-check-debt); each names a build, each was built and re-run green, and none is recorded as a finding. Reconciled with--ran.@objectstack/speclocal project 488 files / 14,182 tests passed; the tooling suites that name the edited scripts, both projects, 10 files / 220 tests passed (sharded-artifacts,check-generated-ledger,dist-freshness,dist-freshness-adoption,api-surface-dual-kind-rows.pin,build-schemas-check-mode,def-key-collisions,root-index,export-list,docs-import-surface).pnpm --filter @objectstack/spec typecheckgreen.eslint . --no-inline-config --format jsonatb33898f5dexamined 6856 files, 0 errors, 0 warnings, exit 0. The population is eslint's own config resolution and the count is read from its JSON output; type-aware linting is not enabled ineslint.config.mjs(noparserOptions.project, no typed rules), so this diff cannot move an untouched file's verdict either way.check:nul-bytesgreen over 8906 files, plus a direct scan of all 31 changed paths for the wider control-byte class — no matches.scripts/check-single-claim-paths.mjsin the diffstat is not mine: it arrived with the one-commitorigin/mainmerge (16cb493d5) this PR carries.Acceptance notes
.claude/skills/spec-property-retirement/SKILL.mdline 124 listsapi-surface-signaturesas an instance of a retirement shape, and that row goes stale with this landing. ⛔ Left untouched on purpose:.claude/**is a governed surface, so editing it would make this whole PR maintainer-landed for a one-word prose nit. Noted, not filed.packages/spec/scripts/build-schemas.tsline 830 carries the same stale mention. Left untouched because PR feat(spec)!: publish the two named refinement patterns the runtime already enforces #18952 holds that file; noted, not filed, with the later lander as the natural carrier.scripts/pm/check-widening-tells.mjs(PR fix(pm): check-widening-tells reports a key re-typed INTO a universal acceptor #18948),scripts/pm/dispatch-gates.mjs(PR skills(pm-dispatch): key the clause-② contract review by lane — spec and skills owe it on every round, other lanes owe none #18903) and.github/workflows/lint.yml(PRs chore(gates): retire check-type-source-resolution (maintainer ruling E on #18373) #18946, feat(spec): ship a per-release section in spec-changes.json, verified against both tarballs #18889, feat(scripts): refuse an undeclared mode-160000 gitlink in the index #18414). All are hand-written files where a text conflict is visible rather than silent, and all three of my hunks are small and far from theirs. Whoever lands second resolves.维护者速读(草稿)
改了什么。
@objectstack/spec从今天起为它的每一个公开导出留一份"形状快照" —— 不是哈希,而是打包后.d.ts里那段声明原文,按入口点分成 17 个文件签入仓库,并配一道 CI 闸门:重新生成后对不上就红,失败信息里直接给出重新生成的命令。同时退休了旧的 27 条签名哈希文件。为什么改。 原来的 pin 只记"某个名字还在不在",5336 行里只有 27 行能看出"形状变没变"。也就是说:把一个接口字段改名、砍掉一个联合成员、改一个函数签名 —— 这些都是会让客户升级后编译失败的破坏性改动 —— 全部一路绿灯。本次 PR 里有实测:改了
JobRunOutcome的一个字段名之后,旧闸门check:api-surface退出码 0(看不见),新闸门退出码 1(点名了那个 interface)。路线是 2026-09-06 决策批次 #60 里您逐字「同意」的那一条。风险与代价(含回滚)。 代价是体积:12.08 MiB 文本、23.7 万行,压缩后 1.02 MiB,相当于 npm 包增长约 5.8%。更值得注意的是分布极不均匀 —— 最大的 4 个 schema 各自超过 2 万行声明文本,一旦它们变动,复核席位面对的是一份 2 万行的 diff;而裁决选"文本不选哈希"的理由恰恰是"diff 可读"。这一点我按实测如实报告,未自行改动路线。回滚成本很低:从
files[]去掉一行即可停止随包发布;整道闸门回滚就是撤销本 PR,不留任何数据迁移。席位意见。
你要做的。 只有一件事需要您判断:12 MiB / 23.7 万行这个量级,以及最大 4 个 schema 的 diff 可读性,是否仍符合当初选 A 方案时的预期。若认为需要收窄,那是裁决层面的一次增补,不是本 PR 的返工。其余部分已按裁决落地并自证。
Generated by Claude Code