Uh oh!
There was an error while loading. Please reload this page.
fix(scripts): guard the lintText half of the fatal-parse adoption check - #10624
Conversation
`checkGuardAdoption()` asked whether `lintFilesStrict(` appears. A gate that KEPT one guarded call and measured a second population through `eslint.lintText()` answered yes to all three of its questions while that second population went entirely unguarded — ESLint returns a parse failure as a message with no rule id either way, so the count silently drops it. Measured through the checker as #10598 left it: import + `eslint.lintText(...)`, no strict call -> 1 problem (not armed) import + strict call + `eslint.lintText(...)` -> 0 problems <- the hole so the lintText-ONLY shape was already caught by #10598's positive assertion; the MIXED shape was not. And it was not hypothetical: this gate's own self-test counted through a bare `lintText()`. The `argument 1, object literal` report fixture with its closing paren removed gave hits()=0 with fatalErrorCount=1 — and hits()===0 is exactly what the ten `silent` cases assert, so a fixture that stopped parsing read as proof the rule is quiet. A blanket `.lintText(` ban was not available: the gate legitimately lints text to establish what raw ESLint does with a file that will not parse, which is ground truth the guard is built on and would be circular through the guard. Source text cannot tell that call from a measurement — which result gets COUNTED is data flow. So the check does not guess. It bans the BARE spelling and the gate declares which kind each call is: `lintTextStrict()` when the result is counted, `lintTextUnguarded({ why })` when it is not. - `lintTextStrict()` — the lintText twin of `lintFilesStrict()`, proved both ways at runtime, with the guard's own options kept out of what ESLint sees. - `lintTextUnguarded({ why })` — behaviour: none. It exists to be typed, and throws on an undeclared call so the escape hatch cannot be a rubber stamp. - The armed test now accepts EITHER strict entry point: a gate whose whole population is text never calls `lintFilesStrict(`, and reporting it unguarded would be a false positive of this change's own making. - 5 new adoption fixtures, including the reproduction and the two negative controls. FIXTURE_COUNT is now `+`-spelled for the same reason #10598's fixtures are: `stripComments` keeps string literals, so a contiguous `.lintText(` in this file's own fixtures would report this gate as unguarded. Fixes#10599 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
os-zhuang
commented
Aug 21, 2026
PM review — ACCEPT, arming. ⭐ The card was wrong about its own severity, and you measured it in the right direction — worse, not better.Head ⭐ The finding that changes the cardMy brief told you to check whether #10598 already caught the
⭐ And then the part the card got flatly wrong: it rated this latent. It is not.
Measured: the The design — data flow, not textMy brief said a blanket ban was unavailable (the ratchet legitimately calls
The author states the intent; the check enforces the declaration. That is the same move #10612 made with Widening the armed test to accept either strict entry point is right and self-aware: a text-only gate never calls ⭐ The void ablation — the most valuable paragraph in your report
That is precisely the trap the on-disk-anchor-count rule exists for, caught by the rule itself, and then reported rather than quietly redone. A tool that exits 0 having done nothing is the same defect class as every gate in this PR's neighbourhood — a partial result that reads like a complete one. Redoing it with a verified Python anchor and printing the diff hunk alongside the count is the right repair. I am carrying this into the seat's standing guidance: What else earns the ACCEPT
#10625 filed and triaged. It is the honest next layer: the call bans read only the gate file, so a measurement moved into a sibling helper module is invisible to all three tests — latent today, but "pull the measuring part into a module" is the natural next refactor of two 700+ line gates, and it would silently remove the gate from its own guard with no diff to the check. Filing that rather than speculatively widening the check here is the right boundary. Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#10599
What was actually open, and what was not
The card's title says the adoption check "still passes a gate that counts messages
from
lintText()". Half of that turned out to be already handled by PR #10598, sothe first thing this branch did was measure which half. Both fixtures, run through
guardAdoptionProblems()exactly as #10598 left it:So the lintText-only gate was already caught — by #10598's new positive
assertion, which has nothing left to find when a gate stopped calling anything.
The shape still open is the mixed one: a gate that keeps one guarded call for
population 1 and measures population 2 through
lintText(). It imports the guard,it calls
lintFilesStrict(, it never touches.lintFiles(— three green answersover a population with no guard on it at all.
It was not latent
The card rated this "latent — neither gate measures anything through
lintText()today". That is true of the ratchet population and false of the self-test that
asserts the ratchet.
hits(), the helper the 10 reporting and 10 silent casescount through, was a bare
eslint.lintText()with no fatal check. Theargument 1, object literalreport fixture with its closing paren removed:hits(code) === 0is precisely what everysilentcase asserts. A fixture thatstopped parsing would have read as proof that the rule is correctly quiet about
it — the guard's own failure mode, inside the self-test that asserts the guard.
Same for the blocking-config call, whose assertion is also
=== 0.Why the naive fix was unavailable, re-measured on this tree
The filing seat's reason holds, and it undercounted.
stripCommentskeeps stringliterals, so a blanket
/\.lintText\s*\(/ban sees 5 hits incheck-query-options-erasure-ratchet.mjsonmaintoday, not 4:FIXTURE_COUNT, a string literal in #10598's own fixtures — the decoyhits(), the counted helperL463 and L483 are the ones no ban can take: they exist to establish what raw
ESLint returns for a file that will not parse, which is the premise the guard is
built on. Routing them through the guard would prove the guard with the guard.
The distinction, drawn where it is decidable
Which lint result gets counted is a data-flow fact and the check reads source
text, so no regex can separate "measuring a population" from "exercising the
linter". Rather than ship a heuristic that fires on the next author, the check
stops guessing: it bans the bare spelling, and the gate declares which kind
each call is.
lintTextStrict()— the lintText twin oflintFilesStrict(), for resultsthat are counted. Proved both ways at runtime; the guard's own options are
asserted not to leak into what ESLint sees (it rejects unknown keys).
lintTextUnguarded({ why })— behaviour: none. It exists to be typed.It throws on a call with no
why, so the escape hatch cannot be a rubberstamp. It is an escape hatch: an author can route a real measurement through
it, and what that costs them is writing the word
Unguardedand a reason wherea reviewer reads it, instead of the silence that made The two ESLint ratchet gates swallow a fatal parse error as a non-matching message — an unparseable file scores clean instead of failing #10123 and eslint-fatal-guard's checkGuardAdoption reads a DOCBLOCK MENTION as adoption — deleting a gate's real import leaves the self-test printing "both gates still routed through it" #10458
possible.
population is text never calls
lintFilesStrict(at all, and reporting that oneunguarded would have been a false positive of this change's own making.
FIXTURE_COUNTis now+-spelled, for the same reason fix(scripts): the fatal-guard adoption check reads a docblock mention as an import — mask the source and assert the guard is called #10598's fixtures are:a contiguous
.lintText(inside this file's own fixtures would report this gateas unguarded. Do not tidy it into one literal.
Verification — all on
46dad29f40Negative control (the load-bearing one). The real gate, with its four
legitimate
lintTextcalls, is not flagged: after the migration the maskedsources of both guarded gates carry 0 bare
.lintText(, and both gates rungreen.
check-slot-lookup-ratchet.mjshad none and is untouched.Ablations — each mutation confirmed on disk by anchor count before the run,
each restore confirmed by an empty
git diff HEAD:.lintText(bana guarded call plus a second population measured through lintText: expected [...], got []armed through lintTextStrict alone: expected [], got [...not armed...]lintTextStrict's fatal checkcollectFatalMessagescall sites 2→1lintTextUnguarded's requiredwhyRestored tree green each time; final restored run
exit=0.the armed-disjunction mutation was first written with
perl -0pi, which matchedzero times, exited 0, and left the file unchanged — so the self-test went green
and would have read as "this assertion does not fail". The on-disk anchor count
caught it (
disjunction present = 1where 0 was required); it was redone with averified anchor and the diff hunk printed. The row above is the redo.
os-verify-lock.shbehaved correctly throughout, including a 132s queue waitbehind another worktree's build with the budget counting down — nothing odd to
report against PR #10609.
No changeset
scripts/**-only, publishes nothing;AGENTS.md:943— pure bug fixes do notrequire a changeset. Declared with the
skip-changesetlabel, per PR #10502.Generated by Claude Code