From e9f7d458f0e7622ea5eb923c2c15f0dc83a97cde Mon Sep 17 00:00:00 2001 From: slaguru666 <111923774+slaguru666@users.noreply.github.com> Date: Sun, 13 Sep 2026 20:04:45 +0100 Subject: [PATCH] check-figures: a cell inherits its column's header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit R-300 was right, and it was right with a live defect rather than an argument: a bare **24** in a table whose own header row says "Median rounds", reported OK by this guard, against a baseline holding 25. The scan read one line at a time, so measurement status living two lines above was invisible. Fixed by reading the header. A cell now 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, and it is deliberately a different mechanism from the shapes: the shapes guess at phrasing, the header does not have to. The general point in R-300 still stands and the file now says so where the shapes are defined: a vocabulary learned from the marked figures cannot contain the phrasing of the figure nobody marked. check-unmarked attacks that from the other end, treating bold as the corpus's own mark of a published figure. Two bugs found while testing, both mine, both caught before commit: - 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 reported roughly 130 correctly-cited rows as unheld — the best-marked tables in the corpus. Comments are now blanked rather than removed, so every offset still points at the right character. - "3.48 of 6" — the 6 is the party size the mean is out of, not a measurement. Coverage goes from 25 figures, 7 marked, to 120 figures, 102 marked. Proved again by breaking it, five ways, every restore byte-identical: the historical bare **24** fails at its own line, a stripped prose marker fails, a planted "median 24 rounds" fails, a new bare figure in an uncited document trips the ratchet, and neither a threshold column header nor any of the 102 cited cells fires. Co-Authored-By: Claude Opus 5 --- tools/check-figures.mjs | 92 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 2 deletions(-) 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 = /