diff --git a/tools/check-figures.mjs b/tools/check-figures.mjs index 3f49fab..fe5e481 100644 --- a/tools/check-figures.mjs +++ b/tools/check-figures.mjs @@ -15,6 +15,23 @@ * printed its four-player wipe rate uncited twice while citing it correctly four times — * and that figure is the one that was published at 58.7% until a truncated sweep was found. * + * ⚠ THE SHAPES HAVE A BLIND SPOT AND IT IS STRUCTURAL. R-299 said this guard "learns its + * vocabulary from the marked figures and applies it to the unmarked" — and a vocabulary + * learned that way cannot contain the phrasing of the figure nobody marked. The concurrent + * session proved it against a live defect: a THIRD stale 24, in a table cell, reported OK by + * this file. Its measurement status lived in the header row two lines above — + * + * | | Median rounds | + * |---|---| + * | Six hollow men + three of the column | **24** | <- the baseline says 25 + * + * — and the scan read one line at a time, so a bare number in a column the document itself + * calls "Median rounds" was invisible. Fixed below by reading the header: a cell inherits + * the measurement status of its column, which is a claim the DOCUMENT makes rather than one + * this file's vocabulary has to anticipate. That is the narrow repair. The general point + * stands and is worth keeping in view — see `check-unmarked`, which attacks the same problem + * from the other end by treating bold as the corpus's own mark of a published figure. + * * So this is the asymmetric counter for the other direction, and the fifth in this repo: * * check-cited reads markers -> is the figure beside this marker right? @@ -104,6 +121,71 @@ for (const rel of ["tools/pack-tables-baseline.json", "tools/fight-tail-baseline walk(JSON.parse(readFileSync(abs, "utf8")), []); } +/* WHAT MAKES A COLUMN A MEASUREMENT, and therefore every number in it a figure. + Drawn from the baselines' own field names, exactly as the shapes above are. A bare `%` is + deliberately NOT here: a column of percentages may be skill ratings, and this corpus + prints plenty of those. */ +const MEASURE_HEADER = /\b(median|mean|average|wiped?|wipes|hurt|down|deaths?|swing|rounds?)\b/i; + +/** Split a markdown table row into cells, keeping each cell's offset in the line. */ +function cells(line) { + const out = []; + let at = line.indexOf("|"); + if (at < 0) return out; + at += 1; + for (;;) { + const end = line.indexOf("|", at); + if (end < 0) break; + out.push({ text: line.slice(at, end), start: at }); + at = end + 1; + } + return out; +} + +/** For every line, the header row of the table it is a body row of — or null. */ +function tableHeaders(lines) { + const owner = new Array(lines.length).fill(null); + for (let i = 1; i < lines.length; i++) { + if (!/^\s*\|[\s:|-]+\|\s*$/.test(lines[i])) continue; /* the |---|---| separator */ + if (!/^\s*\|/.test(lines[i - 1])) continue; + for (let j = i + 1; j < lines.length && /^\s*\|/.test(lines[j]); j++) owner[j] = lines[i - 1]; + } + return owner; +} + +/* A CITATION MARKER IS FULL OF DIGITS AND NONE OF THEM ARE FIGURES. `packs line.hollow6.hurt` + holds a 6; `fight-tail cut.over15` holds a 15. Scanning raw cell text made the guard + report every correctly-cited row in the document as unheld — 130 false positives, on the + tables that are the best-marked thing in the corpus. Comments are BLANKED rather than + removed so every offset still points at the right character of the real line. */ +const maskComments = line => line.replace(//g, m => " ".repeat(m.length)); + +/** Figures in a table row, by the header of their own column rather than by phrasing. */ +export function cellFiguresIn(line, header) { + if (!header) return []; + const heads = cells(header); + const masked = maskComments(line); + const out = []; + for (const [i, cell] of cells(masked).entries()) { + if (!MEASURE_HEADER.test(heads[i]?.text ?? "")) continue; + const re = /(\d+(?:\.\d+)?)/g; + let m; + while ((m = re.exec(cell.text))) { + /* "3.48 of 6" — the 6 is the party size the mean is out of, not a measurement. */ + if (/\bof\s+$/i.test(cell.text.slice(0, m.index))) continue; + const numEnd = cell.start + m.index + m[1].length; + out.push({ + kind: `column:${(heads[i].text.replace(/[*|]/g, "").trim() || "?").toLowerCase()}`, + value: m[1], + text: cell.text.replace(/\s+/g, " ").trim() || m[1], + at: numEnd, + marked: MARKED_AFTER.test(line.slice(numEnd)) + }); + } + } + return out; +} + /* The marker check, deliberately looser than check-cited's CITE. That reader is strict on purpose; here, ANY citation-shaped comment beside the figure means somebody marked it and check-cited owns the question of whether they marked it correctly. Two guards disagreeing @@ -149,13 +231,19 @@ for (const [label, abs] of playableFiles()) { }); const optedIn = /