Skip to content

refactor(spec): split the migration registry's three append tables into per-entry files (#7297) - #7454

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-7297-registry-per-entry-split
Aug 10, 2026
Merged

refactor(spec): split the migration registry's three append tables into per-entry files (#7297)#7454
os-zhuang merged 2 commits into
mainfrom
claude/issue-7297-registry-per-entry-split

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #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. 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.ts has three hand-authored append surfaces, and the #6957 measurement names two of them (step17's semantic list and RETIRED_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.ts is not in this PR. The ruling names "both hand-authored append registries", and the other one it names is scripts/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 its NOT_DRIVER_MANAGED entry so the next reader does not re-derive it.

Design, and the one place it is forced away from the pilot

scripts/adr-anchors.mjs assembles its shards with readdirSync at read time, so no aggregate is checked in at all. That option does not exist here: MIGRATIONS_BY_MAJOR / RETIRED_*_BY_MAJOR are re-exported from @objectstack/spec's root barrel and reach browser bundles through it, and spec's src/ 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.mjs still reads every id: line out of LEDGER_SOURCES, composeSpecChanges still 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.

packages/spec/src/migrations/entries/
  README.md
  semantic/17.aggregation-node-distinct-retired.ts      (59 files)
  retired-keys/17.data__AggregationNode__distinct.ts    (16 files)
  retired-defs/17.ui__WidgetManifest.ts                 (45 files)

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:

✓ RETIRED_KEYS_BY_MAJOR[17]: 16 entries, identical as a set
✓ RETIRED_DEFS_BY_MAJOR[17]: 45 entries, identical as a set
✓ step11: 2 semantic entries, byte-identical content per id, sorted by id
✓ step12: 1   ✓ step13: 5   ✓ step14: 0   ✓ step15: 1   ✓ step16: 1
✓ step17: 49 semantic entries, byte-identical content per id, sorted by id

✓ IDENTITY PROVEN — 59 semantic + 16 retired keys + 45 retired defs

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 (main moved ~30 merges since the card's measurement):

card said measured on this branch
46/46 semantic entries move 59/59 (106 of 118 migrationId occurrences change position)
~6 positional prose cross-references 12
LEDGER_SOURCES taught to read a directory not needed — see below

spec-changes.json and docs/protocol-upgrade-guide.md reorder and nothing else: their line multisets are byte-identical to main at commit 1 (sort | md5sum matches 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 merge on synthetic branches off this HEAD:

arm setup result
A two cards registering different retired keys MERGE CLEAN, and check:migration-registry green after the merge
B (control) two cards editing the same entry CONFLICT — in the entry file and the region. The harness is not blind.
C (before) arm A's identical pair, replayed against the pre-split tree CONFLICT in registry.ts — the 6-of-11 shape
D (residue) two cards registering keys whose ids sort adjacently CONFLICT, in the generated region only

C 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.

⚠️ What arm D means, stated plainly. The authored surface is conflict-free by construction — different entries are different files. The generated region is not: two entries whose ids sort adjacently insert at the same anchor and still conflict textually. A committed static aggregate cannot avoid that, and both alternatives are closed (uncommitted build artifacts are option (b), rejected; a read-time assembly needs node:fs in 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, --check fails if it does not match them, and the only correct resolution is gen: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.tsgen: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, so check:generated is now 12/12. It goes first because spec-changes.json and 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 / .gitattributesregistry.ts deliberately stays in NOT_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's rationale) is still hand-written, and the driver defers the whole file to one side. Routing it to merge=os-regen would 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. Its extractIds reads id: lines out of the source text, and under a concatenation design registry.ts still carries every one of them. Teaching it to read a directory would also introduce a missingSources red 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)

prediction result
deleting an entry file → RED ✓ exit 1, "registry.ts is stale … Run: gen:migration-registry"
hand-editing inside a marked region → RED ✓ exit 1, same gate
renaming an entry file away from its id → RED naming the rule ✓ exit 1, "filename does not match its entry id (ui/WidgetEvent) — expected 17.ui__WidgetEvent.ts"
an entry under a major with no region → RED, never a silent drop ✓ exit 1, "registers under protocol major 9, but registry.ts has no <os-generated retired-def:9> region"
after each, back to green with a clean tree dirty paths: 0

Gates

Pre-existing, unchanged: eslint packages/spec/src/migrations/registry.ts fails with Parsing error: Maximum call stack size exceeded (the deep +-concatenation chains). Byte-for-byte the same failure and exit code on main in 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.json and 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 the os-regen pending 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

claude added 2 commits August 10, 2026 12:49
…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
@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 10, 2026 1:01pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec.

106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/tenancy-modes.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/permissions/system-context.mdx (via packages/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/apps.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

7 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx (via @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)

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.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation dependencies Pull requests that update a dependency file tooling labels Aug 10, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 10, 2026 14:08
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 10, 2026
Merged via the queue into main with commit f293d45 Aug 10, 2026
28 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-7297-registry-per-entry-split branch August 10, 2026 14:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Split migrations/registry.ts's two append tables into per-entry files (registry half of #6957's ruling)

2 participants