Skip to content

Commit 5b2ad1b

Browse files
claude[bot]claude
andauthored
fix(scripts): measure-self-test-floor's probe awards HELD only on a printed handshake (#15372)
`probeEarlyReturn` decided its verdict on the mutated run's exit code alone (`mut.status === 0 ? 'DEFEATED' : 'HELD'`), so a dispatch spelled `process.exit(runSelfTest() === 0 ? 0 : 1)` scored HELD: the early return makes `runSelfTest()` yield `undefined`, `undefined === 0` is false, and the process exits 1 having printed ZERO BYTES. Nothing noticed anything. The row already carried `mutatedBytes` and `mutatedHead`; the verdict just did not read them. A non-zero exit is now a hold only when the mutated run also SPOKE (printed a non-blank line — the same reading `mutatedHead` publishes and the HELD listing quotes). The silent case gets its own verdict, ACCIDENT, tallied and listed apart so the census never counts it among the holds. The verdict deliberately does not match refusal WORDING: the repair landed in three spellings and teaching this verdict any of them is the coupling #14968 is filed to remove. Additive only: `mutatedSpoke` joins the row, no `--json` key is renamed, and `classifyFloor` is untouched (ROSTER/COUNT/NONE are identical). The instrument ships no `--self-test`; its controls run inline on every invocation. A third fixture joins them — the measured accident shape — asserted to read ACCIDENT with a non-zero exit and zero bytes, plus the other direction: the handshake-protected fixture must be one that SPOKE when defeated. Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk Co-authored-by: Claude <noreply@anthropic.com>
1 parent a623a15 commit 5b2ad1b

1 file changed

Lines changed: 78 additions & 7 deletions

File tree

scripts/measure-self-test-floor.mjs

Lines changed: 78 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,35 @@
4141
* `undefined`, and `process.exit(undefined)` is exit 0), through
4242
* `selfTest(); main();`, or through a top-level block with no callee at all.
4343
* So hole 2 is decided by BEHAVIOUR: inject `return;` as the first statement of
44-
* the function the dispatch calls, run it, read the exit code. Exit 0 is the
45-
* defect. Hole 1 has no equally generic mutation -- a battery is not a
46-
* mechanically identifiable unit across 158 differently shaped self-tests -- so
47-
* it is decided by a published static criterion instead, stated below.
44+
* the function the dispatch calls, run it, and read what it SAYS as well as what
45+
* it exits. Exit 0 is the defect. Hole 1 has no equally generic mutation --
46+
* a battery is not a mechanically identifiable unit across 158 differently shaped
47+
* self-tests -- so it is decided by a published static criterion instead, stated
48+
* below.
49+
*
50+
* ## A non-zero exit is NOT a handshake: DEFEATED / HELD / ACCIDENT
51+
*
52+
* A handshake is a gate NOTICING that its self-test left early and SAYING so. An
53+
* exit code alone cannot tell that apart from an accident: a dispatch spelled
54+
* `process.exit(runSelfTest() === 0 ? 0 : 1)` turns the early return's `undefined`
55+
* into `undefined === 0` -> false -> exit 1, having printed ZERO BYTES. Nothing
56+
* detected anything; the arithmetic of a comparison against a missing return value
57+
* did it. Scoring that HELD is the same accident-versus-handshake mistake this
58+
* instrument exists to expose, made by the instrument itself -- and it inflates
59+
* exactly the completion picture a green `--probe` sweep is quoted for.
60+
*
61+
* So the mutated run must also SPEAK, and the verdict is three-valued:
62+
*
63+
* DEFEATED exit 0 -- the early return went unnoticed.
64+
* HELD exit != 0 AND the mutated run printed a non-blank line.
65+
* ACCIDENT exit != 0 AND it printed nothing -- a non-zero exit with no
66+
* refusal behind it. NOT counted among HELD, ever.
67+
*
68+
* The `mutatedBytes` / `mutatedHead` fields the row already carried are what this
69+
* reads; `mutatedSpoke` publishes the reading. Deliberately the verdict does NOT
70+
* match the refusal WORDING: the repair landed in three spellings and teaching
71+
* this verdict any of them is the coupling #14968 is filed to remove. Reporting
72+
* WHICH handshake a file carries is that card's column, not this verdict's job.
4873
*
4974
* ## The controls, which run on EVERY invocation
5075
*
@@ -188,13 +213,20 @@ export function probeEarlyReturn(absFile, entry, { timeout = 120000 } = {}) {
188213
if (baseOut === mutOut && base.status === mut.status) {
189214
return { verdict: 'NOT MEASURED', why: 'mutation had no observable effect' };
190215
}
216+
// Did the mutated run SAY anything? Read as "printed a non-blank line", the
217+
// same reading `mutatedHead` already publishes and quotes -- a run whose whole
218+
// output is a newline has non-zero bytes and still refused nothing.
219+
const mutatedHead = mutOut.split('\n').find((l) => l.trim()) ?? '';
220+
const mutatedSpoke = mutatedHead !== '';
191221
return {
192-
verdict: mut.status === 0 ? 'DEFEATED' : 'HELD',
222+
// Exit code alone cannot tell a refusal from an accident -- see the header.
223+
verdict: mut.status === 0 ? 'DEFEATED' : mutatedSpoke ? 'HELD' : 'ACCIDENT',
193224
entry,
194225
baselineExit: base.status,
195226
mutatedExit: mut.status,
196227
mutatedBytes: mutOut.length,
197-
mutatedHead: mutOut.split('\n').find((l) => l.trim()) ?? '',
228+
mutatedHead,
229+
mutatedSpoke,
198230
};
199231
} finally {
200232
rmSync(probePath, { force: true });
@@ -246,6 +278,27 @@ const SOUND_GATE = [
246278
'',
247279
].join('\n');
248280

281+
/**
282+
* The measured ACCIDENT shape, reduced: a dispatch that compares the self-test's
283+
* return value against a number. An early return makes that comparison false and
284+
* the process exits 1 having printed NOTHING -- a non-zero exit with no refusal
285+
* behind it. This is the shape `scripts/audits/14744-before-update-per-row-value-
286+
* census.mjs` carries and that an exit-code-only verdict scored HELD (#15324).
287+
*/
288+
const ACCIDENT_GATE = [
289+
'#!/usr/bin/env node',
290+
'function runSelfTest() {',
291+
' const failures = [];',
292+
" if (1 !== 1) failures.push('x');",
293+
" console.log('fixture self-test: 1 case passes');",
294+
' return failures.length;',
295+
'}',
296+
"if (process.argv.includes('--self-test')) {",
297+
' process.exit(runSelfTest() === 0 ? 0 : 1);',
298+
'}',
299+
'',
300+
].join('\n');
301+
249302
/**
250303
* Both instruments, against both directions. Returns the failures; the caller
251304
* refuses on any. Nothing here reads the repo, so a control failure is always
@@ -266,16 +319,28 @@ export function runControls() {
266319
try {
267320
const holed = join(dir, 'holed-gate.mjs');
268321
const sound = join(dir, 'sound-gate.mjs');
322+
const accident = join(dir, 'accident-gate.mjs');
269323
writeFileSync(holed, HOLED_GATE);
270324
writeFileSync(sound, SOUND_GATE);
325+
writeFileSync(accident, ACCIDENT_GATE);
271326
const h = probeEarlyReturn(holed, 'selfTest');
272327
const s = probeEarlyReturn(sound, 'selfTest');
328+
const a = probeEarlyReturn(accident, 'runSelfTest');
273329
say(h.verdict === 'DEFEATED',
274330
`POSITIVE CONTROL FAILED: the probe read a known-holed gate as ${h.verdict} (${h.why ?? ''})`);
275331
say(h.mutatedBytes === 0,
276332
'POSITIVE CONTROL FAILED: the known-holed gate printed something; the measured shape prints NOTHING');
277333
say(s.verdict === 'HELD',
278334
`NEGATIVE CONTROL FAILED: the probe read a handshake-protected gate as ${s.verdict} (${s.why ?? ''})`);
335+
// The discriminator, in both directions. A gate that HOLDS must be one that
336+
// SPOKE; a non-zero exit that printed nothing is an ACCIDENT and must never
337+
// be counted among the holds (#15324).
338+
say(s.mutatedSpoke === true && s.mutatedBytes > 0,
339+
'NEGATIVE CONTROL FAILED: the handshake-protected gate printed nothing when defeated; HELD is supposed to mean it REFUSED out loud');
340+
say(a.verdict === 'ACCIDENT',
341+
`POSITIVE CONTROL FAILED: the probe read a silent non-zero exit as ${a.verdict} (${a.why ?? ''}) -- an exit code is not a handshake`);
342+
say(a.mutatedExit !== 0 && a.mutatedBytes === 0 && a.mutatedSpoke === false,
343+
`POSITIVE CONTROL FAILED: the accident fixture no longer produces the measured shape (exit ${a.mutatedExit}, ${a.mutatedBytes} byte(s)); the ACCIDENT verdict above would then be passing for the wrong reason`);
279344
} finally {
280345
rmSync(dir, { recursive: true, force: true });
281346
}
@@ -399,12 +464,18 @@ function main() {
399464
}
400465
const defeated = rows.filter((r) => r.probe.verdict === 'DEFEATED');
401466
const held = rows.filter((r) => r.probe.verdict === 'HELD');
467+
const accidents = rows.filter((r) => r.probe.verdict === 'ACCIDENT');
402468
const unmeasured = rows.filter((r) => r.probe.verdict === 'NOT MEASURED');
403469
console.log('\nHole 2 -- silently defeated by an early `return` in the self-test (MEASURED):');
404-
console.log(` ${defeated.length} DEFEATED, ${held.length} HELD, ${unmeasured.length} NOT MEASURED.`);
470+
console.log(` ${defeated.length} DEFEATED, ${held.length} HELD, ${accidents.length} ACCIDENT, ${unmeasured.length} NOT MEASURED.`);
405471
console.log(` of the defeated, ${defeated.filter((r) => r.probe.mutatedBytes === 0).length} printed NOTHING at all and still exited 0.`);
406472
for (const r of held) console.log(` HELD ${r.file} -- ${r.probe.mutatedHead.slice(0, 96)}`);
473+
for (const r of accidents) console.log(` ACC ${r.file} -- exited ${r.probe.mutatedExit} printing ${r.probe.mutatedBytes} byte(s); no refusal, so NOT a hold`);
407474
for (const r of unmeasured) console.log(` n/m ${r.file} -- ${r.probe.why}`);
475+
if (accidents.length) {
476+
console.log(`\n⚠ ACCIDENT is not a hold. Those ${accidents.length} file(s) exit non-zero because a comparison`);
477+
console.log(' against a missing return value happened to be false, not because anything noticed.');
478+
}
408479
console.log('\n⛔ The two numbers are ORTHOGONAL and are never summed: a gate with a perfect');
409480
console.log(' floor is still defeated by hole 2, because the floor never runs either.');
410481
}

0 commit comments

Comments
 (0)