@@ -400,73 +400,147 @@ export function auditLedger({ ledger, carriers, named, selfTested, sourceOf }) {
400400 return findings ;
401401}
402402
403- /** Walk `scripts/` for candidate entry points. */
404- function walkScripts ( dir , out = [ ] ) {
403+ /**
404+ * Walk `scripts/` for candidate entry points, keyed relative to `root`.
405+ *
406+ * `root` is a parameter rather than the module constant so `collectPopulation`
407+ * below can be driven at a tree that is NOT this repo — which is the only way
408+ * the `#4690` refusals it applies can be exercised by a self-test instead of
409+ * merely coded (#15414).
410+ */
411+ function walkScripts ( dir , root = ROOT , out = [ ] ) {
405412 for ( const entry of readdirSync ( dir ) ) {
406413 if ( entry === 'node_modules' ) continue ;
407414 const full = join ( dir , entry ) ;
408- if ( statSync ( full ) . isDirectory ( ) ) walkScripts ( full , out ) ;
409- else if ( SCRIPT_EXT . test ( entry ) ) out . push ( relative ( ROOT , full ) . split ( sep ) . join ( '/' ) ) ;
415+ if ( statSync ( full ) . isDirectory ( ) ) walkScripts ( full , root , out ) ;
416+ else if ( SCRIPT_EXT . test ( entry ) ) out . push ( relative ( root , full ) . split ( sep ) . join ( '/' ) ) ;
410417 }
411418 return out ;
412419}
413420
414- function main ( ) {
415- const scriptsDir = join ( ROOT , 'scripts' ) ;
416- const workflowDir = join ( ROOT , WORKFLOW_DIR ) ;
417- const refuse = ( message ) => {
418- console . error ( `\ncheck-self-test-wired: REFUSED — ${ message } \n` ) ;
419- process . exit ( 1 ) ;
420- } ;
421- if ( ! existsSync ( scriptsDir ) ) refuse ( 'scripts/ does not exist, so nothing was read (#4690).' ) ;
422- if ( ! existsSync ( workflowDir ) ) refuse ( `${ WORKFLOW_DIR } does not exist, so nothing was read (#4690).` ) ;
421+ /**
422+ * ⛔ THE `#4690` FLOORS, as a pure function over a COMPLETED reading (#15414).
423+ *
424+ * Held here, and applied by `collectPopulation` below, because TWO gates
425+ * consume this population and "nothing to check" versus "the reader is broken"
426+ * has to read the same way in both. Every arm is a REFUSAL naming what could
427+ * not be read, never a quiet pass.
428+ *
429+ * `rootCarriers` is the ROOT WALK's own answer and deliberately not the
430+ * combined set: a tree whose root walk stopped finding carriers has a broken
431+ * reader even when the package-local lane still produced one, and the combined
432+ * set is exactly what would hide that.
433+ *
434+ * The last arm — an empty POPULATION — is new to this file and was previously
435+ * only in `check-self-test-workflow-commands.mjs`. Moving the population here
436+ * moved that floor with it, which is the direction that costs nothing: it
437+ * cannot fire on a tree where any script CI runs ships a `--self-test`, and on
438+ * one where none does, a confident green was the old behaviour.
439+ *
440+ * @returns {string | null } the refusal message body, or null when the reading stands
441+ */
442+ export function refusalFor ( { files, rootCarriers, workflows, pkgScriptCount, named, population } ) {
443+ if ( files . length === 0 ) return 'the walk over scripts/ found no files — a broken walk, not a clean tree (#4690).' ;
444+ if ( rootCarriers . size === 0 ) {
445+ return 'no script under scripts/ carries a `--self-test` — this tree has dozens, so the reader is broken (#4690).' ;
446+ }
447+ if ( workflows . length === 0 ) return `${ WORKFLOW_DIR } holds no workflow files (#4690).` ;
448+ if ( pkgScriptCount === 0 ) return 'the root package.json declares no scripts (#4690).' ;
449+ if ( named . size === 0 ) return 'no workflow names any scripts/ file — the workflow reader is broken (#4690).' ;
450+ if ( population . length === 0 ) {
451+ return 'no script CI runs ships a `--self-test` — the population reader is broken, not the tree (#4690).' ;
452+ }
453+ return null ;
454+ }
423455
456+ /**
457+ * THE POPULATION — one definition, two gates (#15414).
458+ *
459+ * "Which scripts does CI run that ship a `--self-test`" is one question, and it
460+ * was answered twice: here, and again by a private root walk inside
461+ * `check-self-test-workflow-commands.mjs`. The two answers were not the same
462+ * one, and could not be: this file admits a SECOND population source (the
463+ * package-local gate lane, below), that file did not, and the drift was silent
464+ * in the direction that matters — the sibling gate's scope line read `168` to
465+ * this one's `169`, refused nothing, and the missing script's self-test output
466+ * was in no sweep. A gate whose `#4690` refusals all fire on an EMPTY
467+ * population has nothing to say about a population that is complete-minus-one.
468+ *
469+ * So the whole read lives here and is EXPORTED: the walk, the sources, the
470+ * workflow corpus, the alias expansion, the package-local admission and the
471+ * floors. The sibling gate consumes it and adds no walk of its own — the same
472+ * "one definition, two gates" its header already promised for the extraction
473+ * half (`collectInvocations`, `carriesSelfTest`, `codeOf`).
474+ *
475+ * ⛔ Not a convenience wrapper: a consumer that re-derives ANY part of this is
476+ * back in the drift this export exists to end, and the drift's whole signature
477+ * is that both sides stay green while they disagree.
478+ *
479+ * @param {{root?: string} } [options] `root` defaults to this repo; a different
480+ * tree is how the refusals above are exercised rather than merely coded.
481+ */
482+ export function collectPopulation ( { root = ROOT } = { } ) {
424483 const sourceOf = ( relPath ) => {
425484 try {
426- return readFileSync ( join ( ROOT , relPath ) , 'utf8' ) ;
485+ return readFileSync ( join ( root , relPath ) , 'utf8' ) ;
427486 } catch {
428487 return null ;
429488 }
430489 } ;
490+ const blank = ( refusal , over = { } ) => ( {
491+ refusal,
492+ files : [ ] ,
493+ walked : new Set ( ) ,
494+ sources : new Map ( ) ,
495+ rootCarriers : new Set ( ) ,
496+ carriers : new Set ( ) ,
497+ named : new Map ( ) ,
498+ selfTested : new Map ( ) ,
499+ workflows : [ ] ,
500+ population : [ ] ,
501+ packageLocal : [ ] ,
502+ workflowDir : WORKFLOW_DIR ,
503+ sourceOf,
504+ ...over ,
505+ } ) ;
431506
432- const files = walkScripts ( scriptsDir ) ;
433- if ( files . length === 0 ) refuse ( 'the walk over scripts/ found no files — a broken walk, not a clean tree (#4690).' ) ;
507+ const scriptsDir = join ( root , 'scripts' ) ;
508+ const workflowDir = join ( root , WORKFLOW_DIR ) ;
509+ if ( ! existsSync ( scriptsDir ) ) return blank ( 'scripts/ does not exist, so nothing was read (#4690).' ) ;
510+ if ( ! existsSync ( workflowDir ) ) return blank ( `${ WORKFLOW_DIR } does not exist, so nothing was read (#4690).` ) ;
434511
435- const carriers = new Set ( ) ;
512+ const files = walkScripts ( scriptsDir , root ) ;
513+ const walked = new Set ( files ) ;
514+ const sources = new Map ( ) ;
515+ const rootCarriers = new Set ( ) ;
436516 for ( const relPath of files ) {
437517 const source = sourceOf ( relPath ) ;
438- if ( source === null ) refuse ( `${ relPath } could not be read.` ) ;
439- if ( carriesSelfTest ( relPath , source ) ) carriers . add ( relPath ) ;
440- }
441- if ( carriers . size === 0 ) {
442- refuse ( 'no script under scripts/ carries a `--self-test` — this tree has dozens, so the reader is broken (#4690).' ) ;
518+ if ( source === null ) return blank ( `${ relPath } could not be read.` , { files, walked } ) ;
519+ sources . set ( relPath , source ) ;
520+ if ( carriesSelfTest ( relPath , source ) ) rootCarriers . add ( relPath ) ;
443521 }
444522
445- const workflowNames = readdirSync ( workflowDir ) . filter ( ( f ) => / \. y a ? m l $ / . test ( f ) ) . sort ( ) ;
446- if ( workflowNames . length === 0 ) refuse ( `${ WORKFLOW_DIR } holds no workflow files (#4690).` ) ;
447- const workflows = workflowNames . map ( ( name ) => ( {
448- name,
449- text : readFileSync ( join ( workflowDir , name ) , 'utf8' ) ,
450- } ) ) ;
523+ const workflows = readdirSync ( workflowDir )
524+ . filter ( ( f ) => / \. y a ? m l $ / . test ( f ) )
525+ . sort ( )
526+ . map ( ( name ) => ( { name, text : readFileSync ( join ( workflowDir , name ) , 'utf8' ) } ) ) ;
451527
452- let pkgScripts = { } ;
528+ let pkgScripts = null ;
453529 try {
454- pkgScripts = JSON . parse ( readFileSync ( join ( ROOT , 'package.json' ) , 'utf8' ) ) . scripts ?? { } ;
530+ pkgScripts = JSON . parse ( readFileSync ( join ( root , 'package.json' ) , 'utf8' ) ) . scripts ?? { } ;
455531 } catch {
456- refuse ( 'the root package.json could not be read or parsed.' ) ;
532+ return blank ( 'the root package.json could not be read or parsed.' , { files , walked , sources , rootCarriers , workflows } ) ;
457533 }
458- if ( Object . keys ( pkgScripts ) . length === 0 ) refuse ( 'the root package.json declares no scripts (#4690).' ) ;
459534
460535 const { named, selfTested } = collectInvocations ( workflows , pkgScripts ) ;
461- if ( named . size === 0 ) refuse ( 'no workflow names any scripts/ file — the workflow reader is broken (#4690).' ) ;
462536
463537 // The population's SECOND source: the package-local gate lane (#15342).
464538 //
465539 // `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.
540+ // invokes by a package-local path is outside the walk's carriers no matter
541+ // how the anchor keys it — and a script that is in no population is audited
542+ // by neither `auditPopulation` (it iterates carriers) nor `auditLedger`.
543+ // Fixing the key alone would have left that half exactly as silent as before.
470544 //
471545 // ⛔ NOT a second walk. The subject of this gate is "a script CI RUNS whose
472546 // self-test CI must run too", so what CI names is the honest population
@@ -481,39 +555,72 @@ function main() {
481555 // Admitted on exactly the terms the root walk uses, and no looser: the file
482556 // must EXIST and its CODE (comments masked) must carry the literal. A named
483557 // 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
558+ // phantom #15342 was about, and admitting one would re-create it one layer
485559 // 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.
560+ // allowed to name, and the anchor already declines to invent keys.
487561 //
488562 // ⛔ The skip is membership in the WALK'S OWN OUTPUT, never `startsWith` on a
489563 // re-spelling of its root. That spelling is a bare top-level word wearing a
490564 // separator, so it reaches the dispatch derivation's hint set as the plain
491565 // literal `scripts` and joins the SHRINK-ONLY escapable-literal species
492566 // (#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 ) ;
567+ // already declares the nameable spelling. Measured when this landed: the
568+ // `startsWith` form added exactly that row, FRESH, to both of this gate's
569+ // families. The set form says what the predicate means -- "the root walk did
570+ // not already produce this path" -- and declares nothing.
571+ const carriers = new Set ( rootCarriers ) ;
498572 for ( const relPath of named . keys ( ) ) {
499573 if ( walked . has ( relPath ) ) continue ;
500574 const source = sourceOf ( relPath ) ;
501575 if ( source === null ) continue ;
576+ sources . set ( relPath , source ) ;
502577 if ( carriesSelfTest ( relPath , source ) ) carriers . add ( relPath ) ;
503578 }
504579
580+ // Sorted, so the two consumers iterate in one order and their scope lines are
581+ // comparable line by line.
582+ const population = [ ...carriers ] . filter ( ( s ) => named . has ( s ) ) . sort ( ) ;
583+ const packageLocal = population . filter ( ( s ) => ! walked . has ( s ) ) ;
584+ const reading = {
585+ files,
586+ walked,
587+ sources,
588+ rootCarriers,
589+ carriers,
590+ named,
591+ selfTested,
592+ workflows,
593+ population,
594+ packageLocal,
595+ workflowDir : WORKFLOW_DIR ,
596+ sourceOf,
597+ } ;
598+ return { ...reading , refusal : refusalFor ( { ...reading , pkgScriptCount : Object . keys ( pkgScripts ) . length } ) } ;
599+ }
600+
601+ function main ( ) {
602+ const refuse = ( message ) => {
603+ console . error ( `\ncheck-self-test-wired: REFUSED — ${ message } \n` ) ;
604+ process . exit ( 1 ) ;
605+ } ;
606+
607+ // The whole read — walk, sources, workflow corpus, aliases, the package-local
608+ // admission and the `#4690` floors — is `collectPopulation` above, so the
609+ // sibling gate consumes the SAME answer rather than a second one (#15414).
610+ const read = collectPopulation ( ) ;
611+ if ( read . refusal ) refuse ( read . refusal ) ;
612+ const { files, carriers, named, selfTested, population, packageLocal, workflows, sourceOf } = read ;
613+
505614 const findings = [
506615 ...auditPopulation ( { carriers, named, selfTested, ledger : SELF_TEST_RUN_OTHERWISE } ) ,
507616 ...auditLedger ( { ledger : SELF_TEST_RUN_OTHERWISE , carriers, named, selfTested, sourceOf } ) ,
508617 ] ;
509618
510- const members = [ ...carriers ] . filter ( ( s ) => named . has ( s ) ) ;
511- const wired = members . filter ( ( s ) => selfTested . has ( s ) ) ;
512- const packageLocal = [ ...carriers ] . filter ( ( s ) => ! walked . has ( s ) ) ;
619+ const wired = population . filter ( ( s ) => selfTested . has ( s ) ) ;
513620 const scope =
514621 ` scope: ${ files . length } file(s) under scripts/, ${ carriers . size } carrying \`--self-test\` in code ` +
515622 `(comments masked, ${ packageLocal . length } of them package-local gate(s) CI names by path); ` +
516- `${ members . length } of those are run by ${ workflows . length } workflow(s); ` +
623+ `${ population . length } of those are run by ${ workflows . length } workflow(s); ` +
517624 `${ wired . length } have their self-test run through the flag, ${ SELF_TEST_RUN_OTHERWISE . length } through a recorded route.` ;
518625
519626 if ( findings . length > 0 ) {
@@ -524,7 +631,7 @@ function main() {
524631 }
525632
526633 console . log (
527- `✓ check-self-test-wired: every one of the ${ members . length } script(s) CI runs that ship a ` +
634+ `✓ check-self-test-wired: every one of the ${ population . length } script(s) CI runs that ship a ` +
528635 '`--self-test` has that self-test run by CI.' ,
529636 ) ;
530637 console . log ( scope ) ;
@@ -583,12 +690,13 @@ const SELF_TEST_BATTERIES = Object.freeze({
583690 'live corpus' : 3 ,
584691 'ledger hygiene' : 9 ,
585692 'live ledger' : 4 ,
693+ 'the exported population' : 8 ,
586694} ) ;
587695
588696// DELETING an entry silences that battery's floor exactly as effectively as
589697// zeroing it, so the registry's own size is pinned too. Adding a battery raises
590698// this number; removing one is the same ⛔ deliberate edit as lowering a count.
591- const SELF_TEST_BATTERY_FLOOR = 9 ;
699+ const SELF_TEST_BATTERY_FLOOR = 10 ;
592700
593701// The key an assertion is filed under when no battery is open. It is not a
594702// declared battery, so it reds by the same set difference rather than silently
@@ -947,6 +1055,67 @@ function selfTest() {
9471055 }
9481056 }
9491057
1058+ // ── The EXPORTED population: what the sibling gate now consumes (#15414) ──
1059+ //
1060+ // `check-self-test-workflow-commands.mjs` used to answer "which scripts does
1061+ // CI run that ship a `--self-test`" for itself, with a private root walk, and
1062+ // the two answers drifted apart by one file the moment this gate grew its
1063+ // package-local source. The export is the repair; these cases are what makes
1064+ // it an instrument rather than a refactor.
1065+ //
1066+ // The refusal arms are driven through `refusalFor` on HAND-BUILT readings,
1067+ // because that is the only way to reach them: on this tree every one of them
1068+ // is unreachable by construction, which is exactly the property that let the
1069+ // sibling gate's floors sit unexercised while its population was wrong.
1070+ battery ( 'the exported population' ) ;
1071+ {
1072+ const healthy = {
1073+ files : [ 'scripts/g.mjs' ] ,
1074+ rootCarriers : new Set ( [ 'scripts/g.mjs' ] ) ,
1075+ workflows : [ { name : 'lint.yml' , text : '' } ] ,
1076+ pkgScriptCount : 1 ,
1077+ named : new Map ( [ [ 'scripts/g.mjs' , new Set ( [ 'lint.yml' ] ) ] ] ) ,
1078+ population : [ 'scripts/g.mjs' ] ,
1079+ } ;
1080+ ok ( refusalFor ( healthy ) === null , 'control — a healthy reading was refused, so the arms below prove nothing' ) ;
1081+ ok (
1082+ ( refusalFor ( { ...healthy , files : [ ] } ) ?? '' ) . includes ( 'broken walk' ) ,
1083+ 'an empty walk was not refused — "nothing to check" and "the walk found nothing" would read alike (#4690)' ,
1084+ ) ;
1085+ ok (
1086+ ( refusalFor ( { ...healthy , rootCarriers : new Set ( ) } ) ?? '' ) . includes ( 'this tree has dozens' ) ,
1087+ 'a root walk that found no carrier was not refused — and the combined set must NOT rescue it, or a '
1088+ + 'broken root reader hides behind one package-local hit' ,
1089+ ) ;
1090+ ok (
1091+ ( refusalFor ( { ...healthy , named : new Map ( ) } ) ?? '' ) . includes ( 'workflow reader is broken' ) ,
1092+ 'a workflow reader that named nothing was not refused' ,
1093+ ) ;
1094+ ok (
1095+ ( refusalFor ( { ...healthy , population : [ ] } ) ?? '' ) . includes ( 'population reader is broken' ) ,
1096+ 'an EMPTY population was not refused — this is the floor the sibling gate leans on now that it '
1097+ + 'no longer computes one of its own (#4690)' ,
1098+ ) ;
1099+
1100+ // The live reading, which is what both gates actually run on.
1101+ const live = collectPopulation ( ) ;
1102+ ok (
1103+ live . refusal === null && live . population . length > 0 ,
1104+ `the live population could not be read (${ live . refusal ?? 'empty' } ), so the cases below prove nothing (#4690)` ,
1105+ ) ;
1106+ ok (
1107+ live . packageLocal . length > 0
1108+ && live . population . includes ( 'packages/lint/scripts/check-reference-carrier-shape.mjs' ) ,
1109+ 'the EXPORT dropped the package-local half. A consumer of it is then back in the root-walk-only '
1110+ + 'population this card exists to end, and nothing on either side would redden (#15414)' ,
1111+ ) ;
1112+ ok (
1113+ live . population . every ( ( s ) => typeof live . sources . get ( s ) === 'string' ) ,
1114+ 'a population member has no entry in `sources` — the consumer indexes `sources` BY member to run '
1115+ + 'its prefilter, so a gap there is a crash or a silently unfiltered script' ,
1116+ ) ;
1117+ }
1118+
9501119 // ── The floor: every declared battery RAN, and ran its cases ─────────────
9511120 //
9521121 // Evaluated here, after every battery has had its chance and BEFORE the
0 commit comments