diff --git a/scripts/check-changeset-no-major.mjs b/scripts/check-changeset-no-major.mjs index cd623e2fa7..f57e8087ad 100644 --- a/scripts/check-changeset-no-major.mjs +++ b/scripts/check-changeset-no-major.mjs @@ -354,6 +354,8 @@ import { tmpdir } from 'node:os'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { isEntrypoint } from './invoked-as.mjs'; + const __dirname = dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = resolve(__dirname, '..'); @@ -1522,7 +1524,27 @@ function selfTest() { const argv = process.argv.slice(2); -if (argv.includes('--self-test')) { +/** + * The guard is INVERTED so the dispatch chain below keeps its indentation: + * the imported case is the empty first branch, and every mode that was here + * before is untouched in the `else if` chain. + * + * Measured before this landed: importing this module for its exports ran the + * whole gate inside the importer, and then `main()`'s trailing + * `process.exit(exitCode)` ended that process mid-import — carrying status 0. + * The importer never reached the statement after its own `import()`, and a + * caller reading the status alone cannot tell that apart from a clean import. + * + * Nothing imports this file today (every reference in `.github/**`, + * `package.json` and `scripts/**` spawns it as `node scripts/...`), so the + * guard silences no census: the only top-level statement it moves behind + * `isEntrypoint` is CLI dispatch. + */ +const invokedDirectly = isEntrypoint(import.meta.url); + +if (!invokedDirectly) { + // imported as a module — expose the exports and do nothing else +} else if (argv.includes('--self-test')) { if (selfTest() !== SELF_TEST_VERDICT) { console.error( '\n✗ check-changeset-no-major self-test: selfTest() returned without reaching its verdict,\n' diff --git a/scripts/check-empty-changeset.mjs b/scripts/check-empty-changeset.mjs index 18143fa8b8..cf431bb41d 100644 --- a/scripts/check-empty-changeset.mjs +++ b/scripts/check-empty-changeset.mjs @@ -143,6 +143,8 @@ import { tmpdir } from 'node:os'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { isEntrypoint } from './invoked-as.mjs'; + const __dirname = dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = resolve(__dirname, '..'); @@ -1445,7 +1447,26 @@ function selfTest() { const argv = process.argv.slice(2); -if (argv.includes('--self-test')) { +/** + * The guard is INVERTED so the dispatch chain below keeps its indentation: + * the imported case is the empty first branch, and every mode that was here + * before is untouched in the `else if` chain. + * + * Measured before this landed: importing this module for its exports ran the + * whole gate inside the importer and wrote this gate's verdict to the + * importer's stdout before the exports came back — foreign output in a tool + * that never asked for it. + * + * Nothing imports this file today (every reference in `.github/**`, + * `package.json` and `scripts/**` spawns it as `node scripts/...`), so the + * guard silences no census: the only top-level statement it moves behind + * `isEntrypoint` is CLI dispatch. + */ +const invokedDirectly = isEntrypoint(import.meta.url); + +if (!invokedDirectly) { + // imported as a module — expose the exports and do nothing else +} else if (argv.includes('--self-test')) { if (selfTest() !== SELF_TEST_VERDICT) { console.error( '\n✗ check-empty-changeset self-test: selfTest() returned without reaching its verdict,\n' diff --git a/scripts/check-entry-guard.mjs b/scripts/check-entry-guard.mjs index b74a30ab08..b9e1209575 100644 --- a/scripts/check-entry-guard.mjs +++ b/scripts/check-entry-guard.mjs @@ -457,10 +457,7 @@ export function importUnsafeStatements(source) { * a line in here. An entry whose file has since been fixed fails as STALE and * names itself, which is what stops this from rotting into an allowlist. */ -const KNOWN_IMPORT_UNSAFE = new Set([ - 'scripts/check-changeset-no-major.mjs', - 'scripts/check-empty-changeset.mjs', -]); +const KNOWN_IMPORT_UNSAFE = new Set([]); /** * The SCAN SURFACE, written in the syntax `scripts/pm/dispatch-gates.mjs` can diff --git a/scripts/pm/dispatch-gates.mjs b/scripts/pm/dispatch-gates.mjs index 7d11d5dc5a..3a42cbfc5b 100644 --- a/scripts/pm/dispatch-gates.mjs +++ b/scripts/pm/dispatch-gates.mjs @@ -12150,20 +12150,34 @@ function selfTest() { JSON.stringify({ verdict: verdict?.verdict, hints: entry?.hints }), ); // The ablation, run in-place: strip the declared SUBTREE from the live hint - // set and the verdict must fall back to what it was before this landed. + // set and the verdict must fall back to NOT MATCHED — that is the whole + // claim, since a brand-new file is nameable only through the subtree half. // Without it the case above could pass through any hint that happened to // cover the probe, and the reader could not tell which half was load-bearing. + // + // WHICH not-matched verdict it lands on is not fixed, and pinning one + // spelling was a latent trap: for `check:entry-guard` the residual depends + // on whether its KNOWN_IMPORT_UNSAFE roster still contributes path literals + // as hints — `silent` while it held entries, `undetermined` once it emptied + // and the stripped hint set is bare. That ledger is ⛔ SHRINK-ONLY and + // reaching zero is its GOAL, so the day it emptied this case went red over + // a gate that had not changed at all. Either verdict proves the subtree + // hint is the load-bearing half, so both are accepted — spelled as an + // explicit pair rather than `!== 'matched'`, so a NEW verdict value added + // later cannot slip through here as a pass. const undeclared = entry ? { ...entry, hints: entry.hints.filter((h) => !h.includes('/*')) } : null; + const residual = undeclared ? classifyEntry(undeclared, [unwrittenScript]).verdict : null; t( - `…and it is the subtree declaration doing it: strip it and ${gate} goes back to silent`, + `…and it is the subtree declaration doing it: strip it and ${gate} goes back to NOT MATCHED`, // The length check is what stops this passing VACUOUSLY. With no subtree - // hint to remove, `undeclared` is the entry itself and `silent === silent` - // reads as a pass — measured, on the ablation run that removed both - // declarations: this case stayed green while the two above went red. + // hint to remove, `undeclared` is the entry itself and a not-matched + // verdict compared against itself reads as a pass — measured, on the + // ablation run that removed both declarations: this case stayed green + // while the two above went red. Boolean(undeclared) && undeclared.hints.length < entry.hints.length && - classifyEntry(undeclared, [unwrittenScript]).verdict === 'silent', - JSON.stringify({ before: entry?.hints?.length, after: undeclared?.hints?.length }), + ['silent', 'undetermined'].includes(residual), + JSON.stringify({ before: entry?.hints?.length, after: undeclared?.hints?.length, residual }), ); }