Skip to content

Commit b12a5bd

Browse files
committed
fix(devx): verdict handshake for check-platform-checklist and dispatch-gates self-tests
Both dispatches discarded their self-test's completion, so a `return` above the verdict printed nothing and still exited 0 — an unfinished self-test reported as a verified gate. Measured before the repair, on this tree: * `scripts/pm/dispatch-gates.mjs` — an early return at the real definition of `selfTest()` took the run from "1288 cases pass" to ZERO bytes of output and exit 0. The card carried this row as NOT MEASURED because the mechanical probe anchors on the first `function selfTest() {` in the source, which here is a fixture STRING: the injection lands inside a template literal and only ever yields a SyntaxError. That is a limit of the instrument, not of the file; a hand-read anchor measures it in one run. * `scripts/check-platform-checklist.mjs` — five batteries returning `{ checked, failures }` and combined by the caller. A section that stops running took the 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` instead yields `undefined` and CRASHES the combine — an exit code alone reads that crash as a handshake rather than as the accident it is. dispatch-gates takes the landed sentinel form. check-platform-checklist takes the landed FLAG form (`check-durability-degradation-log-level.mjs`, `check-dispatcher-error-vocabulary.mjs`) for the reason those files state: the return value is load-bearing, so the handshake cannot BE the return value. Its guard is checked on both legs — including the inline one, which is the only leg CI reaches — before the `.failures` read, so a bare `return` now reports itself instead of crashing its reader. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
1 parent ca3fd4b commit b12a5bd

2 files changed

Lines changed: 81 additions & 1 deletion

File tree

scripts/check-platform-checklist.mjs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -704,6 +704,45 @@ function foldedCallMessage(path, call, spelling, singular) {
704704
);
705705
}
706706

707+
// ── Self-test verdict handshake ─────────────────────────────────────────────
708+
//
709+
// Five batteries, each returning `{ checked, failures }` for a caller to
710+
// report. A `return` above a battery's own end prints nothing, registers no
711+
// failure, and yields a SMALLER `checked` that both legs below read as a pass.
712+
// Measured on this file: a section that stopped running took the `--self-test`
713+
// verdict from 141 assertions to 119, exited 0, and still claimed in prose that
714+
// the direction it had skipped "REFUSES an empty/renamed/reshaped" table. A
715+
// bare `return` is no better — it yields `undefined` and CRASHES the combine
716+
// below, and an exit code alone reads that crash as a handshake rather than as
717+
// the accident it is.
718+
//
719+
// So each battery sets its own flag as its last act and every caller checks it.
720+
// The return value is load-bearing here (it carries `checked` and `failures`),
721+
// so the handshake is a flag rather than a returned sentinel — the spelling
722+
// `check-durability-degradation-log-level.mjs` and
723+
// `check-dispatcher-error-vocabulary.mjs` carry, for that same reason.
724+
let trapReachedVerdict = false;
725+
let provisioningReachedVerdict = false;
726+
let unreferencedReachedVerdict = false;
727+
let metaCallReachedVerdict = false;
728+
let citationsReachedVerdict = false;
729+
730+
/**
731+
* One wording, ten call sites — five batteries across the two legs that run
732+
* them. The check, the message and the exit code are the landed ones; only the
733+
* duplication is factored out.
734+
*/
735+
function requireReachedVerdict(name, reached) {
736+
if (reached) return;
737+
console.error(
738+
`\n✗ check-platform-checklist self-test: ${name}() returned without reaching its verdict,\n`
739+
+ 'so its assertions did not all run and no failure of theirs could be reported.\n'
740+
+ 'Running the gate on top of a self-test that never finished would report an\n'
741+
+ 'unverified gate as a verified one.\n',
742+
);
743+
process.exit(1);
744+
}
745+
707746
/**
708747
* The positive control. Proves the extractor reads a good table AND refuses an
709748
* empty / renamed / reshaped one, and that the item-side checker catches both
@@ -770,6 +809,7 @@ function selfTestTrapVocabulary() {
770809
t('C8 an empty-string trap is flagged', trapProblems({ traps: [''] }, vocab).length === 1);
771810
t('C9 a trap listed twice on one item is flagged', trapProblems({ traps: ['stale-dist', 'stale-dist'] }, vocab).some((m) => m.includes('twice')));
772811

812+
trapReachedVerdict = true;
773813
return { checked, failures };
774814
}
775815

@@ -877,6 +917,7 @@ function selfTestProvisioningUse() {
877917
const hintedEmpty = check('qa-contributor-bound-member', 'records-forms');
878918
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`'));
879919

920+
provisioningReachedVerdict = true;
880921
return { checked, failures };
881922
}
882923

@@ -978,6 +1019,7 @@ function selfTestUnreferencedRecipes() {
9781019
t('R18 a `$comment` is never reported unreferenced', flag(ALL).length === 0 && none.every((r) => !r.recipe.startsWith('$')));
9791020
t('R19 an area that defines no recipes contributes nothing to flag', !none.some((r) => r.area === 'records-forms'));
9801021

1022+
unreferencedReachedVerdict = true;
9811023
return { checked, failures };
9821024
}
9831025

@@ -1136,6 +1178,7 @@ export const NEIGHBOURING_MAP: Readonly<Record<string, string>> = Object.freeze(
11361178
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/<type>` segment',
11371179
live.refusal === null && !live.folded.some((f) => live.canonical.includes(f)));
11381180

1181+
metaCallReachedVerdict = true;
11391182
return { checked, failures };
11401183
}
11411184

@@ -1216,6 +1259,7 @@ function selfTestSourceLineCitations() {
12161259
t('S6 an ADR section reference is not a citation', n('ADR-0025 §3.3 and #13479') === 0);
12171260
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);
12181261

1262+
citationsReachedVerdict = true;
12191263
return { failures, checked };
12201264
}
12211265

@@ -1225,6 +1269,11 @@ if (process.argv.slice(2).includes('--self-test')) {
12251269
const unref = selfTestUnreferencedRecipes();
12261270
const metaCall = selfTestMetaCallSpelling();
12271271
const cites = selfTestSourceLineCitations();
1272+
requireReachedVerdict('selfTestTrapVocabulary', trapReachedVerdict);
1273+
requireReachedVerdict('selfTestProvisioningUse', provisioningReachedVerdict);
1274+
requireReachedVerdict('selfTestUnreferencedRecipes', unreferencedReachedVerdict);
1275+
requireReachedVerdict('selfTestMetaCallSpelling', metaCallReachedVerdict);
1276+
requireReachedVerdict('selfTestSourceLineCitations', citationsReachedVerdict);
12281277
const failures = [...trap.failures, ...prov.failures, ...unref.failures, ...metaCall.failures, ...cites.failures];
12291278
if (failures.length === 0) {
12301279
console.log(
@@ -1243,6 +1292,7 @@ if (process.argv.slice(2).includes('--self-test')) {
12431292

12441293
// The extractor's own positive control, before it is trusted with anything.
12451294
const trapControl = selfTestTrapVocabulary();
1295+
requireReachedVerdict('selfTestTrapVocabulary', trapReachedVerdict);
12461296
if (trapControl.failures.length) {
12471297
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");
12481298
for (const f of trapControl.failures) console.error(` ✗ ${f}`);
@@ -1252,6 +1302,7 @@ if (trapControl.failures.length) {
12521302
// Same, for the provisioning resolve: a green from a check that cannot fire is
12531303
// indistinguishable from the green this gate printed before it existed.
12541304
const provisioningControl = selfTestProvisioningUse();
1305+
requireReachedVerdict('selfTestProvisioningUse', provisioningReachedVerdict);
12551306
if (provisioningControl.failures.length) {
12561307
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");
12571308
for (const f of provisioningControl.failures) console.error(` ✗ ${f}`);
@@ -1263,6 +1314,7 @@ if (provisioningControl.failures.length) {
12631314
// recipes on the real ledger are referenced, so this direction's output is
12641315
// permanently empty and its green says nothing on its own.
12651316
const unreferencedControl = selfTestUnreferencedRecipes();
1317+
requireReachedVerdict('selfTestUnreferencedRecipes', unreferencedReachedVerdict);
12661318
if (unreferencedControl.failures.length) {
12671319
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');
12681320
for (const f of unreferencedControl.failures) console.error(` ✗ ${f}`);
@@ -1275,6 +1327,7 @@ if (unreferencedControl.failures.length) {
12751327
// subject population is zero — so nothing but this battery can tell a working
12761328
// direction from a deleted one.
12771329
const metaCallControl = selfTestMetaCallSpelling();
1330+
requireReachedVerdict('selfTestMetaCallSpelling', metaCallReachedVerdict);
12781331
if (metaCallControl.failures.length) {
12791332
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");
12801333
for (const f of metaCallControl.failures) console.error(` ✗ ${f}`);
@@ -1288,6 +1341,7 @@ if (metaCallControl.failures.length) {
12881341
// from the ledger staying clean — which is precisely the exit-0-by-construction
12891342
// shape this check was added to end.
12901343
const citationControl = selfTestSourceLineCitations();
1344+
requireReachedVerdict('selfTestSourceLineCitations', citationsReachedVerdict);
12911345
if (citationControl.failures.length) {
12921346
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');
12931347
for (const f of citationControl.failures) console.error(` ✗ ${f}`);

scripts/pm/dispatch-gates.mjs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10514,6 +10514,23 @@ export function bannerLines({ identity, paths = [], drift = null }) {
1051410514
// the tree, in both directions.
1051510515
// ---------------------------------------------------------------------------
1051610516

10517+
/**
10518+
* Returned by `selfTest()` only after its verdict line is printed, and compared
10519+
* at the dispatch: a `return` that leaves the function above that line prints
10520+
* NOTHING and still exits 0, because the dispatch discarded the result. Measured
10521+
* on this file before this guard existed: an early return took the run from
10522+
* "1288 cases pass" to zero bytes of output and exit 0 — a self-test that never
10523+
* finished, reported as one that passed.
10524+
*
10525+
* The mechanical probe in `scripts/measure-self-test-floor.mjs` cannot read this
10526+
* file (its anchor matches the first `function selfTest() {` in the source, which
10527+
* here is a FIXTURE STRING, so the injection lands inside a template literal and
10528+
* only ever produces a SyntaxError). That is a limit of the instrument, not a
10529+
* property of this file, and it is why the entry is hand-read there. Anchoring an
10530+
* early return on the real definition below measures it in one run.
10531+
*/
10532+
const SELF_TEST_VERDICT = 'dispatch-gates self-test reached its verdict';
10533+
1051710534
function selfTest() {
1051810535
const cases = [];
1051910536
// Stream the verdict the moment it is decided (#14281) rather than only at
@@ -17737,6 +17754,8 @@ function selfTest() {
1773717754
process.exit(1);
1773817755
}
1773917756
console.log(`✓ dispatch-gates self-test: ${cases.length} cases pass.`);
17757+
17758+
return SELF_TEST_VERDICT;
1774017759
}
1774117760

1774217761
// ── CLI ─────────────────────────────────────────────────────────────────────
@@ -17791,7 +17810,14 @@ if (invokedDirectly) {
1779117810
const argvPaths = argv.paths;
1779217811
const wantsChanged = process.argv.includes('--changed');
1779317812
if (process.argv.includes('--self-test')) {
17794-
selfTest();
17813+
if (selfTest() !== SELF_TEST_VERDICT) {
17814+
console.error(
17815+
'\n✗ dispatch-gates self-test: selfTest() returned without reaching its verdict,\n'
17816+
+ 'so no success line was printed. Exiting 0 here would report a self-test\n'
17817+
+ 'that never finished as a self-test that passed.\n',
17818+
);
17819+
process.exit(1);
17820+
}
1779517821
} else if (argv.malformed) {
1779617822
console.error(`dispatch-gates: ${argv.malformed}.`);
1779717823
process.exit(2);

0 commit comments

Comments
 (0)