From 89d084e5bd1d4b9b6b3f183c074d59e10aa28fe6 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Thu, 30 Jul 2026 02:50:18 -0700 Subject: [PATCH] fix(scripts): --compare was accusing the parser of a blind spot it does not have (#2682) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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. --- scripts/lifecycle-column-census.mjs | 82 +++++++++++++++++++++++++---- 1 file changed, 73 insertions(+), 9 deletions(-) diff --git a/scripts/lifecycle-column-census.mjs b/scripts/lifecycle-column-census.mjs index 281b598a6a..2a57cc8f6e 100644 --- a/scripts/lifecycle-column-census.mjs +++ b/scripts/lifecycle-column-census.mjs @@ -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);