ci(lint): run check:migration-registry in Lint & Repo Gates, scoped by the gate-family selector - #19812
Conversation
…y the gate-family selector registry.ts is the generated concatenation of packages/spec/src/migrations/ entries/** and ships in the spec package's dist; check:migration-registry is the only proof the two agree and it ran in no CI step. It becomes a step in the required Lint & Repo Gates job, behind a new migration_registry family of scripts/ci/select-gate-families.sh whose read-set is the migration tree, the generator, and the spec package.json and tsconfig.json. The selector's fail-open branches run it like every other family; the self-test pins the family's verdicts and the workflow step. Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Reviewed and posted 2026-09-23T12:33Z by the at-tier review subagent the ① Derived judgmentsRead-set — complete. The generator imports only Lit and dark, on the real step (
Selector at the head ( Self-test: base Placement: job Composition: family ids are named in exactly three files — selector, self-test, Sentences that ship — reproduced: "ran in no CI step" (base Hygiene: 186 added lines + commit message: 0 control bytes, 0 model identifiers (the one pattern hit is the ② Semver levelNone. Diff = ③ Boundary flags
CI (35 check-runs on the head, de-duplicated by name keeping the latest start → 31 names). Success: Blocking: none. Implemented-by: VERDICT: PASS |
Fixes #19753
Clause-②: no
Ruling (not reopened here)
The maintainer's 「同意 A′」, recorded by triage in comment 5790385022 on #19753:
check:migration-registrybecomes a step inside the requiredLint & Repo Gatesjob. It runs only when a change touchespackages/spec/src/migrations/**, and it hangs on the existing gate-family selector. It is not a separatepaths:-filtered required check, which would never report on unrelated PRs.Design
scripts/ci/select-gate-families.sh: adds the new familymigration_registry. It comes first inFAMILIESbecause its step is the earliest scoped step in the job. Its read-set is everything the gate reads:packages/spec/src/migrations/**: the entries and the generatedregistry.ts. An edit to either side alone is the drift the gate reports.packages/spec/scripts/build-migration-registry.ts.packages/spec/package.json, which pnpm resolves the script through.packages/spec/tsconfig.json, which tsx loads. It extends the root tsconfig, and root config already runs every family.Every other path skips. A skip here is a ratchet-grade claim: no changed path is one the gate reads. The selector's fail-open branches still run this family like every other one. Those branches are: an unscoped event (push on main, the hourly run), a missing or unresolvable base, a failed or empty diff, an unclassified path, and a deletion or rename.
.github/workflows/lint.yml: adds one step,Migration registry matches its entry files, withif: steps.gate-families.outputs.migration_registry != 'skip'andrun: pnpm --filter @objectstack/spec check:migration-registry. It sits right afterADR anchorsbecause the job's ordering note puts cheap gates first. The selector step's comment now counts TEN families. The region PR chore(pm): the queue guard reads its record recognisers from record-recognisers.mjs; check-clause2-carriers.mjs is deleted #19803 edits (hunk at 1247) is untouched.scripts/ci/select-gate-families.selftest.sh:registry.ts, the generator, the spec manifest and tsconfig, a sibling spec script, a spec source outside the tree, a lookalike directory, and a pull_request feature branch.expect_all_runthat became false (scripts/helper.mjs) now lists the nine families it runs.pin_stepfor the new step.Rosters: none needed updating. Only the selector, its self-test and
lint.ymlname family ids.check:migration-registrywas already classified inpackages/spec/scripts/check-generated.ts.On
mainfirst (ruling item 3)pnpm --filter @objectstack/spec check:migration-registryata5afe382baexited 0 withsrc/migrations/registry.ts is current (232 semantic, 207 retired-key, 183 retired-def). The registry is not stale, soregistry.tsis untouched and this PR has no changeset: nothing published changes.Duration (ruling item 4)
Wall clock of the exact step command, in a 4-core container:
It needs no build: tsx reads one directory and splices text. When a PR's diff is outside the read-set, the step is skipped and costs about 0 s.
Verification (head
e1825aabbf)a5afe382bapasses 44 cases / 228 checks. Head passes 56 cases / 293 checks.merge_group):run migration_registry reads packages/spec/src/migrations/entries/semantic/17.action-descriptor-is-async-retired.ts (M, workspace).skip migration_registry no changed path is in its read-set.run migration_registry merge group base ... unresolvable.registry.ts is stale. The edit had landed: in the entry, the old text went from 1 copy to 0 and the new text from 0 to 1, while the registry still carried 0 copies of the new text.gen:migration-registryrewrote one line, it exits 0.git diff HEADempty):if:as== 'run': 4 FAIL.--filterfrom the step: the pin FAILs.dispatch-gates --repo objectstack-ai/objectstack --commands, 51 commands) pluscheck:issue-citations(self-test and diff-scoped) plus the gate itself:pnpm check:type-check-debt, which exited 3 withPREREQUISITE NOT MET(it needs the wholepackages/*closure built). That result is NOT MEASURED, not red. The diff touches no package source or tsconfig, so it is left to CI.pnpm check:pm-dispatch-gateswas still running when this PR opened. Its verdict is in theos-dev-reporton [finding]check:migration-registryruns in NO CI workflow step, so a PR that edits a migration entry without regeneratingregistry.tsships a stale registry with CI fully green — and card #19523 is the live repro #19753.Lint & Repo Gatesrun: the new step should read skip. This diff touches only.github/workflows/andscripts/ci/, and neither is in the read-set. So the first CI execution of the step on the real tree is the push-on-main run after merge, where every family runs. The exact command was run locally, and the self-test pins it.Acceptance notes
packages/spec?" says every spec generated-artifact gate lives inTypeScript Type Check. That was already untrue for this gate, which ran nowhere, and after this PR it lives inLint & Repo Gates, scoped. AGENTS.md is a governed surface. Owner: none..github/workflows/**, so per triage, seat auto-merge answers 422 and the PR needs a human merge.Generated by Claude Code