Skip to content

Commit 2c22f57

Browse files
committed
fix(pm): discover a package-local gate invocation and key it to its real path (#15342)
`lint.yml` runs one gate by a package-local path — `node packages/lint/ scripts/check-reference-carrier-shape.mjs --self-test`, plus the bare production invocation on the next line. Both tools that read that corpus anchored on the literal `scripts/`, and failed in opposite ways. `dispatch-gates` required `node ` to be followed IMMEDIATELY by `scripts/`, so neither `DIRECT_CHECK_INVOCATION` nor `SELF_TEST_INVOCATION` matched the step at all: the gate was in NO family, and `--residue` could not report it either — it is absent from the universe the matched / silent / undetermined buckets partition. Measured on `65846bc46` for a card editing that gate's own file: `--commands` named it zero times, all 27 `--residue` mentions were the dev's own changed path echoed back, and the step surfaced only in the always-runs STEP tail, as one of the 33 CI steps the derivation names no family for. Both patterns gain the same directory prefix, spelled identically. A segment must start with an alphanumeric or `_`, so `..` is refused — a path this ROOT cannot resolve is how a phantom identity key gets minted. A `scripts/` segment is still required, so no new species is admitted (`node packages/cli/bin/ run.js` stays out). Over 7472 tracked files: check families discovered 252 -> 254 (+2, ZERO lost) gate files 201 -> 202 (+1, ZERO lost) watch-hint (gate, file) pairs 240947 -> 254181 (+13234, ZERO lost) existing families re-attributed 0 — every pre-existing family's pair count is byte-identical before and after always-runs steps naming NO family 33 -> 32 The gate declares four subtree hints, so it lands in the derivation for cards touching `packages/**`, `examples/**`, `scripts/**` and `apps/**`, and by identity for a card editing its own file — not in the residue. Two existing tail cases pinned the defect as a property. Neither is weakened: each is inverted and gains a positive control, so the step's absence from the tail cannot go green through a tail that stopped walking. check-self-test-wired keyed the same path by SUBSTRING, filing it under `scripts/check-reference-carrier-shape.mjs` — a key with no file behind it, audited by nobody. `INVOCATION_RE` consumes the prefix and gains a left boundary; the population gains a second source, admitted on the root walk's own terms. Its two new skip predicates are membership in the walk's OWN OUTPUT rather than `startsWith('scripts/')`: that re-spelling reaches the dispatch derivation as the bare literal `scripts` and joined the shrink-only escapable-literal species FRESH on both of this gate's families. Self-tests grow four batteries: `left boundary` (6) and `live corpus` (3) in check-self-test-wired (50 -> 59 cases, floor 7 -> 9), and eight fixture cases plus two live pins in dispatch-gates. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
1 parent 7c690ef commit 2c22f57

2 files changed

Lines changed: 185 additions & 7 deletions

File tree

scripts/check-self-test-wired.mjs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -484,8 +484,19 @@ function main() {
484484
// phantom this card is about, and admitting one would re-create it one layer
485485
// down. It is not a refusal either: this gate does not own what a workflow is
486486
// allowed to name, and the anchor above already declines to invent keys.
487+
//
488+
// ⛔ The skip is membership in the WALK'S OWN OUTPUT, never `startsWith` on a
489+
// re-spelling of its root. That spelling is a bare top-level word wearing a
490+
// separator, so it reaches the dispatch derivation's hint set as the plain
491+
// literal `scripts` and joins the SHRINK-ONLY escapable-literal species
492+
// (#10705) -- a population no `hintCovers` can name, declared by a gate that
493+
// already declares the nameable spelling three lines up. Measured when this
494+
// landed: the `startsWith` form added exactly that row, FRESH, to both of
495+
// this gate's families. The set form says what the predicate means -- "the
496+
// root walk did not already produce this path" -- and declares nothing.
497+
const walked = new Set(files);
487498
for (const relPath of named.keys()) {
488-
if (relPath.startsWith('scripts/')) continue;
499+
if (walked.has(relPath)) continue;
489500
const source = sourceOf(relPath);
490501
if (source === null) continue;
491502
if (carriesSelfTest(relPath, source)) carriers.add(relPath);
@@ -498,7 +509,7 @@ function main() {
498509

499510
const members = [...carriers].filter((s) => named.has(s));
500511
const wired = members.filter((s) => selfTested.has(s));
501-
const packageLocal = [...carriers].filter((s) => !s.startsWith('scripts/'));
512+
const packageLocal = [...carriers].filter((s) => !walked.has(s));
502513
const scope =
503514
` scope: ${files.length} file(s) under scripts/, ${carriers.size} carrying \`--self-test\` in code ` +
504515
`(comments masked, ${packageLocal.length} of them package-local gate(s) CI names by path); ` +

scripts/pm/dispatch-gates.mjs

Lines changed: 172 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1109,6 +1109,73 @@ export function jobPathPopulations(workflowText, workflowFile) {
11091109
return out;
11101110
}
11111111

1112+
/**
1113+
* ## The DIRECTORY PREFIX both matchers below carry, and why it is shared (#15342)
1114+
*
1115+
* This repo has a package-local gate lane, and `lint.yml` really runs one of
1116+
* its gates by path — `node packages/lint/scripts/check-reference-carrier-
1117+
* shape.mjs --self-test`, with the bare production invocation on the next line.
1118+
* Both patterns used to open on the literal `scripts/` immediately after
1119+
* `node `, so neither matched that step AT ALL. The gate was in no family, and
1120+
* `--residue` could not report it either: it is absent from the universe the
1121+
* matched / silent / undetermined buckets partition, which is the #11397 state
1122+
* one lane over. Measured on the base tree for a card editing that gate's OWN
1123+
* file: `--commands` named it zero times, every one of the 27 `--residue`
1124+
* mentions was the dev's own CHANGED PATH echoed back as an input, and the step
1125+
* surfaced only in the always-runs STEP tail, as one of the 33 unconditional CI
1126+
* steps this derivation names no family for.
1127+
*
1128+
* ## What the prefix admits, and what it deliberately refuses
1129+
*
1130+
* - A prefix segment must START with an alphanumeric or `_`, so `..` is not
1131+
* one. A climbing spelling cannot be resolved against this ROOT without
1132+
* knowing what it is relative to, and `entry.files` would then carry a key
1133+
* with no file behind it — the phantom `resolveCheckToFiles`' docblock
1134+
* prices at "strictly worse than the bug being fixed". Zero occur in the
1135+
* workflow corpus today; refusing to match is the safe direction, and the
1136+
* sibling anchor in `scripts/check-self-test-wired.mjs` refuses it in the
1137+
* same spelling for the same reason.
1138+
* - A `scripts/` segment is still REQUIRED, so no new file species is
1139+
* admitted. `node packages/cli/bin/run.js` is the only other non-root path
1140+
* any workflow invokes with `node`, and it stays out.
1141+
* - The prefix cannot RE-ATTRIBUTE an existing match. For a path that already
1142+
* began at `scripts/` the group matches empty and the remainder of each
1143+
* pattern is byte-identical, so every key this tool minted before it is
1144+
* minted after it. Asserted below rather than left as this sentence.
1145+
*
1146+
* ## Priced in both directions, over 7472 tracked files (base `65846bc46`)
1147+
*
1148+
* check families discovered 252 -> 254 (+2, ZERO lost)
1149+
* gate files 201 -> 202 (+1, ZERO lost)
1150+
* watch-hint (gate, file) pairs 240947 -> 254181 (+13234, ZERO lost)
1151+
* existing families re-attributed 0 — every pre-existing family's pair count
1152+
* is byte-identical before and after
1153+
* always-runs steps naming NO family 33 -> 32 (18 -> 17 distinct commands)
1154+
*
1155+
* The +13234 is exactly 2 x 6617: the gate declares four subtree hints
1156+
* (`packages/**`, `examples/**`, `scripts/**`, `apps/**`) and CI runs it twice,
1157+
* so it arrives as two families under two keys — the split #14880 made, working
1158+
* as designed. Becoming a GATE FILE is the one direction of this change that
1159+
* could SUBTRACT (`discoverFamilies` excludes gate files from import-following),
1160+
* so it is measured rather than argued: no existing family's pair count moved,
1161+
* in either direction, so the net subtraction is zero.
1162+
*
1163+
* ## Why `SELF_TEST_INVOCATION` is widened too, on ZERO specimens
1164+
*
1165+
* Today's one package-local specimen carries a `check-` basename, so the direct
1166+
* matcher takes it and the self-test matcher skips it by design — that half
1167+
* gains no recall at all. It is widened anyway because the two matchers'
1168+
* coordination contract is that they mint BYTE-IDENTICAL keys for one script,
1169+
* and two adjacent patterns with two different path grammars cannot hold it: a
1170+
* package-local gate NOT named `check-*` would then be discovered by neither,
1171+
* which is this card's silence wearing a different filename. Same standard as
1172+
* the extension right-boundary in `resolveCheckToFiles` — zero recall today,
1173+
* zero cost today (both numbers above are the direct matcher's alone), class
1174+
* closed before the first specimen arrives. ⛔ Keep the two prefixes spelled
1175+
* identically; the self-test asserts that both accept the same package-local
1176+
* path, so a drift reds rather than going quiet.
1177+
*/
1178+
11121179
/**
11131180
* A `run:` step that invokes a repo script with `--self-test`. The flag is the
11141181
* SCRIPT'S OWN declaration that this invocation verifies the script rather than
@@ -1130,7 +1197,7 @@ export function jobPathPopulations(workflowText, workflowFile) {
11301197
* its own, which is correct: the inner script is the gate.
11311198
*/
11321199
const SELF_TEST_INVOCATION =
1133-
/node[ \t]+(scripts\/[\w./-]+\.mjs)(?:[ \t]+-{1,2}[A-Za-z0-9][\w-]*)*[ \t]+--self-test\b/g;
1200+
/node[ \t]+((?:[A-Za-z0-9_][\w.-]*\/)*scripts\/[\w./-]+\.mjs)(?:[ \t]+-{1,2}[A-Za-z0-9][\w-]*)*[ \t]+--self-test\b/g;
11341201

11351202
/**
11361203
* A `run:` step that invokes a `check-`named repo script directly, WITH the
@@ -1150,7 +1217,7 @@ const SELF_TEST_INVOCATION =
11501217
* outcomes. Joining is how that hazard is removed rather than merely refused.
11511218
*/
11521219
const DIRECT_CHECK_INVOCATION =
1153-
/node[ \t]+(scripts\/[\w./-]*check-[\w.-]+\.mjs)([^\n;|&<>()]*)/g;
1220+
/node[ \t]+((?:[A-Za-z0-9_][\w.-]*\/)*scripts\/[\w./-]*check-[\w.-]+\.mjs)([^\n;|&<>()]*)/g;
11541221

11551222
/**
11561223
* Splice a shell line-continuation back into ONE line, so a matcher reading a
@@ -11519,6 +11586,67 @@ function selfTest() {
1151911586
t('extracts direct node scripts/check-*.mjs', invs.some((i) => i.check === 'scripts/check-nul-bytes.mjs' && i.direct));
1152011587
t('ignores non-check runs', !invs.some((i) => String(i.check).includes('build')));
1152111588

11589+
// ── The package-local gate lane: a path is keyed WHOLE (#15342) ───────────
11590+
//
11591+
// `lint.yml` invokes `packages/lint/scripts/check-reference-carrier-shape.mjs`
11592+
// by path, twice. Before the directory prefix documented beside the patterns,
11593+
// `node ` had to be followed IMMEDIATELY by `scripts/`, so neither matcher saw
11594+
// that step at all: no family, no hints, and nothing for `--residue` to place.
11595+
//
11596+
// Cases 1-3 and 6 FAIL against the base spelling — measured by applying this
11597+
// battery to `/node[ \t]+(scripts\/…)/` in a scratch ablation — which is what
11598+
// makes them an instrument rather than a restatement of the operators. Cases
11599+
// 4, 5, 7 and 8 hold on BOTH spellings: they are the controls that prove the
11600+
// widening did not buy its new answers by dropping the old ones.
11601+
{
11602+
const PKG = 'packages/lint/scripts/check-reference-carrier-shape.mjs';
11603+
const pkgInvs = extractCheckInvocations(
11604+
['jobs:', ' lint:', ' steps:', ' - name: package-local gate', ' run: |',
11605+
` node ${PKG} --self-test`, ` node ${PKG}`].join('\n'),
11606+
'lint.yml',
11607+
);
11608+
t(
11609+
'a gate CI invokes by a PACKAGE-LOCAL path is discovered at all — unfound, it is in no family, so no '
11610+
+ 'card can be told to run it and --residue has no bucket to place it in either (#15342)',
11611+
pkgInvs.some((i) => i.check === `${PKG} --self-test` && i.direct),
11612+
);
11613+
t(
11614+
'…and its bare production invocation arrives as the SECOND family under its own key, the #14880 split',
11615+
pkgInvs.some((i) => i.check === PKG && i.direct),
11616+
);
11617+
t(
11618+
'…both keyed by the REAL path, which is what `entry.files` carries and `existsSync` then opens',
11619+
pkgInvs.length === 2 && pkgInvs.every((i) => i.script === PKG),
11620+
);
11621+
t(
11622+
'control — no PHANTOM root key is minted beside them: a `scripts/…` TAIL keyed as a path in its own '
11623+
+ 'right names a file this ROOT does not hold, and every audit downstream then passes for the wrong reason',
11624+
!pkgInvs.some((i) => String(i.script ?? i.check).startsWith('scripts/')),
11625+
);
11626+
t(
11627+
'control — a CLIMBING spelling is refused rather than resolved: `..` is not a prefix segment, because '
11628+
+ 'a path this ROOT cannot resolve is exactly how a phantom identity key gets minted',
11629+
extractCheckInvocations(' - run: node ../scripts/check-x.mjs\n', 'x.yml').length === 0,
11630+
);
11631+
t(
11632+
'the SELF-TEST matcher carries the same prefix grammar — a package-local gate not named `check-*` '
11633+
+ 'would otherwise be discovered by neither matcher, which is this silence one filename over',
11634+
extractCheckInvocations(' - run: node packages/lint/scripts/carrier-census.mjs --self-test\n', 'x.yml')
11635+
.some((i) => i.check === 'packages/lint/scripts/carrier-census.mjs --self-test' && i.direct),
11636+
);
11637+
t(
11638+
'control — the prefix widens the DIRECTORY a gate may sit under and never the SPECIES of file: a path '
11639+
+ 'with no `scripts/` segment is still admitted by neither matcher (`packages/cli/bin/run.js` is live)',
11640+
extractCheckInvocations(' - run: node packages/cli/bin/run.js check\n', 'x.yml').length === 0,
11641+
);
11642+
t(
11643+
'control — a root-spelled invocation is keyed BYTE-IDENTICALLY to before, so the prefix re-attributes '
11644+
+ 'nothing: it matches empty there and the remainder of each pattern is unchanged',
11645+
extractCheckInvocations(' - run: node scripts/check-nul-bytes.mjs\n', 'x.yml')
11646+
.every((i) => i.check === 'scripts/check-nul-bytes.mjs' && i.script === 'scripts/check-nul-bytes.mjs'),
11647+
);
11648+
}
11649+
1152211650
// Block-scalar bodies (#8410). A step written `run: |` keeps its commands on
1152311651
// the following lines; reading only the `run:` line collected "|" and missed
1152411652
// every gate invoked this way. Both scalar styles and both invocation shapes
@@ -11989,6 +12117,20 @@ function selfTest() {
1198912117
}
1199012118
const direct = liveInvs.filter((i) => i.direct);
1199112119
const valueBearing = direct.filter((i) => (i.argvVariables ?? []).length > 0);
12120+
// The live half of the package-local lane (#15342). The fixtures above prove
12121+
// the pattern; these two read the tree CI actually runs, so the day the lane
12122+
// moves this reds here instead of going quiet — and the second one holds the
12123+
// phantom class over EVERY direct invocation, not only over the specimen.
12124+
t(
12125+
'⭐ the live corpus really carries the package-local lane the directory prefix exists for, so the '
12126+
+ 'fixtures above judge a live class. If this reds, the lane moved — re-point it, never widen further',
12127+
direct.some((i) => i.script === 'packages/lint/scripts/check-reference-carrier-shape.mjs'),
12128+
);
12129+
t(
12130+
`every one of the ${direct.length} direct invocation(s) in the live tree resolves to a file that EXISTS `
12131+
+ 'on disk — a key with no file behind it reads as a confident gate identity in both directions (#15342)',
12132+
direct.length > 0 && direct.every((i) => existsSync(nodePath.join(ROOT, i.script))),
12133+
);
1199212134
t(
1199312135
`the live tree really carries ${valueBearing.length} value-bearing invocation(s) across ${new Set(valueBearing.map((i) => i.script)).size} script(s), so the cases above judge a live class`,
1199412136
valueBearing.length > 0,
@@ -18749,7 +18891,20 @@ function selfTest() {
1874918891
const tail = alwaysRunSteps([{ file: 'fixture.yml', text: tailWf }]);
1875018892
const tailNames = tail.rows.map((r) => r.step);
1875118893
t('a step whose family the derivation names is NOT in the tail', !tailNames.includes('A discoverable family'));
18752-
t('a package-local gate invoked by path IS in the tail', tailNames.includes('A package-local gate invoked by path'));
18894+
// #15342 retired this member from the tail by giving the derivation the anchor
18895+
// to discover it, so what is pinned is the RETIREMENT WITH ITS CAUSE: the step
18896+
// has left the tail AND the derivation names a family for it. Either half
18897+
// alone goes green for the wrong reason — a tail that stopped walking, or a
18898+
// family list that claims the step while CI's step sits unaccounted for. The
18899+
// tail's own class ("a step the derivation names NOTHING for is listed") is
18900+
// unweakened: the interpreter member below still holds it, on this fixture and
18901+
// on the live tree.
18902+
t(
18903+
'a package-local gate invoked by path is NOT in the tail any more (#15342) — the derivation names it',
18904+
!tailNames.includes('A package-local gate invoked by path')
18905+
&& extractCheckInvocations(tailWf, 'fixture.yml')
18906+
.some((i) => i.script === 'packages/lint/scripts/check-fixture-shape.mjs'),
18907+
);
1875318908
t('a gate run by another interpreter IS in the tail', tailNames.includes('A gate run by another interpreter'));
1875418909
t('a conditional STEP is excluded and counted', !tailNames.includes('Conditional, so no claim is made about it') && tail.counts.conditionalSteps === 1);
1875518910
t('a conditional JOB is excluded and counted', !tailNames.includes('Never claimed as always-run') && tail.counts.conditionalJobs === 1);
@@ -18806,9 +18961,21 @@ function selfTest() {
1880618961
);
1880718962
const liveCommands = liveTail.rows.flatMap((r) => r.commands);
1880818963
t('the live tail is not empty — an empty one would mean the walk broke, not that CI runs nothing', liveTail.rows.length > 0);
18964+
// #13333's first live instance was a package-local gate invoked by path. It is
18965+
// no longer here, and that is #15342 landing rather than this pin rotting —
18966+
// both halves are asserted for the reason the fixture case above states.
18967+
const liveDirectScripts = new Set(
18968+
readdirSync(nodePath.join(ROOT, '.github/workflows'))
18969+
.filter((f) => /\.ya?ml$/.test(f))
18970+
.flatMap((f) => extractCheckInvocations(readFileSync(nodePath.join(ROOT, '.github/workflows', f), 'utf8'), f))
18971+
.filter((i) => i.direct)
18972+
.map((i) => i.script),
18973+
);
1880918974
t(
18810-
'the live tail names the INSTANCE the card was filed about: a package-local gate invoked by path',
18811-
liveCommands.some((c) => /^node\s+packages\/\S+\/check-[\w.-]+\.mjs/.test(c)),
18975+
'the INSTANCE the card was filed about has LEFT the live tail (#15342), and the derivation now names a '
18976+
+ 'family for it — the tail shrank because the step became accounted for, not because the walk broke',
18977+
!liveCommands.some((c) => /^node\s+packages\/\S+\/check-[\w.-]+\.mjs/.test(c))
18978+
&& liveDirectScripts.has('packages/lint/scripts/check-reference-carrier-shape.mjs'),
1881218979
);
1881318980
// The class assertion. The instance above is invisible because its path is
1881418981
// not under `scripts/`; this one is invisible because its INTERPRETER is not

0 commit comments

Comments
 (0)