diff --git a/scripts/check-self-test-wired.mjs b/scripts/check-self-test-wired.mjs index c07b8b3765..cfca4ddcb0 100644 --- a/scripts/check-self-test-wired.mjs +++ b/scripts/check-self-test-wired.mjs @@ -400,73 +400,147 @@ export function auditLedger({ ledger, carriers, named, selfTested, sourceOf }) { return findings; } -/** Walk `scripts/` for candidate entry points. */ -function walkScripts(dir, out = []) { +/** + * Walk `scripts/` for candidate entry points, keyed relative to `root`. + * + * `root` is a parameter rather than the module constant so `collectPopulation` + * below can be driven at a tree that is NOT this repo — which is the only way + * the `#4690` refusals it applies can be exercised by a self-test instead of + * merely coded (#15414). + */ +function walkScripts(dir, root = ROOT, out = []) { for (const entry of readdirSync(dir)) { if (entry === 'node_modules') continue; const full = join(dir, entry); - if (statSync(full).isDirectory()) walkScripts(full, out); - else if (SCRIPT_EXT.test(entry)) out.push(relative(ROOT, full).split(sep).join('/')); + if (statSync(full).isDirectory()) walkScripts(full, root, out); + else if (SCRIPT_EXT.test(entry)) out.push(relative(root, full).split(sep).join('/')); } return out; } -function main() { - const scriptsDir = join(ROOT, 'scripts'); - const workflowDir = join(ROOT, WORKFLOW_DIR); - const refuse = (message) => { - console.error(`\ncheck-self-test-wired: REFUSED — ${message}\n`); - process.exit(1); - }; - if (!existsSync(scriptsDir)) refuse('scripts/ does not exist, so nothing was read (#4690).'); - if (!existsSync(workflowDir)) refuse(`${WORKFLOW_DIR} does not exist, so nothing was read (#4690).`); +/** + * ⛔ THE `#4690` FLOORS, as a pure function over a COMPLETED reading (#15414). + * + * Held here, and applied by `collectPopulation` below, because TWO gates + * consume this population and "nothing to check" versus "the reader is broken" + * has to read the same way in both. Every arm is a REFUSAL naming what could + * not be read, never a quiet pass. + * + * `rootCarriers` is the ROOT WALK's own answer and deliberately not the + * combined set: a tree whose root walk stopped finding carriers has a broken + * reader even when the package-local lane still produced one, and the combined + * set is exactly what would hide that. + * + * The last arm — an empty POPULATION — is new to this file and was previously + * only in `check-self-test-workflow-commands.mjs`. Moving the population here + * moved that floor with it, which is the direction that costs nothing: it + * cannot fire on a tree where any script CI runs ships a `--self-test`, and on + * one where none does, a confident green was the old behaviour. + * + * @returns {string | null} the refusal message body, or null when the reading stands + */ +export function refusalFor({ files, rootCarriers, workflows, pkgScriptCount, named, population }) { + if (files.length === 0) return 'the walk over scripts/ found no files — a broken walk, not a clean tree (#4690).'; + if (rootCarriers.size === 0) { + return 'no script under scripts/ carries a `--self-test` — this tree has dozens, so the reader is broken (#4690).'; + } + if (workflows.length === 0) return `${WORKFLOW_DIR} holds no workflow files (#4690).`; + if (pkgScriptCount === 0) return 'the root package.json declares no scripts (#4690).'; + if (named.size === 0) return 'no workflow names any scripts/ file — the workflow reader is broken (#4690).'; + if (population.length === 0) { + return 'no script CI runs ships a `--self-test` — the population reader is broken, not the tree (#4690).'; + } + return null; +} +/** + * THE POPULATION — one definition, two gates (#15414). + * + * "Which scripts does CI run that ship a `--self-test`" is one question, and it + * was answered twice: here, and again by a private root walk inside + * `check-self-test-workflow-commands.mjs`. The two answers were not the same + * one, and could not be: this file admits a SECOND population source (the + * package-local gate lane, below), that file did not, and the drift was silent + * in the direction that matters — the sibling gate's scope line read `168` to + * this one's `169`, refused nothing, and the missing script's self-test output + * was in no sweep. A gate whose `#4690` refusals all fire on an EMPTY + * population has nothing to say about a population that is complete-minus-one. + * + * So the whole read lives here and is EXPORTED: the walk, the sources, the + * workflow corpus, the alias expansion, the package-local admission and the + * floors. The sibling gate consumes it and adds no walk of its own — the same + * "one definition, two gates" its header already promised for the extraction + * half (`collectInvocations`, `carriesSelfTest`, `codeOf`). + * + * ⛔ Not a convenience wrapper: a consumer that re-derives ANY part of this is + * back in the drift this export exists to end, and the drift's whole signature + * is that both sides stay green while they disagree. + * + * @param {{root?: string}} [options] `root` defaults to this repo; a different + * tree is how the refusals above are exercised rather than merely coded. + */ +export function collectPopulation({ root = ROOT } = {}) { const sourceOf = (relPath) => { try { - return readFileSync(join(ROOT, relPath), 'utf8'); + return readFileSync(join(root, relPath), 'utf8'); } catch { return null; } }; + const blank = (refusal, over = {}) => ({ + refusal, + files: [], + walked: new Set(), + sources: new Map(), + rootCarriers: new Set(), + carriers: new Set(), + named: new Map(), + selfTested: new Map(), + workflows: [], + population: [], + packageLocal: [], + workflowDir: WORKFLOW_DIR, + sourceOf, + ...over, + }); - const files = walkScripts(scriptsDir); - if (files.length === 0) refuse('the walk over scripts/ found no files — a broken walk, not a clean tree (#4690).'); + const scriptsDir = join(root, 'scripts'); + const workflowDir = join(root, WORKFLOW_DIR); + if (!existsSync(scriptsDir)) return blank('scripts/ does not exist, so nothing was read (#4690).'); + if (!existsSync(workflowDir)) return blank(`${WORKFLOW_DIR} does not exist, so nothing was read (#4690).`); - const carriers = new Set(); + const files = walkScripts(scriptsDir, root); + const walked = new Set(files); + const sources = new Map(); + const rootCarriers = new Set(); for (const relPath of files) { const source = sourceOf(relPath); - if (source === null) refuse(`${relPath} could not be read.`); - if (carriesSelfTest(relPath, source)) carriers.add(relPath); - } - if (carriers.size === 0) { - refuse('no script under scripts/ carries a `--self-test` — this tree has dozens, so the reader is broken (#4690).'); + if (source === null) return blank(`${relPath} could not be read.`, { files, walked }); + sources.set(relPath, source); + if (carriesSelfTest(relPath, source)) rootCarriers.add(relPath); } - const workflowNames = readdirSync(workflowDir).filter((f) => /\.ya?ml$/.test(f)).sort(); - if (workflowNames.length === 0) refuse(`${WORKFLOW_DIR} holds no workflow files (#4690).`); - const workflows = workflowNames.map((name) => ({ - name, - text: readFileSync(join(workflowDir, name), 'utf8'), - })); + const workflows = readdirSync(workflowDir) + .filter((f) => /\.ya?ml$/.test(f)) + .sort() + .map((name) => ({ name, text: readFileSync(join(workflowDir, name), 'utf8') })); - let pkgScripts = {}; + let pkgScripts = null; try { - pkgScripts = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8')).scripts ?? {}; + pkgScripts = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).scripts ?? {}; } catch { - refuse('the root package.json could not be read or parsed.'); + return blank('the root package.json could not be read or parsed.', { files, walked, sources, rootCarriers, workflows }); } - if (Object.keys(pkgScripts).length === 0) refuse('the root package.json declares no scripts (#4690).'); const { named, selfTested } = collectInvocations(workflows, pkgScripts); - if (named.size === 0) refuse('no workflow names any scripts/ file — the workflow reader is broken (#4690).'); // The population's SECOND source: the package-local gate lane (#15342). // // `walkScripts` is anchored at the repo-root `scripts/` dir, so a gate CI - // invokes by a package-local path is outside `carriers` no matter how the - // anchor above keys it — and a script that is in no population is audited by - // neither `auditPopulation` (it iterates carriers) nor `auditLedger`. Fixing - // the key alone would have left that half exactly as silent as before. + // invokes by a package-local path is outside the walk's carriers no matter + // how the anchor keys it — and a script that is in no population is audited + // by neither `auditPopulation` (it iterates carriers) nor `auditLedger`. + // Fixing the key alone would have left that half exactly as silent as before. // // ⛔ NOT a second walk. The subject of this gate is "a script CI RUNS whose // self-test CI must run too", so what CI names is the honest population @@ -481,39 +555,72 @@ function main() { // Admitted on exactly the terms the root walk uses, and no looser: the file // must EXIST and its CODE (comments masked) must carry the literal. A named // path with nothing behind it is left OUT rather than admitted — that is the - // phantom this card is about, and admitting one would re-create it one layer + // phantom #15342 was about, and admitting one would re-create it one layer // down. It is not a refusal either: this gate does not own what a workflow is - // allowed to name, and the anchor above already declines to invent keys. + // allowed to name, and the anchor already declines to invent keys. // // ⛔ The skip is membership in the WALK'S OWN OUTPUT, never `startsWith` on a // re-spelling of its root. That spelling is a bare top-level word wearing a // separator, so it reaches the dispatch derivation's hint set as the plain // literal `scripts` and joins the SHRINK-ONLY escapable-literal species // (#10705) -- a population no `hintCovers` can name, declared by a gate that - // already declares the nameable spelling three lines up. Measured when this - // landed: the `startsWith` form added exactly that row, FRESH, to both of - // this gate's families. The set form says what the predicate means -- "the - // root walk did not already produce this path" -- and declares nothing. - const walked = new Set(files); + // already declares the nameable spelling. Measured when this landed: the + // `startsWith` form added exactly that row, FRESH, to both of this gate's + // families. The set form says what the predicate means -- "the root walk did + // not already produce this path" -- and declares nothing. + const carriers = new Set(rootCarriers); for (const relPath of named.keys()) { if (walked.has(relPath)) continue; const source = sourceOf(relPath); if (source === null) continue; + sources.set(relPath, source); if (carriesSelfTest(relPath, source)) carriers.add(relPath); } + // Sorted, so the two consumers iterate in one order and their scope lines are + // comparable line by line. + const population = [...carriers].filter((s) => named.has(s)).sort(); + const packageLocal = population.filter((s) => !walked.has(s)); + const reading = { + files, + walked, + sources, + rootCarriers, + carriers, + named, + selfTested, + workflows, + population, + packageLocal, + workflowDir: WORKFLOW_DIR, + sourceOf, + }; + return { ...reading, refusal: refusalFor({ ...reading, pkgScriptCount: Object.keys(pkgScripts).length }) }; +} + +function main() { + const refuse = (message) => { + console.error(`\ncheck-self-test-wired: REFUSED — ${message}\n`); + process.exit(1); + }; + + // The whole read — walk, sources, workflow corpus, aliases, the package-local + // admission and the `#4690` floors — is `collectPopulation` above, so the + // sibling gate consumes the SAME answer rather than a second one (#15414). + const read = collectPopulation(); + if (read.refusal) refuse(read.refusal); + const { files, carriers, named, selfTested, population, packageLocal, workflows, sourceOf } = read; + const findings = [ ...auditPopulation({ carriers, named, selfTested, ledger: SELF_TEST_RUN_OTHERWISE }), ...auditLedger({ ledger: SELF_TEST_RUN_OTHERWISE, carriers, named, selfTested, sourceOf }), ]; - const members = [...carriers].filter((s) => named.has(s)); - const wired = members.filter((s) => selfTested.has(s)); - const packageLocal = [...carriers].filter((s) => !walked.has(s)); + const wired = population.filter((s) => selfTested.has(s)); const scope = ` scope: ${files.length} file(s) under scripts/, ${carriers.size} carrying \`--self-test\` in code ` + `(comments masked, ${packageLocal.length} of them package-local gate(s) CI names by path); ` + - `${members.length} of those are run by ${workflows.length} workflow(s); ` + + `${population.length} of those are run by ${workflows.length} workflow(s); ` + `${wired.length} have their self-test run through the flag, ${SELF_TEST_RUN_OTHERWISE.length} through a recorded route.`; if (findings.length > 0) { @@ -524,7 +631,7 @@ function main() { } console.log( - `✓ check-self-test-wired: every one of the ${members.length} script(s) CI runs that ship a ` + + `✓ check-self-test-wired: every one of the ${population.length} script(s) CI runs that ship a ` + '`--self-test` has that self-test run by CI.', ); console.log(scope); @@ -583,12 +690,13 @@ const SELF_TEST_BATTERIES = Object.freeze({ 'live corpus': 3, 'ledger hygiene': 9, 'live ledger': 4, + 'the exported population': 8, }); // DELETING an entry silences that battery's floor exactly as effectively as // zeroing it, so the registry's own size is pinned too. Adding a battery raises // this number; removing one is the same ⛔ deliberate edit as lowering a count. -const SELF_TEST_BATTERY_FLOOR = 9; +const SELF_TEST_BATTERY_FLOOR = 10; // The key an assertion is filed under when no battery is open. It is not a // declared battery, so it reds by the same set difference rather than silently @@ -947,6 +1055,67 @@ function selfTest() { } } + // ── The EXPORTED population: what the sibling gate now consumes (#15414) ── + // + // `check-self-test-workflow-commands.mjs` used to answer "which scripts does + // CI run that ship a `--self-test`" for itself, with a private root walk, and + // the two answers drifted apart by one file the moment this gate grew its + // package-local source. The export is the repair; these cases are what makes + // it an instrument rather than a refactor. + // + // The refusal arms are driven through `refusalFor` on HAND-BUILT readings, + // because that is the only way to reach them: on this tree every one of them + // is unreachable by construction, which is exactly the property that let the + // sibling gate's floors sit unexercised while its population was wrong. + battery('the exported population'); + { + const healthy = { + files: ['scripts/g.mjs'], + rootCarriers: new Set(['scripts/g.mjs']), + workflows: [{ name: 'lint.yml', text: '' }], + pkgScriptCount: 1, + named: new Map([['scripts/g.mjs', new Set(['lint.yml'])]]), + population: ['scripts/g.mjs'], + }; + ok(refusalFor(healthy) === null, 'control — a healthy reading was refused, so the arms below prove nothing'); + ok( + (refusalFor({ ...healthy, files: [] }) ?? '').includes('broken walk'), + 'an empty walk was not refused — "nothing to check" and "the walk found nothing" would read alike (#4690)', + ); + ok( + (refusalFor({ ...healthy, rootCarriers: new Set() }) ?? '').includes('this tree has dozens'), + 'a root walk that found no carrier was not refused — and the combined set must NOT rescue it, or a ' + + 'broken root reader hides behind one package-local hit', + ); + ok( + (refusalFor({ ...healthy, named: new Map() }) ?? '').includes('workflow reader is broken'), + 'a workflow reader that named nothing was not refused', + ); + ok( + (refusalFor({ ...healthy, population: [] }) ?? '').includes('population reader is broken'), + 'an EMPTY population was not refused — this is the floor the sibling gate leans on now that it ' + + 'no longer computes one of its own (#4690)', + ); + + // The live reading, which is what both gates actually run on. + const live = collectPopulation(); + ok( + live.refusal === null && live.population.length > 0, + `the live population could not be read (${live.refusal ?? 'empty'}), so the cases below prove nothing (#4690)`, + ); + ok( + live.packageLocal.length > 0 + && live.population.includes('packages/lint/scripts/check-reference-carrier-shape.mjs'), + 'the EXPORT dropped the package-local half. A consumer of it is then back in the root-walk-only ' + + 'population this card exists to end, and nothing on either side would redden (#15414)', + ); + ok( + live.population.every((s) => typeof live.sources.get(s) === 'string'), + 'a population member has no entry in `sources` — the consumer indexes `sources` BY member to run ' + + 'its prefilter, so a gap there is a crash or a silently unfiltered script', + ); + } + // ── The floor: every declared battery RAN, and ran its cases ───────────── // // Evaluated here, after every battery has had its chance and BEFORE the diff --git a/scripts/check-self-test-workflow-commands.mjs b/scripts/check-self-test-workflow-commands.mjs index f814a03729..62a434e765 100644 --- a/scripts/check-self-test-workflow-commands.mjs +++ b/scripts/check-self-test-workflow-commands.mjs @@ -58,6 +58,29 @@ * `scripts/check-self-test-wired.mjs`, which already owns the answer to "which * scripts does CI run that ship a `--self-test`". One definition, two gates. * + * ## That sentence was a PROMISE for one release, and it was false (#15414) + * + * What was imported was the EXTRACTION (`collectInvocations`, `carriesSelfTest`, + * `codeOf`) — not the population. This gate then built its own from a private + * `walkScripts` anchored at the repo-root `scripts/` dir, and the sibling gate + * had meanwhile grown a SECOND population source: the package-local gate lane + * CI names by path (#15342). Two answers to one question, and they disagreed 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 was `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, because every `#4690` refusal + * below fires on an EMPTY population or an empty candidate set. A population + * that is complete-minus-one refuses nothing and prints a confident scope line. + * + * So the population now arrives through `collectPopulation`, exported by the + * gate that owns it. ⛔ This file adds no walk of its own — a second walk is + * how the drift got here, and a re-derivation that agrees today is a + * re-derivation that can stop agreeing without either side going red. + * * ## The verdict comes from real output, never from a rule about source * * The static scan below only SELECTS which self-tests to run. Whether a token @@ -93,17 +116,33 @@ * nothing" are the two readings this gate is built to keep apart. */ -import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs'; +import { readFileSync, existsSync } from 'node:fs'; import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { spawnSync } from 'node:child_process'; import { isEntrypoint } from './invoked-as.mjs'; -import { collectInvocations, carriesSelfTest, codeOf } from './check-self-test-wired.mjs'; +import { codeOf, collectPopulation, refusalFor } from './check-self-test-wired.mjs'; const ROOT = join(fileURLToPath(new URL('.', import.meta.url)), '..'); + +/** + * The workflow corpus this gate's population is derived FROM. + * + * Nothing here reads it — `collectPopulation` does. It is declared anyway, for + * two reasons that are both mechanical: + * + * - `scripts/pm/dispatch-gates.mjs` derives this gate's family by scanning + * THIS module's source for path-shaped literals. Deleting the literal along + * with the read would drop this gate off every card that edits a workflow — + * while its verdict still moves with those files, since a workflow is what + * decides which self-tests are in the population at all. + * - it is PINNED against `read.workflowDir` in `main()` below, so it is a live + * coupling rather than a decoration: the day the shared reader's corpus root + * moves, this gate refuses out loud instead of quietly declaring a directory + * it no longer depends on. + */ const WORKFLOW_DIR = '.github/workflows'; -const SCRIPT_EXT = /\.(mjs|mts|js|sh)$/; /** How long one self-test may take before the gate refuses rather than guesses. */ const TIMEOUT_MS = 120_000; @@ -161,6 +200,30 @@ const CANDIDATE_V2 = /(?:^|["'`]|\\n)[ \t]*::[A-Za-z][\w-]/m; * Could this script's CODE (comments masked — prose never prints) print a * workflow command? * + * ## Package-local members are filtered on exactly these terms — decided, not + * ## defaulted (#15414) + * + * The population now includes gates CI names by a package-local path, and the + * question asked here is whether they get a lane of their own. They do NOT, and + * the reason is that the predicate's subject is the FILE: "could this code print + * a workflow command" is a property of the bytes, and where the file sits says + * nothing about it. A lane-specific arm would be a second matching rule with no + * measurement behind it, in a gate whose whole design note is that its verdict + * comes from real output precisely because matching rules rot in silence. + * + * What that decision COSTS, measured on this tree at the commit that took it: + * `packages/lint/scripts/check-reference-carrier-shape.mjs` — the one + * package-local member — is NOT selected. Its code carries neither form: no + * `##[`, and no `::` that could reach the start of a printed line. So the + * candidate set is 17 before and 17 after, and this change spawns ZERO extra + * subprocesses today. + * + * The price is deferred, not waived, and it is small: that gate's `--self-test` + * runs in 0.68s wall (measured, shared box), against ~14s for the 17 already + * selected. If it ever gains a token in code it joins the candidate set on the + * same terms as any root script, and paying 0.7s to scan the output of a + * self-test CI runs on every pull request is the trade this gate exists to make. + * * @param {string} relPath * @param {string} source */ @@ -171,15 +234,6 @@ export function isCandidate(relPath, source) { // --------------------------------------------------------------------------- -function walkScripts(dir, out = []) { - for (const entry of readdirSync(dir).sort()) { - const abs = join(dir, entry); - if (statSync(abs).isDirectory()) walkScripts(abs, out); - else if (SCRIPT_EXT.test(entry)) out.push(abs.slice(ROOT.length + 1).split('\\').join('/')); - } - return out; -} - /** * Run one self-test and hand back everything it said. * @@ -203,52 +257,26 @@ export function runSelfTest(relPath) { } function main() { - const scriptsDir = join(ROOT, 'scripts'); - const workflowDir = join(ROOT, WORKFLOW_DIR); const refuse = (message) => { console.error(`\ncheck-self-test-workflow-commands: REFUSED — ${message}\n`); process.exit(1); }; - if (!existsSync(scriptsDir)) refuse('scripts/ does not exist, so nothing was read (#4690).'); - if (!existsSync(workflowDir)) refuse(`${WORKFLOW_DIR} does not exist, so nothing was read (#4690).`); - - const files = walkScripts(scriptsDir); - if (files.length === 0) refuse('the walk over scripts/ found no files — a broken walk, not a clean tree (#4690).'); - - const sources = new Map(); - const carriers = new Set(); - for (const relPath of files) { - let source; - try { - source = readFileSync(join(ROOT, relPath), 'utf8'); - } catch { - refuse(`${relPath} could not be read.`); - } - sources.set(relPath, source); - if (carriesSelfTest(relPath, source)) carriers.add(relPath); - } - if (carriers.size === 0) { - refuse('no script under scripts/ carries a `--self-test` — this tree has dozens, so the reader is broken (#4690).'); - } - const workflowNames = readdirSync(workflowDir).filter((f) => /\.ya?ml$/.test(f)).sort(); - if (workflowNames.length === 0) refuse(`${WORKFLOW_DIR} holds no workflow files (#4690).`); - const workflows = workflowNames.map((name) => ({ name, text: readFileSync(join(workflowDir, name), 'utf8') })); - - let pkgScripts = {}; - try { - pkgScripts = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8')).scripts ?? {}; - } catch { - refuse('the root package.json could not be read or parsed.'); - } - - const { named } = collectInvocations(workflows, pkgScripts); - if (named.size === 0) refuse('no workflow names any scripts/ file — the workflow reader is broken (#4690).'); - - const population = [...carriers].filter((s) => named.has(s)).sort(); - if (population.length === 0) { - refuse('no script CI runs ships a `--self-test` — the population reader is broken, not the tree (#4690).'); + // ONE definition, two gates (#15414). The walk, the sources, the workflow + // corpus, the alias expansion, the package-local admission and every `#4690` + // floor belong to `check-self-test-wired.mjs` and are IMPORTED. This gate's + // own work begins at the prefilter below, and it adds no walk: the drift this + // replaced was a second walk that agreed for a while and then quietly did not. + const read = collectPopulation(); + if (read.refusal) refuse(read.refusal); + if (read.workflowDir !== WORKFLOW_DIR) { + refuse( + `the shared population was read from \`${read.workflowDir}\`, but this gate declares ` + + `\`${WORKFLOW_DIR}\` to the dispatch derivation. One of the two moved, and a gate naming a ` + + 'corpus it no longer depends on is derived onto the wrong cards in silence.', + ); } + const { population, packageLocal, sources } = read; const candidates = population.filter((s) => isCandidate(s, sources.get(s))); if (candidates.length === 0) { @@ -274,8 +302,11 @@ function main() { } const scope = - ` scope: ${population.length} script(s) CI runs ship a \`--self-test\`; ${candidates.length} mention a ` + - 'workflow-command token in code (comments masked) and were RUN, and their real stdout+stderr was scanned.'; + ` scope: ${population.length} script(s) CI runs ship a \`--self-test\` (${packageLocal.length} of them ` + + 'package-local gate(s) CI names by path, present because this population is the one ' + + 'check-self-test-wired.mjs exports rather than a second walk taken here); ' + + `${candidates.length} mention a workflow-command token in code (comments masked) and were RUN, and ` + + 'their real stdout+stderr was scanned.'; if (findings.length > 0) { console.error(`\ncheck-self-test-workflow-commands: ${findings.length} finding(s)\n`); @@ -341,12 +372,13 @@ const SELF_TEST_BATTERIES = Object.freeze({ 'innocent output': 4, 'prefilter reads CODE, never prose': 5, 'end to end on the real defect site': 5, + 'the population is imported, never re-walked': 8, }); // DELETING an entry silences that battery's floor exactly as effectively as // zeroing it, so the registry's own size is pinned too. Adding a battery raises // this number; removing one is the same ⛔ deliberate edit as lowering a count. -const SELF_TEST_BATTERY_FLOOR = 6; +const SELF_TEST_BATTERY_FLOOR = 7; // The key an assertion is filed under when no battery is open. It is not a // declared battery, so it reds by the same set difference rather than silently @@ -447,6 +479,82 @@ function selfTest() { ok(scanOutput(r.output).length === 0, `${target} --self-test still prints a line the runner would parse — this is the #11886 defect, live`); } + // ── The population is the sibling gate's, and this file takes no walk (#15414) ── + // + // The defect these close: this gate used to derive its own population from a + // private root walk, so the package-local gate `lint.yml` self-tests on every + // pull request was in NO population here and its output was never scanned. + // Both halves need an instrument, and they need different ones: + // + // - that the imported population CONTAINS the package-local member is a + // live reading, taken against the tree CI actually runs; + // - that this file does not quietly grow a second walk again is a claim + // about THIS SOURCE, and only an own-source pin can hold it. A re-derived + // population that agrees today is one that can stop agreeing with nothing + // going red on either side — which is precisely how the 169/168 split got + // here and stayed. + battery('the population is imported, never re-walked'); + { + const SPECIMEN = 'packages/lint/scripts/check-reference-carrier-shape.mjs'; + const live = collectPopulation(); + ok( + live.refusal === null && live.population.length > 0, + `the imported population could not be read (${live.refusal ?? 'empty'}), so the cases below prove nothing (#4690)`, + ); + ok( + live.population.includes(SPECIMEN) && live.packageLocal.length > 0, + `${SPECIMEN} is not in the population this gate scans. CI runs its --self-test on every pull request; ` + + 'out of the population, its output is in no sweep and nothing says so (#15414)', + ); + ok( + typeof isCandidate(SPECIMEN, live.sources.get(SPECIMEN) ?? '') === 'boolean', + 'the prefilter could not be applied to the package-local member — it is filtered on exactly the same ' + + 'terms as a root script, and its VALUE is a measurement of that file, deliberately not pinned here', + ); + ok( + live.workflowDir === WORKFLOW_DIR, + 'the shared reader derives the population from a corpus root this gate does not declare, so the ' + + 'dispatch derivation would name this gate for the wrong cards', + ); + + // Own-source. The needles are ASSEMBLED: spelled out, they would be found + // in this very fixture and the pin would red on itself forever. + let ownCode = ''; + try { + ownCode = codeOf('scripts/check-self-test-workflow-commands.mjs', readFileSync(fileURLToPath(import.meta.url), 'utf8')); + } catch { + ownCode = ''; + } + ok(ownCode.length > 0, 'this gate could not read its own source, so the own-source pins below prove nothing (#4690)'); + ok( + ['readdir', 'stat'].every((fn) => !ownCode.includes(`${fn}Sync(`)), + 'this gate walks a directory again. The population is imported for a reason: a second walk here is ' + + 'what drifted from the sibling gate by one file, silently, in both directions (#15414)', + ); + ok( + ownCode.includes('collectPopulation()') && ownCode.includes('check-self-test-wired.mjs'), + 'the population no longer arrives from the gate that owns it — "one definition, two gates" is back ' + + 'to being a sentence in a header rather than a mechanism', + ); + + // The refusals this gate delegated still fire, in both the pure and the + // disk arm. Neither is reachable on a healthy tree, which is exactly why + // they are driven here rather than trusted. + ok( + (refusalFor({ + files: ['scripts/g.mjs'], + rootCarriers: new Set(['scripts/g.mjs']), + workflows: [{ name: 'lint.yml', text: '' }], + pkgScriptCount: 1, + named: new Map([['scripts/g.mjs', new Set(['lint.yml'])]]), + population: [], + }) ?? '').includes('population reader is broken') + && (collectPopulation({ root: join(ROOT, '.github') }).refusal ?? '').includes('scripts/ does not exist'), + 'an empty population, or a tree with no scripts/ at all, stopped being a REFUSAL. "Nothing to check" ' + + 'and "the reader is broken" are the two readings this gate is built to keep apart (#4690)', + ); + } + // ── The floor: every declared battery RAN, and ran its cases ───────────── // // Evaluated here, after every battery has had its chance and BEFORE the @@ -500,7 +608,8 @@ function selfTest() { console.log( 'check-self-test-workflow-commands --self-test: both measured parse rules pinned (legacy form ' + 'anywhere in a line, current form only at line start), the innocent-output and Perl-namespace ' + - 'cases, the comment mask in both directions, and one end-to-end run of the real defect site' + + 'cases, the comment mask in both directions, one end-to-end run of the real defect site, and the ' + + 'imported population held against the live tree with no second walk taken here' + ` — ${declaredBatteries.length} declared batteries, ${totalCases} cases registered, every battery` + ' at or above its pinned floor.', );