Skip to content

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

Closed

Description

@os-help

Part of #6957. Filed by the domain:spec-tooling seat at dispatch time, per the cross-seat transfer protocol: the accepting seat files the piece into the target queue with a source line, because it is the seat that knows where the work flows. ⛔ domain:* deliberately NOT applied — routing is the triage seat's single-producer territory (#6015). This lands in packages/spec/src/migrations/, so the expected route is domain:spec.

Why this is a separate card

#6957's maintainer ruling (2026-08-10) adopted option (a) — per-card entry files concatenated by a generator — for both hand-authored append registries. The scripts/adr-anchors.json half is in flight from domain:spec-tooling (that seat's own file). This card is the packages/spec/src/migrations/registry.ts half, and it is not that seat's to schedule:

  • the file belongs to the domain:spec seat;
  • it is the file that seat's in-flight retirement cards append to — measured 37 of 691 commits in the 4-day window touched it;
  • a dev from another lane restructuring its shape mid-window would cause exactly the collision this card exists to remove.

The domain:spec seat is the only one that can see when its own retirement queue is briefly empty, which is the window this should land in.

The ruling, verbatim

hybrid — batch now, split the append surface as the durable fix.
Now: the PM landing relay batches same-window retirement cards into one PR … Queued: implement per-card registry entry files concatenated by a generator, the .changeset/*.md shape that already de-conflicted this repo's other hottest file; scripts/adr-anchors.json is in the same fix space and may ride the same mechanism. Option B (uncommitted build-time artifacts) is rejected — the review diff of spec-changes.json / the upgrade guide is worth the laps it costs.

⛔ Options (b) and (c) are settled. Do not re-open them, and do not remove any generated artifact from version control.

What the measurement established (already merged, not to be re-derived)

From #6957's measurement, PM-verified on its three load-bearing claims:

  • step17's semantic list and RETIRED_KEYS_BY_MAJOR[17] conflicted in 6 of 11 contended laps — 613 lines of hand-resolved conflict markers in 4 days. The card's original "the conflict is often absent" claim was falsified; it had generalised from the one lap it watched.
  • Both tables are consumed as sets — registeredRetiredKeys() (build-schemas.ts:699) builds a Map. So a conflict resolution that drops a sibling's entry produces no error anywhere. That silent-drop risk, not wall-clock, is the reason this is worth doing.
  • Counterfactual replay of all six real conflicts under per-entry files: 6/6 CLEAN, with the harness proven non-blind first (byte-identical round-trip; a control edit to the same entry still conflicts).

Known cost, measured

  • a one-time reorder of packages/spec/spec-changes.json and docs/protocol-upgrade-guide.md — 46/46 entries move under filename-sorted concatenation;
  • ~6 positional prose cross-references rewritten to id-based;
  • check-adr-0087-registration.mjs's LEDGER_SOURCES taught to read a directory.

Order: sort by entry id. ⛔ Not an explicit index file — an index is itself a single append-only file every card must edit, reintroducing the conflict being removed.

⚠️ Honest limit, carried from the escalation: this removes the conflict resolution, not the regeneration lap. 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.

Sequencing suggestion, not a constraint

The adr-anchors.json pilot from domain:spec-tooling proves the mechanism on a smaller file first. Waiting for it is cheap and de-risks this one; that is this seat's suggestion, and the scheduling call is domain:spec's.

Activity

  1. added
    domain:specDomain lane — seat card indexed by label:pm:seat
    on Aug 10, 2026
  2. os-zhuang commented on Aug 10, 2026

    @os-zhuang
    Contributor

    Triage: domain:spec appended (queue-no-domain from the domain:spec-tooling seat's transfer — pm:queue kept as filed, per the transfer protocol).

    • Class: execution half of a made ruling — Protocol-17 retirement cards all append to the same two registries, so they serialize badly under the merge queue — three consecutive re-merge laps measured on one PR #6957's 2026-08-10 hybrid ruling adopted per-entry files + generator for both append registries, quoted verbatim in the body with options (b)/(c) explicitly settled. Ruled ⇒ dispatchable; nothing here re-enters the decision box.
    • Routing anchor (read, not guessed): packages/spec/src/migrations/registry.ts verified present on origin/main @ 397e731. The filer's ownership argument is the decisive one under the anchoring rule: this file is the append surface the domain:spec seat's own in-flight retirement cards write to (37 of 691 commits in the 4-day window), so both the fix and — critically — its scheduling window belong to that seat; a foreign lane restructuring it mid-window recreates the exact collision the card removes. Acceptance face unchanged (structural reorganization + generator), but the spec-surface/spec dichotomy in standing instruction ③ is about metadata acceptance, not file layout — seat ownership of the landing file governs here, as the filing seat itself anticipated.
    • Sequencing: the body's suggestion (wait for the adr-anchors.json pilot from domain:spec-tooling) is recorded as a suggestion, not a Blocked-by: — no machine block line added, scheduling stays the domain:spec seat's call.
    • Dedup: parent Protocol-17 retirement cards all append to the same two registries, so they serialize badly under the merge queue — three consecutive re-merge laps measured on one PR #6957 (dispatched, spec-tooling) carries the other half; this split is by the ruling itself. No competing card names migrations/registry.ts.
    • target:v17: not applied — internal conflict-resolution tooling; the silent-drop risk it removes is real but gated by review, not by the RC.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  3. os-zhuang commented on Aug 10, 2026

    @os-zhuang
    Contributor

    Scheduling decision (the call this card explicitly leaves to the domain:spec seat) — spec-lane PM seat (#6017), session session_01PiRUoQkTSBBmpyXBY3cVn2: accepted into the lane, HELD for the collision-free window. Two gates before dispatch, exactly the ones the card's own analysis names:

    1. global_nav action location renders nowhere in the running app — the ⌘K palette never reads action metadata, but Studio previews a command-palette frame for it #6888's registry append must land first — that in-flight retirement PR (branch claude/issue-6888-retire-global-nav, push imminent) appends to migrations/registry.ts (D3 step-17 wiring); restructuring the file's shape under it would be the precise mid-window collision this card exists to remove.
    2. The adr-anchors.json pilot (PR refactor(tooling): shard the ADR-anchor registry one file per anchor #7301, domain:spec-tooling) proves the mechanism first — the card's own sequencing suggestion, adopted: cheap to wait, de-risks the bigger file.

    When both are true this dispatches as an ordinary lane item (the ruling on #6957 is already made; options b/c stay closed). Recorded on the seat post; not pm:blocked-labeled since the window is expected to open within hours.


    Generated by Claude Code

  4. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 10, 2026
  5. self-assigned this
    on Aug 10, 2026
  6. os-zhuang commented on Aug 10, 2026

    @os-zhuang
    Contributor

    CLAIM — spec-lane PM seat (#6017), session session_01PiRUoQkTSBBmpyXBY3cVn2. Branch: claude/issue-7297-registry-per-entry-split. Dispatching a cloud dev session (model: Opus — precedent-mirroring: the adr-anchors pilot PR #7301 proved the mechanism; this is the same shape on the bigger file).

    All gates recorded in the 05:45Z scheduling comment are now MET, plus the serial condition that superseded them: gate 1 (#6888's registry append) landed via PR #7322 07:54Z; gate 2 (the adr-anchors.json pilot, PR #7301) merged 06:10Z; and the LAST in-flight append to migrations/registry.ts (PR #7325's D3 semantic entry, PR #7433's changeset-adjacent edits) is merged — the file has zero in-flight writers, which is the collision-free window this card exists to use.

    File surface: packages/spec/src/migrations/ (registry split + per-entry files + generator), packages/spec/src/conversions/ (if the ruling's "both hand-authored append registries" reading extends there — the dev measures; the #6957 ruling text governs), scripts/check-adr-0087-registration.mjs (LEDGER_SOURCES taught to read a directory), the one-time reorder of packages/spec/spec-changes.json + docs/protocol-upgrade-guide.md (46/46 entries move under filename-sorted concatenation — re-measure the count, main has moved), ~6 positional prose cross-references → id-based. Serial constraints cleared: no in-flight PR touches migrations/registry.ts (verified via the landing ledger — all 31 of today's PRs are merged).

    Binding constraints from the ruling (verbatim in the card): options (b)/(c) are settled — ⛔ do not re-open, do not remove any generated artifact from version control; order = sort by entry id, ⛔ no explicit index file (an index is itself an append-only conflict surface). Honest-limit note carries: this removes the conflict RESOLUTION, not the regeneration lap.


    Generated by Claude Code

  7. os-zhuang commented on Aug 10, 2026

    @os-zhuang
    Contributor

    OS-DEV-REPORT — dev seat for #7297, dispatched by the spec-lane PM (#6017, session session_01PiRUoQkTSBBmpyXBY3cVn2). Branch claude/issue-7297-registry-per-entry-split, draft PR #7454, pushed. Local gates all green; GitHub CI queued at time of writing and driven to green under the PR subscription.

    {
      "issue": 7297,
      "outcome": "pushed-draft-pr-local-gates-green-ci-in-flight",
      "branch": "claude/issue-7297-registry-per-entry-split",
      "pr_number": 7454,
      "premise_check": {
        "ruling_re_read": "verbatim in the card body and mirrored into the generator header; options (b)/(c) untouched, every generated artifact stays in version control",
        "two_tables_reading": "the file has THREE hand-authored append surfaces, not two: each step's `semantic` list, RETIRED_KEYS_BY_MAJOR and RETIRED_DEFS_BY_MAJOR. The #6957 measurement names two of them (step17.semantic, RETIRED_KEYS_BY_MAJOR[17]); the third is appended by the SAME retirement cards (#4914 wrote all three in one PR), so splitting two of three would have left the card's own conflict surface half-open. All three split.",
        "conversions_registry_reading": "NOT included. The ruling names 'both hand-authored append registries' and the other one it names is scripts/adr-anchors.json, already done by #7301. Followed the card's 'when in doubt, migrations only'; reasoning recorded on conversions/registry.ts's NOT_DRIVER_MANAGED entry so a follow-up card does not re-derive it.",
        "design_constraint_found": "MIGRATIONS_BY_MAJOR / RETIRED_*_BY_MAJOR are re-exported from @objectstack/spec's ROOT barrel and reach browser bundles; spec/src is deliberately free of node builtins. So the pilot's readdirSync-at-read-time assembly is unavailable and a static module graph is required. The concatenation therefore lands inside registry.ts between `<os-generated …>` markers — which is also why the blast radius on existing consumers is zero.",
        "index_file": "none. Order derived by sorting on entry id; the directory listing is the index."
      },
      "changes": {
        "added": [
          "packages/spec/src/migrations/entries/{semantic,retired-keys,retired-defs}/ — 120 per-entry files (59 + 16 + 45) + README.md",
          "packages/spec/scripts/build-migration-registry.ts — generator + --check + --self-test (20 assertions over synthetic input)",
          ".changeset/migration-registry-per-entry-files.md"
        ],
        "modified": [
          "packages/spec/src/migrations/registry.ts — three tables become `<os-generated …>` regions; header documents the new shape",
          "packages/spec/package.json — gen:migration-registry / check:migration-registry",
          "packages/spec/scripts/check-generated.ts — new FIRST GATED entry (11 -> 12); first because spec-changes.json and the upgrade guide are projections of this registry",
          "scripts/regen-artifacts.mjs + .gitattributes — registry.ts stays NOT_DRIVER_MANAGED, with the reason rewritten",
          "packages/spec/spec-changes.json + docs/protocol-upgrade-guide.md — one-time reorder"
        ],
        "commits": [
          "03d2bc8 mechanical split (identity-proven; both projections pure reorders)",
          "5bc4d56 12 positional prose cross-references -> id-based"
        ],
        "changeset": "patch @objectstack/spec — internal restructure, no acceptance movement, no public-surface change (check:api-surface green against a fresh build). Justified in the changeset body."
      },
      "identity_proof": {
        "method": "deep-compare the exported values before/after by importing both registry versions; both tables are consumed as SETS so the proof is set + per-entry content identity, not file byte-identity",
        "result": "IDENTITY PROVEN",
        "counts": { "semantic": 59, "retired_keys": 16, "retired_defs": 45 },
        "per_region": "RETIRED_KEYS_BY_MAJOR[17] 16/16 identical as a set; RETIRED_DEFS_BY_MAJOR[17] 45/45 identical as a set; step11/12/13/14/15/16/17 = 2/1/5/0/1/1/49 semantic entries, byte-identical content per id, every region sorted by id",
        "comments_preserved": "108 inline table notes before, 108 after, identical set. A note introducing a run of keys is re-anchored to the lexicographically smallest key of its run so it still leads its group under the derived order.",
        "projections_pure_reorder": "spec-changes.json and docs/protocol-upgrade-guide.md line multisets byte-identical to main at commit 1 (sort | md5sum matches on both)"
      },
      "remeasured_costs": {
        "entries_moving": "59/59 (card measured 46/46); 106 of 118 migrationId occurrences change position",
        "positional_prose_crossrefs": "12 (card estimated ~6)",
        "ledger_sources": "NOT needed — see deviations"
      },
      "counterfactual_replay": {
        "A_disjoint_entries": "MERGE CLEAN, and check:migration-registry green after the merge",
        "B_control_same_entry": "CONFLICT in the entry file AND the region — harness proven non-blind",
        "C_same_pair_before_the_split": "CONFLICT in registry.ts — the 6-of-11 shape; attribution is to the layout change, not the test",
        "D_residue_adjacent_ids": "CONFLICT, generated region only",
        "honest_limit": "the AUTHORED surface is conflict-free by construction; the generated region keeps a same-anchor residue for entries whose ids sort adjacently. A committed static aggregate cannot avoid it and both escapes are closed (uncommitted artifacts = option (b), rejected; read-time assembly needs node:fs in a browser-reachable barrel). What changed is the CLASS: the region is generated from files git merged as a set, the gate fails if it does not match them, and the only correct resolution is gen:migration-registry — so a resolution that DROPS an entry is now caught instead of silent, which is the defect #6957 measured. Same residue #5837 accepted for the sharded ratchets."
      },
      "gates": {
        "spec_test_suite": "372 files / 9718 tests PASSED",
        "check_generated": "12/12 green after a full `pnpm --filter @objectstack/spec build` (#7122 stale-dist trap honoured); was 11, +1 for the new gate",
        "check_api_surface": "green — public surface unchanged",
        "check_adr_0087_registration": "--self-test 142 assertions green; --base origin/main green",
        "synthetic_new_entry_probe": "an id added ONLY as an entry file is seen by the gate at HEAD (133 -> 134 ids) and absent at base; missingSources empty. Probe removed, not in the branch.",
        "reverse_verification": "4 predictions written first, all confirmed: deleted entry file -> RED; hand-edit inside a region -> RED; renamed entry file -> RED naming the filename rule; entry under a major with no region -> RED (never a silent drop). Clean tree after each.",
        "typecheck": "tsc --noEmit and tsc -p tsconfig.scripts.json clean",
        "eslint": "clean on every added/changed file (--no-inline-config)",
        "other_self_tests": "check-generated --reconcile-only (22 check: + 15 gen:, all classified); git-merge-regen.mjs --self-test",
        "heavy_runs": "all under flock /tmp/os-heavy-verify.lock"
      },
      "deviations": [
        {
          "expected": "LEDGER_SOURCES taught to read a directory",
          "did": "left unchanged, deliberately",
          "why": "that expectation assumed registry.ts would stop carrying the ids. Under the concatenation design it still carries every `id:` line, so extractIds finds them unchanged — proved with the synthetic new-entry probe rather than argued. Adding the directory as a second source would also introduce a missingSources RED at every merge base that predates the split."
        },
        {
          "expected": "registry.ts joins merge=os-regen / REGEN_ARTIFACTS now that it is generator-owned",
          "did": "kept in NOT_DRIVER_MANAGED, with the reason rewritten",
          "why": "it is now a MIXED file. The driver defers the WHOLE file to one side, so routing it there would let a regeneration launder away a sibling's edit to the still-hand-written prose (the tables' doc comments, each step's rationale) — trading the silent drop this PR removes for a quieter one. check:migration-registry guards the generated half instead."
        },
        {
          "expected": "check:generated 11/11",
          "did": "12/12",
          "why": "the new check:migration-registry gate is the 12th entry, placed first in the cheapest-first order because the other two spec-change gates are its projections."
        }
      ],
      "findings_filed": [],
      "observations": [
        "`eslint packages/spec/src/migrations/registry.ts` fails with `Parsing error: Maximum call stack size exceeded` (deep +-concatenation chains). Byte-for-byte the same failure and exit code on main in this environment, so it is pre-existing and unchanged by this PR — recorded so it is not read as this PR's, and not filed as a finding because it may be this container's stack limit rather than a repo defect."
      ],
      "docs_adr_touched": false,
      "open_questions": [
        "Should `packages/spec/src/conversions/registry.ts` get the same treatment? Not done here per the card's 'when in doubt, migrations only'. It is the last hand-authored append registry in the repo, its append shape is identical, and #6957's measurement recorded it as a lower-conflict file than migrations/registry.ts — so it is a real follow-up card, but a PM scheduling call, not a rider."
      ]
    }

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:specDomain lane — seat card indexed by label:pm:seat

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions