Skip to content

ci(lint): run each scoped Lint & Repo Gates family only on the paths it reads - #16754

Merged
baozhoutao merged 3 commits into
mainfrom
claude/issue-16496-scope-lint-gate-families
Sep 8, 2026
Merged

ci(lint): run each scoped Lint & Repo Gates family only on the paths it reads#16754
baozhoutao merged 3 commits into
mainfrom
claude/issue-16496-scope-lint-gate-families

Conversation

@claude

@claude claude Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Fixes #16496

Lint & Repo Gates is the merge queue's critical path after #16453. This lands the selector the card rules for: on merge_group and pull_request, five gate families run only when the changed paths touch the files that family reads; push on main and any scheduled run keep the full battery. One script under scripts/ci/ with a self-test beside it, in the shape select-shard-packages.sh set.

Clause-②: yes (card ruling 4) — a required check verifies fewer families at merge time. needs:contract-review is hung on both carriers.

The family list (card ruling 4)

family id step median s (10 merge-group runs) read-set the selection keys on (derived from the gate's own source)
pm_dispatch_gates PM dispatch-gates self-test 597 .github/**, scripts/** (every gate source, not only scripts/pm/), packages/*/scripts/**, every package.json, .claude/**, skills/** (the frame-sync COPIES table), AGENTS.md, CLAUDE.md, tsconfig.json, every .gitignore (nested ones included, via the exposed-scratch-dir sweep); the CONTENT of every JS/TS file in the tree (compound-anchor census of function ...SelfTest...( declarations, exposedScratchDirs over every mkdtempSync/mkdirSync caller), so a modification of ANY masked-source path runs it; PLUS the tracked NAME set via git ls-files (test-file residue, dead-literal and scripts/**/*.d.mts assertions), so any ADDED path runs it too
query_options_erasure Engine query-options erasure ratchet 110.5 packages/**/*.{ts,tsx,mts,cts}, scripts/query-options-erasure-baseline.json (HEAD and merge base), eslint.config.mjs, top-level scripts/*.mjs helpers it imports
slot_lookup Slot-lookup ratchet 57.5 same population and shape, with scripts/slot-lookup-baseline.json
verify_lock Verify-lock entry-point self-test 100 scripts/pm/os-verify-lock.sh ($SELF), plus what case (h) reaches by running the real entry point from the repo root: filter_preflight -> scripts/pnpm-filter-targets.mjs --preflight, its top-level scripts/*.mjs imports (invoked-as.mjs, workspace-enumerator.mjs), pnpm-workspace.yaml (root config) and every workspace package.json
comment_mask_corpus Comment mask agrees with a real parser over the whole corpus 48 every .ts .tsx .mts .cts .js .mjs .cjs .jsx file outside build directories (SOURCE_EXTENSIONS / SKIPPED_DIRECTORIES in the script)

Root configuration (package.json, pnpm-lock.yaml, pnpm-workspace.yaml, turbo.json, tsconfig.json, eslint.config.mjs, .gitignore, .gitattributes, .npmrc, .nvmrc) runs every family. ESLint is not scoped (card ruling 2). No other step is scoped: every remaining step is either under 30 s at the median or its reads could not be named from its source in this sitting (card ruling 3: not named, not scoped).

A docs-only or changeset-only group therefore skips 913 s of the 1468.5 s job median.

Fail-open, and how it is proved

The selector (scripts/ci/select-gate-families.sh) runs EVERY family when: the event is not merge_group/pull_request; the base is absent or unresolvable (after a fetch attempt); git diff fails; the changed list is EMPTY (both events — PM ruling, the #10057 shape on a pull_request); any path is one the classifier does not recognise (a new top-level directory, an unlisted root file); any path carries a control character; any change is a deletion, rename or type change (--no-renames, so a rename is D + A). A family skips ONLY when every changed path is positively classified into a class it provably never reads; every case in the script falls through to "run".

The YAML half spells if: steps.gate-families.outputs.FAMILY != 'skip', so an ABSENT output (selector never ran, wrote nothing) runs the step. scripts/ci/select-gate-families.selftest.sh (39 cases, 187 checks, offline, ~3 s) drives every branch above and additionally reads the real lint.yml to pin: the selector step's id and env, the != 'skip' spelling on every scoped step (and the absence of any == 'run'), that the scoped set equals --families, and that each if: sits on the step running that family's command. The card's four minimum cases are present by name.

Live runs against this branch (both shapes) select everything: .github/workflows/lint.yml (M, workflow) and package.json (M, root-config) — so this PR's own merge-group summary will show 5 run / 0 skipped with reasons, not a skipped family. The skipped rendering is pinned by the self-test's step-summary check and will first be seen live on the next docs-only group; the seat reads that one. Recorded as a deviation from acceptance item 2 rather than worked around.

Measured first (card ruling 3; PM assumption A1)

Medians over the last ten completed merge_group runs of Lint & Type Check (run ids 34175859565 … 34177868806, all 2026-09-08T01:12Z–02:13Z; 161 steps each): job median 1468.5 s (min 1194, max 1539). The card's single-sample figures moved: dispatch-gates self-test 518 → 597 s, erasure ratchet 112 → 110.5, verify-lock 100 → 100, ESLint 69 → 66. Two families the card did not name sit above 45 s: slot-lookup ratchet 57.5 s and the comment-mask corpus 48 s — both have nameable read-sets and are scoped. The full 161-row table with a read-set column is in the report comment on #16496.

The load-bearing premise, tested per step (A2)

  • The card says the PM dispatch-gates self-test "exercises scripts/pm/** and .claude/** only". False: check-dispatch-gates.mjs spawns the tool's full --self-test, whose live-tree cases discover every workflow file, resolve every check:* script through root and package package.jsons, read every gate source under scripts/** and packages/*/scripts/** for watch hints, reach skills/objectstack-pm-dispatch/SKILL.md through the frame-sync COPIES table, read tsconfig.json and .gitignore, and sweep git ls-files for hint reachability and test-file residue. It also reads the CONTENT of every JS/TS file (the compound-anchor census and the exposed-scratch-dir sweep, both with assertions that can go red) and nested .gitignore files — measured by the contract reviewer in patch round 1 after round 0 had under-derived it. The key list is implemented wider than card ruling 2's list accordingly: any masked-source modification, any .gitignore, and additions/deletions anywhere run it.
  • Verify-lock self-test: reads itself, and — corrected in patch round 1 — its case (h) runs bash "$SELF" -c ... from the real repo root, which preflights through scripts/pnpm-filter-targets.mjs and enumerates the workspace. Scoped to itself, top-level scripts/*.mjs, and every package.json.
  • Both ratchets: packages/** TS only (LINT_TARGET), their baseline (also at the merge base), eslint.config.mjs where both rules are defined inline, and top-level scripts/*.mjs helpers.
  • Comment-mask corpus: a filesystem walk by extension. Scoped by extension.

Contention and the name collision (A3, A4)

  • A3: re-measured at push time — 19 open PRs, every file page fetched (PR chore: version packages #15334 paged to exhaustion), zero hits on .github/workflows/lint.yml, scripts/ci/, root package.json; control on the read: 100 .changeset/ entries on chore: version packages #15334's first page. lint.yml is unchanged on main since the branch point 5e53d73d.
  • A4: the Lint & Repo Gates string in cross-repo-issue-closer.yml is a COMMENT naming the required status context; there is one job by that name, in lint.yml. Nothing else was touched.

A prose conflict, resolved in favour of the card

The comments beside the dispatch-gates and verify-lock steps said the steps are "deliberately UNCONDITIONAL … weakening this gate is a maintainer call, and no reading of this note authorises one." The card carries that call (「同意你的建议,你负责执行派发所有可行的优化」, card ruling 2 names these two families first). Both notes are rewritten to say what changed and why the read-set-scoped if: is not the exemption they refused (a label or a paths filter with no reader behind it). The notes beside the other self-test steps still say "unconditional, like the self-tests above" — those steps ARE still unconditional, so they were left as they are.

Gates run locally (final commit f12f2e1c7)

Derived with node scripts/pm/dispatch-gates.mjs --changed --commands on the deepened checkout; reconciled with --ran: "55 derived famil(ies) accounted for — 55 run, 0 NOT-MEASURED". All green with their own verdict lines, including pnpm check:select-gate-families (all 39 cases passed, 187 checks), pnpm check:pm-dispatch-gates (✓ dispatch-gates self-test: 1534 cases pass — run detached per its header and waited on in the foreground; one earlier run of the same tree failed one CONTROL case only because this container's checkout was shallow with a moved origin/main, which is the tool's documented SHALLOW refusal, not the tree), check-self-test-wired (190/190 self-tests wired), check:bash32-floor (31 tracked shell files), check:declared-population-live, check:watch-hint-literal, check:nul-bytes, the step-collector and workflow-command gates, check:required-contexts, check:workflow-status-functions, the comment-mask corpus walk (6343 files, 0 disagree).

NOT MEASURED locally, declared to CI (each printed PREREQUISITE NOT MET, exit 3, because they read built dist/ and this diff builds no package): check:dts-closure, check:dual-build-cjs-loads, check:sourcemap-no-sources-content, check:type-check-debt. pnpm lint (repo-level scan) is CI's run and was not run here; the diff touches no ESLint population file. Changeset: none — nothing published moves (scripts/ci/**, .github/** and a private root package.json script entry are outside every released package's files[]); skip-changeset is hung.

The dispatch derivation is a consumer of this change: extractWatchHints over the new files yields exactly scripts/ci/select-gate-families.sh and .github/workflows/lint.yml for the self-test and nothing for the selector (the precedent yields one and one), so no fabricated MATCHED lead is added.

Patch round 1 (contract review FAIL -> fixed)

The reviewer measured two key sets narrower than their gates' true read-sets: pm_dispatch_gates skipped a workspace source edit although the self-test reads every source's content, and verify_lock was described as reading one file although its case (h) preflights through scripts/pnpm-filter-targets.mjs and the workspace manifests. Both family_reads arms are widened (commit f12f2e1c7), the headers and lint.yml notes corrected, the two self-test cases that pinned the wrong behaviour flipped, and two cases added (a nested .gitignore runs pm_dispatch_gates; a non-source, non-manifest workspace file still skips every family). Re-run on the patched tree: self-test 39/39, and the ten lint.yml/scripts/** readers named above plus check:pm-dispatch-gates (verdict in the report comment). Derived set unchanged: 55 derived, 55 run, 0 unrun.

验收备注


Generated by Claude Code

)

Lint & Repo Gates is the merge queue's critical path after #16453 (median
1468 s over the last ten merge-group runs; the PM dispatch-gates self-test
alone 597 s), and none of its expensive steps read a merge group's file
surface. A selector under scripts/ci/ -- the shape select-shard-packages.sh
set -- classifies the changed paths on merge_group and pull_request and
skips a family only when every path is one that family provably never
reads. Every doubt runs every family: an unscoped event, an unresolvable
base, an empty or failed diff, an unclassified path, a deletion or rename.
The workflow steps spell `!= 'skip'`, so an absent output runs the step too.

Five families are scoped, each with its read-set derived from the gate's
own source: the PM dispatch-gates self-test, the query-options erasure
ratchet, the slot-lookup ratchet, the verify-lock self-test and the
comment-mask corpus walk. ESLint is not scoped. push on main and any
scheduled run keep the full battery.

The self-test drives every branch offline and also reads lint.yml to pin
the YAML half of the contract: the selector step's id and env, the
`!= 'skip'` spelling on every scoped step, and that the scoped set equals
the decided set.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
…rivation

The self-test now spells its fixture paths unquoted and pins the verify-lock
step by its script name, so scripts/pm/dispatch-gates.mjs extracts exactly
the two files it reads (the selector and lint.yml) and check-self-test-wired
does not read a pinned command fragment as a flag of this file. Header
paths in both scripts are spelled without backticks for the same reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
…han their gates

pm_dispatch_gates now runs on a modification of any JS/TS source anywhere
and of any nested .gitignore: the dispatch-gates self-test reads the
content of every source file (compound-anchor census, exposed-scratch-dir
sweep) and consults nested ignore rules, so a workspace source edit could
have reddened the step the selector skipped. verify_lock now runs on a
modification of any top-level scripts/*.mjs and any workspace package.json:
case (h) of its suite runs the real entry point from the repo root, which
preflights through scripts/pnpm-filter-targets.mjs and enumerates the
workspace. Headers, the lint.yml notes and the self-test cases say so; two
new cases pin the nested .gitignore and the non-source workspace file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU

Copy link
Copy Markdown
Contributor

Contract review (CONTRACT_REVIEW_TIER, isolated seat) — PR #16754 @ 59e6122

Verdict: CHANGES REQUIRED (at the reviewed head 59e6122d8; see F1/F2 and the head-moved note in §8)
Ruling implemented: partially — the selector, its self-test, the fail-open shape, the ESLint exclusion, the push/schedule full battery and the job summary all match the ruling; but at 59e6122 two scoped families skip on changes their gate actually reads, which is the one thing ruling 1 ("only when the changed paths touch the files it reads") and ruling 3 ("a step whose reads cannot be named is not scoped") forbid.

Reviewed independently from the PR head fetched into a throwaway ref (git diff 5e53d73d..59e6122d8, 4 files: .github/workflows/lint.yml M, package.json M, scripts/ci/select-gate-families.sh A, scripts/ci/select-gate-families.selftest.sh A) and from each scoped gate's own source at that ref. Nothing in the PR body was taken on trust.

1. The ruling (issue #16496, body section "## Ruling" — there is no separate "Ruling recorded" comment on the card; the maintainer authority it cites is 「同意你的建议,你负责执行派发所有可行的优化」), quoted verbatim

  1. On merge_group and pull_request, each gate family in Lint & Repo Gates runs only when the changed paths touch the files it reads. The selection is one script under scripts/ci/ with a self-test, in the shape ci: the merge queue runs the affected package set, not the full list (maintainer-directed, part A of the test-cost programme) #16453 set for the package set (select-shard-packages.sh), and the job prints the families it ran and the families it skipped. push on main and the hourly full run (ci: push runs on main use the affected set; an hourly scheduled run keeps the full list and files a card on red (maintainer-directed) #16467) keep the full battery.
  2. First family: PM dispatch-gates self-test (518 s), keyed to scripts/pm/**, .claude/**, AGENTS.md, CLAUDE.md, .github/workflows/**, package.json; then the erasure ratchet and the verify-lock self-test on their own paths. ESLint is not scoped.
  3. Measure first: list every step of the job with its median over the last ten merge-group runs and the paths it reads, before writing the selector. A step whose reads cannot be named is not scoped.
  4. Clause-②: yes. The required check verifies fewer families at merge time. Contract tier; the family list is part of the PR body.

The PR body's citations check out against the card text: "card ruling 2" = ESLint not scoped + the first-family key list; "card ruling 3" = measure first / not named, not scoped; "card ruling 4" = clause-② yes + family list in the body. Ruling 1's "prints ran and skipped" is met (publish() writes the log line, $GITHUB_OUTPUT and a $GITHUB_STEP_SUMMARY table). Ruling 1's "push on main and the hourly full run keep the full battery" is met structurally: lint.yml triggers are push(main) / pull_request(main) / merge_group only, and the script's first case runs everything for any event other than merge_group/pull_request. Ruling 2's key list was widened (the card's own premise that the dispatch-gates self-test reads scripts/pm/** + .claude/** only is false — confirmed below), and two families the card did not name (slot_lookup, comment_mask_corpus) were scoped; both moves are inside ruling 1 + 3 as long as the read-sets are complete. The ten-run median table (161 rows) is on the card as ruling 3 asks.

2. What moved, and the read-sets re-derived from source

lint.yml steps whose if: changed (all new, none pre-existing): Slot-lookup ratchet, Engine query-options erasure ratchet, Comment mask agrees with a real parser over the whole corpus, PM dispatch-gates self-test, Verify-lock entry-point self-test — each if: steps.gate-families.outputs.<id> != 'skip'. New steps: Select the gate families this run pays for (id: gate-families, env OS_GATE_EVENT_NAME / OS_GATE_PR_BASE_REF / OS_GATE_MERGE_GROUP_BASE_SHA) and Gate-family selection self-test (pnpm check:select-gate-families). ESLint step: no if: — not scoped, as ruling 2 requires. No other step's condition changed.

family what the gate really reads (my derivation) selector at 59e6122 verdict
query_options_erasure / slot_lookup scripts/check-*-ratchet.mjs: LINT_TARGET = packages/**/*.{ts,tsx,mts,cts} (overrideConfigFile: true, no projectService, so no package tsconfigs), baseline JSON at HEAD and git show <merge-base>: , eslint.config.mjs (which itself readFileSyncs both baselines; NEVER_LINTED/SLOT_LOOKUP_UNSWEPT inline), imports ./import-prerequisite.mjs, ./invoked-as.mjs, ./eslint-fatal-guard.mjs, ./eslint-stack-headroom.mjs (all top-level scripts/*.mjs) packages/** TS, scripts/*.mjs, scripts/*.json, root-config complete
comment_mask_corpus collectSources() walks the tree by SOURCE_EXTENSIONS (.ts .tsx .mts .cts .js .mjs .cjs .jsx) outside SKIPPED_DIRECTORIES; imports ./js-comment-mask.mjs, ./invoked-as.mjs, ./import-prerequisite.mjs; the independent parser comes from node_modules (pnpm-lock.yaml = root-config) any masked-source path anywhere, checked before class complete
verify_lock mode_self_test reads $SELF and a temp dir — and its case (h) (os-verify-lock.sh ~L4087–4160) runs bash "$SELF" -c 'echo pnpm --filter @objectstack/adapter-hono test' from repo_root ($(dirname "$SELF")/../..), which goes through filter_preflightnode scripts/pnpm-filter-targets.mjs --preflight, which imports ./invoked-as.mjs + ./workspace-enumerator.mjs and reads pnpm-workspace.yaml and every workspace package.json; the cases assert that @objectstack/adapter-hono is refused (exit 2) and @objectstack/hono (= packages/adapters/hono/package.json) is accepted scripts/pm/os-verify-lock.sh + root-config only; class scripts and class workspace (incl. */package.json) → skip incomplete → F1
pm_dispatch_gates scripts/pm/check-dispatch-gates.mjs spawns dispatch-gates.mjs --self-test. Confirmed reads: every workflow file, root + package package.json scripts, every gate source under scripts/** and packages/*/scripts/** (watch hints, argv defaults), .claude/** + skills/** via the frame-sync COPIES table, tsconfig.json, git ls-files name sweeps. Also, and not in the PR's read-set: the CONTENT of every JS/TS file in the treeexposedScratchDirs({}) (L20445) reads every tracked .[cm]?[jt]sx? body via inTreeScratchDirs and asserts exposed.length === 0 (consulting nested .gitignores); the stampsAnErrorCodeLiteral census (L16533–16544) reads every non-test .ts body and asserts the specimen packages/spec/src/conversions/types.ts still stamps a code and the hit ratio stays in band M to a workspace file that is neither package.json nor under scripts/ → skip incomplete → F2

3. Fail-open (scripts/ci/select-gate-families.sh)

Traced every exit: event ∉ {merge_group,pull_request} → run all (no warning, by design); empty OS_GATE_PR_BASE_REF / OS_GATE_MERGE_GROUP_BASE_SHA → warn + run all; fetch failure → warn, then resolution decides; unresolvable merge-base / base sha → warn + run all; git diff non-zero → run all; control character in a path → run all; empty changed list → warn + run all (both events); any status other than A/M (D, T; renames arrive as D+A under --no-renames) → run all; any unknown class (new top-level dir, unlisted root file such as lychee.toml, tsup.config.ts, .mcp.json) → run all. family_reads has *) return 0 on every case; only positively-classified classes return 1. Exit 0 on every decision, exit 2 only on missing RUNNER_TEMP. YAML half: all five conditions spell != 'skip'; no == 'run'; an absent output therefore runs the step, and the self-test pins both spellings against the real lint.yml. pull_request diffs against merge-base(origin/<base>, HEAD) on the merge ref; merge_group diffs base_sha..HEAD so a multi-PR group is the union. The mechanism is sound; the two holes are in the read-sets (F1, F2), not in the fail-open plumbing.

4. Governed paths

Touched: no. Diff files are exactly .github/workflows/lint.yml, package.json, scripts/ci/select-gate-families.sh, scripts/ci/select-gate-families.selftest.sh. Nothing under docs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md, content/docs/releases/**. .github/workflows/** is CI, not a governed surface — but this PR makes a required check (Lint & Repo Gates) verify fewer families at merge time, i.e. clause-② per card ruling 4, so this is a maintainer-only merge regardless of any review verdict; needs:contract-review is on both carriers. The PR is a draft, mergeable_state: blocked.

5. Changeset

skip-changeset is correct: nothing published moves — one private root package.json script entry (check:select-gate-families), scripts/ci/**, .github/**. Check Changeset green on 59e6122.

6. CI on 59e6122d8 (41 check runs at my last poll before the head moved)

29 success, 11 skipped (opt-in / path-filtered jobs), 0 failure, 1 in progress: Lint & Repo Gates — its log ends ##[error]The operation was canceled at 03:57:33Z because the branch was force-advanced to f12f2e1 (see §8). So the reviewed head never produced a Lint & Repo Gates verdict, and its own selector step output could not be read from the log. No red gate on 59e6122.

7. Tests

scripts/ci/select-gate-families.selftest.sh exists, is wired as root check:select-gate-families and run by the Gate-family selection self-test step in the same job. check-self-test-wired keys on scripts carrying a --self-test literal; neither new file carries one, so it has nothing to demand here (the self-test is wired by the workflow step regardless). The card's four minimum cases are present by name: M:scripts/pm/tool.mjs (dispatch-gates runs), docs-only group (0 run / 5 skipped, summary row pinned), M:.github/workflows/lint.yml (runs), A:brand-new-dir/thing.txt and M:lychee.toml (every family). Also covered: empty diff, no base, unfetchable base, shallow clone fetch-by-sha, deletion, rename, control-char path, PR base fetch, and the YAML pin. 39 cases at 59e6122 (37 run_case + 2 manual).

8. Head moved during this review

At 03:57:24Z the branch advanced to f12f2e1c7 ("widen two read-sets the contract review measured narrower than their gates"). I diffed 59e6122..f12f2e1: it adds is_masked_source || */.gitignore → run to pm_dispatch_gates, adds */package.json and top-level scripts/*.mjs to verify_lock, updates both script/workflow notes, and adds/rewrites 6 self-test cases. That delta closes F1 and F2 as I derived them (the preflight's two imports are both top-level scripts/*.mjs; the specimen and scratch-dir census files are all masked sources). It is not the reviewed head, so this verdict stands for 59e6122; a re-read at f12f2e1 is owed once its Lint & Repo Gates is green.

Findings

F1 — verify_lock skips on changes its self-test reads (at 59e6122). Case (h) of mode_self_test exercises scripts/pnpm-filter-targets.mjs --preflight against the real workspace and asserts on real package names; the selector skips the family for scripts/*.mjs (class scripts) and for any packages/*/package.json (class workspace, including status A — only pm_dispatch_gates has the "any A runs" rule). A group that changes the preflight's exit-code contract, or adds a package named @objectstack/adapter-hono, would go red on push to main only. The PR body's "reads exactly one file in the tree (itself); measured by grepping its mode_self_test body" is false. Expectation: key verify_lock on top-level scripts/*.mjs, every */package.json and pnpm-workspace.yaml (done in f12f2e1), and correct the family-list row in the PR body, which ruling 4 makes part of the contract.

F2 — pm_dispatch_gates skips on modified workspace source it reads (at 59e6122). The self-test's live exposedScratchDirs({}) and stampsAnErrorCodeLiteral censuses read the body of every tracked JS/TS file and assert over them (e.g. packages/spec/src/conversions/types.ts must still stamp a code; every mkdtempSync/mkdirSync site must be ignore-covered, consulting nested .gitignores); the selector treats an M to any packages|apps|examples/** file that is not a manifest or under scripts/ as inert. Expectation: any masked-source M anywhere and any .gitignore runs the family (done in f12f2e1), and the PR body's read-set row is updated to say so.

F3 — PR body family table is stale relative to the code that fixes F1/F2. Ruling 4: "the family list is part of the PR body." The body at the time of this review still states the 59e6122 read-sets. Expectation: the author (not this seat — I do not edit the PR) updates the table rows for pm_dispatch_gates and verify_lock to match the script header at f12f2e1 before the maintainer merges.

F4 — Acceptance item 2 cannot be shown by this PR's own merge-group run. Its diff touches root package.json (root-config → every family), so its summary will read 5 run / 0 skipped. The PR declares this. Expectation: the seat reads the first post-merge docs-only or changeset-only group's job summary and records it on #16496 before the card is closed; the card's acceptance line should not be ticked on this PR's run alone.

F5 — Card premise correction, informational. The card's "Measured" paragraph ("exercises scripts/pm/** and .claude/** only") is wrong, and the PR's own first derivation was also narrower than the gate (F1/F2). Ruling 3's "reads cannot be named → not scoped" is only as safe as the naming; the selector self-test proves the selector, not the read-set. Expectation: none for this PR beyond F1–F3; if a scoped family ever goes red on the hourly full run (#16467) while green in the queue, treat it as a read-set gap first, per acceptance item 3.

No approval, review, label or merge action was taken by this seat.


Generated by Claude Code

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

Labels

ci/cd dependencies Pull requests that update a dependency file needs:contract-review size/xl skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

3 participants