check-figures: the guard's body belongs behind the entry-point check

The fold exported scanDocument for the string tests, and importing it also ran
the guard. check-behaviour imports that module, so a figure defect called
process.exit(1) inside check-behaviour: it reported check-figures' failure under
its own name having run zero of its 108 tests.

The build went red, which is why this was survivable, but it went red in the
wrong place and every behavioural test was silently not running while appearing
to. A guard that stops another guard from running, and cannot say so, is the
worst version of the fault this file exists to catch.

Body now sits behind import.meta.main, the idiom step5-split.mjs already uses.
Importing yields the four readers and nothing else. Tested by spawning a fresh
process, because the property is "importing has no effect" and a source
assertion would pass on a file that grew a second side effect elsewhere.

Reverting the check fails exactly one test. With a defect planted,
check-behaviour runs 108 green and check-figures fails in its own slot.

Mine, introduced by R-307. R-309.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
slaguru666
2026-09-13 20:39:46 +01:00
co-authored by Claude Opus 5
parent bd9b3bdd27
commit d0db7954bc
3 changed files with 138 additions and 67 deletions
+17
View File
@@ -33,6 +33,7 @@ import { STARTER_AUTHORITY, STARTER_GROUPS, STARTER_CASE, STARTER_TEAM }
from "./scenario-starter.mjs";
import { castMarkersIn, castLikeIn } from "./declared-cast.mjs";
import { scanDocument } from "./check-figures.mjs";
import { spawnSync } from "node:child_process";
import { tagsIn, classifiedFiles, playableFiles } from "./check-scenarios.mjs";
import { beatsIn, beatLikeIn } from "./outcome-coverage.mjs";
import { step5Split, split as step5Of, THING_RATING } from "./step5-split.mjs";
@@ -999,6 +1000,22 @@ test("a document that cites nothing is held by the ratchet, not the strict regim
assert.ok(r.unmarked >= 1, "but its bare figures are still counted for the ratchet");
});
test("importing check-figures runs the readers and NOT the guard", () => {
/* THE REGRESSION THIS EXISTS FOR: for one commit the guard's body ran at import, so a
figure defect called process.exit(1) inside THIS file — check-behaviour reporting
check-figures' failure under its own name, having run none of its tests. Spawned rather
than asserted on the source, because the property is "importing has no effect", and only
a fresh process can tell you that. */
const mod = new URL("./check-figures.mjs", import.meta.url).href;
const r = spawnSync(process.execPath,
["--input-type=module", "-e", `await import(${JSON.stringify(mod)});`],
{ encoding: "utf8" });
assert.equal(r.status, 0, "importing check-figures must not exit non-zero");
assert.equal((r.stdout + r.stderr).trim(), "",
"importing check-figures must print nothing — the guard's body belongs behind the "
+ "import.meta.main check, or a figure defect stops this file mid-run");
});
test("the reader reports the line the figure is actually on", () =>
assert.match(firstFault(doc("one\ntwo\nwipes **8.3%** here")), /x\.md:3/));
+77 -67
View File
@@ -370,82 +370,92 @@ export function scanDocument(label, src) {
return { rows, strict, waived, unmarked, optedIn };
}
const strict = [];
const loose = {};
const rows = [];
const waived = [];
/* ── running the guard ─────────────────────────────────────────────────────────────────
EVERYTHING BELOW RUNS ONLY WHEN THIS FILE IS THE ENTRY POINT, and the reason is not tidiness.
`scanDocument` is exported so check-behaviour can test the readers on strings (R-292), and
for one commit importing it ALSO ran the whole guard: a figure defect called `process.exit(1)`
inside check-behaviour, which then reported check-figures' failure under its own name having
run ZERO of its 107 tests. A guard that stops another guard from running while appearing to
participate is the worst version of the fault this file exists to catch. Same idiom as
step5-split.mjs. */
if (import.meta.main || process.argv[1]?.endsWith("check-figures.mjs")) {
const strict = [];
const loose = {};
const rows = [];
const waived = [];
for (const [label, abs] of playableFiles()) {
const r = scanDocument(label, readFileSync(abs, "utf8"));
rows.push(...r.rows); strict.push(...r.strict); waived.push(...r.waived);
if (!r.optedIn) loose[label] = r.unmarked;
}
for (const [label, abs] of playableFiles()) {
const r = scanDocument(label, readFileSync(abs, "utf8"));
rows.push(...r.rows); strict.push(...r.strict); waived.push(...r.waived);
if (!r.optedIn) loose[label] = r.unmarked;
}
/* ── the ratchet, for documents outside the citation regime ────────────────────────────── */
/* ── the ratchet, for documents outside the citation regime ────────────────────────────── */
const base = existsSync(BASELINE) ? JSON.parse(readFileSync(BASELINE, "utf8")) : null;
const base = existsSync(BASELINE) ? JSON.parse(readFileSync(BASELINE, "utf8")) : null;
if (UPDATE) {
writeFileSync(BASELINE, JSON.stringify({
note: "Unmarked measurement figures in scenarios that use no citations. May fall, never rise. "
+ "Generated by tools/check-figures.mjs --update.",
uncited: loose
}, null, 1) + "\n");
console.log(`check-figures: recorded ${Object.keys(loose).length} document(s) outside the citation regime`);
process.exit(0);
}
if (UPDATE) {
writeFileSync(BASELINE, JSON.stringify({
note: "Unmarked measurement figures in scenarios that use no citations. May fall, never rise. "
+ "Generated by tools/check-figures.mjs --update.",
uncited: loose
}, null, 1) + "\n");
console.log(`check-figures: recorded ${Object.keys(loose).length} document(s) outside the citation regime`);
process.exit(0);
}
const problems = [...strict];
const problems = [...strict];
if (!base) {
problems.push(` no ratchet at tools/figures-baseline.json — run: node tools/check-figures.mjs --update`);
} else {
for (const [label, n] of Object.entries(loose)) {
const was = base.uncited?.[label];
if (was === undefined) {
problems.push(` ${label} uses no citations and is not in the ratchet. Re-record with --update, `
+ `or give it its first citation marker and it joins the strict regime.`);
} else if (n > was) {
problems.push(` ${label}: unmarked measurement figures went ${was} -> ${n}. This document cites `
+ `nothing, so the ratchet is all that holds it: the count may fall, never rise.`);
if (!base) {
problems.push(` no ratchet at tools/figures-baseline.json — run: node tools/check-figures.mjs --update`);
} else {
for (const [label, n] of Object.entries(loose)) {
const was = base.uncited?.[label];
if (was === undefined) {
problems.push(` ${label} uses no citations and is not in the ratchet. Re-record with --update, `
+ `or give it its first citation marker and it joins the strict regime.`);
} else if (n > was) {
problems.push(` ${label}: unmarked measurement figures went ${was} -> ${n}. This document cites `
+ `nothing, so the ratchet is all that holds it: the count may fall, never rise.`);
}
}
/* A document that LEAVES the loose set has gained its first citation, which is good news
and must not be reported as a missing row. A document that vanishes entirely has been
deleted or reclassified, and that is worth saying out loud. */
for (const label of Object.keys(base.uncited ?? {})) {
if (!(label in loose) && !rows.some(r => r.label === label)) {
problems.push(` ${label} is in the ratchet but no longer in the corpus. If it was renamed or `
+ `deleted, re-record with --update.`);
}
}
}
/* A document that LEAVES the loose set has gained its first citation, which is good news
and must not be reported as a missing row. A document that vanishes entirely has been
deleted or reclassified, and that is worth saying out loud. */
for (const label of Object.keys(base.uncited ?? {})) {
if (!(label in loose) && !rows.some(r => r.label === label)) {
problems.push(` ${label} is in the ratchet but no longer in the corpus. If it was renamed or `
+ `deleted, re-record with --update.`);
if (LIST) {
for (const r of rows) {
console.log(` ${r.marked ? "cited " : "BARE "} ${r.label}:${r.line} [${r.kind}] ${r.text}`);
}
problems.forEach(p => console.log(p));
process.exit(0);
}
}
if (LIST) {
for (const r of rows) {
console.log(` ${r.marked ? "cited " : "BARE "} ${r.label}:${r.line} [${r.kind}] ${r.text}`);
if (problems.length) {
console.error("check-figures: FAILED — measured figures are printed with nothing holding them");
problems.forEach(p => console.error(p));
console.error(` check-cited resolves the markers that exist; it cannot see a figure that has none. `
+ `That is how "a median 24 rounds" survived ten passes against a baseline holding 21.`);
process.exit(1);
}
/* WAIVERS ARE PRINTED ON EVERY GREEN BUILD. A figure nobody checks is the defect this guard
exists for, and a waived figure is one we have decided to stop checking — so the decision
stays on screen rather than becoming the default. Six of them is a short list; sixty would
be a finding. */
if (waived.length) {
console.log(`check-figures: ${waived.length} figure(s) waived, each on the record:`);
for (const w of waived) console.log(` ${w.label}:${w.line} ${w.text} — ${w.reason}`);
}
problems.forEach(p => console.log(p));
process.exit(0);
}
if (problems.length) {
console.error("check-figures: FAILED — measured figures are printed with nothing holding them");
problems.forEach(p => console.error(p));
console.error(` check-cited resolves the markers that exist; it cannot see a figure that has none. `
+ `That is how "a median 24 rounds" survived ten passes against a baseline holding 21.`);
process.exit(1);
}
/* WAIVERS ARE PRINTED ON EVERY GREEN BUILD. A figure nobody checks is the defect this guard
exists for, and a waived figure is one we have decided to stop checking — so the decision
stays on screen rather than becoming the default. Six of them is a short list; sixty would
be a finding. */
if (waived.length) {
console.log(`check-figures: ${waived.length} figure(s) waived, each on the record:`);
for (const w of waived) console.log(` ${w.label}:${w.line} ${w.text} — ${w.reason}`);
}
const marked = rows.filter(r => r.marked).length;
const looseTotal = Object.values(loose).reduce((a, b) => a + b, 0);
console.log(`check-figures: OK — ${rows.length} measurement-shaped figures across ${new Set(rows.map(r => r.label)).size} `
+ `scenarios, ${marked} marked and ${waived.length} waived; ${looseTotal} bare in `
+ `${Object.keys(loose).length} document(s) that cite nothing, held at the ratchet`);
const marked = rows.filter(r => r.marked).length;
const looseTotal = Object.values(loose).reduce((a, b) => a + b, 0);
console.log(`check-figures: OK — ${rows.length} measurement-shaped figures across ${new Set(rows.map(r => r.label)).size} `
+ `scenarios, ${marked} marked and ${waived.length} waived; ${looseTotal} bare in `
+ `${Object.keys(loose).length} document(s) that cite nothing, held at the ratchet`);
}