chore(spec): backfill nine ADR-0087 ledger entries missing from the v17 stock (#6350) - #7624
Conversation
…17 stock (#6350) The #6148 completeness gate is diff-only by design (#6129), so every breaking changeset already in the v17 train was never compared against the ADR-0087 ledger. This is the one-time manual reconciliation the maintainer ruled for. The gate is untouched and still judges diffs only. Measured today: 274 declared-breaking changesets in stock, 106 in the residue, 61 flagged as never having touched a ledger file. Judging all 61 by hand found nine genuinely missing entries, registered here as D3 semantic migrations, and each of the nine stock changesets now carries its adr-0087 disposition marker. Two had shipped half a retirement: tool.requiresConfirmation carries a live retiredKey() tombstone and SortNodeSchema carries aliases:{direction:'order'}, but neither had the ledger half. A retirement needs both. spec-changes.json and docs/protocol-upgrade-guide.md are regenerated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0198a2mSFFM18npoJfNtNv8z
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 7 release-owned page(s) also reference the affected code. These are read-only:
|
…wave as one v17 ADR-0087 entry (objectstack-ai#7630) (objectstack-ai#7927) PR objectstack-ai#7624's stock reconciliation judged eleven declared-breaking v17 changesets owed an ADR-0087 ledger entry and deliberately did not write one: the grouping was the open question, and the registries are consumed as a SET, so a wrong grouping produces no error anywhere. The maintainer ruled on objectstack-ai#7630 (2026-08-12) for one entry per major, mirroring the registry's only two precedents of this shape (`ui-schemas-strict-unknown-keys` at 15, `dashboard-widget-strict-unknown-keys` at 16), against the alternatives of eleven entries or a per-surface grouping. The eleven batches fold into a single D3 semantic migration, `authoring-schemas-strict-unknown-keys`, with the per-batch provenance written inside the entry so an archaeologist can still walk it batch by batch while an upgrader reads one prescription covering the whole wave. Each of the eleven stock changesets carries its `<!-- adr-0087: registered ... -->` disposition marker, so the judgement is recorded where the next auditor reads. `registry.ts`'s generated region, `spec-changes.json` and `docs/protocol-upgrade-guide.md` are regenerated, never hand-edited. The two adjacent seams recorded on objectstack-ai#7630 are deliberately untouched: the 45 `~` rows and the 7 borderline candidates PR objectstack-ai#7624 judged not-owed. Claude-Session: https://claude.ai/code/session_01MPerimH1rzBZRsQvk4VvBD Co-authored-by: Claude <noreply@anthropic.com>
Fixes #6350
One-time manual ADR-0087 ledger reconciliation over the v17 stock, per the maintainer ruling (comment 5250635216). ⛔ The gate is untouched and still judges diffs only — nothing in CI starts auditing inventory, for the ruling's stated reason (#6129).
Fresh measurement — today vs the stale card
Every number on the card was from 2026-08-09 and flagged as a floor. Re-measured on
origin/main@e124711:!)~)The set did not grow, so the full 61 were judged rather than sampled. The stock grew by 17 breaking changesets while the residue shrank — every one of those 17 landed carrying a disposition, which is the forward gate working.
?instead of!/~(the script says so itself and prints the fix). Counting wasgrep -c/wc -lthroughout, nevergrep | head. Arithmetic reconciles: 39 answered + 0 unpublished-exempt + 129 no-prescription-exempt + 106 residue = 274. No in-repo pin of these quantities exists — the classifier's arms are pinned (self-test S1–S7) but no count is; the only in-repo number ischeck-adr-0087-registration.mjs:1622's prose "227", which is explicitly a statement about filing time, not a ratchet.Positive controls — the scanner proved it can see before any zero was believed
Coverage was judged with a harness that separates ledger subjects (
id:/surface:/export const entry) from prose mentions (text inside another entry'sreason/acceptanceCriteria). That distinction is the whole point: it is exactly the confusion that gave the issue-number proxy precision 0/3.IStorageService(pinned as having an entry)semantic/17.storage-service-list-retired.ts:7 surface: 'contracts.IStorageService.list'HttpServer(pinned as lacking one)ctx.user.roles— drawn from the candidate set itselfsemantic/17.actor-user-roles-to-positions.tssurface matches the changeset's face verbatimRecordDetailsProps— the target's own defui/RecordDetailsProps:layoutfound;sections/hideFieldsabsentThe last two are controls drawn from the targets' own unique content, not generic ones: the same search, on the same def and the same corpus, finds real coverage where it exists.
Registered — 9 entries, with the evidence each was missing
runtime-httpserver-wrapper-retired(#5122)HttpServerdelegating wrapperHttpServer*hits are prose in other entries, one of which (http-server-runtime-vocabulary-retired) registers #5295'ssystem/http-server.zod.tsvocabulary — a different face. Sibling fixture R13 has the same shape and an entry.record-details-sections-object-form(#5611)RecordDetailsProps.sections+hideFieldsui/RecordDetailsProps:layout), proving the search reaches it;sections/hideFieldshave 0 subject hits. Authorable metadata on the publish/parse path.data-driver-query-omit-object(#5181)IDataDriverquery-parameter contractdata-driver-find-stream-retired— a different member. Two later, smaller driver-parameter changes (#6321, #6083) both registered and both cite #5181 as background; the larger sibling they derive from never got an entry.sort-node-direction-rejected(#4721)orderBy[].direction→orderdirectionis a common word, the textbook proxy trap. Half a retirement had shipped:SortNodeSchemacarriesaliases: { direction: 'order' }in code with no ledger half.tool-requires-confirmation-retired(#3715)tool.requiresConfirmationretiredKey()tombstone stands inai/tool.zod.ts:59with the full prescription, and 0 ledger subjects. Perregistry.ts: the tombstone is the proof the removal was declared, the ledger entry is the documentation channel — "a retirement needs both".export-axis-opt-in(#3544)allowExportunset flips to denyrest-requireauth-default-flip(12) andaction-descriptor-resume-authority-default-flip(17).apimethod-enum-shrink(#3543)enable.apiMethods8 legacy valueshistory→get,search→list), which is why it is semantic not mechanical.sharing-rule-recipient-reconcile(#1878)group/guest/ owner-typesharing-recipient-role-to-position= the role→position rename;sharing-rule-access-level-full-to-edit= access levels). Security surface: silent under-sharing.client-delete-result-success(#5638)DeleteDataResult.deleted→successif (r.deleted)has readundefinedsince it was written.Each of the nine stock changesets now carries its
<!-- adr-0087: registered … -->marker, so the judgement is recorded where the next auditor reads rather than only in a PR body.Reconciliation is measurable in the repo's own standing audit surface (
--audit-stock, before → after): answered 39 → 48, residue 106 → 97, flagged candidates 61 → 52.Judged NOT missing — 34 candidates, with what covers each
Full accounting of the 61: 9 registered + 34 not-missing + 11 owed-but-not-written + 7 borderline-not-owed = 61. All 61 were judged; 11 of the judgements were not acted on, and that is declared below rather than rounded to done.
Wrong channel — a pure def/export rename (7). Not my judgement call; the repo rules on it twice.
scripts/lib/renamed-defs.tslistsretiredKey()+ an ADR-0087 conversion as remedy #2 and rejects it: "semantically false… Registering it would pollute the ADR-0087 registries with a migration consumers must not run."migrations/registry.ts:4000: "A rename is not a retirement… an entry here would be a false claim that the contract shrank." Positive control: 4 of the 7 are literally in theRENAMED_DEFStable —action-location-dual-source-c17(:109),package-dependency-dual-source-c7(:120),http-method-defkey-collision(:139),field-mapping-tri-source-c12(:83–84). Of the rest,rate-limit-config-dual-source-c9is documented by name in that table's header as ABSORBED (renamed then retired in the same unreleased major; carried by theconnector-rate-limit-config-removedconversion, which is present);http-method-dual-source-c14re-exports an existing type with zero shape change;dual-source-cross-form-convergence'sTransformTypeSchemahalf is covered by the retired-defshared/FieldMappingTransform.Covered by a named existing entry (2).
tidy-donkeys-yawn([runtime]ctx.user的roles别名(值是positions)没有关闭日期 —— #5613 给 ctx.session 装了迁移窗口,同名同值的 ctx.user 面仍是无限期别名(observation) #6011) →actor-user-roles-to-positions, surfaceaction body / AI route: ctx.user.roles (req.user.roles)— an exact face match.!means "no commit that touched this changeset also touched a ledger file", and that is true here because the backfill landed in a separate PR (feat(spec): ADR-0087 台账登记 ctx.user.roles 的立即退役 (#6011) #6138) long after.!≠ omission, and this is the proof by construction.apis-loud-reject-and-apiregistry-retirement(声明式apis:(ApiEndpoint)入站面全链路零执行:元数据装载成功、路由从未挂载、matchEndpoint全仓无实现 #4936/ApiRegistry/api-registryplugin 只在packages/core/examples/里被装配,无任何真实 composition 挂载 ——ApiEndpointRegistrationSchema因此整面零执行 #4939) →declarative-apis-endpoints-live, which cites 声明式apis:(ApiEndpoint)入站面全链路零执行:元数据装载成功、路由从未挂载、matchEndpoint全仓无实现 #4936 by number and supersedes it: v17 ships the executor, so telling an upgrader "apis:is refused" would be false for the version they are upgrading to.Declared-only family removed — no author path, no consumer (14).
dev-plugin-protocol-family-removed,kernel-metadata-loader-envelope-removed,lifecycle-event-registry-enforced,plugin-lifecycle-hooks-retired,prune-dead-audit-config-cluster,prune-dead-capabilities-descriptor,prune-orphan-featureflag-schema,objectql-dead-use-retired,session-dual-source-c4,event-schema-dual-source-c6,environment-artifact-dual-source-c10,tenant-plan-dual-source-c16,etl-author-shape-aliases,retire-the-dev-stub-table. Each states its own exemption and gives the reason the exemption is real (no load path parses it, so no authored source can carry the keys); the two most explicit cite theplugin-runtime.zod.ts DynamicLoadingConfigprecedent (#3950). The ETL family additionally is registered, as retired-defsautomation/ETL*.Runtime behaviour or wire shape, with no authored metadata and no removed authorable key (11).
empty-capability-answers-501,retire-the-dispatcher-auth-mock,dispatcher-error-code-is-semantic,settings-error-details-declared-slot,findone-requires-a-predicate,deprecate-kernel-assignment-notifications,better-auth-1-7-0-rc-2-and-prod-dep-batch(a third-party upgrade shipping its own idempotent boot-time backfill),compose-stacks-i18n-single-valued,hook-context-api-scoped-contextandsubscribe-metadata-event-subject(both type narrowings — additive for already-correct code),action-param-option-visible-when(a capability opened, not removed).The #4001 unknown-key strictness wave and its authoring-gate neighbours:
unknown-key-strictness-tier-a,-step2,-automation-batch11,-ui-batch13,-ui-batch15,-ui-batch16,view-subblock-strictness-batch18,strict-automation-control-flow-state-machine,rare-jars-shave,user-filters-allow-add-tab-promote-and-close,view-union-identity-precondition.These are authorable metadata with real rename tables and 0 subject coverage, and the class is demonstrably registrable —
ui-schemas-strict-unknown-keys(major 15) anddashboard-widget-strict-unknown-keys(major 16) are exactly this shape. I judge them owed. I did not write them because the precedent registers a wave as one entry per major, not one per batch, and choosing the grouping myself would harden an invented shape into a set-consumed ledger where a wrong grouping produces no error anywhere. This is a maintainer call and is the single largest piece of remaining work on this card.Flagged rather than rounded:
http-server-config-retired(nine keys with a full FROM → TO table, but the changeset shows there was no authoring entry at all — they could not be written down),retire-dev-service-marker,durable-suspended-screen-refetch(a sync→async contract break),sys-comment-retire-visibility-reply-count,membership-grade-not-capability-channel,converge-activation-event-schema(defineStudioPlugin'sactivationEventsstring→object is authored),views-translation-key-runtime-identity(translation-bundle keys). Any of these could reasonably be ruled owed.Gates run (with invocation scope)
pnpm --filter @objectstack/spec run check:migration-registrypnpm --filter @objectstack/spec run check:spec-changespnpm --filter @objectstack/spec run check:upgrade-guidepnpm --filter @objectstack/spec run typechecktsc --noEmit+ scripts + test-typecheck)pnpm --filter @objectstack/spec exec vitest run src/migrations/migrations.test.tsnode scripts/check-adr-0087-registration.mjs --self-testnode scripts/check-adr-0087-registration.mjs --base origin/mainnode scripts/check-empty-changeset.mjs --base origin/mainChangeset included (
@objectstack/spec: minor) — the entries changespec-changes.json, the published upgrade guide, and whatos migrate metaemits.⛔ Untouched, per scope: the gate's diff-only behaviour,
packages/spec/src/**/*.zod.ts, andcontent/docs/releases/**.Could not verify
~rows were not examined. They are outside this card's!set, but~only means some commit touched a ledger file — it does not prove the entry covers that face. That is an unexamined seam of the same shape.ApiRegistrysub-face of 声明式apis:(ApiEndpoint)入站面全链路零执行:元数据装载成功、路由从未挂载、matchEndpoint全仓无实现 #4936 has no entry of its own; I judge it not owed (a registry class, not authored metadata) but it is the weakest of the not-missing calls.Generated by Claude Code