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:
co-authored by
Claude Opus 5
parent
bd9b3bdd27
commit
d0db7954bc
@@ -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
@@ -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`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user