Repository navigation
Commit ed1de8c
build(spec): the migration registry is generated at build and leaves git (#22706)
Fixes #22554
Clause-②: no
Executes ruling B on #22554 (comment 6092692730, maintainer 「同意」), which
completes #22449 B′ (comment 6078203801):
`packages/spec/src/migrations/registry.ts` is generated at build and
leaves git, at its unchanged path. The 3 source imports, the script
import and the test imports are untouched. The published package is
unchanged apart from `package.json` scripts and the input-hash stamps
(measured below).
**Landing:** +4,054 / −31,059 across 25 files, about 35,100 changed
lines (GitHub's reading at head `81ae60b20a`). That is over the
3,000-line line, so this PR takes the maintainer's APPROVED review, as
the ruling notes. It stays draft. The two PM-tool fixes it needs co-land
here (see "Two PM tools, co-landed here").
## A premise the ruling stated, measured false: the registry was a MIXED
file
The ruling says "the entry files stay the only source". At `cb3bb9333f`,
`registry.ts` had 30,718 lines (the ruling's 30,124 is an earlier
count). Of these, 27,268 sit inside the six os-generated regions,
concatenated from 899 entry files (419 semantic, 258 retired-key, 222
retired-def). The other **3,451 lines are hand-written**: the module
header, the two value imports, `MIGRATION_SUPPORT_FLOOR`, step 17's
rationale, `STEP18_RATIONALE` (about 1,937 lines of keyed fragments),
`step18`, `MIGRATIONS_BY_MAJOR`, `MIGRATION_MAJORS` and the two tables'
doc comments. The generator used the file as its own template.
So the hand-written part needs a committed home. In the ruling's intent,
it becomes **`packages/spec/src/migrations/registry.ts.template`**: the
old file with every region emptied, produced mechanically. The generator
now writes `registry.ts` whole from that template plus `entries/`.
Regenerating from an absent file reproduced the committed blob byte for
byte (`feb4843913`) before the header rewording below.
The template is deliberately **not** a `.ts` file, for two reasons. With
empty regions, its `TranslationDataSchema` import is unused, and the
root tsconfig's `noUnusedLocals` would red `tsc`. More important, a
`.ts` template would be importable, and an import of it would silently
receive empty tables.
**Measured cost of this to the ruling's "no shared file for a D3 PR":**
of the last 60 commits that touched `registry.ts`, 33 also changed
hand-written lines, mostly one `STEP18_RATIONALE` fragment. Those edits
now land in the template. It is keyed and sorted, so two retirements
insert at different lines (#20535, pinned by
`step18-rationale-merge.test.ts`, which now merges the template).
Region-only PRs (the other 27 of 60) touch no shared file at all.
## The ruling's measure-first readings (condition 2)
1. **Generation wall time.** Measured at BASE with `tsx
scripts/build-migration-registry.ts --self-test --check` and a
TMPDIR-isolated tsx cache, load about 3.3 on 4 cores. Cold runs: 1,808 /
1,583 / 1,685 ms. Warm runs: 1,611 / 1,426 / 1,563 ms. `pnpm run
gen:migration-registry` took 1,465 ms. A fresh `pnpm install
--frozen-lockfile` including the generation took 11 s.
2. **`prepare` on a workspace package runs on `pnpm install`.** I
measured this on a fresh worktree with an uncommitted probe `prepare` on
`@objectstack/spec`. pnpm 10.31.0 printed `packages/spec prepare$ …` and
wrote the marker with cwd `packages/spec`. The probe was then restored
byte-identical (blob `de5c9fa2dd`, `git diff HEAD` empty). The real
`prepare` (`pnpm gen:migration-registry`) was then measured on a second
fresh worktree at `765ae6bde4`. Install printed `✓ wrote
src/migrations/registry.ts (419 semantic, 258 retired-key, 222
retired-def)` and `git status` stayed clean. The editor is therefore not
red before the first build. `prepare` is strict: a malformed entry fails
the install with the generator's located message. A lenient one would
leave the previous run's file on disk to be read stale. Also measured:
`pnpm pack` runs `prepare` and leaves it out of the packed manifest.
3. **`typecheck` order.** The script is now `pnpm gen:migration-registry
&& tsc --noEmit && pnpm check:scripts-typecheck && pnpm
check:test-typecheck`, so all three run after generation. turbo's
`@objectstack/spec#typecheck` also depends on the generation task. In
the fresh worktree the typecheck went green with no `dist/` present.
## What changed
- **`registry.ts`**: removed from git and ignored (root `.gitignore`,
beside `packages/spec/json-schema/`), together with the staging file of
the generator's atomic write.
- **The generator** (`build-migration-registry.ts`):
- reads the template, writes `registry.ts` only when its bytes differ,
and writes it by rename, so a parallel `tsc`, vitest worker or importer
never reads half a file;
- runs its self-test on every generation (`--self-test` alone stops
after it). The self-test gains an ordering case (code-unit order; a
locale compare fails it) and a determinism and fixed-point case;
- header (condition 3): quotes the rule B completes and cites #22449 B′
(6078203801) and #22554 B (6092692730) as superseding the Option-B
clause of #6957's 2026-08-10 ruling.
- **`packages/spec/package.json`**: `build` starts with
`gen:migration-registry` (before `gen:schema`); `typecheck`, `test` and
`test:repo` start with it; `prepare` runs it; `check:migration-registry`
is removed.
- **`turbo.json`**: `@objectstack/spec#gen:migration-registry` has
`outputs: ["src/migrations/registry.ts"]`, with inputs the template, the
entries, the generator and the manifest. `#typecheck` (new), `#test` and
`#test:repo` depend on it, and `#build` lists the file among its outputs
so a cache-hit build restores it.
- **Template header**: reworded in place to say the file is generated
whole and git-ignored, and where each kind of edit goes. The line count
is unchanged, so the shipped source maps cannot move.
- **Retired**: the `lint.yml` step "Migration registry matches its entry
files" and the `migration_registry` gate family in
`scripts/ci/select-gate-families.sh`. Its self-test pins go too: 12
cases, with the floor moved from 71 cases / 369 checks to the measured
59 / 304. No other family's verdicts changed.
## Readers of the committed copy, adapted
A census of `git grep migrations/registry` (112 files) plus a run of
every derived gate found these path-dependent readers. Each is changed
only as far as the retirement requires.
- **`scripts/check-adr-0087-registration.mjs`** read the ledger with
`git show REV:…/registry.ts` and laid out its witness with `git
archive`, so it went red ("ledger source not found at HEAD").
- `ledgerAt` now reads a rev's migration ledger from the template plus
the entry files when that rev tracks the template, and from the
committed registry otherwise, because a merge base can predate this PR.
Measured on HEAD against `cb3bb9333f`: 532 / 591 ids on both sides, with
0 differences either way.
- The witness runs the rev's own registry generator before projecting.
- The `--audit-stock` ledger-touch test counts entry and template
commits.
- A new battery, GR1–GR7 (13 cases), covers the generated era, including
the committed-to-generated transition merge. The roster floor is 50 →
51.
- **`scripts/check-future-spec-major.mjs`**: its exemption row for
`registry.ts` matched nothing once the file left the tracked corpus. The
row is dropped; the entry file that carries the sentence keeps its own
row, and the R2 self-test fixture is re-pointed at it.
- **`packages/spec/scripts/check-generated.ts`**:
`gen:migration-registry` moves to `UNGATED_GENERATORS`, with the
build-output rationale `gen:openapi` already has.
- **`scripts/regen-artifacts.mjs`**: the registry's `NOT_DRIVER_MANAGED`
row becomes `untracked: true` (git never merges it).
- **`.gitattributes`, `entries/README.md`, the `build-schemas.ts`
remedies and test comments**: stop naming the retired check or a
committed lap.
- **Disk readers stay green.** The generated file exists after every
install, so `check:dispatcher-error-vocabulary`'s row and the ESLint
stack-headroom canary still see it. `check:cli-command-ids`,
`check:issue-citations` and `check:cross-package-test-inputs` were also
measured green.
## The published package
`pnpm pack` of `@objectstack/spec` built at BASE `cb3bb9333f` was
compared with the build at HEAD (merge `625c4d202e`; the later commit
only adds the changeset). Both tarballs hold **2,070 files, and 4
differ**:
- `package/package.json`, in `scripts` only: `build`, `typecheck`,
`test` and `test:repo` start with the generation, and
`check:migration-registry` is gone. `prepare` is not in the packed
manifest.
- `dist/.build-input-hash`, `dist/.build-input-hash-dts` and
`json-schema/.build-input-hash-schema` (`53a504fa…` to `ac4356b2…`).
They hash the build inputs, which this PR changes by construction.
Every `dist` JS file, declaration, source map and JSON Schema, and every
shipped `src/**/*.zod.ts`, is byte-identical. Tarball sha256: BASE
`d3d59ac0…`, HEAD `362a845e…`. `pnpm check:published-files` passed at
HEAD `765ae6bde4`.
## A fresh checkout builds, typechecks and tests with no committed
registry
This ran on a new detached worktree at `765ae6bde4`, where `registry.ts`
was absent:
- `pnpm install --frozen-lockfile`: `prepare` generated the file.
- `pnpm --filter @objectstack/spec typecheck`: `VERDICT command-exit 0`,
before any build.
- `pnpm --filter @objectstack/spec build`: `VERDICT command-exit 0`,
with input hash `ac4356b2…`, identical to the primary worktree's build.
- `registry.ts` deleted, then `pnpm --filter @objectstack/spec test`:
the script wrote it again (`✓ wrote src/migrations/registry.ts`).
Result: `Test Files 642 passed (642)`, `Tests 19164 passed | 1 todo
(19165)`, `VERDICT command-exit 0`.
## Two PM tools, co-landed here (`scripts/pm`, declared to the
`domain:skills` seat on #7623)
- **`scripts/pm/os-regen-merge.sh`**: self-test case 9c pinned the
registry as a TRACKED `NOT_DRIVER_MANAGED` row, which is untracked now.
It pins `skills/README.md` / `gen:skill-docs` / `skills/README.md:
merge: unspecified` instead, the same shape (tracked, MIXED, marked
regions, a generator); all cases pass. The header's class-3 roster
(about `:359`–`:366`) now names two MIXED rows, with the registry noted
as the former third. Comment text only.
### A touch on the frozen `scripts/pm/dispatch-gates.mjs`, under ruling
208's exception
Ruling 208 R6, as
`.claude/skills/pm-dispatch/references/instrument-discipline.md` carries
it: 「工具位只有一个,先花在删除上;`dispatch-gates.mjs` 冻结,只在它喂的 workflow 坏了时碰」. This
PR makes `packages/spec/src/migrations/registry.ts` generated and
git-ignored, so the hint a gate's import of the registry yields names a
file no card can touch. Two things broke as a result:
`check:pm-dispatch-gates`, a `Lint & Repo Gates` leg, went red on its
extension-narrowing agreement case; and a card editing only an entry
file or the template stopped deriving five families that import the
registry (`check:spec-changes`, `check:upgrade-guide`,
`check:authorable-surface`, `check:query-options-erasure`,
`check:role-word`). That is the workflow the file feeds breaking, so the
file is touched under the ruling's exception, in the W3 split's shape:
- **Data**: `scripts/pm/dispatch-gates.data.mjs`, +43/-2. The declared
row `GENERATED_MODULE_SOURCES` (the registry module, and its committed
sources `registry.ts.template` and `entries/`) with its rationale.
- **Engine**: `scripts/pm/dispatch-gates.mjs`, +11/-1 against
`765ae6bde4`. The data import and re-export, a five-line
`generatedModuleSources` lookup and one `||` clause in `hintCovers`.
- **Self-test**: `scripts/pm/dispatch-gates.self-test.mjs`. It pins the
mapping with a control, proves the row against the tree on every run
(module untracked and ignored by a tracked ignore file, sources
tracked), and keeps mapped hints out of the extension-narrowing
agreement case. One census line pin moved 734 -> 736 with the import.
Removing the clause turns exactly the mapping case red (1 of 1828).
Declared to the `domain:skills` seat on #7623; that seat's answer is
comment 6100894139 on #22554.
## Governed text this makes false (reported, not edited)
`.claude/skills/spec-property-retirement/SKILL.md:213` says the D3 step
lives in `packages/spec/src/migrations/registry.ts`, and that a step-18
retirement adds its `STEP18_RATIONALE` fragment and an earlier step's
`conversionIds` and `rationale` there. After this PR those hand edits
belong in `registry.ts.template`: an edit to `registry.ts` is never
committed. The proposed wording is in the os-dev report.
## Concurrency
- **Open PR #22691** (`claude/issue-22658-skills-package`) edits
`turbo.json`. Whichever lands second resolves the textual overlap.
- **#19939 pass 4 S1 has landed** as PR #22715 (`f66fdc7973`) and is
merged here (`84612b393b`, through `scripts/pm/os-regen-merge.sh`). Its
modify/delete on `registry.ts` was resolved by `git rm` after proving
its 4 hunks lie inside the os-generated regions on both sides (0
hand-written lines). The registry generated on the merged tree then
equals `origin/main`'s committed blob `9aaae83bde` byte for byte, apart
from the declared header lines 19, 21–26 and 38–42; the ledger reads 591
ids (532 migration) on both sides, 0 differences. The branch
`claude/issue-15204-s1-position-permission-sets` (epic #15194) is
unchanged: it changes only generated regions, so the same `git rm`
resolves it losslessly.
- **General rule for any branch forked before this lands:** a branch
whose `registry.ts` diff touches lines outside the os-generated markers
must port those lines into `registry.ts.template` before `git rm`.
Otherwise the edit is silently dropped.
## Verification at HEAD `84612b393b` (patch round) and `765ae6bde4`
(round 1)
- **Patch round, at `84612b393b`** (`81ae60b20a` adds only the
`os-regen-merge.sh` header comment; its self-test reads "all cases pass"
there): `pnpm --filter @objectstack/spec typecheck` VERDICT 0; `pnpm
--filter @objectstack/spec test` VERDICT 0, `Test Files 642 passed
(642)`, `Tests 19197 passed | 1 todo (19198)`; `pnpm
check:pm-dispatch-gates` "1828 cases pass" and `check-dispatch-gates.mjs
--slow` "2088 cases pass"; `os-regen-merge.sh --self-test` "all cases
pass"; the gate union re-derived on the 25 paths, 146 commands, "146
derived, 146 run, 0 NOT-MEASURED, 0 UNRUN", all exit 0 (including
`check:dual-build-cjs-loads`, measured this time). CI on `84612b393b`:
34 success, 2 skipped. The bullets below are round 1's.
- **`@objectstack/spec`**: typecheck, build and test, all green
(`VERDICT command-exit 0` each). These ran on the fresh worktree above,
at this commit, under `scripts/pm/os-verify-lock.sh`.
- **Gates**: the union `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` derives from the 21 changed paths is
146 commands, all run at this commit with exit codes captured before any
pipe:
- **144 green.** One of them, `check:dts-closure`, first exited 1,
naming the half-built `dist/` of two plugins that my own earlier gate
runner had left when its timeout killed `check:type-check-debt`
mid-build. I removed those git-ignored partial trees and reran it: exit
0, 171/171 declaration files across 71 packages.
- **1 red**: `check:pm-dispatch-gates` (above).
- **1 NOT MEASURED**: `check:dual-build-cjs-loads`, which exited 3
because it needs every workspace package built. This PR changes no
package's `dist`, and the spec tarball diff above shows its own `dist`
is byte-identical.
- **`dispatch-gates --ran`** over the exit-coded record: "146 derived,
145 run, 1 NOT-MEASURED, 0 UNRUN".
- **Self-tests run directly**:
- `node scripts/check-adr-0087-registration.mjs --self-test`: 463
assertions.
- `bash scripts/ci/select-gate-families.selftest.sh`: 59 cases, 304
checks.
- `node scripts/regen-artifacts.mjs --self-test` and `node
scripts/git-merge-regen.mjs --self-test`: exit 0.
- `tsx packages/spec/scripts/build-migration-registry.ts --self-test`:
ok.
- **ESLint, narrowed to the changed lintable files plus the generated
`registry.ts`**: 11 files, 0 errors, 0 warnings, 0 fatal, run with the
`lint` script's own `--stack-size=4000 --no-inline-config`.
- Population: read from `eslint.config.mjs`, whose objects glob
`**/*.{ts,…}`. `--print-config` on `registry.ts.template` answers
`undefined`, so the template is outside it.
- Invariance: no `parserOptions.project` (no type-aware rules), so this
diff cannot move a verdict on an untouched file. The repo-wide run is
CI's.
## Ablations (one-time proofs, each restored to the HEAD blob under the
tool's own trap)
All were run through `node scripts/ablation-replace.mjs` on the
committed tree.
1. `migrationLedgerTextAt` forced to the committed era: the ADR-0087
self-test goes red on GR1, GR3 and both GR7 cases.
2. The witness's registry-generation step disabled: the real gate
against `origin/main` goes red, with "Cannot find module
'../src/migrations/registry'" in the witness.
3. The entry comparator swapped for `localeCompare`: the generator's
self-test goes red on the new ordering case.
## Acceptance notes
- `scripts/regen-artifacts.mjs` cites `.gitignore:61`, `:73` and `:108`
for three other untracked rows. These were already off by 2 before this
PR, and the six lines added here move two of them further. They are not
gated. Noted, not changed.
- The `packages/spec/src/migrations/registry.ts` path keeps its name and
exports, so `export-origins/migrations.json`, `SYNC_ARCHITECTURE.md` and
the source comments that name it stay true.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01S3aAf11JjbW1mSGL1EhfFj)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent f59a73c commit ed1de8c
25 files changed
Lines changed: 4054 additions & 31061 deletions
File tree
- .changeset
- .github/workflows
- packages/spec
- scripts
- src/migrations
- entries
- scripts
- ci
- pm
Lines changed: 26 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
64 | 64 | | |
65 | 65 | | |
66 | 66 | | |
67 | | - | |
68 | | - | |
69 | | - | |
70 | | - | |
71 | | - | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
72 | 71 | | |
73 | 72 | | |
74 | 73 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
283 | 283 | | |
284 | 284 | | |
285 | 285 | | |
286 | | - | |
| 286 | + | |
287 | 287 | | |
288 | 288 | | |
289 | | - | |
290 | | - | |
| 289 | + | |
| 290 | + | |
291 | 291 | | |
292 | 292 | | |
293 | 293 | | |
| |||
399 | 399 | | |
400 | 400 | | |
401 | 401 | | |
402 | | - | |
403 | | - | |
404 | | - | |
405 | | - | |
406 | | - | |
407 | | - | |
408 | | - | |
409 | | - | |
410 | | - | |
411 | | - | |
412 | | - | |
413 | | - | |
414 | | - | |
415 | | - | |
416 | | - | |
417 | | - | |
418 | | - | |
419 | | - | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
420 | 408 | | |
421 | 409 | | |
422 | 410 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
62 | 62 | | |
63 | 63 | | |
64 | 64 | | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
65 | 71 | | |
66 | 72 | | |
67 | 73 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
264 | 264 | | |
265 | 265 | | |
266 | 266 | | |
267 | | - | |
| 267 | + | |
| 268 | + | |
268 | 269 | | |
269 | 270 | | |
270 | 271 | | |
| |||
295 | 296 | | |
296 | 297 | | |
297 | 298 | | |
298 | | - | |
299 | 299 | | |
300 | 300 | | |
301 | 301 | | |
302 | 302 | | |
303 | | - | |
304 | | - | |
| 303 | + | |
| 304 | + | |
305 | 305 | | |
306 | 306 | | |
307 | 307 | | |
| |||
320 | 320 | | |
321 | 321 | | |
322 | 322 | | |
323 | | - | |
| 323 | + | |
324 | 324 | | |
325 | 325 | | |
326 | 326 | | |
| |||
Lines changed: 6 additions & 4 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
6 | | - | |
7 | | - | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
8 | 9 | | |
9 | 10 | | |
10 | 11 | | |
| |||
66 | 67 | | |
67 | 68 | | |
68 | 69 | | |
69 | | - | |
70 | | - | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
71 | 73 | | |
72 | 74 | | |
73 | 75 | | |
| |||
0 commit comments