Repository navigation
Commit dfa427b
ci(lint): key the PM dispatch-gates family on the files its battery opens (#22092)
Fixes #22076
Clause-②: no
## What changes
The `pm_dispatch_gates` arm of `scripts/ci/select-gate-families.sh`
decides the `if:` of the `PM dispatch-gates self-test` step in `Lint &
Repo Gates` on `pull_request` and `merge_group`. It now keys on the
files the battery actually **opens**, measured, instead of on whole
directories. Three classes stop selecting the step:
1. **Data and prose under `scripts/` outside `scripts/pm/`** (`.json`,
`.md`, `.txt`: ratchet baselines, pinned ledgers, fixture logs).
Exception: a file whose basename some file spells as a quoted relative
specifier (`./…` or `../…`) still runs it. That is the one edge by which
the discovery could open such a file: it follows imports out of gate
sources.
2. **Agent prose and JSON outside the pm-dispatch rulebook**:
`skills/**`, `AGENTS.md`, `CLAUDE.md`, `.claude/agents/**`,
`.claude/settings.json`, and the other skills under `.claude/skills/`.
3. **A test under a workspace package's `scripts/`** whose basename no
`package.json` and nothing under `.github/` spells.
Unchanged:
- `push` to `main` and the scheduled run take the selector's unscoped
`*)` event branch and run the whole battery
(`select-gate-families.sh:27`–`:28`).
- `Lint & Repo Gates` stays a required context.
- The battery keeps one tier.
- There is no edit to `scripts/pm/dispatch-gates.mjs`,
`scripts/pm/check-dispatch-gates.mjs` or `.github/workflows/lint.yml`.
- The other nine families are untouched.
Both new guards fail open: any `git grep` answer other than "no match"
runs the family. Every extension the arm does not list also runs it.
## Read-set: before → after (the `pm_dispatch_gates` arm)
| change touches | before | after |
|---|---|---|
| `scripts/pm/**` (any file) | run | run |
| a code file under the root `scripts/` | run | run |
| `.json` / `.md` / `.txt` under `scripts/`, outside `scripts/pm/` | run
| **skip**, unless a relative specifier names it |
| a non-test file under a package's `scripts/` | run | run |
| a test under a package's `scripts/` | run | **skip**, unless a
manifest or `.github/` names it |
| any `package.json` | run | run |
| `.github/**` | run | run |
| `.claude/skills/pm-dispatch/**` | run | run |
| `.claude/**` code (hook `.sh`, workflow `.js`) | run | run |
| `skills/**`, `AGENTS.md`, `CLAUDE.md`, other `.claude/**`
`.md`/`.json` | run | **skip** |
| product code, docs, changesets | skip | skip |
| root config, unknown path, deletion/rename, empty diff | every family
| every family |
## How the read-set was measured
I ran the full battery once (`node scripts/pm/dispatch-gates.mjs
--self-test`, tree `3d9188502e`, a detached worktree) under a preload
hook. The hook was installed through `NODE_OPTIONS=--import`, so every
node child the battery spawns carried it too. It recorded:
- every `readFileSync` / `readdirSync` / `existsSync` / `statSync` /
`openSync` under the repo root, with the innermost `selfTest()` line
that caused it;
- every child process with its argv and cwd.
Result: 1,976 cases pass, exit 0. There were 66 node processes, 8,430
distinct files opened under the repo root, and 57 `git ls-files` calls
at the root.
What the battery opens, by reader:
| reader | files opened |
|---|---|
| compound-anchor census (`dispatch-gates.mjs:18406`) and
exposed-scratch-dir sweep (`:26643`), whole tree | every masked source,
8,319 (`.ts .tsx .mts .mjs .js`) |
| error-code literal census (`:20834`) | every non-test TypeScript file,
3,208 |
| shell-mask census (`:19045`) | every tracked `.sh`, 30 (10 of them
`.claude/hooks/*.sh`) |
| discovery and live cases | all 39 workflows and 2 composite actions;
281 code files under `scripts/` and 40 under `scripts/pm/`; 31 under
`packages/{spec,lint,platform-objects,services/service-messaging}/scripts/`
(29 gate sources, 2 `i18n-extract.config.ts`, no test); root
`package.json`, `packages/{client,lint,spec}/package.json`, root
`tsconfig.json`, root `.gitignore` |
| rulebook live cases (`:26122`–`:26290`) | 32 `.md` under
`.claude/skills/pm-dispatch/` |
| live specimens | 10 named product files (e.g.
`packages/objectql/src/engine.ts`, `packages/rest/src/rest-server.ts`) |
Never opened: any `.json` beyond those four manifests and
`tsconfig.json`, and any `.md` outside the rulebook. The same holds for:
- any `.txt`;
- any `.yml` outside workflows and actions;
- anything in `skills/**`, `AGENTS.md` and `CLAUDE.md`;
- `.claude/agents/**`, `.claude/settings.json`, `.claude/launch.json`
and the other `.claude/skills/*`;
- 136 of the 167 files under package `scripts/` dirs.
Name-level reads only:
- `existsSync` on `.claude/agents/os-dev.md`, `skills/` and three
`skills/*/SKILL.md`. These are the governed read floor and the
frame-sync `COPIES` table, which the battery reads by importing
`scripts/check-skill-frame-sync.mjs`.
- The `.github/workflows/` listing.
- The tracked name set.
Why the narrowing does not depend on this one tree:
- `resolveCheckToFiles` admits only
`scripts/…\.(mjs|cjs|js|sh|ts|mts|cts)` as a gate source.
- `firstPartyImportBindings` follows only `./`/`../` specifiers that
resolve under the root `scripts/`, and never into a package's
`scripts/`.
- The rulebook is read by a hard-coded root (`SKILL_RULEBOOK_ROOT`).
So the only tree-dependent ways into the three classes are a relative
import, and a manifest or workflow naming a package-scripts test. Both
are guarded.
## Which input classes can move a verdict, and how each was measured
| input class | battery reader | can a modification move a verdict? |
measured by |
|---|---|---|---|
| `scripts/pm/**`, root `scripts/` code, package `scripts/` gate
sources, workflows/actions, the four manifests, rulebook | discovery and
live cases | yes | read hook (content opens attributed to non-census
`selfTest()` lines) |
| `.claude/**` and `packages/**` code, `.sh` anywhere | whole-tree
censuses | yes, census-grade | read hook (census lines `:18406`,
`:26643`, `:20834`, `:19045`) |
| 10 named product files | live specimens | yes, already off the PR path
since #19498 | read hook |
| `scripts/` `.json`/`.md`/`.txt`, agent prose/JSON | nobody; existence
only for 5 names | **no** | read hook (zero opens);
`resolveCheckToFiles` / import-follow code reading |
| package `scripts/` tests, unwired | the two whole-tree censuses only |
census-grade only, as for a `src/` test | read hook; `git grep` of every
manifest and `.github/` for each of the 67 such tests (2 are named:
`root-entry-type-nameability.pin.test.ts` is a `lint.yml` step, and
`export-list.test.ts` is mentioned in a `ci.yml` comment) |
| tracked names | hint reachability, test-file residue | an **addition**
can | `git ls-files` spawns in the hook log |
## Coverage that moves from PR time to `push` / scheduled runs
- **For the two ratchet-grade classes** (scripts data/prose, agent
prose/JSON): only the **name half** moves.
- An added file there is seen by the tracked-name sweep, and is now
judged by its class, as every other class has been since #19498.
- No content coverage moves: no case reads these bytes.
- A deletion or rename still runs every family.
- **For unwired package-scripts tests:** the compound-anchor census and
the exposed-scratch-dir sweep over those files. This is the same census
coverage #19498 already moved for tests under `src/`.
- **Nothing else moves.** A data file a gate imports, and a test a
manifest or workflow wires, both still run the step at PR time.
## Effect, replayed
I replayed the real selector, before and after, over the last 300
first-parent merges on `main` ending at `3d9188502e`. Each merge ran as
a `merge_group` entry with base = first parent, in a no-checkout
worktree.
- The family is selected on **95 → 55** merges (31.7% → 18.3%). Forty
flip `run → skip` and none flip the other way.
- Of those 40:
- 32 were selected only by a scripts data file outside `scripts/pm/`. 25
of them are `scripts/engine-double-contract.pinned.json`, plus
`doc-authoring-prose-id.baseline.json`, `test-shard-timings.json` and
others.
- 7 touch agent prose: `skills/**`.
- 1 touches a package-scripts test. That one is `93125aeeb8`, PR #22048,
the queue entry the card measured at 10.2 min for the step. It touched
`packages/spec/scripts/conversions-major18-merge.test.ts` and
`pure-schema-construction.test.ts`.
- 3 merges stay `run` because the specifier guard's superset grep hit:
- `eslint.config.mjs` reads
`./scripts/query-options-erasure-baseline.json` through `new URL`;
- a comment in `scripts/docs-audit/README.md` names
`handwritten-docs.json`.
## Pins (selector self-test case names)
Triage pin 1, "a PR touching only product code skips the family":
- `pull_request: a PR touching only packages/*/src/** runs no PM
self-test (#22076 pin)` (new; the card's own pin)
- `pull_request: a spec source, an unwired test under
packages/spec/scripts and a changeset run no PM self-test (#22076)`
(new; the #22048 shape)
- `pull_request: the PR #19314 shape pays the two ratchets and the
corpus, and no tooling self-test (the measurement this narrowing was
ruled from)` (kept)
Triage pin 2, "a PR touching a gate's source, a workflow's gate wiring
or `scripts/pm/**` runs it":
- `pull_request: a PR touching scripts/pm/** runs the PM self-test,
prose included (#22076 pin)` (new)
- `merge_group: a data file under scripts/pm still runs the PM self-test
-- scripts/pm is in its read-set whole` (new)
- `merge_group: a test under a package scripts/ that a check script
names is a gate source and still runs the PM self-test` (new)
- `merge_group: a data file a relative import specifier names still runs
the PM self-test -- the one edge by which the discovery opens a data
file` (new)
- `merge_group: a workflow change runs every family built on the
dispatch derivation, and not the scripts-only sweep` (kept)
- `merge_group: a composite action is part of the workflow tree the
derivation discovers` (kept)
- `merge_group: a scripts/ subdirectory script is a gate source only`
(kept)
- `merge_group: a package-local script is a gate source (the derivation
families) and a masked source (corpus)` (kept)
Triage pin 3, "the scheduled run still runs it (control)":
- `schedule: a change the PR path skips the PM self-test for still runs
it -- the hourly full run is the backstop (#22076 control)` (new)
- `push: the same change on main still runs the PM self-test (#22076
control)` (new)
- Non-vacuity partner: `merge_group: the same change, scoped, skips the
PM self-test -- so the two controls above are not vacuous` (new)
Other new or re-pinned cases:
- `a ratchet baseline … not the PM self-test`, re-pinned. It previously
expected the PM self-test to run.
- `agent prose the battery never opens … runs no family`, re-pinned. It
previously expected the PM self-test alone.
- `prose and a fixture log under scripts/ …`
- `CLAUDE.md and the .claude settings JSON run no family`
- `the pm-dispatch rulebook runs the PM self-test alone`
- `a .claude hook shell script still runs the PM self-test alone`
- `a scripts/ file of a kind the arm does not list still runs the PM
self-test -- fail-open on the extension`
- `a test under a package scripts/ that no manifest or workflow names …
skips`
The floor goes from 56 cases / 293 checks to **71 cases / 369 checks**.
## Reverse verification (on committed `1ed9df2285`)
- **Leg A:** the selector restored to `3d9188502e` (`git restore
--source`; on-disk blob `e6c2f3c2` == base blob), run under the new
self-test. Exit 1, with 7 cases red and 20 checks failing — every
narrowing case, plus the non-vacuity partner. Restore: blob `76f368b7`
== HEAD, and `git diff HEAD` is empty.
- **Leg B:** `node scripts/ablation-replace.mjs --delete` on the
`named_by_relative_specifier` call (anchor 1 → 0). Exit 1; exactly `a
data file a relative import specifier names still runs the PM self-test`
goes red (3 checks). Restore proven: blob == HEAD.
- **Leg C:** the same on the `named_in_gate_wiring` call. Exit 1;
exactly `a test under a package scripts/ that a check script names …
still runs the PM self-test` goes red (3 checks). Restore proven.
## Gates (head `1ed9df2285`)
I derived the gate list with `node scripts/pm/dispatch-gates.mjs
--commands --repo objectstack-ai/objectstack`: 27 commands. I ran each
one and wrote its exit code to disk before reading it.
All 26 non-battery commands exit 0. They include:
- `pnpm check:select-gate-families` (71 cases / 369 checks)
- `check:bash32-floor`, `check:parse-guard`, `check:nul-bytes`
- `check-ci-filter-parity`, `check-self-test-workflow-commands` (with
and without `--self-test`), `check-comment-mask-corpus`,
`check:entry-guard`
`pnpm check:pm-dispatch-gates` (the control) ran detached on
`1ed9df2285` (not under the verify lock), on a shared 4-core container
at load 5–8. Result: **exit 0**, `✓ dispatch-gates self-test: 1976 cases
pass.`, `the battery took 1004.0s on this box`, wall 1,006 s
(2026-10-07T14:39:27Z → 14:56:13Z). That is a reading of a contended
agent box, not of a runner. The instrumented measurement run at
`3d9188502e` also passed all 1,976 cases. The record is reconciled with
`--ran`: `✓ dispatch-gates --ran: 27 derived famil(ies) accounted for —
27 run, 0 NOT-MEASURED`.
## Premise checks
1. **"Selects the step on most PRs."** By rule, any `package.json` and
anything under any `scripts/` selected it. Measured over 300 merges, it
selected 95, which is 31.7%, not most. The scripts data files carried 32
of those 95 on their own.
2. **"The censuses are deliberately off the PR path."** In code this
means the selector returns `skip` for product code, so those inputs
never select the family. It does **not** mean the censuses are skipped
once the family is selected: the battery has one tier, and every
selected run pays all of them. The hook found four whole-tree censuses,
not the two the old comment named. The two it did not name are the
error-code literal census over 3,208 files and the `.sh` census. It also
found 10 live product specimens. The header now names all of them.
3. **"A `package.json` change that does not touch a `check:*` script may
not move the verdict."** It may not, but the narrowing is not taken. In
the replay, 13 merges touched a workspace manifest:
- 10 also touched `pnpm-lock.yaml` (root config, so every family runs
anyway);
- 2 are structural (a deletion runs every family anyway);
- 1 alone would gain.
The set of manifests the discovery reads is also tree-dependent: on this
tree it is root, `client`, `lint` and `spec`, decided by the `--filter`
rows workflows spell. A `scripts`/`name` field diff would need `node` in
the selector and a parse fail-open branch to save about 1 run in 300.
## Notes
- **File surface.** The selector has no `--self-test` flag. Its
self-test is the sibling `scripts/ci/select-gate-families.selftest.sh`
(`pnpm check:select-gate-families`), which is where the pins live, so
the diff is those two files.
- **Not taken: the card's "two tiers".** Triage did not take it: it
edits the frozen `dispatch-gates.mjs`.
- **NOT MEASURED: the card's "wall under 18 minutes" and "the
merge_group run's critical path with run ids".** Reason: they can be
read only from CI runs after this lands, on a product-only PR and on a
queue entry.
- **Not narrowed: `.github/` files outside workflows and actions**
(`CODEOWNERS`, `labeler.yml`, `dependabot.yml`). The battery never opens
them, but none of the 300 merges touched them, so the arm would buy
nothing.
- **Not narrowed: the three other derivation families.**
`declared_population_live`, `bare_root_worklist` and
`self_test_workflow_commands` share `reads_gate_tree` and still run on
scripts data and on package-scripts tests. Whether they can narrow too
is not measured here.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01VF48aw8RPG6wzDnMgp6rtw)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent aa71c4d commit dfa427b
2 files changed
Lines changed: 311 additions & 29 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
20 | 20 | | |
21 | 21 | | |
22 | 22 | | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
23 | 33 | | |
24 | 34 | | |
25 | 35 | | |
| |||
77 | 87 | | |
78 | 88 | | |
79 | 89 | | |
80 | | - | |
| 90 | + | |
81 | 91 | | |
82 | 92 | | |
83 | 93 | | |
84 | 94 | | |
85 | 95 | | |
86 | 96 | | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
87 | 101 | | |
88 | 102 | | |
89 | 103 | | |
| |||
96 | 110 | | |
97 | 111 | | |
98 | 112 | | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
99 | 125 | | |
| 126 | + | |
100 | 127 | | |
101 | 128 | | |
102 | 129 | | |
| |||
107 | 134 | | |
108 | 135 | | |
109 | 136 | | |
| 137 | + | |
110 | 138 | | |
111 | 139 | | |
112 | 140 | | |
| |||
297 | 325 | | |
298 | 326 | | |
299 | 327 | | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
300 | 349 | | |
301 | 350 | | |
302 | 351 | | |
| |||
502 | 551 | | |
503 | 552 | | |
504 | 553 | | |
| 554 | + | |
| 555 | + | |
| 556 | + | |
| 557 | + | |
| 558 | + | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
| 563 | + | |
| 564 | + | |
| 565 | + | |
| 566 | + | |
| 567 | + | |
505 | 568 | | |
506 | 569 | | |
507 | 570 | | |
| |||
516 | 579 | | |
517 | 580 | | |
518 | 581 | | |
519 | | - | |
| 582 | + | |
520 | 583 | | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
| 591 | + | |
| 592 | + | |
521 | 593 | | |
| 594 | + | |
| 595 | + | |
| 596 | + | |
| 597 | + | |
| 598 | + | |
| 599 | + | |
| 600 | + | |
| 601 | + | |
| 602 | + | |
| 603 | + | |
| 604 | + | |
| 605 | + | |
| 606 | + | |
| 607 | + | |
| 608 | + | |
| 609 | + | |
| 610 | + | |
| 611 | + | |
| 612 | + | |
| 613 | + | |
522 | 614 | | |
523 | 615 | | |
524 | 616 | | |
525 | 617 | | |
526 | 618 | | |
527 | 619 | | |
528 | 620 | | |
529 | | - | |
| 621 | + | |
| 622 | + | |
| 623 | + | |
| 624 | + | |
| 625 | + | |
| 626 | + | |
| 627 | + | |
| 628 | + | |
| 629 | + | |
| 630 | + | |
| 631 | + | |
| 632 | + | |
| 633 | + | |
530 | 634 | | |
| 635 | + | |
531 | 636 | | |
| 637 | + | |
| 638 | + | |
| 639 | + | |
| 640 | + | |
| 641 | + | |
| 642 | + | |
| 643 | + | |
532 | 644 | | |
533 | 645 | | |
534 | 646 | | |
| |||
649 | 761 | | |
650 | 762 | | |
651 | 763 | | |
| 764 | + | |
| 765 | + | |
| 766 | + | |
| 767 | + | |
| 768 | + | |
| 769 | + | |
| 770 | + | |
| 771 | + | |
| 772 | + | |
| 773 | + | |
| 774 | + | |
| 775 | + | |
| 776 | + | |
| 777 | + | |
| 778 | + | |
| 779 | + | |
| 780 | + | |
| 781 | + | |
| 782 | + | |
| 783 | + | |
| 784 | + | |
| 785 | + | |
| 786 | + | |
| 787 | + | |
| 788 | + | |
| 789 | + | |
| 790 | + | |
| 791 | + | |
| 792 | + | |
| 793 | + | |
| 794 | + | |
| 795 | + | |
| 796 | + | |
| 797 | + | |
| 798 | + | |
| 799 | + | |
| 800 | + | |
652 | 801 | | |
653 | 802 | | |
654 | 803 | | |
| |||
753 | 902 | | |
754 | 903 | | |
755 | 904 | | |
756 | | - | |
| 905 | + | |
757 | 906 | | |
758 | 907 | | |
759 | 908 | | |
| |||
0 commit comments