Skip to content

Commit a4ef7dc

Browse files
claude[bot]claude
andauthored
fix(gates): give check-self-test-workflow-commands the population it says it imports (#15506)
Its header promised "one definition, two gates", but what it imported was the EXTRACTION (collectInvocations, carriesSelfTest, codeOf) -- not the population. It built its own from a private walkScripts anchored at the repo-root scripts/ dir, while check-self-test-wired grew a second population source (the package-local gate lane CI names by path). Two answers to one question, drifting by exactly one file: check-self-test-wired 169 of those are run by 30 workflow(s) check-self-test-workflow-commands 168 script(s) CI runs ship a `--self-test` The missing member is packages/lint/scripts/check-reference-carrier-shape.mjs, which lint.yml runs with --self-test on every pull request. Its output was in no sweep, and nothing said so: every #4690 refusal in that gate fires on an EMPTY population or an empty candidate set, so a population that is complete-minus-one refuses nothing and prints a confident scope line. check-self-test-wired now exports the whole read as collectPopulation() -- the walk, the sources, the workflow corpus, the alias expansion, the package-local admission -- plus refusalFor(), the #4690 floors as a pure function over a completed reading. The workflow-commands gate consumes it and takes NO walk of its own; walkScripts, readdirSync and statSync are gone from that file, and an own-source case pins that they stay gone. A re-derivation that agrees today is one that can stop agreeing with nothing going red on either side, which is precisely how 169/168 got here. Two decisions recorded in the code: isCandidate treats package-local members exactly like root ones -- no lane of its own. The predicate's subject is the file's bytes; where the file sits says nothing about whether its code can print a workflow command, and a lane-specific arm would be a second matching rule with no measurement behind it. Measured cost: the one package-local member carries neither form, so the candidate set is 17 before and 17 after and this change spawns ZERO extra subprocesses today. Deferred price if it ever gains a token: 0.68s wall for that self-test, against ~14s for the 17 already selected. WORKFLOW_DIR stays declared in the workflow-commands gate although nothing there reads it any more. dispatch-gates derives that gate's family by scanning its source for path literals, so deleting it would drop the gate off every card that edits a workflow while its verdict still moves with those files. It is pinned against read.workflowDir in main(), so it is a live coupling rather than a decoration. Derivation for a .github/workflows/lint.yml card is byte-identical before and after. Floors raised for cases ADDED, never lowered: wired 9 -> 10 batteries (59 -> 67 cases), workflow-commands 6 -> 7 (23 -> 31). Fixes #15414 Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk Co-authored-by: Claude <noreply@anthropic.com>
1 parent e601c04 commit a4ef7dc

2 files changed

Lines changed: 384 additions & 106 deletions

File tree

scripts/check-self-test-wired.mjs

Lines changed: 219 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -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) => /\.ya?ml$/.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) => /\.ya?ml$/.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

Comments
 (0)