callstack / callstack/agent-device
gates: let the eager-closure budget tolerate a module split without admitting new eager work
- Dominant language
- TypeScript
- Stars
- 4.6k
- Forks
- 299
- Avg merge
- 10h 17m
- Merged PRs (30d)
- 515
Description
## Purpose
Decide how the eager-closure gate (`scripts/__tests__/eager-closure-budgets.ts`) should treat a module split, so that extracting a large module inside a gated closure stops being impossible without weakening the property the gate protects.
## Current behavior
Every entry that exists at the merge-base is held to NO GROWTH of its eager import closure, counted in modules. There is no approval path for growth of an existing entry (`APPROVED_OVER_CEILING` covers first-introduced entries only). A module that is in a gated closure therefore cannot be split: one module becoming N modules is +N−1 for every entry that evaluates it.
Measured on PR #2423 at `b21efb0`: extracting `packages/platform-apple/src/runner/runner-cache-metadata.ts` (621 lines) into `runner-phase-budget.ts` + `runner-toolchain-probe.ts` grows seven entries by two each: app-lifecycle-facade 120→122, app-resolution-facade 61→63, doctor-facade 101→103, perf-facade 60→62, physical-device-facade 47→49, runner-operations-facade 100→102, `runner/index.ts` 13→15. The review on #2423 asked for exactly that extraction for ownership clarity, and it had to be deferred.
## What the gate protects
The ADR-0019 loading shape: a façade must not evaluate implementation it does not need at import time. The module count is a proxy for eager work. A split does not add eager work when the new modules together evaluate what the old one did, but it does add modules, so the proxy reads it as growth.
## Design question
Choose a rule that admits healthy extraction without admitting new eager work. Candidates, none proven:
1. Count something closer to eager work than module count: total source bytes, or statements at module top level, of the closure. A pure split keeps it flat; a new static edge to real implementation raises it. Costs: a heavier walker; renames and formatting churn move the number.
2. Recognize a split structurally: the new modules are all new at the head, and each is imported only by modules that were in the old module's importer set at the merge-base. As the reviewer of #2423 noted, this does not by itself prove no new eager work (a "new" module can carry a new heavy edge).
3. An explicit, reviewed approval row for growth of an existing entry (`APPROVED_GROWTH`), naming the issue, the reason, and the expected delta, retired automatically once the merge-base carries the new shape. Same mechanism as the existing ceiling rows; costs a hand-edited row per split.
4. Combine 2 and 1: accept a structural split only when the closure's byte or statement total does not grow.
## Deliverable
A short design note choosing one, with the walker change and the test that plants (a) a pure split and (b) a split that smuggles a new heavy edge, showing the first passes and the second fails. Then re-run the #2423 extraction as the first consumer.
## Non-goals
Loosening no-growth for anything other than splits; changing category ceilings.
Contributor guide
Assessment
This issue has not been assessed yet.