Recorded from the #10625 dev seat (session session_01DdCnBGcHeufjrq7drTD3wt, branch
claude/issue-10625-guard-adoption-file-bound) while extending checkGuardAdoption() to a gate's
import closure. Not actionable there — it is a different answer to the class, not a wider version
of the same one.
The observation
#10123 -> #10458 -> #10599 -> #10625 are not four defects. They are one sentence losing a level of
indirection each time:
the check proves a fact about the text it read, and the claim it is used to make is about
the measurement.
Each card moved the boundary out by one: raw text -> code-not-prose -> the lintText method ->
the gate's whole import closure. On cost to evade that converges, and #10625 leaves the
population derived rather than listed, so a third module is already covered.
On kind it does not converge, and no further patch of the same shape will. Every test in
guardAdoptionProblems() is a regex over source text, and which lint result is COUNTED is a
data-flow fact. Source text cannot decide it — that is exactly why #10599 had to move the
decision to an author declaration at the call site. The escapes that remain are small but real:
a non-literal call spelling (eslint['lintFiles'](…)), a computed import() specifier (now
reported rather than silent, but reported is not covered), and any future ESLint entry point the
ban list does not know the name of.
The shape that would retire the class instead of bounding it
Stop letting a guarded gate hold an ESLint instance at all. Today both gates do:
import{ESLint}from'eslint';consteslint=newESLint({overrideConfig: measuringConfig(drop), … });Every adoption test exists because that object is in the gate's hands and any method on it is a
measurement the guard cannot see. If scripts/eslint-fatal-guard.mjs owned construction and
handed back a narrow API — something like createMeasuredLinter({ gate, repoRoot, config })
returning count(ruleId, targets) / countText(ruleId, code) and nothing else — then an
unguarded measurement stops being a spelling a regex has to recognise and becomes a capability
the gate never receives. The adoption check collapses from four source-text tests to one:
new ESLint( appears in no guarded closure outside the guard module.
Cost, honestly
This is a real refactor of two gates that are both past 700 lines, and both of them pass ESLint
instances around deliberately: canaryParseFailures() in scripts/eslint-stack-headroom.mjs
takes the gate's own instance precisely so the canary is proved on the channel the gate really
measures with, and this gate's --self-test needs raw ESLint behaviour as ground truth for the
guard itself. A narrow API has to keep both of those expressible or it will be worked around,
and a worked-around capability boundary is worse than the ban it replaced.
So this is filed as a direction to weigh, not a queued task. It may also be the right answer to
say NO to: the closure walk from #10625 may simply be enough, and a startup-stage repo does not
owe every latent class a structural fix. What is worth not losing is the reasoning, so the fifth
card of this shape is a deliberate decision rather than a reflex.
Refs: #10123 (the card that created the guard) · #10458 (comments-as-code) · #10599 (the
lintText declaration) · #10625 (the import closure).
Generated by Claude Code
Generated by Claude Code
Recorded from the #10625 dev seat (session
session_01DdCnBGcHeufjrq7drTD3wt, branchclaude/issue-10625-guard-adoption-file-bound) while extendingcheckGuardAdoption()to a gate'simport closure. Not actionable there — it is a different answer to the class, not a wider version
of the same one.
The observation
#10123 -> #10458 -> #10599 -> #10625 are not four defects. They are one sentence losing a level of
indirection each time:
Each card moved the boundary out by one: raw text -> code-not-prose -> the
lintTextmethod ->the gate's whole import closure. On cost to evade that converges, and #10625 leaves the
population derived rather than listed, so a third module is already covered.
On kind it does not converge, and no further patch of the same shape will. Every test in
guardAdoptionProblems()is a regex over source text, and which lint result is COUNTED is adata-flow fact. Source text cannot decide it — that is exactly why #10599 had to move the
decision to an author declaration at the call site. The escapes that remain are small but real:
a non-literal call spelling (
eslint['lintFiles'](…)), a computedimport()specifier (nowreported rather than silent, but reported is not covered), and any future ESLint entry point the
ban list does not know the name of.
The shape that would retire the class instead of bounding it
Stop letting a guarded gate hold an
ESLintinstance at all. Today both gates do:Every adoption test exists because that object is in the gate's hands and any method on it is a
measurement the guard cannot see. If
scripts/eslint-fatal-guard.mjsowned construction andhanded back a narrow API — something like
createMeasuredLinter({ gate, repoRoot, config })returning
count(ruleId, targets)/countText(ruleId, code)and nothing else — then anunguarded measurement stops being a spelling a regex has to recognise and becomes a capability
the gate never receives. The adoption check collapses from four source-text tests to one:
new ESLint(appears in no guarded closure outside the guard module.Cost, honestly
This is a real refactor of two gates that are both past 700 lines, and both of them pass ESLint
instances around deliberately:
canaryParseFailures()inscripts/eslint-stack-headroom.mjstakes the gate's own instance precisely so the canary is proved on the channel the gate really
measures with, and this gate's
--self-testneeds raw ESLint behaviour as ground truth for theguard itself. A narrow API has to keep both of those expressible or it will be worked around,
and a worked-around capability boundary is worse than the ban it replaced.
So this is filed as a direction to weigh, not a queued task. It may also be the right answer to
say NO to: the closure walk from #10625 may simply be enough, and a startup-stage repo does not
owe every latent class a structural fix. What is worth not losing is the reasoning, so the fifth
card of this shape is a deliberate decision rather than a reflex.
Refs: #10123 (the card that created the guard) · #10458 (comments-as-code) · #10599 (the
lintTextdeclaration) · #10625 (the import closure).Generated by Claude Code
Generated by Claude Code