Skip to content

[finding] the guard-adoption class is bounded four times over, never retired — a capability boundary would end it, a fifth source-text patch will not #10666

Description

@claude

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions