refactor(spec): split the migration registry's three append tables into per-entry files (#7297) - #7454
Conversation
…to per-entry files (#7297) The registry half of #6957's 2026-08-10 ruling, option (a): per-card entry files concatenated by a generator, the `.changeset/*.md` shape. `registry.ts` carried three hand-authored APPEND tables — each step's `semantic` list, `RETIRED_KEYS_BY_MAJOR` and `RETIRED_DEFS_BY_MAJOR`. Every retirement card appended to the same tail line of the same two of them, so two cards in one window were a textual conflict by construction: `step17`'s semantic list and `RETIRED_KEYS_BY_MAJOR[17]` conflicted in 6 of 11 contended re-merge laps, 613 hand-resolved lines in four days. Both tables are consumed as SETS, so a resolution that drops a sibling's entry produces no error anywhere — that silent drop, not wall-clock, is what this removes. Entries now live one file per entry under `src/migrations/entries/` (59 semantic + 16 retired keys + 45 retired defs), concatenated into `registry.ts`'s `<os-generated …>` regions by `gen:migration-registry` and verified by `check:migration-registry`. The filename is a pure function of the entry id; order is derived by sorting on it; there is deliberately no index. This commit is MECHANICAL: every exported value is identical entry-for-entry, and the two projections reorder without changing a byte of content (their line multisets are unchanged). `scripts/adr-anchors/` (#7301) is the pilot mirrored. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Py3V8MDCEEYhZrR3NvNVu5
…nces at entry ids (#7297) Twelve notes in the split-out entries located a sibling by POSITION — "the entry above", "the notification pair above", "the trio at the top of this list", "`etl-pipeline-layer-retired` below". Position was a fact about append order, and append order is exactly what the split replaced with a derived sort, so each of these is now either wrong or about to be. Each is rewritten to name what it means: the sibling's id, the table it lives in, or — for the three copies of the "no backticks in `surface`" note, which pointed at whichever entry happened to carry the long form — the reason itself, inline. Content-only; the id set, every table's membership and the public API are untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Py3V8MDCEEYhZrR3NvNVu5
|
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:
|
Closes #7297. The registry half of #6957's 2026-08-10 ruling, option (a): per-card entry files concatenated by a generator, the
.changeset/*.mdshape.scripts/adr-anchors/(PR #7301) is the pilot this mirrors.Scope reading — three tables, not two; migrations only
The card's title says "two append tables".
migrations/registry.tshas three hand-authored append surfaces, and the #6957 measurement names two of them (step17'ssemanticlist andRETIRED_KEYS_BY_MAJOR[17]). The third,RETIRED_DEFS_BY_MAJOR, is appended by the same retirement cards — #4914 wrote to all three in one PR — so splitting two of three would have left the card's own conflict surface half-open. All three are split.conversions/registry.tsis not in this PR. The ruling names "both hand-authored append registries", and the other one it names isscripts/adr-anchors.json, already done by #7301. Per the card's "when in doubt, migrations only", a conversions split is a follow-up with its own measurement; the reasoning is recorded on itsNOT_DRIVER_MANAGEDentry so the next reader does not re-derive it.Design, and the one place it is forced away from the pilot
scripts/adr-anchors.mjsassembles its shards withreaddirSyncat read time, so no aggregate is checked in at all. That option does not exist here:MIGRATIONS_BY_MAJOR/RETIRED_*_BY_MAJORare re-exported from@objectstack/spec's root barrel and reach browser bundles through it, and spec'ssrc/is deliberately free of node builtins. A bundled library needs a static module graph, and a static graph over N entries needs one file that names all N.So the concatenation lands where it already was — inside
registry.ts, between<os-generated …>markers — which is also what keeps the blast radius at zero:check-adr-0087-registration.mjsstill reads everyid:line out ofLEDGER_SOURCES,composeSpecChangesstill folds the same objects, the public API is unchanged, and the review diff the ruling explicitly paid for is exactly the diff it was before. There is no index file: order is derived by sorting on the entry id, and the directory listing is the index.Identity proof — the split moves entries, it does not change them
Commit 1 is mechanical. Both tables are consumed as sets, so the proof is set-and-content identity, run by deep-comparing the exported values before and after:
Every entry id present on both sides; every entry's content deep-equal to its pre-split self; every region sorted by id. The 108 inline
//notes attached to table entries are preserved exactly (before=108 after=108 identical=True) — a note that introduced a run of keys is re-anchored to the lexicographically smallest key of its run, so it still leads its own group under the derived order.Re-measured costs (
mainmoved ~30 merges since the card's measurement):migrationIdoccurrences change position)LEDGER_SOURCEStaught to read a directoryspec-changes.jsonanddocs/protocol-upgrade-guide.mdreorder and nothing else: their line multisets are byte-identical tomainat commit 1 (sort | md5summatches on both files). Commit 2's 12 prose rewrites are the only content change, and they are the only reason those two files differ by content at all.Counterfactual replay — with the harness proven non-blind first
Real merges,
git mergeon synthetic branches off this HEAD:check:migration-registrygreen after the mergeregistry.ts— the 6-of-11 shapeC is the arm that matters for attribution: the same two edits that conflict before the split merge clean after it, so the outcome changed because the layout changed, not because the test did.
node:fsin a browser-reachable barrel).What changed there is the class of the failure, not its existence, and that is the class #6957 actually measured: the region is generated from files git merged as a set,
--checkfails if it does not match them, and the only correct resolution isgen:migration-registry. A resolution that drops an entry is now caught by a gate instead of landing silently — which was the whole reason to do this, since both tables are sets and a dropped entry produces no error anywhere. The historical shape (every card appending to the same tail line, so every contended lap collided) is gone; what remains is a same-anchor residue, the same residue #5837 accepted for the sharded ratchets.Generator and gate wiring
packages/spec/scripts/build-migration-registry.ts—gen:migration-registry/check:migration-registry(the check runs--self-test --check; 20 assertions drive every red path with synthetic input).check-generated.ts— new first entry, socheck:generatedis now 12/12. It goes first becausespec-changes.jsonand the upgrade guide are projections of this registry: a registry left stale after an entry file was added reports as three stale artifacts, of which only this one names the cause.regen-artifacts.mjs/.gitattributes—registry.tsdeliberately stays inNOT_DRIVER_MANAGED. It is now a mixed file: a conflict inside a marked region must be resolved by regenerating, but everything outside the markers (the tables' load-bearing doc comments, each step'srationale) is still hand-written, and the driver defers the whole file to one side. Routing it tomerge=os-regenwould let a regeneration launder away a sibling's prose edit — trading the silent drop this PR removes for a quieter one.LEDGER_SOURCES— deliberately unchanged, against the card's expectation. ItsextractIdsreadsid:lines out of the source text, and under a concatenation designregistry.tsstill carries every one of them. Teaching it to read a directory would also introduce amissingSourcesred at every merge base that predates the split. Proved rather than argued, with a synthetic new-entry probe: an id added only as an entry file is seen by the gate at HEAD and absent at base (ids at base: 133 → head: 134,gate SEES the probe entry: true,missing: []). The probe was removed; it is not in the branch.Reverse verification (predicted in writing, then driven)
17.ui__WidgetEvent.ts"<os-generated retired-def:9>region"dirty paths: 0Gates
pnpm --filter @objectstack/spec test— 372 files, 9718 tests passedcheck:generatedafter a fullpnpm --filter @objectstack/spec build(the gen:api-surface / gen:export-origins read the dist without asserting it is FRESH — a stale dist launders a phantom breaking removal into the committed baseline #7122 stale-dist trap) — 12/12 green, includingcheck:api-surface: the public surface is unchangedcheck:migration-registry --self-test --check,check-generated --reconcile-only(22check:+ 15gen:scripts, all classified),git-merge-regen.mjs --self-testcheck-adr-0087-registration.mjs --self-test(142 assertions) and--base origin/main— greentsc --noEmit,tsc -p tsconfig.scripts.json,eslint --no-inline-configon every added/changed file — cleanPre-existing, unchanged:
eslint packages/spec/src/migrations/registry.tsfails withParsing error: Maximum call stack size exceeded(the deep+-concatenation chains). Byte-for-byte the same failure and exit code onmainin this environment — reported so it is not read as this PR's.Honest limit, carried from the escalation
This removes the conflict resolution, not the regeneration lap.
spec-changes.jsonand the upgrade guide are still committed projections — option (b) was rejected, the review diff is worth the laps it costs — and the lap still fires from the merge queue and theos-regenpending marker. If the goal is "make retirement cards fast", this is not it. If the goal is "stop a retirement being silently dropped", it is.Generated by Claude Code