callstack / callstack/agent-device

gates: let the eager-closure budget tolerate a module split without admitting new eager work

Open
#2,469 0 comments 0 reactions 0 assignees View on GitHub
refactor
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.