check-figures: a cell inherits its column's header
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
960fb6be27
commit
e9f7d458f0
+90
-2
@@ -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(/<!--[\s\S]*?-->/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 = /<!--[ \t]*cite[ \t]*:/i.test(src);
|
||||
const headers = tableHeaders(lines);
|
||||
let unmarked = 0;
|
||||
|
||||
lines.forEach((ln, i) => {
|
||||
if (fenced.has(i)) return;
|
||||
for (const f of figuresIn(ln)) {
|
||||
/* Shape pass and header pass can find the same number — "median 21" inside a cell of a
|
||||
column headed Median. Counted once, at the position it occupies. */
|
||||
const seen = new Set();
|
||||
const found = [...figuresIn(ln), ...cellFiguresIn(ln, headers[i])]
|
||||
.filter(f => (f.at === undefined ? true : !seen.has(f.at) && seen.add(f.at)));
|
||||
for (const f of found) {
|
||||
rows.push({ label, line: i + 1, ...f, optedIn });
|
||||
if (f.marked) return;
|
||||
if (f.marked) continue;
|
||||
unmarked++;
|
||||
if (optedIn) {
|
||||
const uncitable = uncitableValues.get(f.value);
|
||||
|
||||
Reference in New Issue
Block a user