Skip to content

fix(pm): discover gates invoked with --self-test, whose scripts carry no check- prefix - #11554

Merged
os-steve merged 3 commits into
mainfrom
claude/issue-11404-invocation-matcher-self-test
Aug 24, 2026
Merged

fix(pm): discover gates invoked with --self-test, whose scripts carry no check- prefix#11554
os-steve merged 3 commits into
mainfrom
claude/issue-11404-invocation-matcher-self-test

Conversation

@claude

@claudeclaudeBot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Fixes#11404

extractCheckInvocations recognised exactly two spellings — a check:* npm script, or a script whose basename carries check-. A gate following neither convention was not a family at all: absent from the matched list, the convention-triggered list, the unreachable-by-construction list, and from all three residue buckets, because it never entered the universe those buckets partition. node scripts/pm/bare-root-worklist.mjs --self-test shipped a red on the required Lint & Repo Gates lane for a diff whose seven derived families were all run and all green.

The route, and why it beats the widening

Route (a): key on --self-test. The flag is the script's own declaration that this invocation verifies the script instead of doing its work — not a heuristic over filenames. The discriminating specimen is in the tree twice: scripts/partition-test-shards.mjs and scripts/pr-labels.mjs are each invoked both ways — --self-test in lint.yml, and doing real work in ci.yml / pr-automation.yml. No filename rule can separate the gate invocation from the work invocation of one script.

The refused direction — any scripts/** script in a run: step — admits 12 distinct scripts across the 26 workflow files, and the three it adds beyond this matcher are all non-gate tooling: release-github-releases.mjs (the release run), run-with-stall-guard.mjs (a test wrapper, 7 invocations), docs-audit/affected-docs.mjs (a docs-drift query). That is the fabrication direction hintCovers prices at +139084 pairs.

A check- basename is skipped by the new matcher rather than re-keyed: 21 of the 30 scripts invoked with --self-test already have one and are already families under their bare path key. Admitting them again under a … --self-test key would split each into two and move its matches — a re-attribution reported as a gain. Skipping makes zero re-attribution a property of the code, not a number that happened to come out right.

A self-test family follows no import — the half that keeps this honest

bare-root-worklist.mjs statically imports ./dispatch-gates.mjs, whose module-body literals are join bases (packages/plugins, packages/drivers, packages/services) and a tier glob (packages/spec/src/**) — not a population anything reads. Inheriting them handed that one gate 2553 (gate, file) pairs, 96% of the change's entire price, every one a lead the gate would never justify.

That exact fabrication is already a decided verdict: scripts/pm/check-dispatch-gates.mjs exists as a separate file that spawns the tool rather than importing it, for no other reason (#8162; its header measures the same literals reaching the same trees). It arrives here by a new route and is refused the same way. Cost of the refusal on this tree, measured: zero — of the nine self-test families only bare-root-worklist inherits anything at all.

Independent confirmation: with the narrowing ablated, the escapable-literal ledger inside this tool's own self-test goes FRESH on scripts/pm/bare-root-worklist.mjs --self-test scripts — a second, unrelated gate catching the same fabrication.

The price, measured the way #11512 priced import-following

Over 6465 tracked files, before → after:

beforeafterdelta
check families discovered140149+9, zero lost
watch-hint (gate, file) pairs5277452880+106, zero lost
existing matches re-attributed0

Re-attribution was checked as a property, not a total: every pre-existing (file, family, hint) claim was recorded before and looked up after — 0 no longer standing — and no existing family's pair count moved by one.

The +106 is 0.08% of the refused widening. Six of the nine new families contribute no pairs at all; the three that do declare the population themselves — release-rehearsal-clone.mjs +75 (the .changeset tree its self-test clones), pm/ci-failure.mjs +30 (.github), pr-labels.mjs +1 (.github/labeler.yml).

The direction that could subtract, measured rather than argued: all nine promoted scripts become gate files, which discoverFamilies excludes from import-following. Four were being followed on the base tree over 111 import edges — invoked-as.mjs by 77 families, ts-parse.mjs by 18, js-comment-mask.mjs by 14, pm/git-history.mjs by 2 — and all four declare zero path literals, so the edges carried nothing to lose. The three that do declare literals are followed by no family at all. Net: zero hints, zero pairs. Asserted in the self-test, because a lead that stops appearing looks exactly like a lead that was never earned.

The incident, reproduced: absent before, present after

PR #11397's real diff (scripts/check-i18n-coverage.mjs + scripts/pm/bare-root-worklist.mjs), derived from a worktree pinned at the base cd932772 and from this branch:

BEFORE (cd932772) 7 matched families, the gate ABSENT from every bucket
AFTER 8 matched families, including:
- node scripts/pm/bare-root-worklist.mjs --self-test [lint.yml]
matched via scripts/pm/bare-root-worklist.mjs ⇢ gate script 'scripts/pm/bare-root-worklist.mjs'

For a diff touching only another gate's source, the gate now appears in the residue's undetermined bucket, where before it appeared in no bucket at all.

Assumptions in the card that did not survive re-derivation

The card's numbers predate #11512, which changed this file. Re-derived on this base:

  • discoverFamilies().byCheck.size is 140, not 139. The two empty filters (bare-root, --self-test) reproduce exactly — the root cause is unchanged.
  • "Five lint.yml steps": there are nine--self-test steps in lint.yml whose script lacks check-, and 10 such invocations across all workflows.
  • "13 distinct such scripts across all workflows": 12. Located by content; the five quoted line numbers had all drifted.
  • "bare-root-worklist declares no population of its own" is not a limitation — it is enforced. The card reads it as an obstacle the derivation cannot get past. Its own self-test asserts, mechanically, that extractWatchHints over its source returns zero, with the stated reason "it reads gate sources, never a repo subtree, and a stray path literal in it would name it for cards it has nothing to say about." Giving it a hint for this card's convenience would fail the very gate the card is about. The landed import-following derivation did not make it reachable — it made it inheritable, which is the fabrication refused above.

Non-vacuity

Ablation, two legs, each mutating one anchored line, proving the mutation on disk by counting both the removed and the injected text, restoring from a pristine copy, and confirming byte-identity with git hash-object. Wrapped in trap restore EXIT INT TERM. No build leg: node scripts/pm/dispatch-gates.mjs --self-test executes this source file directly and bare-root-worklist.mjs imports it by relative path — nothing resolves through a package exports field, so no dist/ copy can serve a stale answer.

The first ablation run aborted both legs on its own guard — the injected-form check was grepping the perl-escaped spelling, so it could not prove the text had landed. Recorded because a guard that had merely trusted the editor's exit code would have read a no-op as a result.

#9187's fixture was re-based, and that is part of the delivery: its invisible-step fixture was verbatim node scripts/some-mapper.mjs --self-test, which this matcher now discovers, so it had stopped being an example of the thing it illustrates. The retired shape is pinned two cases below as discovered now, so the pair records the move rather than losing it.

What this does NOT close

bare-root-worklist --self-test enters the universe with zero hints — reached by identity, and otherwise printed as a family whose population is not path-expressible. That is #11199's ground, one level down, and it is deliberately not folded in: this card asks why a gate never becomes a family, #11199 asks what an existing family's population is. Progress with a name is the deliverable — before this change --residue could not report the gate as anything.

Verification

Derived gate union re-run at final HEAD cd272342, exit codes captured before any pipe:

check:cross-package-test-inputs (self-test + live) exit=0
check:entry-guard (self-test + live) exit=0
check:parse-guard (self-test + live) exit=0
check:pm-dispatch-gates exit=0 579 cases pass
check:pnpm-filter-targets (3 legs) exit=0
check-ci-filter-parity.mjs (self-test + live) exit=0

Beyond the derived list, by reading what this diff actually touches: bare-root-worklist.mjs --self-test (exit=0 — the gate this card is about, and a direct consumer of the edited module), check:nul-bytes (exit=0, self-test + live), and the eight other newly-promoted self-test gates (all exit=0).

Lint is narrowed and the narrowing is declared: eslint --no-inline-config --format json over the one changed file — 1 file linted, 0 errors, 0 warnings. The population is read from the repo's own eslint.config.mjs (the CI invocation), whose header records, with a positive control, that this repo "never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file" — so this diff cannot move the verdict on any file it does not contain. The repo-wide sweep is CI's run.

No changeset: this PR touches one internal tooling file and publishes nothing.

Generated by Claude Code


Generated by Claude Code

`collectInvocations` recognised exactly two spellings — a `check:*` npm script
and a script whose basename carries `check-`. A gate following neither was not
a family at all: absent from the matched list, the convention list, the
unreachable list, and from all three residue buckets, because it never entered
the universe those partition. `node scripts/pm/bare-root-worklist.mjs
--self-test` shipped a red on the required lane for a diff whose seven derived
families were all run and all green, and no local derivation could have named
it.
The third matcher keys on the flag rather than on a name: `--self-test` is the
script's own declaration that this invocation verifies the script instead of
doing its work. Two scripts in this tree are invoked BOTH ways, which no
filename rule can separate. A `check-` basename is skipped here so an existing
family is never split into two keys.
A self-test family follows no import. `bare-root-worklist.mjs` statically
imports `dispatch-gates.mjs`, whose module-body literals are join bases and
tier globs; inheriting them handed one gate 2553 fabricated (gate, file)
pairs — the same fabrication `check-dispatch-gates.mjs` exists to avoid by
spawning the tool rather than importing it.
Measured over 6465 tracked files: 140 -> 149 families, 52774 -> 52880 pairs
(+106), zero lost, zero re-attributed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
The matcher ablation destructured undefined and crashed, printing no case
name — a self-test that dies reports "something is broken" where the value
is "this exact case went red".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
All nine promoted scripts become gate files, not six; four were followed on
the base tree over 111 import edges and all four declare zero literals, which
is why the net subtraction is zero.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 24, 2026
@claudeclaudeBot changed the title fix(pm): discover gates invoked as node scripts/<x>.mjs --self-testfix(pm): discover gates invoked with --self-test, whose scripts carry no check- prefixAug 24, 2026
@os-steve
os-steve marked this pull request as ready for review August 24, 2026 03:31
@os-steve
os-steve added this pull request to the merge queueAug 24, 2026
Merged via the queue into main with commit cea8c7dAug 24, 2026
35 checks passed
@os-steve
os-steve deleted the claude/issue-11404-invocation-matcher-self-test branch August 24, 2026 03:49
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-steve@claude