@@ -152,14 +152,50 @@ const SCRIPT_EXT = /\.(mjs|mts|js|sh)$/;
152152const 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 */
161197const INVOCATION_RE =
162- / ( s c r i p t s \/ [ A - Z a - z 0 - 9 _ . \- / ] + \. (?: m j s | m t s | j s | s h ) ) ( \s + - - s e l f - t e s t (? ! [ \w - ] ) ) ? / g;
198+ / (?< ! [ \w . \- / ] ) (?: \. \/ ) * ( (?: [ A - Z a - z 0 - 9 _ ] [ A - Z a - z 0 - 9 _ . \- ] * \/ ) * s c r i p t s \/ [ A - Z a - z 0 - 9 _ . \- / ] + \. (?: m j s | m t s | j s | s h ) ) ( \s + - - s e l f - t e s t (? ! [ \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() {
496565const 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 ) => / \. y a ? m l $ / . 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