diff --git a/scripts/measure-self-test-floor.mjs b/scripts/measure-self-test-floor.mjs index 151c963d03..b9d6d38dcf 100644 --- a/scripts/measure-self-test-floor.mjs +++ b/scripts/measure-self-test-floor.mjs @@ -603,6 +603,169 @@ export function probeEarlyReturn(absFile, entry, { timeout = 120000, placement = } } +// --------------------------------------------------------------------------- +// Instrument 3 -- WHICH verdict handshake a self-test carries, read from code +// --------------------------------------------------------------------------- + +/** + * The extent of one definition's BODY in masked code: from the `{` the anchor + * lands on to the `}` that closes it, by counting braces. + * + * ⛔ NOT "the first `}` at column 0". That rule is the tree's convention for + * where a top-level definition ENDS, and it is wrong often enough to matter: + * `scripts/check-error-code-casing.mjs` closes an inline arrow argument with + * `});` at column 0 inside `selfTest`, 107 lines before the function really + * ends. Read that way, the flag `selfTest` sets as its last act falls OUTSIDE + * its own body and the file classifies `none` -- measured, three files (also + * `check-optional-error-sink-contract`, `check-org-identifier`), all three + * carrying a perfectly ordinary handshake. + * + * Counting is safe HERE and only here because the text is already masked: every + * brace inside a comment, a string, a template or a regex literal has been + * blanked, so the ones that remain are the ones the parser sees. + */ +export function definitionSpan(code, name) { + const pats = [ + new RegExp( + `^[ \\t]*(?:export\\s+(?:default\\s+)?)?(?:async\\s+)?function\\s+${name}\\s*\\([^)]*\\)` + + `\\s*(?::\\s*[A-Za-z_$][\\w$<>\\[\\]|. ]*\\s*)?\\{`, + 'm', + ), + new RegExp(`^[ \\t]*(?:export\\s+)?const\\s+${name}\\s*=\\s*(?:async\\s*)?\\([^)]*\\)\\s*(?::[^=]*)?=>\\s*\\{`, 'm'), + ]; + for (const re of pats) { + const m = code.match(re); + if (!m) continue; + const at = m.index + m[0].length; + let depth = 1; + for (let i = at; i < code.length; i++) { + if (code[i] === '{') depth++; + else if (code[i] === '}' && --depth === 0) return { at, end: i }; + } + } + return null; +} + +/** + * SENTINEL -- the self-test's RETURN VALUE is compared against a NAMED operand: + * `if (selfTest() !== SELF_TEST_VERDICT)`, `if ((await selfTest()) !== ...)`. + * + * BOUNDARY -- the operand must be an IDENTIFIER. A comparison against a LITERAL + * is the ACCIDENT shape this file's header is about: over a self-test that + * returned early, `runSelfTest() === 0` is `undefined === 0` -> false -> exit 1, + * having printed ZERO BYTES. Nothing noticed anything; the arithmetic of a + * comparison against a missing return value did it, and calling that a handshake + * is the mistake the DEFEATED/HELD/ACCIDENT verdict exists to refuse. The word + * literals are excluded for the same reason they are literals. + * + * ⚠️ So `selfTest() !== undefined` would read `none` here, and no file in this + * tree spells it that way today. Left unadmitted rather than written blind -- + * the same protocol the DISPATCH criterion publishes for its two unwitnessed + * spellings: widen with a control in both directions, and publish the delta. + */ +const HANDSHAKE_RETURN_COMPARED = + /(?:await\s+)?\(?\s*(?:await\s+)?([A-Za-z_$][\w$]*)\s*\(\s*\)\s*\)?\s*(?:!==|===|!=|==)\s*([A-Za-z_$][\w$]*)\b/g; +const LITERAL_OPERAND = /^(?:undefined|null|true|false|NaN)$/; + +/** + * The FLAG and HELPER shapes share one carrier and differ only in WHO reads it. + * + * The carrier is a variable that CROSSES THE BOUNDARY out of the self-test: a + * module-level binding, assigned `true` INSIDE the self-test's body as its last + * act, and read OUTSIDE it by the dispatch. Both halves are structural, and + * both are load-bearing: + * + * MODULE-LEVEL a `let`/`var` at column 0. A binding declared inside the + * self-test cannot outlive the call, so it can carry nothing -- + * and the ordinary accumulator `let ok = true` is exactly that. + * Without this half `scripts/check-regen-pending.mjs` reads + * `flag`, on an `ok` set inside `decisionTableSelfTest` and an + * unrelated `(ok) => !ok` arrow PARAMETER 60 lines later. There + * is no scope analysis here; the column-0 declaration is what + * stands in for one. + * CROSSES set inside the body, read outside it. A boolean set and read + * within one function is a local decision, not a handshake. + * + * Then the two shapes: + * + * FLAG the DISPATCH reads it itself, negated -- `if (!selfTestReachedVerdict)`. + * HELPER the dispatch HANDS it to a callee that refuses on its behalf -- + * `requireReachedVerdict('selfTest', selfTestReachedVerdict)`. The + * callee must be defined in this file and its body must PRODUCE A + * FAILURE, which is what separates a refusal from any other function + * that happens to take a boolean. + * + * ⭐ The helper is therefore not a third mechanism -- it is the FLAG with its + * refusal factored out of ten inlined copies, which is exactly the merit the + * ruling that admitted it turned on. Recognised as its own shape because the + * census question is "which spelling is this file written in", and answering it + * is this column's whole job. + */ +const MODULE_LEVEL_BINDING = /^(?:let|var)\s+([A-Za-z_$][\w$]*)/gm; +const SET_TRUE = /([A-Za-z_$][\w$]*)\s*=\s*true\b/g; +const negatedRead = (name) => new RegExp(String.raw`!\s*${name}\b`, 'g'); +const handedToCall = (name) => new RegExp(String.raw`([A-Za-z_$][\w$]*)\s*\(\s*[^()]*?\b${name}\s*\)`, 'g'); + +/** + * ⭐ THE ONE RECOGNISER (#14968). Every caller -- `--json`, the human census + * column, the per-shape summary -- reads THIS, so a fifth landed shape is a + * change here and nowhere else. That is the whole point: the repair had landed + * in three spellings, every handshake question was answered by a hand-written + * grep, and three seats in one shift got three different wrong answers from + * three different greps whose completeness nobody could check. + * + * Returns `'sentinel' | 'flag' | 'helper' | 'none'`, read from the MASKED, + * line-anchored source -- the same text `selfTestDefs` and the injection anchor + * read. Masked, because a spelling quoted inside a fixture string or described + * in a docblock is not a handshake the dispatch can perform; this file is full + * of both, and so are the gates that reason about self-tests. + * + * ⛔ NOT keyed on the three landed NAMES. `SELF_TEST_VERDICT`, + * `selfTestReachedVerdict` and `requireReachedVerdict` appear nowhere in this + * function: what it reads is the value handed back and the comparison that + * consumes it. `classifyFloor` keyed on the NAME `SELF_TEST_BATTERIES` once and + * called a fixture floored after its roster had been removed; the control below + * renames every landed spelling out of a fixture and requires the same verdict. + * + * ORDER: sentinel, then helper, then flag. Measured on this base: NO file in the + * census carries two shapes, so the order decides nothing today -- the live + * check below publishes that overlap as a number rather than leaving it assumed. + * + * ⛔ It recognises the shapes; it does not legislate them. Two of the three are + * deliberate -- the flag exists because those self-tests' own exit codes are + * load-bearing, so the handshake cannot BE the return value -- and unifying the + * tree is explicitly not this instrument's business. + */ +export function classifyHandshake(src) { + const code = maskCommentsAndLiterals(src); + + for (const m of code.matchAll(HANDSHAKE_RETURN_COMPARED)) { + if (/self.?test/i.test(m[1]) && !LITERAL_OPERAND.test(m[2])) return 'sentinel'; + } + + const spans = selfTestDefs(src) + .map((name) => definitionSpan(code, name)) + .filter((s) => s !== null); + const insideSelfTest = (at) => spans.some((s) => at >= s.at && at < s.end); + + const moduleLevel = new Set([...code.matchAll(MODULE_LEVEL_BINDING)].map((m) => m[1])); + const carried = new Set(); + for (const m of code.matchAll(SET_TRUE)) { + if (moduleLevel.has(m[1]) && insideSelfTest(m.index)) carried.add(m[1]); + } + + let flag = false; + for (const name of carried) { + for (const m of code.matchAll(handedToCall(name))) { + if (insideSelfTest(m.index) || /self.?test/i.test(m[1])) continue; + const def = definitionSpan(code, m[1]); + if (def && PRODUCES_FAILURE.test(code.slice(def.at, def.end))) return 'helper'; + } + for (const m of code.matchAll(negatedRead(name))) if (!insideSelfTest(m.index)) flag = true; + } + return flag ? 'flag' : 'none'; +} + // --------------------------------------------------------------------------- // The controls -- run on EVERY invocation, before any number is printed // --------------------------------------------------------------------------- @@ -918,6 +1081,69 @@ const MASKED_DEFS_GATE = [ /** What `selfTestDefs` must collect from the fixture above, and in this order. */ const MASKED_DEFS_EXPECTED = ['selfTest', 'runSelfTestTwice']; +/** + * The HANDSHAKE DECOY: both spellings the recogniser reads, standing in the two + * texts the mask blanks -- a `selfTest() !== SELF_TEST_VERDICT` comparison in a + * COMMENT, and the flag's `= true` assignment inside a fixture TEMPLATE within + * the self-test's own body. Neither is a handshake the dispatch can perform, and + * the file's real dispatch is the bare `selfTest();` that notices nothing. + * + * The two decoys are un-hidden INDEPENDENTLY below, which is what makes this a + * reading of the MASK rather than of a fixture whose spellings were never + * recognisable: strip the comment marker and the same text reads `sentinel`; + * strip the template delimiters and it reads `flag`. Masked, both are gone and + * the answer is `none`. + */ +const HANDSHAKE_DECOY_GATE = [ + '#!/usr/bin/env node', + "const SELF_TEST_VERDICT = 'reached';", + 'let selfTestReachedVerdict = false;', + '// if (selfTest() !== SELF_TEST_VERDICT) process.exit(1);', + 'function selfTest() {', + ' const FIXTURE = `', + ' selfTestReachedVerdict = true;', + ' `;', + " if (FIXTURE.length < 1) { console.error('the fixture text went missing'); process.exit(1); }", + " console.log('fixture self-test: 1 case passes');", + '}', + "if (process.argv.includes('--self-test')) {", + ' selfTest();', + ' if (!selfTestReachedVerdict) { console.error(\'no verdict\'); process.exit(1); }', + '}', + '', +].join('\n'); + +/** The two decoy texts, each looked up where it is hidden. */ +const DECOY_SENTINEL_TEXT = 'selfTest() !== SELF_TEST_VERDICT'; +const DECOY_FLAG_TEXT = 'selfTestReachedVerdict = true;'; + +/** + * The COLUMN-ZERO BRACE gate, reduced: an inline arrow argument whose `});` + * closes at column 0 INSIDE the self-test, ahead of the line that sets the + * handshake flag. This is the shape `scripts/check-error-code-casing.mjs` + * carries -- and `check-optional-error-sink-contract.mjs` and + * `check-org-identifier.mjs` with it. Read with a "body ends at the first `}` at + * column 0" rule, the flag assignment falls outside its own function's body, the + * carrier never crosses anything, and all three ordinary handshakes read `none`. + * `definitionSpan` counts braces over masked text instead; this fixture is why. + */ +const EARLY_BRACE_GATE = [ + '#!/usr/bin/env node', + 'let selfTestReachedVerdict = false;', + 'const check = (fn) => fn();', + 'function selfTest() {', + ' check(() => {', + " console.log('fixture self-test: 1 case passes');", + '});', + ' selfTestReachedVerdict = true;', + '}', + "if (process.argv.includes('--self-test')) {", + ' selfTest();', + " if (!selfTestReachedVerdict) { console.error('no verdict'); process.exit(1); }", + '}', + '', +].join('\n'); + /** * The SLOW gate, reduced: a self-test that outlasts the budget it is probed * under. It sleeps rather than spins -- a control that runs on EVERY invocation @@ -1177,6 +1403,78 @@ export function runControls() { `LEDGER ROW INVALID: ${file} carries a field this reader does not read (${fields.join(', ')}); a misspelled \`timeoutMs\` is not a smaller budget, it is NO budget -- the default, and the SIGTERM this field exists to end`); } + // Instrument 3 -- WHICH handshake, one fixture per landed shape and both + // directions on each. What is at stake is the question the card was filed + // about: every handshake census in this family was a hand-written grep for one + // of three spellings, and three of them in one shift gave three wrong answers. + say(classifyHandshake(SOUND_GATE) === 'sentinel', + `HANDSHAKE CONTROL FAILED: a dispatch comparing the self-test's RETURN VALUE against a named verdict read ${classifyHandshake(SOUND_GATE)}, not sentinel`); + say(classifyHandshake(WALKING_GATE) === 'flag', + `HANDSHAKE CONTROL FAILED: a module-level flag set inside the self-test and read negated at the dispatch read ${classifyHandshake(WALKING_GATE)}, not flag`); + say(classifyHandshake(HELPER_HANDSHAKE_GATE) === 'helper', + `HANDSHAKE CONTROL FAILED: a flag handed to a refusing helper read ${classifyHandshake(HELPER_HANDSHAKE_GATE)}, not helper`); + say(classifyHandshake(HOLED_GATE) === 'none', + `HANDSHAKE CONTROL FAILED: a bare \`selfTest();\` dispatch that discards the completion read ${classifyHandshake(HOLED_GATE)}, not none`); + // ... and the pair that makes `helper` a reading of the HANDSHAKE rather than + // of the file: the SAME fixture with only the `requireReachedVerdict` call + // deleted. One line is the whole difference, and it is the handshake. + say(classifyHandshake(HELPER_HANDSHAKE_GATE_HOLED) === 'none', + `HANDSHAKE CONTROL FAILED: the helper fixture with only its handshake CALL deleted still read ${classifyHandshake(HELPER_HANDSHAKE_GATE_HOLED)}; the flag is still declared and still set, so anything but none is the NAME being read and not the mechanism`); + // The ACCIDENT boundary, which is the sharpest thing this recogniser has to + // get right: `runSelfTest() === 0` IS a comparison of the return value, and it + // is NOT a handshake -- an early return makes it `undefined === 0`, exit 1, + // ZERO bytes printed, nothing noticed. Reading it sentinel would publish the + // exact accident-versus-handshake confusion this file exists to expose. + say(classifyHandshake(ACCIDENT_GATE) === 'none', + `HANDSHAKE CONTROL FAILED: a return value compared against a LITERAL read ${classifyHandshake(ACCIDENT_GATE)}; that is the ACCIDENT shape, where a comparison against a missing return value exits non-zero having printed nothing`); + say(classifyHandshake(TERNARY_EXIT_GATE) === 'none', + `HANDSHAKE CONTROL FAILED: a bare \`process.exit( ? 0 : 1)\` dispatch read ${classifyHandshake(TERNARY_EXIT_GATE)}; nothing there compares the completion against anything`); + // ⭐ NAME-INDEPENDENCE, the direction the card is about. Every landed spelling + // is renamed out of all three fixtures -- `SELF_TEST_VERDICT`, + // `selfTestReachedVerdict`, `requireReachedVerdict` -- and the verdicts must + // not move. A recogniser keyed on those names passes every control above and + // fails all three of these; `classifyFloor` keyed on the NAME + // `SELF_TEST_BATTERIES` once and called a fixture floored with its roster gone. + const renamed = (fixture) => fixture + .replaceAll('SELF_TEST_VERDICT', 'FIXTURE_COMPLETION_TOKEN') + .replaceAll('VERDICT', 'COMPLETION_TOKEN') + .replaceAll('selfTestReachedVerdict', 'batteriesAllRan') + .replaceAll('requireReachedVerdict', 'insistTheyRan'); + say(classifyHandshake(renamed(SOUND_GATE)) === 'sentinel', + 'NAME-INDEPENDENCE CONTROL FAILED: the sentinel fixture stopped reading sentinel once its verdict constant was renamed; the recogniser is keyed on a NAME, which is the defect one level up'); + say(classifyHandshake(renamed(WALKING_GATE)) === 'flag', + 'NAME-INDEPENDENCE CONTROL FAILED: the flag fixture stopped reading flag once `selfTestReachedVerdict` was renamed; the recogniser is keyed on a NAME'); + say(classifyHandshake(renamed(HELPER_HANDSHAKE_GATE)) === 'helper', + 'NAME-INDEPENDENCE CONTROL FAILED: the helper fixture stopped reading helper once `requireReachedVerdict` was renamed; the recogniser is keyed on a NAME'); + + // The DECOY, and the fixture-validity guards without which it says nothing: a + // decoy that is not comment or literal content is testing something else. + const hsFlags = scanSource(HANDSHAKE_DECOY_GATE); + const sentinelDecoy = HANDSHAKE_DECOY_GATE.indexOf(DECOY_SENTINEL_TEXT); + const flagDecoy = HANDSHAKE_DECOY_GATE.indexOf(DECOY_FLAG_TEXT); + say(sentinelDecoy >= 0 && hsFlags.comment[sentinelDecoy] === 1, + 'CONTROL FIXTURE INVALID: the sentinel decoy is not COMMENT content, so the decoy verdict below says nothing about comments being masked away'); + say(flagDecoy >= 0 && hsFlags.literal[flagDecoy] === 1, + 'CONTROL FIXTURE INVALID: the flag decoy is not LITERAL content, so the decoy verdict below says nothing about fixture STRINGS being masked away'); + say(classifyHandshake(HANDSHAKE_DECOY_GATE) === 'none', + `HANDSHAKE DECOY CONTROL FAILED: a handshake spelled only inside a comment and a fixture template was recognised as ${classifyHandshake(HANDSHAKE_DECOY_GATE)}; a census reading raw text credits every gate that merely QUOTES the repair with having it`); + // ... and both decoys un-hidden INDEPENDENTLY, which is what makes the verdict + // above a reading of the mask rather than of two texts that were never + // recognisable in the first place. + say(classifyHandshake(HANDSHAKE_DECOY_GATE.replace('// ', '')) === 'sentinel', + 'CONTROL FIXTURE INVALID: with its comment marker removed the sentinel decoy is still not recognised, so it was never a handshake spelling and the decoy verdict passes for the wrong reason'); + say(classifyHandshake(HANDSHAKE_DECOY_GATE.replaceAll('`', '')) === 'flag', + 'CONTROL FIXTURE INVALID: with the template delimiters removed the flag decoy is still not recognised, so it was never a handshake spelling and the decoy verdict passes for the wrong reason'); + + // The BODY EXTENT, on the shape that made three live files read `none`: an + // inline arrow argument closing with `});` at column 0 inside the self-test, + // ahead of the flag assignment. + const earlyBraceLines = EARLY_BRACE_GATE.split('\n'); + say(earlyBraceLines.findIndex((l) => l.startsWith('}')) < earlyBraceLines.findIndex((l) => l.includes('selfTestReachedVerdict = true')), + 'CONTROL FIXTURE INVALID: the fixture no longer closes a brace at column 0 AHEAD of its flag assignment, so a first-column-brace body rule would reach the assignment anyway and the verdict below tests nothing'); + say(classifyHandshake(EARLY_BRACE_GATE) === 'flag', + `BODY EXTENT CONTROL FAILED: a self-test whose body contains a column-0 \`});\` read ${classifyHandshake(EARLY_BRACE_GATE)}; its flag is set INSIDE the function and the body extent is what says so -- three live files carry exactly this and read none without brace counting`); + // Instrument 1, both directions. say(classifyFloor(maskComments(HOLED_GATE)) === 'NONE', 'POSITIVE CONTROL FAILED: a self-test deciding success by failures.length alone was not classified NONE'); @@ -1534,12 +1832,76 @@ export function population() { if (!DISPATCH.test(code)) continue; const file = abs.slice(ROOT.length + 1).split(sep).join('/'); if (file === CENSUS_SELF) continue; - rows.push({ file, abs, floor: classifyFloor(code), defs: selfTestDefs(src) }); + rows.push({ file, abs, floor: classifyFloor(code), defs: selfTestDefs(src), handshake: classifyHandshake(src) }); } if (rows.length === 0) throw new Error('the census found no self-test dispatch at all -- refusing rather than reporting zero'); return rows; } +/** + * The three LANDED NAMES, kept as the census's SECOND OPINION and never as its + * answer. Each is the grep a seat actually wrote when it needed this reading. + * + * ⛔ This is not a fallback recogniser and nothing reads it for a verdict. It + * exists so the live check below can ask the completeness question the card was + * filed about -- "does the population carrying a handshake match the population + * the recogniser can see?" -- against a reading that shares NO code with the + * recogniser. Agreement is the evidence; the moment they disagree, one of the + * two is incomplete and a census printed from either is a number nobody can + * check. Measured on this base: they agree on all 181 rows, file for file. + * + * A FIFTH shape therefore lands in TWO places -- here and in the recogniser -- + * and that is deliberate. A shape only the recogniser knows would silently make + * this control weaker than the thing it controls. + */ +const LANDED_HANDSHAKE_NAMES = [ + /!==\s*[A-Za-z_$][\w$]*VERDICT|===\s*[A-Za-z_$][\w$]*VERDICT/, + /[A-Za-z_$][\w$]*ReachedVerdict/, + /requireReachedVerdict\s*\(/, +]; + +/** + * THE LIVE COMPLETENESS CHECK, over the real census rather than over fixtures. + * + * ⛔ Deliberately NOT in `runControls()`, whose whole invariant is that nothing + * there reads the repo, so a control failure is always the instrument. This one + * reads the tree, and it can fail because of what LANDED in it -- which is + * exactly what it is for. It refuses rather than degrading: a census that quietly + * reports the smaller of two disagreeing populations commits the defect this + * instrument exists to measure, one level up. + * + * The card's own words for why the assertion is not enough: "a hand-written grep + * whose completeness nobody can check". This is the check. + */ +export function handshakeCensusFailures(rows) { + const failures = []; + const seenBy = { recogniser: [], names: [] }; + for (const r of rows) { + const code = maskCommentsAndLiterals(readFileSync(r.abs, 'utf8')); + if (r.handshake !== 'none') seenBy.recogniser.push(r.file); + if (LANDED_HANDSHAKE_NAMES.some((re) => re.test(code))) seenBy.names.push(r.file); + } + const byName = new Set(seenBy.names); + const byShape = new Set(seenBy.recogniser); + const recogniserOnly = seenBy.recogniser.filter((f) => !byName.has(f)); + const namesOnly = seenBy.names.filter((f) => !byShape.has(f)); + if (recogniserOnly.length > 0) { + failures.push( + `${recogniserOnly.length} file(s) carry a shape the recogniser reads but no LANDED SPELLING knows: ` + + `${recogniserOnly.join(', ')}. A new shape landed and the second opinion was not widened with it, ` + + 'so this census no longer has one.', + ); + } + if (namesOnly.length > 0) { + failures.push( + `${namesOnly.length} file(s) carry a landed handshake spelling the recogniser reads as \`none\`: ` + + `${namesOnly.join(', ')}. The column would report those gates as having NO handshake -- the ` + + 'exact wrong reading, in the flattering direction, that #14968 was filed about.', + ); + } + return failures; +} + function main() { const controlFailures = runControls(); if (controlFailures.length > 0) { @@ -1551,6 +1913,14 @@ function main() { } const rows = population(); + const censusFailures = handshakeCensusFailures(rows); + if (censusFailures.length > 0) { + console.error('measure-self-test-floor: THE HANDSHAKE CENSUS IS NOT COMPLETE -- no census printed.\n'); + for (const f of censusFailures) console.error(` - ${f}`); + console.error('\nThe recogniser and the landed spellings disagree about WHICH files carry a handshake.'); + console.error('Widen `classifyHandshake` and `LANDED_HANDSHAKE_NAMES` together, with a control on each.\n'); + process.exit(1); + } const wantProbe = process.argv.includes('--probe'); if (wantProbe) { for (const r of rows) { @@ -1575,6 +1945,18 @@ function main() { if (byFloor.ROSTER.length) console.log(` roster: ${byFloor.ROSTER.join(', ')}`); if (byFloor.COUNT.length) console.log(` count candidates: ${byFloor.COUNT.join(', ')}`); + const byShape = { sentinel: 0, flag: 0, helper: 0, none: 0 }; + for (const r of rows) byShape[r.handshake]++; + console.log('\nHandshake SHAPE -- which verdict handshake each file carries, read from CODE (#14968):'); + console.log( + ` ${byShape.sentinel} sentinel, ${byShape.flag} flag, ${byShape.helper} helper, ` + + `${byShape.none} none, of ${rows.length}.`, + ); + console.log(' ⛔ The shapes are RECOGNISED, not legislated: the flag form exists because those'); + console.log(' self-tests\' own exit codes are load-bearing, so the handshake cannot BE the'); + console.log(' return value. This column answers "which spelling", never "which is right".'); + for (const r of rows) console.log(` ${r.handshake.padEnd(8)} ${r.floor.padEnd(6)} ${r.file}`); + if (!wantProbe) { console.log('\nHole 2 -- no verdict handshake: NOT MEASURED (pass --probe; it runs every self-test twice).'); return;