Skip to content

Commit 7c690ef

Browse files
committed
fix(pm): key a package-local gate invocation to its real path (#15342)
`check-self-test-wired` anchored `INVOCATION_RE` on the bare literal `scripts/`, so lint.yml's `node packages/lint/scripts/check-reference- carrier-shape.mjs --self-test` matched as a SUBSTRING and was filed under `scripts/check-reference-carrier-shape.mjs` — a key with no file behind it. Both directions were silent: the real file sits outside the root walk, so it was in no population and never audited, and the phantom key could never be reconciled against a carrier either. Two anchors move, and only these: - `INVOCATION_RE` consumes the directory prefix into the key and gains a left boundary, so a path is keyed WHOLE. A leading `./` is stripped (cut-rc.yml and release.yml import from `"./scripts/…"`, and keying those elsewhere would drop two live invocations); `..` is not a prefix segment, because a path this ROOT cannot resolve is how the phantom was minted. - the population gains a second source: a script CI NAMES that lives outside the root walk, admitted on the root walk's own terms (must exist, must carry the literal in code). Not a second walk — see the comment in main(). Measured on the live corpus: 186 named paths, 0 without a file on disk (before: exactly 1). Carriers 182 -> 183, members 168 -> 169, wired 164 -> 165. Self-test grows two batteries — `left boundary` (6) and `live corpus` (3); 50 cases -> 59, floor 7 -> 9. No existing case is weakened. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
1 parent 65846bc commit 7c690ef

1 file changed

Lines changed: 164 additions & 4 deletions

File tree

scripts/check-self-test-wired.mjs

Lines changed: 164 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,14 +152,50 @@ const SCRIPT_EXT = /\.(mjs|mts|js|sh)$/;
152152
const ROOT_DIR_WATCH_HINTS = ['scripts/**/*.mjs', 'scripts/**/*.mts', 'scripts/**/*.sh'];
153153

154154
/**
155-
* A `scripts/...` path, optionally followed by `--self-test`.
155+
* A `scripts/...` path, optionally prefixed by the directories it lives under
156+
* and optionally followed by `--self-test`.
156157
*
157158
* The trailing `(?![\w-])` is the right boundary: without it `--self-test-extra`
158159
* reads as an invocation of `--self-test`, which is #10534's defect wearing this
159160
* gate's hat.
161+
*
162+
* ## The LEFT boundary, and why the prefix is read rather than cut off (#15342)
163+
*
164+
* This repo has a package-local gate lane, and `lint.yml` really does run one
165+
* of its gates by path: `node packages/lint/scripts/check-reference-carrier-
166+
* shape.mjs --self-test`. The pattern used to open on the bare literal
167+
* `scripts/` with nothing to its left, so it matched that path as a SUBSTRING
168+
* and filed the gate under `scripts/check-reference-carrier-shape.mjs` — a key
169+
* with no file behind it. Both directions of that were silent: the real file
170+
* was never audited (it is not under the root walk, so it is in no population),
171+
* and the phantom key could never be reconciled against a carrier either, so
172+
* neither `auditPopulation` nor `auditLedger` had anything to say. A gate whose
173+
* whole subject is "the self-test CI ships is the self-test CI runs" was
174+
* answering about a different script than the one CI executes.
175+
*
176+
* So the directory prefix is CONSUMED into the key rather than cut away, and
177+
* `(?<![\w.\-/])` is the left boundary that makes the key a whole path instead
178+
* of a tail of one. Both halves are load-bearing and were measured on this
179+
* tree's own corpus before landing:
180+
*
181+
* - a leading `./` is stripped rather than kept, because it is the same file
182+
* under a different spelling and paths are compared by EXACT equality here.
183+
* `cut-rc.yml` and `release.yml` both import from `"./scripts/check-docs-
184+
* image-tag.mjs"`; keying those to `./scripts/…` would silently drop two
185+
* live invocations the old pattern did credit.
186+
* - a segment must START with an alphanumeric or `_`, so `..` is not a
187+
* prefix segment. A `../…` path cannot be resolved against this ROOT
188+
* without knowing what it is relative to, and inventing a key for it is how
189+
* the phantom above was minted in the first place. Zero occur in the
190+
* workflow corpus today; refusing to match is the safe direction, and the
191+
* population half below never admits a path it cannot read.
192+
*
193+
* Measured after this change over the live corpus: 186 named paths, ZERO of
194+
* which lack a file on disk (before: exactly one, the phantom above). The
195+
* `live corpus` battery holds both halves of that reading.
160196
*/
161197
const INVOCATION_RE =
162-
/(scripts\/[A-Za-z0-9_.\-/]+\.(?:mjs|mts|js|sh))(\s+--self-test(?![\w-]))?/g;
198+
/(?<![\w.\-/])(?:\.\/)*((?:[A-Za-z0-9_][A-Za-z0-9_.\-]*\/)*scripts\/[A-Za-z0-9_.\-/]+\.(?:mjs|mts|js|sh))(\s+--self-test(?![\w-]))?/g;
163199

164200
/**
165201
* ⛔ SHRINK-ONLY. Scripts CI runs whose self-test IS run by CI, but not through
@@ -424,16 +460,49 @@ function main() {
424460
const { named, selfTested } = collectInvocations(workflows, pkgScripts);
425461
if (named.size === 0) refuse('no workflow names any scripts/ file — the workflow reader is broken (#4690).');
426462

463+
// The population's SECOND source: the package-local gate lane (#15342).
464+
//
465+
// `walkScripts` is anchored at the repo-root `scripts/` dir, so a gate CI
466+
// invokes by a package-local path is outside `carriers` no matter how the
467+
// anchor above keys it — and a script that is in no population is audited by
468+
// neither `auditPopulation` (it iterates carriers) nor `auditLedger`. Fixing
469+
// the key alone would have left that half exactly as silent as before.
470+
//
471+
// ⛔ NOT a second walk. The subject of this gate is "a script CI RUNS whose
472+
// self-test CI must run too", so what CI names is the honest population
473+
// boundary out here; a walk over every `packages/*/scripts` dir would admit
474+
// files CI never runs, which `auditPopulation` discards on the next line
475+
// anyway, and would owe `ROOT_DIR_WATCH_HINTS` a declaration that is no
476+
// longer set-equal to the root walk it is documented to mirror. The root walk
477+
// stays the ROOT half — it is what the `#4690` floors above are written
478+
// against ("this tree has dozens"), and a naming-derived population could not
479+
// carry those.
480+
//
481+
// Admitted on exactly the terms the root walk uses, and no looser: the file
482+
// must EXIST and its CODE (comments masked) must carry the literal. A named
483+
// path with nothing behind it is left OUT rather than admitted — that is the
484+
// phantom this card is about, and admitting one would re-create it one layer
485+
// down. It is not a refusal either: this gate does not own what a workflow is
486+
// allowed to name, and the anchor above already declines to invent keys.
487+
for (const relPath of named.keys()) {
488+
if (relPath.startsWith('scripts/')) continue;
489+
const source = sourceOf(relPath);
490+
if (source === null) continue;
491+
if (carriesSelfTest(relPath, source)) carriers.add(relPath);
492+
}
493+
427494
const findings = [
428495
...auditPopulation({ carriers, named, selfTested, ledger: SELF_TEST_RUN_OTHERWISE }),
429496
...auditLedger({ ledger: SELF_TEST_RUN_OTHERWISE, carriers, named, selfTested, sourceOf }),
430497
];
431498

432499
const members = [...carriers].filter((s) => named.has(s));
433500
const wired = members.filter((s) => selfTested.has(s));
501+
const packageLocal = [...carriers].filter((s) => !s.startsWith('scripts/'));
434502
const scope =
435503
` scope: ${files.length} file(s) under scripts/, ${carriers.size} carrying \`--self-test\` in code ` +
436-
`(comments masked); ${members.length} of those are run by ${workflows.length} workflow(s); ` +
504+
`(comments masked, ${packageLocal.length} of them package-local gate(s) CI names by path); ` +
505+
`${members.length} of those are run by ${workflows.length} workflow(s); ` +
437506
`${wired.length} have their self-test run through the flag, ${SELF_TEST_RUN_OTHERWISE.length} through a recorded route.`;
438507

439508
if (findings.length > 0) {
@@ -496,17 +565,19 @@ function main() {
496565
const SELF_TEST_BATTERIES = Object.freeze({
497566
'comment mask': 7,
498567
'right boundary': 4,
568+
'left boundary': 6,
499569
'alias resolution': 4,
500570
'population verdict': 4,
501571
'population declaration': 7,
572+
'live corpus': 3,
502573
'ledger hygiene': 9,
503574
'live ledger': 4,
504575
});
505576

506577
// DELETING an entry silences that battery's floor exactly as effectively as
507578
// zeroing it, so the registry's own size is pinned too. Adding a battery raises
508579
// this number; removing one is the same ⛔ deliberate edit as lowering a count.
509-
const SELF_TEST_BATTERY_FLOOR = 7;
580+
const SELF_TEST_BATTERY_FLOOR = 9;
510581

511582
// The key an assertion is filed under when no battery is open. It is not a
512583
// declared battery, so it reds by the same set difference rather than silently
@@ -588,6 +659,60 @@ function selfTest() {
588659
);
589660
}
590661

662+
// ── Left boundary: a path is keyed WHOLE, never as a tail of one (#15342) ─
663+
//
664+
// Every case here fails against the pre-#15342 anchor (`/(scripts\/…)/`, no
665+
// left boundary, no prefix), which is what makes them an instrument rather
666+
// than a restatement of the operators. Measured by applying this battery to
667+
// the base tree's pattern in a scratch ablation: cases 1-4 red there, 5-6
668+
// stay green — 5 and 6 are the controls that prove the widening did not buy
669+
// its new answers by dropping the old ones.
670+
battery('left boundary');
671+
{
672+
const PKG = 'packages/lint/scripts/check-reference-carrier-shape.mjs';
673+
const got = collectInvocations(wf(` - run: node ${PKG} --self-test\n`), {});
674+
ok(
675+
got.named.has(PKG),
676+
'a gate CI invokes by a PACKAGE-LOCAL path was not keyed to its real path — it is then in no '
677+
+ 'population and its self-test wiring is audited for nobody (#15342)',
678+
);
679+
ok(
680+
got.selfTested.has(PKG),
681+
'the package-local path was named but its `--self-test` was not credited to it',
682+
);
683+
ok(
684+
!got.named.has('scripts/check-reference-carrier-shape.mjs'),
685+
'the package-local path was ALSO filed under a root path with no file behind it — the phantom key '
686+
+ 'a substring match mints, which no carrier can ever reconcile (#15342)',
687+
);
688+
}
689+
{
690+
// A path that is a tail of a longer one must not be minted on its own.
691+
const got = collectInvocations(wf(' - run: node vendor/tools/scripts/g.mjs --self-test\n'), {});
692+
ok(
693+
!got.named.has('scripts/g.mjs'),
694+
'a `scripts/…` SUBSTRING of a longer path was minted as a key in its own right',
695+
);
696+
}
697+
{
698+
// Control 1 — the plain root spelling is unchanged.
699+
const got = collectInvocations(wf(' - run: node scripts/g.mjs --self-test\n'), {});
700+
ok(got.selfTested.has('scripts/g.mjs'), 'the ordinary root spelling stopped being keyed to itself');
701+
}
702+
{
703+
// Control 2 — `./scripts/…` still normalises onto the walk's key. Live
704+
// spelling: `cut-rc.yml` and `release.yml` import from `"./scripts/…"`,
705+
// and keying those anywhere else silently drops two real invocations.
706+
const got = collectInvocations(
707+
wf(' - run: node --input-type=module -e \'import { X } from "./scripts/g.mjs";\'\n'),
708+
{},
709+
);
710+
ok(
711+
got.named.has('scripts/g.mjs'),
712+
'a leading `./` was kept in the key, so the same file under two spellings stopped comparing equal',
713+
);
714+
}
715+
591716
// ── Aliases: reached only when a workflow actually names them ────────────
592717
battery('alias resolution');
593718
{
@@ -702,6 +827,41 @@ function selfTest() {
702827
);
703828
}
704829

830+
// ── The live corpus: the anchor is measured on specimens, not on fixtures ─
831+
//
832+
// A widening measured on zero specimens is not measured. The fixtures above
833+
// prove the pattern; these three read the tree CI actually runs, so the day
834+
// the package-local lane moves, this reds here instead of going quiet.
835+
battery('live corpus');
836+
{
837+
let corpus = null;
838+
try {
839+
const dir = join(ROOT, WORKFLOW_DIR);
840+
const workflows = readdirSync(dir)
841+
.filter((f) => /\.ya?ml$/.test(f))
842+
.sort()
843+
.map((name) => ({ name, text: readFileSync(join(dir, name), 'utf8') }));
844+
const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8')).scripts ?? {};
845+
corpus = collectInvocations(workflows, pkg);
846+
} catch {
847+
corpus = null;
848+
}
849+
ok(corpus !== null && corpus.named.size > 0, 'the live workflow corpus could not be read — the pins below would prove nothing (#4690)');
850+
const keys = corpus === null ? [] : [...corpus.named.keys()];
851+
ok(
852+
keys.length > 0 && keys.every((p) => existsSync(join(ROOT, p))),
853+
'a path this gate keys an invocation to has NO file behind it. Every audit downstream then runs '
854+
+ 'against a script that does not exist, and passes for the wrong reason, in both directions (#15342)',
855+
);
856+
// Named BY NAME on purpose: it is this tree's only package-local gate
857+
// invocation, so it is the whole specimen set for the widening above.
858+
ok(
859+
keys.includes('packages/lint/scripts/check-reference-carrier-shape.mjs'),
860+
"lint.yml's package-local gate is not in the live population. Either the lane moved — re-point this "
861+
+ 'pin at the new specimen — or the anchor regressed to a root-only one and the widening is untested',
862+
);
863+
}
864+
705865
// ── Ledger hygiene: every row must still be true, and still be needed ────
706866
battery('ledger hygiene');
707867
{

0 commit comments

Comments
 (0)