`no APPROVED_OVER_CEILING row is stale` reads the introduced-entry set, which
is derived from `git merge-base origin/main HEAD`. On a push to main the
merge-base IS the head, so nothing is first-introduced and every approval row
reads as stale whatever its real state. That is exactly the shape of the
approving PR's own merge commit: #2329 added the
`packages/command-registry/src/planned-operations.ts` row to merge, and the
merge that followed it called the row dead. Coverage has been red on main
since (run 34099687663), and every branch cut from main after it inherits the
same failure.
- `staleApprovalRows` makes the verdict a named rule and defers it when the
merge-base is the head, where no row is readable at all. Enforcement is not
lost: a row that outlives its PR is still reported on the first branch whose
merge-base could have read it, and the rule is pinned in both directions.
- The `planned-operations.ts` row goes, which is what the rule asks for now
that main carries the entry: its closure (74) is governed by the no-growth
rule from here on, not by the domain-facade ceiling.
Claude-Session: https://claude.ai/code/session_01SfQqXj7JKQVgBA8eg9SMVB
Co-authored-by: Claude <noreply@anthropic.com>
* refactor(layering): ratchet R6, R9 and R10 against the merge-base tree
R6 type-spine inversions, R9's largest type cycle and R10's R7 ownership
pressure now compare the working tree with the same measurement taken over
the merge-base with origin/main, read through the shared committed-tree
reader (one git ls-tree, one git cat-file --batch, no second checkout).
Growth still fails with the same message shape, a shrink needs no edit, and
no change can bank headroom by leaving a number above the tree.
R9's per-zone check gains membership from the reference, so the overflow
message names the file that joined instead of listing the whole zone.
* chore(gates): delete the R6, R9 and R10 pins the merge-base now supplies
TYPE_INVERSION_BASELINE, LARGEST_TYPE_CYCLE_ZONE_CEILINGS, TYPE_CYCLE_BASELINE
and DAEMON_MODULARITY_BASELINE.sessionState were the hand-edited references
these three ratchets compared against. The merge-base measurement replaces
them, so there is no number left to leave above the tree and no entry to raise.
externalDaemonTypesImporters stays: it names files, not a count.
* refactor(closure): walk a source tree through a reader seam
The eager-import-closure walker read the working tree directly through fs, so
every consumer could only ask about the checkout in front of it. Closure
computation now takes a SourceTreeReader; the working tree stays the default,
and a committed git tree answers the same four questions for any tree-ish
without checking it out -- one `git ls-tree` for the tracked set and one
long-lived `git cat-file --batch` for the sources the walker can reach.
Per-tree memoization of package directories and direct edges, plus a
content-keyed parse cache, keep a second tree paying only for what differs.
* chore(gates): eager-closure budgets ratchet against merge-base with per-category ceilings
The 202 façade and 6 hub numeric pins are gone. The six platform façades stay
exact at one module, every other existing entry may evaluate no more than the
same file evaluated at the merge-base with origin/main (renames followed), and
an entry that did not exist there fits a per-category ceiling derived from its
path, or carries an APPROVED_OVER_CEILING row naming issue, reason and owner.
Shrinking now needs no gate edit, and a stale approval fails. The standing
denial -- a façade closure never reaches a concrete platform implementation
before discovery or binding selects an owner -- is unchanged.
* chore(gates): scope stale approvals to introduced entries and keep readers in sync
Address review findings on the eager-closure merge-base ratchet.
- docs/agents/testing.md: drop the new bullet. The file was 386 bytes over the
10,000-byte focused-doc budget, and the gate module's header already owns the
invariant, so the prose was duplication the ownership rule forbids.
- The closure walker's relative resolver no longer tries a .tsx suffix. The repo
defines a production source as .ts (tracked-sources.ts pathspecs and
isProductionSourceFile), so the committed-tree reader never loads .tsx content;
resolving one produced an edge that reader could not read, crashing the ratchet
instead of failing it.
- The APPROVED_OVER_CEILING staleness check now looks only at entries still
first-introduced. Once the merge-base carries an entry, the no-growth rule
governs it and nothing reads its row again, so the row is stale for the same
reason a shrunk entry's row is.