diff --git a/scripts/check-platform-checklist.mjs b/scripts/check-platform-checklist.mjs index 1a27a009e6..801d4de438 100644 --- a/scripts/check-platform-checklist.mjs +++ b/scripts/check-platform-checklist.mjs @@ -704,6 +704,45 @@ function foldedCallMessage(path, call, spelling, singular) { ); } +// ── Self-test verdict handshake ───────────────────────────────────────────── +// +// Five batteries, each returning `{ checked, failures }` for a caller to +// report. A `return` above a battery's own end prints nothing, registers no +// failure, and yields a SMALLER `checked` that both legs below read as a pass. +// Measured on this file: a section that stopped running took the `--self-test` +// verdict from 141 assertions to 119, exited 0, and still claimed in prose that +// the direction it had skipped "REFUSES an empty/renamed/reshaped" table. A +// bare `return` is no better — it yields `undefined` and CRASHES the combine +// below, and an exit code alone reads that crash as a handshake rather than as +// the accident it is. +// +// So each battery sets its own flag as its last act and every caller checks it. +// The return value is load-bearing here (it carries `checked` and `failures`), +// so the handshake is a flag rather than a returned sentinel — the spelling +// `check-durability-degradation-log-level.mjs` and +// `check-dispatcher-error-vocabulary.mjs` carry, for that same reason. +let trapReachedVerdict = false; +let provisioningReachedVerdict = false; +let unreferencedReachedVerdict = false; +let metaCallReachedVerdict = false; +let citationsReachedVerdict = false; + +/** + * One wording, ten call sites — five batteries across the two legs that run + * them. The check, the message and the exit code are the landed ones; only the + * duplication is factored out. + */ +function requireReachedVerdict(name, reached) { + if (reached) return; + console.error( + `\n✗ check-platform-checklist self-test: ${name}() returned without reaching its verdict,\n` + + 'so its assertions did not all run and no failure of theirs could be reported.\n' + + 'Running the gate on top of a self-test that never finished would report an\n' + + 'unverified gate as a verified one.\n', + ); + process.exit(1); +} + /** * The positive control. Proves the extractor reads a good table AND refuses an * empty / renamed / reshaped one, and that the item-side checker catches both @@ -770,6 +809,7 @@ function selfTestTrapVocabulary() { t('C8 an empty-string trap is flagged', trapProblems({ traps: [''] }, vocab).length === 1); t('C9 a trap listed twice on one item is flagged', trapProblems({ traps: ['stale-dist', 'stale-dist'] }, vocab).some((m) => m.includes('twice'))); + trapReachedVerdict = true; return { checked, failures }; } @@ -877,6 +917,7 @@ function selfTestProvisioningUse() { const hintedEmpty = check('qa-contributor-bound-member', 'records-forms'); t('Q20 the same hint reaches an area that has no recipe block of its own', hintedEmpty.length === 1 && hintedEmpty[0].includes('`search:qa-contributor-bound-member`')); + provisioningReachedVerdict = true; return { checked, failures }; } @@ -978,6 +1019,7 @@ function selfTestUnreferencedRecipes() { t('R18 a `$comment` is never reported unreferenced', flag(ALL).length === 0 && none.every((r) => !r.recipe.startsWith('$'))); t('R19 an area that defines no recipes contributes nothing to flag', !none.some((r) => r.area === 'records-forms')); + unreferencedReachedVerdict = true; return { checked, failures }; } @@ -1136,6 +1178,7 @@ export const NEIGHBOURING_MAP: Readonly> = Object.freeze( t('M52 the live map is a bijection-free lookup: no folded spelling is ALSO a canonical singular — so the refusal can never fire on a canonical `/meta/` segment', live.refusal === null && !live.folded.some((f) => live.canonical.includes(f))); + metaCallReachedVerdict = true; return { checked, failures }; } @@ -1216,6 +1259,7 @@ function selfTestSourceLineCitations() { t('S6 an ADR section reference is not a citation', n('ADR-0025 §3.3 and #13479') === 0); t('S7 the README placeholder spelling of the ban is not itself a citation', n('never pin `file.ts:NNN` or a bare `:NNN`') === 0); + citationsReachedVerdict = true; return { failures, checked }; } @@ -1225,6 +1269,11 @@ if (process.argv.slice(2).includes('--self-test')) { const unref = selfTestUnreferencedRecipes(); const metaCall = selfTestMetaCallSpelling(); const cites = selfTestSourceLineCitations(); + requireReachedVerdict('selfTestTrapVocabulary', trapReachedVerdict); + requireReachedVerdict('selfTestProvisioningUse', provisioningReachedVerdict); + requireReachedVerdict('selfTestUnreferencedRecipes', unreferencedReachedVerdict); + requireReachedVerdict('selfTestMetaCallSpelling', metaCallReachedVerdict); + requireReachedVerdict('selfTestSourceLineCitations', citationsReachedVerdict); const failures = [...trap.failures, ...prov.failures, ...unref.failures, ...metaCall.failures, ...cites.failures]; if (failures.length === 0) { console.log( @@ -1243,6 +1292,7 @@ if (process.argv.slice(2).includes('--self-test')) { // The extractor's own positive control, before it is trusted with anything. const trapControl = selfTestTrapVocabulary(); +requireReachedVerdict('selfTestTrapVocabulary', trapReachedVerdict); if (trapControl.failures.length) { console.error("check-platform-checklist: the trap-vocabulary extractor's own positive control FAILED — this check cannot be trusted, and a green from it would mean nothing.\n"); for (const f of trapControl.failures) console.error(` ✗ ${f}`); @@ -1252,6 +1302,7 @@ if (trapControl.failures.length) { // Same, for the provisioning resolve: a green from a check that cannot fire is // indistinguishable from the green this gate printed before it existed. const provisioningControl = selfTestProvisioningUse(); +requireReachedVerdict('selfTestProvisioningUse', provisioningReachedVerdict); if (provisioningControl.failures.length) { console.error("check-platform-checklist: the provisioning-resolve check's own positive control FAILED — a `use` that resolves to nothing would pass, which is the exact defect this check was added to close.\n"); for (const f of provisioningControl.failures) console.error(` ✗ ${f}`); @@ -1263,6 +1314,7 @@ if (provisioningControl.failures.length) { // recipes on the real ledger are referenced, so this direction's output is // permanently empty and its green says nothing on its own. const unreferencedControl = selfTestUnreferencedRecipes(); +requireReachedVerdict('selfTestUnreferencedRecipes', unreferencedReachedVerdict); if (unreferencedControl.failures.length) { console.error('check-platform-checklist: the unreferenced-recipe direction\'s own positive control FAILED — a recipe no item references would pass unreported, and because every real recipe IS referenced, nothing else in this gate would ever notice.\n'); for (const f of unreferencedControl.failures) console.error(` ✗ ${f}`); @@ -1275,6 +1327,7 @@ if (unreferencedControl.failures.length) { // subject population is zero — so nothing but this battery can tell a working // direction from a deleted one. const metaCallControl = selfTestMetaCallSpelling(); +requireReachedVerdict('selfTestMetaCallSpelling', metaCallReachedVerdict); if (metaCallControl.failures.length) { console.error("check-platform-checklist: the `/meta` call-spelling refusal's own positive control FAILED — an executable step instructing a folded plural spelling would pass unreported, which is the exact defect this check was added to close.\n"); for (const f of metaCallControl.failures) console.error(` ✗ ${f}`); @@ -1288,6 +1341,7 @@ if (metaCallControl.failures.length) { // from the ledger staying clean — which is precisely the exit-0-by-construction // shape this check was added to end. const citationControl = selfTestSourceLineCitations(); +requireReachedVerdict('selfTestSourceLineCitations', citationsReachedVerdict); if (citationControl.failures.length) { console.error('check-platform-checklist: the source-line-citation refusal\'s own positive control FAILED — a rotting `file:line` pointer would pass unreported, and because the ledger is clean nothing else here would ever notice.\n'); for (const f of citationControl.failures) console.error(` ✗ ${f}`); diff --git a/scripts/pm/dispatch-gates.mjs b/scripts/pm/dispatch-gates.mjs index 76e646120f..d50fd681d8 100644 --- a/scripts/pm/dispatch-gates.mjs +++ b/scripts/pm/dispatch-gates.mjs @@ -10514,6 +10514,23 @@ export function bannerLines({ identity, paths = [], drift = null }) { // the tree, in both directions. // --------------------------------------------------------------------------- +/** + * Returned by `selfTest()` only after its verdict line is printed, and compared + * at the dispatch: a `return` that leaves the function above that line prints + * NOTHING and still exits 0, because the dispatch discarded the result. Measured + * on this file before this guard existed: an early return took the run from + * "1288 cases pass" to zero bytes of output and exit 0 — a self-test that never + * finished, reported as one that passed. + * + * The mechanical probe in `scripts/measure-self-test-floor.mjs` cannot read this + * file (its anchor matches the first `function selfTest() {` in the source, which + * here is a FIXTURE STRING, so the injection lands inside a template literal and + * only ever produces a SyntaxError). That is a limit of the instrument, not a + * property of this file, and it is why the entry is hand-read there. Anchoring an + * early return on the real definition below measures it in one run. + */ +const SELF_TEST_VERDICT = 'dispatch-gates self-test reached its verdict'; + function selfTest() { const cases = []; // Stream the verdict the moment it is decided (#14281) rather than only at @@ -17737,6 +17754,8 @@ function selfTest() { process.exit(1); } console.log(`✓ dispatch-gates self-test: ${cases.length} cases pass.`); + + return SELF_TEST_VERDICT; } // ── CLI ───────────────────────────────────────────────────────────────────── @@ -17791,7 +17810,14 @@ if (invokedDirectly) { const argvPaths = argv.paths; const wantsChanged = process.argv.includes('--changed'); if (process.argv.includes('--self-test')) { - selfTest(); + if (selfTest() !== SELF_TEST_VERDICT) { + console.error( + '\n✗ dispatch-gates self-test: selfTest() returned without reaching its verdict,\n' + + 'so no success line was printed. Exiting 0 here would report a self-test\n' + + 'that never finished as a self-test that passed.\n', + ); + process.exit(1); + } } else if (argv.malformed) { console.error(`dispatch-gates: ${argv.malformed}.`); process.exit(2);