fix(scripts): --compare was accusing the parser of a blind spot it does not have (#2682)
## The problem
`census --compare` fails on `origin/main` — I verified it at
`bc782d8d92` and at every commit on the branch where I found it. Its
failure message reads:
> The parser has a blind spot; its count cannot be the bar until this is
closed.
That matters because the parser's count **is** the bar the program just
used to declare the closing bar met (triage 0, backlog 722). A red
cross-check asserting the instrument is untrustworthy had to be settled
in one direction or the other.
## It is settled: there is no blind spot
**Measured — 13 divergent sites, all 13 seen by the parser:**
| parser's classification | count |
|---|---|
| `deliberate` | 4 |
| `role` | 5 |
| `status` | 4 |
| **missed entirely** | **0** |
## The bug is in the check
It compared per-bucket totals and failed when the regex's `column` total
exceeded the parser's. That conflates the two things it most needs to
separate:
- the parser **missed** a site → a real hole, the failure worth having;
- the parser **classified it better** → `role`/`status`/`deliberate`
instead of `column`.
The second is the parser's entire reason for existing. So the old form
fired *more* the better the parser got, while accusing it of the one
defect it did not have. The regex is knowingly weaker at telling an
agent role from a column guard — that asymmetry is why the parser was
adopted, and the check was penalising it.
The intent was never wrong; the comment above the check already said the
contract was "a site the REGEX found and the parser missed". Only the
implementation disagreed with it.
## After
```
text classifier: {"column":728,"role":0,"status":182,"deliberate":14}
AST classifier: {"column":722,"role":5,"status":186,"deliberate":17}
parser sees every site the regex does (+131 sites the regex cannot see).
14 the regex calls a column guard, the parser classifies as {"role":5,"status":4,"deliberate":4,"definition":1}.
```
Fails only on a genuinely missed site now, printing the first ten.
Reclassifications are reported rather than failed.
## Scope
Report-only. `--compare` is not in the merge gate — the gate runs
`--strict`, which is why this stayed red and unwatched. Verified:
`--compare` exit 0, `--strict` exit 0, lint clean. No census numbers
change.
This commit is contained in:
@@ -141,21 +141,85 @@ if (compare) {
|
||||
What must NEVER happen is the other direction: a site the REGEX found and the parser missed means
|
||||
the parser has a hole, and then its number cannot be the bar. That is the failure this checks.
|
||||
*/
|
||||
const text = summarizeText(censusFilesText(files));
|
||||
/*
|
||||
FNXC:LifecycleColumnCensus 2026-07-30-11:30:
|
||||
COMPARE SITES, NOT BUCKET TOTALS. This check used to compare the per-bucket counts and fail when
|
||||
the regex's `column` total exceeded the parser's. That conflates the two things it most needs to
|
||||
tell apart:
|
||||
|
||||
- the parser MISSED a site entirely -> a real blind spot, the failure worth having;
|
||||
- the parser saw it and classified it better -> role, status, or deliberate instead of column.
|
||||
|
||||
The second is the parser's entire reason for existing, so the old form fired MORE the better the
|
||||
parser got. It had been failing on `main` while reporting "the parser has a blind spot; its count
|
||||
cannot be the bar" — and that message was false. MEASURED at the time of this change: 13 sites
|
||||
diverged, and all 13 were seen by the parser (4 deliberate, 5 role, 4 status). Zero were missed.
|
||||
|
||||
The check now fails only on a site the regex found and the parser did not, which is what the note
|
||||
above it always said the contract was.
|
||||
*/
|
||||
const textFindings = censusFilesText(files);
|
||||
const text = summarizeText(textFindings);
|
||||
console.log(`\n text classifier: ${JSON.stringify(text.totals)}`);
|
||||
console.log(` AST classifier: ${JSON.stringify(summary.totals)}`);
|
||||
const regressions = ["column", "role", "status", "deliberate"].filter(
|
||||
(kind) => text.totals[kind] > summary.totals[kind],
|
||||
);
|
||||
if (regressions.length > 0) {
|
||||
|
||||
/*
|
||||
FNXC:LifecycleColumnCensus 2026-07-30-13:05 (PR #2682 review — greptile):
|
||||
A SITE KEY CAN REPEAT ON ONE LINE, so this counts occurrences instead of testing set membership.
|
||||
`from === "todo" || to === "todo"` yields TWO findings sharing file:line:columnId; keyed by a Set,
|
||||
one parser match would satisfy both regex findings and hide a genuine miss of the other. Receiver
|
||||
is deliberately NOT part of the key — `c === "todo" || c === "todo"` would collapse again — so the
|
||||
comparison is per-key COUNTS, which cannot be fooled by either shape.
|
||||
*/
|
||||
const siteKey = (f) => `${f.file}:${f.line}:${f.columnId}`;
|
||||
const astByKey = new Map();
|
||||
for (const f of findings) {
|
||||
const list = astByKey.get(siteKey(f)) ?? [];
|
||||
list.push(f);
|
||||
astByKey.set(siteKey(f), list);
|
||||
}
|
||||
const textByKey = new Map();
|
||||
for (const f of textFindings) {
|
||||
const list = textByKey.get(siteKey(f)) ?? [];
|
||||
list.push(f);
|
||||
textByKey.set(siteKey(f), list);
|
||||
}
|
||||
|
||||
const missed = [];
|
||||
for (const [key, list] of textByKey) {
|
||||
const shortfall = list.length - (astByKey.get(key)?.length ?? 0);
|
||||
for (let i = 0; i < shortfall; i += 1) missed.push(list[i]);
|
||||
}
|
||||
|
||||
if (missed.length > 0) {
|
||||
console.error(
|
||||
`\nlifecycle-column-census --compare: the regex found MORE than the parser for ${regressions.join(", ")}.\n` +
|
||||
"The parser has a blind spot; its count cannot be the bar until this is closed.",
|
||||
`\nlifecycle-column-census --compare: the regex found ${missed.length} site(s) the parser did not.\n` +
|
||||
"The parser has a blind spot; its count cannot be the bar until this is closed.\n" +
|
||||
missed.slice(0, 10).map((f) => ` ${f.file}:${f.line} (${f.columnId})`).join("\n"),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
const extra = summary.totals.column - text.totals.column;
|
||||
console.log(` parser is a superset (+${extra} column guards the regex cannot see).`);
|
||||
|
||||
/* Reclassifications are expected and are the parser's value-add, so they are reported, not failed. */
|
||||
const byKind = {};
|
||||
let reclassifiedCount = 0;
|
||||
for (const [key, list] of textByKey) {
|
||||
/* Pair occurrences positionally within a key; equal counts are guaranteed by the miss check above. */
|
||||
const astList = astByKey.get(key) ?? [];
|
||||
list.forEach((f, i) => {
|
||||
const kind = astList[i]?.kind;
|
||||
if (f.kind === "column" && kind !== undefined && kind !== "column") {
|
||||
byKind[kind] = (byKind[kind] ?? 0) + 1;
|
||||
reclassifiedCount += 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
let parserOnly = 0;
|
||||
for (const [key, list] of astByKey) parserOnly += Math.max(0, list.length - (textByKey.get(key)?.length ?? 0));
|
||||
console.log(` parser sees every site the regex does (+${parserOnly} sites the regex cannot see).`);
|
||||
if (reclassifiedCount > 0) {
|
||||
console.log(` ${reclassifiedCount} the regex calls a column guard, the parser classifies as ${JSON.stringify(byKind)}.`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!strict) process.exit(0);
|
||||
|
||||
Reference in New Issue
Block a user