check-cited: make the regex match its own error message, and name the right lines
Two residual defects in the marker check added at 4d7d93a, both found by the
peer session that found the original.
1. THE REGEX WAS LOOSER THAN ITS DOCUMENTATION. The failure text has always
promised the comment must follow its figure "immediately, with nothing but
markup between", but CITE's separators were \s*, which spans newlines — so
a marker on the line after its number resolved and was checked, contrary to
the stated rule. Tightened to [ \t] throughout.
Verified safe before changing it: matched under the loose pattern 25, under
the tight pattern 25, so no citation in CLEAN GROUND relies on crossing a
newline. There is no finding in the document.
2. THE OFFENDER REPORT WAS RIGHT ABOUT THE COUNT AND WRONG ABOUT THE LINES.
It filtered line by line, so a valid cross-line citation was printed as an
offender whenever some other marker was genuinely unread — a maintainer
told "line 15 is broken" would have edited a working citation. That is the
headline defect fixed an hour ago one level down: correct verdict, wrong
reason.
Offenders are now located by position across the whole text, so the lines
named are exactly the markers no CITE match covers. This is the fix that
matters independently of the regex: a marker spanning lines would still be
counted once by ANY_CITE and named nowhere, so tightening alone leaves a
narrower version of the same bug, and position-based reporting stays
correct if anyone ever loosens the separator again.
Placement battery, exit codes read directly: clean tree OK 25 no false
positive; one word between number and marker fails; bare marker alone fails;
two on a line with one attached fails naming only the unattached; cross-line
now fails rather than silently passing.
Three iterations of this guard, three variants of one defect — a marker or a
message that exists and is not read — and all three were found by the session
that did not write it.
npm run check: 16 guards, exit 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
4d7d93abc8
commit
0de463c470
+22
-11
@@ -70,8 +70,13 @@ 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*<!--\s*cite:\s*([\w-]+)\s+([\w.]+)\s*-->/g;
|
||||
"**40**", "25.5%", "1.4x". Anchored to the end so it is the nearest one.
|
||||
[ \t] rather than \s throughout, so a citation cannot span a newline. The failure text
|
||||
below has always promised the comment must follow its figure "immediately, with nothing
|
||||
but markup between"; \s* quietly allowed a marker on the following line, so the code was
|
||||
looser than its own documentation. Making them agree also keeps the offender report
|
||||
honest — see the count check. */
|
||||
const CITE = /([\d]+(?:\.[\d]+)?)[ \t]*(?:%|x)?\**[ \t]*(?:—|-|–)?[ \t]*<!--[ \t]*cite:[ \t]*([\w-]+)[ \t]+([\w.]+)[ \t]*-->/g;
|
||||
/* Every marker, however badly placed. The two counts must agree — see below. */
|
||||
const ANY_CITE = /<!--\s*cite:[^>]*-->/g;
|
||||
|
||||
@@ -89,18 +94,24 @@ for (const [rel, abs] of scenarioFiles()) {
|
||||
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 `
|
||||
const markers = [...text.matchAll(ANY_CITE)];
|
||||
const spans = [...text.matchAll(CITE)].map(m => [m.index, m.index + m[0].length]);
|
||||
/* Located by position, not per line: a valid citation is a marker some CITE match covers.
|
||||
The first version of this filtered line by line, which named any VALID citation that
|
||||
happened to share a line with nothing — reporting a working marker as broken whenever
|
||||
another one was genuinely unread. The count was right and the explanation was wrong,
|
||||
which is the defect this guard exists to prevent, two levels down. */
|
||||
const unread = markers.filter(m => !spans.some(([a, b]) => m.index >= a && m.index < b));
|
||||
if (unread.length) {
|
||||
const lineOf = i => text.slice(0, i).split("\n").length;
|
||||
problems.push(` ${rel}: ${unread.length} 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**<!-- cite: ... -->\`), with nothing but markup between.`);
|
||||
unread.forEach(([n, l]) => problems.push(` line ${n}: ${l.trim().slice(0, 110)}`));
|
||||
unread.forEach(m => {
|
||||
const n = lineOf(m.index);
|
||||
problems.push(` line ${n}: ${text.split("\n")[n - 1].trim().slice(0, 110)}`);
|
||||
});
|
||||
}
|
||||
|
||||
for (const m of text.matchAll(CITE)) {
|
||||
|
||||
Reference in New Issue
Block a user