Skip to content

ci(lint): run check:migration-registry in Lint & Repo Gates, scoped by the gate-family selector - #19812

Merged
os-justin merged 1 commit into
mainfrom
claude/issue-19753-migration-registry-in-lint
Sep 23, 2026
Merged

os-justin merged 1 commit into
mainfrom
claude/issue-19753-migration-registry-in-lint

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #19753

Clause-②: no

Ruling (not reopened here)

The maintainer's 「同意 A′」, recorded by triage in comment 5790385022 on #19753: check:migration-registry becomes a step inside the required Lint & Repo Gates job. It runs only when a change touches packages/spec/src/migrations/**, and it hangs on the existing gate-family selector. It is not a separate paths:-filtered required check, which would never report on unrelated PRs.

Design

  • scripts/ci/select-gate-families.sh: adds the new family migration_registry. It comes first in FAMILIES because 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 generated registry.ts. An edit to either side alone is the drift the gate reports.
    • the generator, 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, with if: steps.gate-families.outputs.migration_registry != 'skip' and run: pnpm --filter @objectstack/spec check:migration-registry. It sits right after ADR anchors because 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:

    • The fixture gains the read-set and its nearest neighbours.
    • 12 new cases: entry M/A/D, a stray non-TS file in an entry dir, 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.
    • Every count line that moved is updated. The one expect_all_run that became false (scripts/helper.mjs) now lists the nine families it runs.
    • A new pin_step for the new step.
    • The floor rises from 42 cases / 220 checks to 56 / 293.
  • Rosters: none needed updating. Only the selector, its self-test and lint.yml name family ids. check:migration-registry was already classified in packages/spec/scripts/check-generated.ts.

On main first (ruling item 3)

pnpm --filter @objectstack/spec check:migration-registry at a5afe382ba exited 0 with src/migrations/registry.ts is current (232 semantic, 207 retired-key, 183 retired-def). The registry is not stale, so registry.ts is 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:

  • 1.37 s when the machine was idle.
  • 1.47 to 1.62 s over 5 runs at load ~6.

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)

  • Selector self-test: base a5afe382ba passes 44 cases / 228 checks. Head passes 56 cases / 293 checks.
  • Selector on the real tree (scratch clone, merge_group):
    • An entry edit gives run migration_registry reads packages/spec/src/migrations/entries/semantic/17.action-descriptor-is-async-retired.ts (M, workspace).
    • A docs-only commit gives skip migration_registry no changed path is in its read-set.
    • An unresolvable base gives run migration_registry merge group base ... unresolvable.
  • Lit test of the gate (scratch clone):
    1. On the clean tree it exits 0.
    2. With one entry's prose edited and the registry not regenerated, it exits 1: 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.
    3. After gen:migration-registry rewrote one line, it exits 0.
  • Ablations (scratch clone; each one restored with the blob equal to HEAD and git diff HEAD empty):
    • Remove the migration-tree arm: 16 FAIL.
    • Spell the if: as == 'run': 4 FAIL.
    • Run base's selector and workflow under head's self-test: 139 FAIL.
    • Drop --filter from the step: the pin FAILs.
  • Derived gates (dispatch-gates --repo objectstack-ai/objectstack --commands, 51 commands) plus check:issue-citations (self-test and diff-scoped) plus the gate itself:
  • Hygiene: 0 control bytes and 0 model identifiers in the added lines. Both scans fire on a lit control.
  • This PR's own Lint & Repo Gates run: the new step should read skip. This diff touches only .github/workflows/ and scripts/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

  • Noted, not filed: AGENTS.md "Touched packages/spec?" says every spec generated-artifact gate lives in TypeScript Type Check. That was already untrue for this gate, which ran nowhere, and after this PR it lives in Lint & Repo Gates, scoped. AGENTS.md is a governed surface. Owner: none.
  • Landing: this touches .github/workflows/**, so per triage, seat auto-merge answers 422 and the PR needs a human merge.

Generated by Claude Code

…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>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: e1825aabbfa36a52bcb1811a2c45ec11026b8b5d

Reviewed and posted 2026-09-23T12:33Z by the at-tier review subagent the domain:spec#5 seat spawned — read: card #19753 and its 9 comments, PR #19812 body / diff / 1 commit / 35 check-runs, AGENTS.md, contract-review.md, the generator, selector, self-test, lint.yml, check-required-contexts.mjs; ran: in a detached worktree at the head (offline install) and a scratch clone — the exact step command, a drift battery, the selector on the head and on 11 committed trees, both self-tests, 5 ablations; NOT MEASURED: the heads of 10 open PRs (not in local objects), the dev's own timings.

① Derived judgments

Read-set — complete. The generator imports only node:fs, node:path, node:url; it reads src/migrations/registry.ts and the three src/migrations/entries/* dirs (both under packages/spec/src/migrations/*, whose bash-case glob spans /), itself, packages/spec/package.json (pnpm resolves the script) and packages/spec/tsconfig.json (tsx; extends root, and root config is root-config → always runs). No other source input exists. Toolchain inputs (node/pnpm versions in .github/**) are workflow class and skip, as for the other nine families by the selector's standing design. Every class the classifier can emit is named in the arm; unknown|root-config run before it.

Lit and dark, on the real step (pnpm --filter @objectstack/spec check:migration-registry, worktree, no build):

Selector at the head (--families prints 10 ids): the PR's own diff → pull_request and merge_group both 5 run, 5 skipped, skip migration_registry — no changed path is in its read-set; matches CI (job 107092174705: step 8 selector success, step 11 Migration registry matches its entry files skipped). push, schedule, base 0000…, base == HEAD (empty diff), no base_sha, no PR base ref → 10 run, 0 skipped, each with its own reason line. Committed trees on the head, merge_group: entry M → run (M, workspace); registry.ts M alone → run; spec package.json M → run; generator M → run; stray notes.txt A → run (1 run, 9 skipped); entry D → structural, 10 run; docs-only → skip (0 run, 10 skipped); packages/spec/src/index.ts M → skip; sibling build-schemas.ts M → skip; lookalike migrations-x/ A → skip; workflow-only M → skip.

Self-test: base a5afe382ba 44 cases / 228 checks, head 56 / 293, both exit 0. Ablations at the head, each restored: delete the packages/spec/src/migrations/* arm → 16 FAIL; drop the id from FAMILIES → 140 FAIL; delete the step's if: → 3 FAIL; spell it == 'run' → 4 FAIL; drop --filter → 1 FAIL (the pin). Untouched control passes.

Placement: job lint, name: Lint & Repo Gates unchanged (no name: edit in the diff; check:required-contexts green on the head); events push (main), pull_request, merge_group, schedule, workflow_dispatch — the step runs on every unscoped event. Sits after install (step 7) and the selector (8), before ESLint (12). No matrix, no new context, required set untouched. The registry is current at a5afe382ba (dev) and at today's origin/main tip 8cbc3c0084 (236 / 210 / 183, my run) ⇒ the first real execution, push-on-main after merge, is expected green.

Composition: family ids are named in exactly three files — selector, self-test, lint.yml (the check-slot-lookup-ratchet.mjs grep hit is a fixture path, not a roster) — all three updated. The check:generated ledger already classifies the script. check:pm-expected-skips rosters skipped check-run NAMES (jobs), not steps; the unmeasured-tail reporter already handles nine steps skipped by their own if:. Stale prose, non-blocking: (1) the on: block's hourly-run comment in lint.yml enumerates the scoped families and omits the tenth; (2) AGENTS.md § Touched packages/spec says every spec generated-artifact gate "live[s] in one job — TypeScript Type Check", and its migration-registry row names only check:spec-changes / check:upgrade-guide — false before (the gate ran nowhere), now false by ruling; governed surface, needs a card.

Sentences that ship — reproduced: "ran in no CI step" (base .github census 0; lit control check:api-surface 4); "check:generated --reconcile-only (in the Type Check · source gates job)" (lint.yml:5456 sits in typecheck-source-gates); "the other nine were steps that ran on every PR before the selector existed" (all nine steps predate the selector's first commit in lint.yml history); "Nothing else in the workspace, nothing outside it" (matches the imports); "~1.4 s" (mine 1.35–1.43 s); "the job's ordering note puts cheap gates first" (lint.yml:151–155); "12 new cases", "44/228 → 56/293", ablation counts 16 / 4 / pin, "Rosters: none needed updating". Claims only: the dev's load-6 timings and the 1905-case check:pm-dispatch-gates figure (that family ran on this diff in CI's green Lint & Repo Gates).

Hygiene: 186 added lines + commit message: 0 control bytes, 0 model identifiers (the one pattern hit is the Claude-Session: trailer key, not an identifier); both lit controls fire.

② Semver level

None. Diff = .github/workflows/lint.yml, scripts/ci/select-gate-families.sh, scripts/ci/select-gate-families.selftest.sh; the root package is "private": true, spec's files[] is dist / json-schema / liveness / …; nothing published moves. skip-changeset is right. Clause-②: no on the PR body and both claims is consistent: none of the five review surfaces is touched.

③ Boundary flags

open_questions: []. Deviations answered: write channel via scripts/pm/ — AGENTS.md wins; label writes left to the seat — skip-changeset is on the PR; the amended trailer — the single pushed commit carries the model-free pair. Out-of-scope findings (2) are noted, not filed — escalated below. Seat's own question (open PRs touching registry.ts): 13 of 23 open PR heads are in local objects; 2 touch the migration tree (#19818, #19637) and both are current head-alone; 10 heads NOT MEASURED (not fetched locally). Landing changes their CI exactly as ruled: a merged tree whose registry is stale goes red on the required job.

CI (35 check-runs on the head, de-duplicated by name keeping the latest start → 31 names). Success: Lint & Repo Gates, TypeScript Type Check and its 4 lanes, Test Core and 6 shards, Dogfood Regression Gate, filter, Governed Surface Queue Guard, Check Documentation Links, the 4 PM claim guards. Skipped, each by its own if:: Check Changeset (the label-triggered rerun, !contains(labels, 'skip-changeset'); its earlier failure was the pre-label run), Auto Label and Check PR Size (action != labeled), Packed-tarball smoke (opt-in) (no needs:pack-smoke), Build Core / Dogfood Verify CLI / Temporal Conformance / the dogfood shard matrix (filter.core false — diff outside packages/**), Build Docs (filter.docs), Console Pin Gate (filter.console). No failure in the latest set; no stop-after-failure skips.

Blocking: none.
Non-blocking: (a) file the AGENTS.md § Touched packages/spec correction as a card — governed surface; the "one job" sentence and the registry row are now false by ruling; (b) the lint.yml on: hourly-run comment omits migration_registry from its list of scoped families; (c) the selector self-test floors one pinned total rather than per-battery names + handshake — pre-existing shape, the dev's own note; (d) the generator's out-of-region blind spot (#18062) is unchanged — not this card.

Implemented-by: claude/issue-19753-migration-registry-in-lint
Reviewed-by: session_01Sfe5YjBLwB9J3y8fvm2xq1 — at-tier review subagent spawned by the domain:spec#5 seat

VERDICT: PASS

@os-justin
os-justin marked this pull request as ready for review September 23, 2026 12:34
@os-justin
os-justin added this pull request to the merge queue Sep 23, 2026
Merged via the queue into main with commit 30854f4 Sep 23, 2026
36 of 37 checks passed
@os-justin
os-justin deleted the claude/issue-19753-migration-registry-in-lint branch September 23, 2026 12:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

3 participants