diff --git a/tools/check-cited.mjs b/tools/check-cited.mjs index 099c053..bfe42e1 100644 --- a/tools/check-cited.mjs +++ b/tools/check-cited.mjs @@ -72,6 +72,8 @@ function resolve(json, dotted) { /* The number immediately before the marker, allowing for markup and a trailing unit: "**40**", "25.5%", "1.4x". Anchored to the end so it is the nearest one. */ const CITE = /([\d]+(?:\.[\d]+)?)\s*(?:%|x)?\**\s*(?:—|-|–)?\s*/g; +/* Every marker, however badly placed. The two counts must agree — see below. */ +const ANY_CITE = //g; let checked = 0; const problems = []; @@ -81,6 +83,26 @@ const rows = []; citations live. The raw file is the thing being checked. */ for (const [rel, abs] of scenarioFiles()) { const text = readFileSync(abs, "utf8"); + + /* A MARKER THIS GUARD CANNOT READ IS WORSE THAN NO MARKER AT ALL, because whoever wrote + it believes they added a check. CITE requires the comment to sit immediately after its + number; put one word between them and the citation is silently skipped while the run + still reports OK with the same count as a clean tree. Found by a peer session writing + a bad citation by accident. So every occurrence is counted and the totals must agree. */ + const seen = (text.match(ANY_CITE) ?? []).length; + const read = [...text.matchAll(CITE)].length; + if (seen !== read) { + const unread = text.split("\n") + .map((l, i) => [i + 1, l]) + .filter(([, l]) => ANY_CITE.test(l) && (ANY_CITE.lastIndex = 0) === 0 + && [...l.matchAll(CITE)].length < (l.match(ANY_CITE) ?? []).length); + problems.push(` ${rel}: ${seen - read} citation marker(s) not attached to a number, so ` + + `nothing checks them — a marker that looks like a citation and is never read is the ` + + `defect this guard exists for. The comment must follow its figure immediately ` + + `(\`**40**\`), with nothing but markup between.`); + unread.forEach(([n, l]) => problems.push(` line ${n}: ${l.trim().slice(0, 110)}`)); + } + for (const m of text.matchAll(CITE)) { const [, printed, name, dotted] = m; checked++; @@ -127,7 +149,7 @@ if (!checked) { } if (problems.length) { - console.error("check-cited: FAILED — a scenario prints a figure its artifact no longer holds"); + console.error("check-cited: FAILED — a scenario's cited figures do not match their artifacts"); problems.forEach(p => console.error(p)); process.exit(1); }