…udget rows
`contracts-entry-closure.test.ts` (from #1959) carried two assertions. Neither
replacement subsumes it alone; together they do, on two different axes.
Its whole-tree scan parsed ~490 candidate files to prove nothing value-imports
the two wide facades. `eslint/no-restricted-imports` states exactly that, and
`allowTypeImports` draws the one distinction that made a custom walker look
necessary: `import type` is erased, so it stays legal. The rule covers every
file rather than the four hubs the deleted test named, and it reaches dynamic
imports too.
Its hub pin checked that four named hubs never reach either facade. The lint
rule forbids the import edge that was the only way that could happen, and
#1960's rows add an axis the old test never had: closure SIZE drift. That
cardinality check is deliberately not described here as strictly stronger,
because it is not — an equal-size graph substitution leaves the count intact and
passes. Size drift and forbidden edges are different properties, and the two
gates own one each.
The override semantics are not additive, so the rule was verified per zone
rather than assumed. A same-rule override REPLACES the parent, so a top-level
rule would have been silently dropped for `src/**`; and the existing blanket
`"off"` for `exec.ts` and the test tree would have exempted the files that
carried most of the cost #1959 removed. The paths are therefore added per zone,
and that `"off"` becomes a facade-only config that keeps the
`node:child_process` exemption it existed for.
Planted red in all three zones — `src/core/capabilities.ts`, a `src/__tests__`
file, and `packages/capture-kit/src` — each flagged, while a type-only import in
the same probe file was not. The first packages probe read as a pass because the
sed that built it produced a type-only import; that zone was re-probed with a
real value import rather than trusting the green.
Relying on config is safe here because misconfiguration fails loudly: a typo'd
rule name makes oxlint exit 1 with "Rule not found in plugin", rather than pass
silently the way the `rg` assertions in #1976 did.
The budgets stay for what no linter can express: a transitive weight property,
where a module already imported grows an import and the cost arrives without any
single file's import list changing. Per-file rules cannot see that, and
`no-restricted-imports` can only ban specifiers named in advance — exactly what
#1950/#1956/#1959 could not have named.
Stacked on #1965 — merge that first. (Also blocked on #1965's open tracked-manifest discovery finding.)
Retires
contracts-entry-closure.test.ts(added in #1959). It carried two assertions. Neither replacement subsumes it alone — together they do, on two different axes.1. Its whole-tree scan → a lint rule
It parsed ~490 candidate files to prove nothing value-imports the two wide façades.
eslint/no-restricted-importsstates exactly that, andallowTypeImportsdraws the one distinction that made a custom walker look necessary:import typeis erased, so it stays legal.The rule is already in this repo's toolchain — oxlint 1.79, and the same rule already bans
node:child_processoutsideexec.ts. It improves on the deleted test in two ways: it covers every file rather than the four hubs the test named, and it reaches dynamic imports too. It also moves the feedback from a CI run into the editor.2. Its hub pin → the lint rule, plus #1960's rows on a second axis
The hub pin checked that four named hubs never reach either façade. The lint rule forbids the import edge that was the only way that could happen. #1960's rows then add an axis the old test never had: closure size drift.
Correction from the previous revision of this description, which claimed the budget was "strictly stronger" and caught "any closure change". That is wrong, and thymikee is right to reject it: a cardinality budget cannot see an equal-size graph substitution — swap one module for another of the same closure size and the count is unchanged, so the row passes. Forbidden edges and size drift are different properties. Each gate owns one; the pair covers what the deleted test covered.
The hub list is deliberately not restored — the lint rule already covers those four files along with everything else.
Why this needed per-zone proof
Override semantics are not additive — a same-rule override replaces the parent. Two traps followed:
src/**, which already has its ownno-restricted-importsoverride;"off"forexec.tsand the test tree would have exempted the files carrying most of the cost perf(contracts): granularize entry surfaces so hub importers stop evaluating the facade clump #1959 removed.So the paths are added per zone, and that
"off"becomes a façade-only config that keeps thenode:child_processexemption it existed for.Planted red in all three zones —
src/core/capabilities.ts, asrc/__tests__file, andpackages/capture-kit/src— each flagged, while a type-only import in the same probe file was not. The first packages probe read as a pass because the sed that built it produced a type-only import; that zone was re-probed with a real value import rather than trusting the green.Relying on config is safe here because misconfiguration fails loudly: a typo'd rule name makes oxlint exit 1 with
Rule 'x' not found in plugin, rather than pass silently the way thergassertions in #1976 did.What a linter cannot replace
The eager-closure budgets stay. They express a transitive weight property: a module already imported grows an import, and the cost arrives without any single file's import list changing. Per-file rules cannot see that, and
no-restricted-importscan only ban specifiers named in advance — precisely what #1950, #1956 and #1959 could not have named ahead of time.Verification
pnpm lintclean,pnpm typecheckclean, budget gate green (207 tests). Net −98 lines of test, +59 lines of config. One iOSSmoke Testsjob failed on a live-simulatorwait timed out for textand passed unchanged on re-run — this branch changes no production code.