Skip to content

fix(prune): the escalation could not see, could not open, could not forget, and reached for the wrong lever - #785

Merged
wenzowski merged 6 commits into
mainfrom
claude/rust-test-performance-c257jj
Aug 31, 2026
Merged

fix(prune): the escalation could not see, could not open, could not forget, and reached for the wrong lever#785
wenzowski merged 6 commits into
mainfrom
claude/rust-test-performance-c257jj

Conversation

@wenzowski

@wenzowski wenzowski commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Closes CLOUD-1240. Closes CLOUD-1244. Closes CLOUD-1246. Closes CLOUD-1249.

Four defects in prune.rs, found by land refusing lap after lap on one container. Every refusal was correct and every one named the environment rather than the diff — the floor is a working sensor. What was wrong sat behind it.

Read CLOUD-1249 first. The other three are about the floor; that one is about the ~11 GB the floor was refusing over, which nothing could reach.

CLOUD-1249 — the cheap lever was never wired up, so a short run took the expensive one

prune.rs's header states the formula and the risk in the same breath:

> keep is 2 rather than 1, and it is a hedge with a stated cost. The newest artifact per stem is what the current build reads; the one before it is what a rebase that reverts would otherwise rebuild from scratch.
>
> Retained bytes after a perfectly successful prune are keep × stems × size. keep is 2 and size is stable; stems is not.

Both correct, and together they are the defect. stems was ~41 at CLOUD-766's census on 2026-08-20. It is ~130 now — one cargo test target per crates/batten/tests/*.rs, with CLOUD-843's campaign adding one per retired gate — at 85–106 MB each. The hedge is therefore ~11 GB on a container with ~26 GB for target plus a lap. It costs more than the lap it exists to survive.

Measured here, with both passes reporting success:

MB
target/debug/deps (3177 files) 18326
everything else under target 234

Neither pass could touch it, and neither was malfunctioning:

  • the retention leaves two copies because keep = 2 is what it was told;
  • the escalation skips them because they are not a [[prune.regrowable]] root.

So a run short of disk dropped incremental — a full cold rebuild, the most expensive thing this module can do — with gigabytes nothing will ever read sitting beside it.

The fix is a third tier, and its placement is the argument. Cost order:

tier what it takes what it costs
1 a regrowable root that is not the basis that tool's next run
2 the undo hedge a rebuild only on a rebase that reverts
3 a basis-moving root a full cold rebuild of everything

Tier 2 goes before tier 3, never after: after is too late, because tier 3 has already moved the basis, the lap is already judged against the cold floor, and the space tier 2 would have found arrives after the decision it would have changed.

1 is the schema's own minimum for keep, so this tightens to the strictest bound a config could legally have declared rather than inventing a number. No new key, nothing in batten.toml moves, and a consumer already at keep = 1 sees a no-op. The standing policy is untouched — this is only what an escalation may do under pressure. It is not basis-moving: the copy taken is by definition not the one the next build reads.

The tightened artifacts are counted as superseded, not as cache, because that is what they are — the same pass re-run at a stricter bound. A reader seeing them under "regrowable cache dropped" would think a cache had gone that had not, and that distinction is exactly what a reader deciding whether to worry is deciding between.

CLOUD-1240 — the escalation knew names, not shapes

One lap from a cleared tree builds across six trees, and four were unreachable:

tree MB reachable?
debug 6429 partly — incremental/build live inside it
semver-checks 1548 yes, declared
aarch64-apple-darwin 1378 no
release 872 no
x86_64-pc-windows-gnu 721 no
perf 562 yes, declared

The header argued the list belongs in config because "which directories a build tree grows is a fact about THIS project". True of a NAME, not of a SHAPE. semver-checks and perf are names somebody chose. target/<triple>/ is what cargo lays down for every --target in every project, and a nested CARGO_TARGET_DIR is identical. Recognising the shape compiles in no consumer identifier.

Derived roots join the warm tier and never move the basis — dropping a nested tree makes only that build full.

CLOUD-1244 — the reclaim was never unable, it was never asked

The escalation gated on config.warm.mb while the refusal is judged against declared.max(observed). Those are one number only until a ratchet observation stands above the declaration — after which there is a band where a run refuses without attempting the reclaim that would clear it.

Every refusal that day fell in the band. Declared 7264; in force 9970 then 10997:

free at refusal above the declaration by escalations
7896 MB 632 0
8777 MB 1513 0
9064 MB 1800 0
10931 MB 3667 0

Ballasting the same tree to 6600 MB — below the declaration — escalated immediately and reclaimed 6061 MB. Those gigabytes were reachable at all four refusals.

All tiers now open on warm_floor_in_force. The re-read between tiers matters as much: comparing it against the declaration inherits the identical gap one step later.

CLOUD-1246 — a corrected reading could not retire what the broken one wrote

CLOUD-1218 fixed how a lap's basis is read. The observations the old reading took stayed in the journal, and Ratchet::raise only climbs:

"ratchet":{"warm":{"mb":10997,"head":"45601adc","measured":"2026-08-31"}}

Measured after that fix landed: land refused at 8356 MB free against a floor of 10997 MB — 1092 MB above the declaration — by the artifact of the bug it had just fixed.

A ratchet is a memory of a measurement, and a measurement is only as good as the instrument that took it. LapJournal now records which reading took its observations; a journal whose stamp is not the running engine's keeps nothing, the open lap included, because a lap opened by the old reading closes into the new one and its spent arithmetic would land in whichever basis the two disagree about.

The stamp moves by hand, only when a fix changes what a reading means — the point of not keying it on the crate version. This is not CLOUD-1218's rejected "let the ratchet decay": decay is a forgetting policy over time and weakens correct observations too; invalidation is a statement about the engine. It is the near neighbour of CLOUD-1218's still-open bullet — that one is a floor being unsatisfiable, this is its provenance.

Outcome is #[non_exhaustive], and that is the declared break

semver refused this branch with constructible_struct_adds_field; the row's §6 had claimed "no public item changes shape", which was false. The break is declared, and the attribute makes it the last of its kind — this is the second discard flag the struct has grown, and without it every future observation is priced at a version bump, which teaches the next author to fold two claims into one boolean. Bump is still patch: below 0.1.0 every releasing type collapses to it.

Shown able to fail

  • Disable tier 2 → the hedge case reds on the surviving count and on incremental still existing; the basis case reds on the basis. The inertness case stays green under the same mutation, which is what makes the pair discriminate rather than merely fire.
  • Revert escalate's guard to config.warm.mb → the four measured readings all sit above the gate with nothing escalating.
  • Relax the generation stamp to "equal or absent" → the verbatim bytes that refused this container survive the read, 10997 back in force.

Plus the inertness clauses: an absent observation must not invent a floor, an observation below the declaration must not lower it, a journal this reading took must be read unchanged, and a tree with headroom keeps its hedge.

Two commits dropped, and this is the part worth reading

This branch also carried a fix for CLOUD-1241 — basis_of's .any() narrowed to .all(). Implemented, tested, green. CLOUD-1218 landed on main mid-branch and refutes that mechanism by name:

> directories_named only yields directories that EXIST, and the reclaim an agent actually performs REMOVES target/debug/deps, which drops that profile out of the walk and leaves a populated target/release/deps satisfying either quantifier. .all() only helps for a deps that survives but is empty, which is not what any of the three instances did.

Correct, and fatal. .all() cannot see the case that actually happens; the passing suite proved the predicate, not the class. CLOUD-1218's .fingerprint marker closes it.

The second dropped commit is stranger: this branch found that reading every deps under the root lets prune.rs's own fixtures decide the answer — one empty target/tmp/<case>/target/debug/deps took the floor in force from 7264 to 17357 MB on a tree holding 2088 + 710 real artifacts. CLOUD-1218 hit the same defect within the hour, from the opposite direction — its fixtures masked warm — and fixed it with cargo_owned. Two sessions, one hour, the same litter, both found by running the binary rather than reading the diff.

CLOUD-1241 is Canceled as a duplicate of CLOUD-1218, Ready block kept rather than gutted: deleting one on withdrawal makes a row read as filed-and-never-groomed, which is a different and false claim.

What this does not fix

The ratchet is still unbounded. A floor learned from a roomier lap can exceed what a completed lap now leaves free, and then only ever refuses — measured here at 11519 MB in force against 9412 MB left by a lap that finished. That is CLOUD-1218's open bullet, recorded there with the numbers.

size is untouched. ~85 MB per test binary is [profile.dev] debug = 1 on workspace code. CLOUD-1211 adopted arm C deliberately, for readable backtraces; its own census measured arm A at 1.60 GB against 15.11 GB. This PR is about the multiplier, not that term.

target/release is deliberately left aloneperf-gate rebuilds it inside the same lap.

The declared floors are untouched. batten.toml is protected, and whether 7264 MB is the right basis is CLOUD-1158's.

Not in scope

No predicate weakens. Regrowable and [[prune.regrowable]] are unchanged, no declared row is removed or narrowed, [prune] keep keeps its declared value, and basis_of is entirely trunk's.

@linear-code

linear-code Bot commented Aug 31, 2026

Copy link
Copy Markdown
CLOUD-1240 The prune escalation cannot reach a NESTED cargo build tree, so every cross-target and nested-`CARGO_TARGET_DIR` root is unreclaimable — measured 2.1 GB, and the floor ratchets up when a lap exceeds it

Why

Measured, lapping PR #775 on 2026-08-31

One mise run verify lap from a cleared target/ builds 11684 MB (the engine's own consumed reading; my independent du -sm said 11560) across six trees:

tree MB reachable by the escalation?
debug 6429 partly — incremental and build are declared, and they live inside it
semver-checks 1548 yes, declared by name
aarch64-apple-darwin 1378 no
release 872 no
x86_64-pc-windows-gnu 721 no
perf 562 yes, declared by name

[[prune.regrowable]] declares incremental, semver-checks, perf, flycheck* and build. The two cross-compilation trees are 2099 MB the escalation structurally cannot see, and nothing supersedes them, so no retention rule reaches them either — they are caches of incremental's exact kind, which is the argument prune.rs's header already makes for the three it does declare.

The feedback loop, which is the part that makes this urgent rather than tidy

The observed warm floor is derived from the worst lap on record. Measured this session, verbatim:

target-prune: the lap opened on 45601ad (2026-08-31) consumed 11684MB
target-prune: that is worse than any warm lap on record, so the observed warm floor rises to 10997MB from the next lap

So a lap that ran short of disk — and therefore reclaimed by hand, or ran cold — raises the bar for every subsequent lap. Before: 9970 MB. After: 10997 MB. The next lap now needs 11684 + 10997 ≈ 22.7 GB free at open, on a container that had 25 GB when completely empty.

Three laps this session closed below the floor at 7896 / 8777 / 9064 MB and were refused. Each refusal was correct and each named the environment — "That is the environment rather than this branch: there is nothing in the diff to reproduce." The floor is a working sensor. What is missing is that the escalation cannot reclaim the roots that consumed the headroom, so the only thing that clears a lap is a human deleting directories by hand. That was done twice in one session; the second time moved free space 7.7 GB → 12 GB and let the lap close.

Why this is the ENGINE's and not a [[prune.regrowable]] row

Two [[prune.regrowable]] rows naming the triples would close it for this repository, and Regrowable::validate accepts them. That is the wrong home, and prune.rs's own header is where the argument has to be settled rather than dodged:

Which directories a build tree grows is a fact about THIS project, so the list is [prune.regrowable] and not a constant here (non-negotiable rule 1).

**That is right for semver-checks, **perf **and **flycheck* and wrong for a nested build tree. target/<triple>/ is not a name anybody chose — it is what cargo does for every --target, in every project, and the same layout appears for a nested CARGO_TARGET_DIR (which is exactly what semver-checks and perf are, so today they are declared by name for something the engine could recognise). A consumer that never cross-compiles has no such directory and matches nothing; a consumer that does gets the reclaim without declaring anything. Recognising cargo's own layout is repo-agnostic in the sense rule 1 means, not a consumer identifier.

The header changes with the code, because it currently argues the opposite.

Refinement — Ready

Mechanism (§1) — a computable predicate over the build root: a directory directly under [prune] root that itself contains a debug/ or release/ subdirectory is a nested cargo build tree. target/debug does not self-match (it holds deps/, build/, incremental/, never a nested debug/), which is the discriminator this rests on and the first thing to test.

Scope (§2) — in: the escalation's root set, and the header paragraph that argues against it. Out, deliberately: removing or narrowing any existing [[prune.regrowable]] row — declared names keep working, and incremental/build are inside target/debug rather than nested trees, so they are not subsumed and must not be dropped from the config; the keep/superseded retention path, untouched; the floor values in batten.toml, which are protected and are a separate row's; and target/release, which perf-gate rebuilds partway through the same lap, so reclaiming it mid-lap would cause a rebuild inside the lap rather than a reclaim from it — the host profile directories are excluded by construction, not by a special case.

Change (§3) — the derived roots join the warm tier with cold = false, on the header's existing test: dropping a nested tree makes only that build cold and leaves the host cargo build warm, which is the identical reasoning already recorded for semver-checks and perf. They are reclaimed after the declared pass, and a directory a declared row already removed is skipped rather than re-attempted, because remove_dir_all on an absent path is an Err the byte accounting must not read as a failed reclaim.

Output (§7) — counts and megabytes, as now. The reclaim line already reports how many roots were dropped and what they freed; a derived root is not distinguished from a declared one in the output, because the reader's question is how much came back.

Commit / bump (§6)fix(prune); bump: patch, which is what fix releases and what below 0.1.0 every releasing type collapses to anyway. Non-breaking for the consumer surface and for the library surface alike: Regrowable and [[prune.regrowable]] are unchanged, and this only widens what the escalation may reclaim.

Blockers (§8) — none. relatedTo CLOUD-1155 (the retirement unit that owns land, where this was measured and where a port would otherwise inherit the gap), CLOUD-1157 (which built the escalation and declared its roots), CLOUD-766 (the artifact retention this exhausts), CLOUD-1158 (the floor basis and its ratchet), CLOUD-1215 (land re-drafting on an environment stop, the same failure seen from the CI-spend side).

Acceptance

  • A tree with target/<triple>/debug/ is reclaimed by the escalation with no [[prune.regrowable]] row naming it, and the run reports the megabytes.
  • Shown able to fail (CLOUD-418): target/debug is NOT reclaimed as a nested tree — the case that discriminates, since a predicate matching it would remove_dir_all the host build every time the floor is breached and report it as a reclaim. A fixture with target/debug/deps and no nested profile directory must be left alone.
  • A directory under the root with no debug/ or release/ inside it is left alone, so the predicate is not "any directory under target".
  • Dropping a nested tree leaves the basis warm, asserted rather than assumed: the run's reported basis after reclaiming only nested trees is not Cold.
  • prune.rs's header no longer argues that every regrowable root is the consumer's, and says which half is.

CLOUD-1241 `basis_of` reads EVERY `deps` under the root, so a cold build with a surviving `target/release/deps` is recorded as a WARM lap — and `raise` only climbs, so one such lap inflates the warm floor permanently

Why

Measured twice in one session, and the second time I caused it myself

[prune.warm] declares 7264 MB. The floor actually in force went 9970 → 10997 MB over one session, and neither number came from a warm lap.

target-prune: the lap opened on 45601adc (2026-08-31) consumed 11684MB
target-prune: that is worse than any WARM lap on record, so the observed WARM floor rises to 10997MB from the next lap

That lap was cold. target/debug had been emptied by hand immediately before it, so cargo rebuilt the entire workspace — which is what the 11684 MB is. It was recorded against the warm basis anyway.

The cause is already written down in the code, as a bound rather than a defect

basis_of decides warm-vs-cold by asking whether any deps under the root holds anything, and its own doc comment names this exact case:

THE BOUND, MEASURED ON THIS ROW'S OWN LANDING LAP AND STATED RATHER THAN LEFT TO BE FOUND: this reads EVERY deps under the root, so a populated target/release/deps reports warm while the DEBUG build the lap is about to run is cold. Scoping it would mean knowing which profile the caller is about to build, which the reclaim is not told and must not guess. It under-denies, which is the direction this function only ever moves in — it can turn a warm reading cold and never the reverse, so it is strictly stronger than reading the escalation alone and never weaker.

The last clause is the part that does not hold, and it is why this is a row. Turning a cold reading warm is exactly what happens here, and it is not the safe direction. Reading every deps makes Warm strictly easier to reach — any one populated directory suffices — so a tree with only target/release/deps alive reports warm while the debug build is cold. Both inflations above are that.

The comment's "under-denies" reasoning is sound for the floor it judges the lap against (judging a cold lap by the warm floor is permissive, which is the stated direction). It does not carry over to the observation it records, and that is the reading that was missed: the same misclassification that permissively admits a lap also writes that lap's cost into the wrong basis, where it is not permissive at all — it is a permanent raise.

Why one bad reading is permanent

Ratchet::raise only ever climbs; there is no decay. A single misclassified lap therefore moves the warm floor up for the life of the clone. The cap added for exactly this risk —

let observed_mb = spent.min(free_mb);

— bounds how far a lap can raise it, and its own comment records why ("an expensive lap used to poison a clone permanently, since raise only ever climbs"). It does not stop a cold lap raising the warm floor at all, because the cap is about magnitude and this is about which basis the number lands in.

Consequence, measured: the warm floor is now 10997 MB against a declared 7264 MB, so a warm lap must leave 51% more than the declaration this repository actually measured — and land refused three laps this session at 7896 / 8777 / 9064 MB, each correctly naming the environment.

Not the same as its neighbours, which is why this is its own row

  • CLOUD-1158 is the declared floor's basis going stale as the test-stem count grows. This is the observed floor being written from a misread lap. I mis-attributed this to that row earlier in the session and am correcting it here.
  • CLOUD-1240 widened what the escalation can reclaim. It does not touch which basis a lap is recorded under, and it interacts with this row in a way worth stating: reclaiming more at close raises free_mb, which un-clamps spent.min(free_mb) and lets the observation rise toward the lap's true cost. That is arguably the honest number, but it is a change in the ratchet's behaviour arriving from a different row, and it should be measured rather than assumed.

Refinement — Ready

Mechanism (§1) — a computable predicate: a lap whose HOST build was cold must not be recorded against the warm basis. The observation Ratchet::raise receives is keyed on basis_of's answer, and that answer is wrong whenever a non-host deps is populated and the host one is not.

Scope (§2) — in: how a lap's basis is decided for the purpose of RECORDING an observation. Out, deliberately: the floor values in batten.toml, which are protected; the spent.min(free_mb) cap, which is a separate and working mitigation for magnitude; the keep/superseded retention path; and CLOUD-1240's derived roots, which decide what is reclaimed and not what basis a lap ran under.

Change (§3), and it is a DECISION rather than a typo — three candidates, and the row picks one with its cost:

  1. Split the reading from the judgement. Keep basis_of permissive for admitting a lap (its stated direction is right there) and use a stricter reading for recording one — a lap is warm for the ratchet only if the host profile's own deps was populated. Cheapest, and it keeps the existing bound where it is sound.
  2. Tell the reclaim the profile. Removes the guess entirely and is what the comment says it will not do without being told; needs a new input from the caller.
  3. Let the ratchet decay. Does not fix the misclassification, but stops any single lap being permanent. Independently arguable and probably wanted anyway.

Output (§7) — counts and megabytes, unchanged. The lap-close line already names the basis it recorded under; that line becomes true rather than gaining a field.

Commit / bump (§6)fix(prune); bump: patch, which is what fix releases and what below 0.1.0 every releasing type collapses to anyway. Non-breaking for the consumer surface and for the library surface alike: no config key moves and no public item changes shape.

Blockers (§8) — none. relatedTo CLOUD-1240 (the reclaim half, and the interaction noted above), CLOUD-1158 (the declared floor's basis, which this is repeatedly mistaken for), CLOUD-766 (the exhaustion), CLOUD-1155 (the retirement unit that owns land, where the per-lap arithmetic is recorded), CLOUD-418 (shown able to fail).

Acceptance

  • A lap whose host deps is empty at open, on a tree where some other deps is populated, is recorded against the cold basis — asserted over a fixture with target/release/deps populated and target/debug/deps empty, which is precisely the shape that produced both inflations.
  • Shown able to fail (CLOUD-418): reverting to the every-deps reading puts that fixture's observation into the warm basis, and the case reds.
  • The warm floor after such a lap is unchanged, so a cold rebuild cannot raise it.
  • The permissive direction is preserved where it was sound: a lap is still ADMITTED under the reading that under-denies, so nothing starts refusing laps that used to run.
  • Whichever candidate §3 takes, the two it rejects are recorded with their cost, so the next reader does not re-derive them.

CLOUD-1244 The escalation opens on the DECLARED warm floor while the refusal is judged against the floor in force, so between the two a lap refuses without ever attempting the reclaim that would clear it

Why

The reclaim was never unable. It was never asked.

prune gates its escalation on the declaration:

if free_mb < config.warm.mb {

and judges the refusal on the floor in force:

let floor_mb = standing.map_or(declared_mb, |observed| declared_mb.max(observed.mb));

Those are the same number only until a ratchet observation stands above the declaration. After that there is a band between them where a run refuses without escalating at all.

Measured, and every refusal in one session fell inside it

[prune.warm] declares 7264 MB. The observed floor stood at 9970 MB, then 10997 MB.

free at refusal declared in force escalation ran? outcome
7896 MB 7264 9970 no refused; reclaimed by hand
8777 MB 7264 9970 no refused; reclaimed by hand
9064 MB 7264 9970 no refused; reclaimed by hand
10931 MB 7264 17357 (cold) no refused; reclaimed by hand
6600 MB 7264 10997 yes 6061 MB reclaimed, basis unmoved

The last row is the controlled arm: the same tree and the same binary, ballasted with a 4200 MB file so free space fell below the declaration. The escalation fired immediately and freed 6061 MB, reporting "none of those roots is the cargo build's basis, so the next build is still warm".

So on all four refusals the reclaim had gigabytes available to it and was never invoked. Each of those laps was then unblocked by a human deleting directories by hand — which is the behaviour the escalation exists to make unnecessary.

The refusal text makes this hard to see, because it reads as though the reclaim had been tried and come up empty:

below the measured warm disk floor, and nothing left to reclaim that would change the basis — the regrowable roots [prune] declares are already gone, were never there, or none of them is the cargo build's own

Every clause of that is about what a reclaim FOUND. None of it ran.

Why it goes unnoticed

The gap only opens once an observation stands above the declaration, so a fresh clone never sees it — the two numbers are equal and the gate is exactly right. It appears the first time a lap ratchets the floor, and from then on it is permanent and grows with the ratchet: the worse the disk pressure, the wider the band in which the reclaim refuses to engage.

Refinement — Ready

Mechanism (§1) — a computable predicate: the number the escalation opens against equals the number the refusal is judged against. Today config.warm.mb versus declared.max(observed); they must be one value.

Scope (§2) — in: the entry condition of both escalation tiers. Out, deliberately: the floor VALUES in batten.toml, which are protected and are CLOUD-1158's; whether the observation should have been recorded at all, which is CLOUD-1241's; what the escalation may reclaim once it runs, which is CLOUD-1240's; and the conditional nature of the escalation itself — it stays conditional, because dropping a cache costs the work that wrote it and paying that every lap trades a rare stall for a permanent tax.

Change (§3) — a named warm_floor_in_force(config, journal) read by both tiers. The warm standing is the right one even where the cold floor is what will ultimately apply: this gate opens the cheap tier, whose rows cost only their own next run, and reading the cold standing would make a basis-moving drop the entry condition for a pass whose contract is that it does not move the basis. The costly tier re-reads free space behind its own guard and must use the same number, or it inherits the identical gap one step later.

Output (§7) — counts and megabytes, unchanged. What changes is that the "nothing left to reclaim" line becomes true when it is printed.

Commit / bump (§6)fix(prune); bump: patch, which is what fix releases and what below 0.1.0 every releasing type collapses to anyway. Non-breaking for the consumer surface and for the library surface alike: no config key moves and no public item changes shape.

Blockers (§8) — none. relatedTo CLOUD-1240 (what the escalation may reclaim once it is asked), CLOUD-1241 (why the observation was inflated in the first place), CLOUD-1158 (the declared floors), CLOUD-766 (the exhaustion), CLOUD-1155 (the retirement unit that owns land, where the per-lap arithmetic is recorded), CLOUD-418 (shown able to fail).

Acceptance

  • With an observation of 10997 standing above a declaration of 7264, a tree at 7896 / 8777 / 9064 / 10931 MB free is below the floor the escalation opens against — the four readings measured, asserted directly.
  • Shown able to fail (CLOUD-418): under the declaration alone all four are at or above the gate, so none escalates. Kept as its own case, so the two numbers cannot quietly become one again.
  • An absent observation leaves the gate exactly as it was, so the change is inert on a fresh clone.
  • An observation below the declaration does not lower the gate — the declaration is a lower bound, which the reporting half already states and the gate must agree with.
  • Both tiers open against the same number.

CLOUD-1246 A ratchet observation taken by an engine whose reading has since been CORRECTED stays in force forever — fixing `basis_of` does not retire the number `basis_of` got wrong

Why

The fix landed and the container is still refused by the number the bug wrote

CLOUD-1241 is fixed at 2b7f57b2 — a cold lap beside a surviving target/release/deps is no longer recorded as warm. The lap it misrecorded is still in the journal:

"ratchet":{"warm":{"mb":10997,"head":"45601adc","measured":"2026-08-31"}}

[prune.warm] declares 7264. The 10997 was written by a lap that was cold, by an engine that has since been corrected — and Ratchet::raise only ever climbs, so the corrected engine reads a number the corrected reading would never have produced, and treats it as measured fact.

Measured consequence on this container after the fix landed: mise run land refused at verify's precondition with free 8356MB against floor 10997MB (warm basis, observed on 45601adc). 8356 clears the declaration by 1092MB. The lap was refused entirely by the artifact of the bug it had just fixed.

The class, stated so it is not read as this one journal

A ratchet is a memory of a MEASUREMENT, and a measurement is only as good as the instrument that took it. Nothing in the journal records which instrument that was, so nothing can distinguish an observation that is still true from one whose reading has been retired. Every consumer that ever ran a pre-fix binary carries the poisoned floor for the life of the clone.

The only remedy today is deleting $GIT_DIR/batten-prune/laps.json by hand. No gate names it, no output mentions it, and the refusal's own remedy line says "Free space outside ./target, or start a fresh session" — which is advice to work around a number that should have been discarded. A refusal naming a remedy that is not the real one is CLOUD-1050's class arriving on the disk surface.

Why this is not CLOUD-1241 and not its candidate 3

CLOUD-1241 §3 lists "let the ratchet decay" as a rejected candidate — "does not fix the misclassification, but stops any single lap being permanent." This row is a different mechanism and a narrower claim:

  • Decay is a forgetting policy over TIME or laps. It weakens every observation, including the correct ones, and it needs a half-life nobody has measured. That candidate stays rejected.
  • Invalidation is a statement about the ENGINE. An observation taken by a reading that has since been corrected is not a weaker fact, it is not a fact at all, and discarding it is exactly as principled as journal_unreadable discarding bytes that will not parse — a precedent this file already carries.

The two are not substitutes and adopting this one does not reopen that one.

Refinement — Ready

Mechanism (§1) — a computable predicate: an observation whose recording engine's generation differs from the running engine's is discarded at read, and the run says so. LapJournal::read already has the shape — an unparseable journal reads as empty and Outcome::report prints why. This is the same discard on a different precondition.

Scope (§2) — in: the journal's own record of what took its observations, and what a read does when that record does not match. Out, deliberately: the floor values in batten.toml, which are protected; time- or lap-based decay, which is CLOUD-1241's rejected candidate 3 and stays rejected; the spent.min(free_mb) cap, which bounds magnitude and is working; and the crate version, which is the wrong key — a patch release that changes nothing about the reading must not discard a correct history.

Change (§3)LapJournal gains a generation stamp, #[serde(default)] so an absent one is the empty string and therefore never matches. On read, a journal whose stamp differs from the compiled-in constant keeps nothing: the ratchet is dropped and the open lap with it, because a lap opened by the old reading closes into the new one and its spent arithmetic would be filed under a basis the two engines disagree about.

The constant is bumped by hand, only when a fix changes what a reading MEANS — which is the whole point of not keying it on the crate version. Its doc comment is the log of which fix each generation is for, so the next author bumping it has to write down why.

Output (§7) — one line, in journal_unreadable's shape and pointer-only: that the standing observations were taken by a superseded reading and were discarded. No megabytes from the discarded record — a number a reader could act on is exactly what this row says was never a fact.

Commit / bump (§6)fix(prune); bump: patch. No config key moves and no public item changes shape; LapJournal is private.

Blockers (§8) — none. relatedTo CLOUD-1241 (the reading this retires the output of), CLOUD-1240 (the reclaim half), CLOUD-1244 (the escalation reading the standing observation, which is what makes a poisoned one expensive rather than merely wrong), CLOUD-1155 (the retirement unit that owns land), CLOUD-418 (shown able to fail).

Acceptance

  • A journal carrying a stamp other than the compiled-in one reads with no standing observation and no open lap, and the run reports the discard.
  • A journal carrying the current stamp reads unchanged — this must be inert on every clone whose history the current reading did produce.
  • A journal with no stamp at all (every journal written before this row) is discarded, which is the case this container is.
  • Shown able to fail (CLOUD-418): with the stamps compared as equal-or-absent, the pre-fix journal survives the read and the 10997 warm observation is still in force — the case reds on exactly the number that produced this row.
  • The declared floor is what binds after a discard, so a run following one is judged by batten.toml alone.

CLOUD-1249 The escalation cannot reach the `keep = 2` undo hedge, which is now ~11 GB — the largest reclaimable thing in the tree, and the one a breached floor never asks for

Why

The module already states the formula and the risk. The world moved and nothing re-read it

prune.rs's header:

keep is 2 rather than 1, and it is a hedge with a stated cost. The newest artifact per stem is what the current build reads; the one before it is what a rebase that reverts would otherwise rebuild from scratch. Keeping two costs one copy per stem and buys back the common undo.

and, on the floor's basis:

Retained bytes after a perfectly successful prune are keep × stems × size. keep is 2 and size is stable; stems is not.

**Both are correct and together they are the defect. **stems was ~41 when CLOUD-766 measured it (2026-08-20). It is now ~130 — one cargo test target per crates/batten/tests/*.rs, and CLOUD-843's campaign adds one per retired gate with ~147 shell suites still standing. size is 85–106 MB per test binary under [profile.dev] debug = 1.

So the hedge is 1 × 130 × ~85 MB ≈ 11 GB, on a container whose usable allowance for target plus one lap is ~26 GB. The hedge now costs more than a lap.

Measured, this container, 2026-08-31

After a session of land laps, with the superseded pass having run and reported success:

MB
target/debug/deps 18326
everything else under target 234

deps held 3177 files: test binaries 85–106 MB each, libbatten-*.rlib at 172 MB × 2. cargo clean -p batten --profile dev took target from 18560 MB to 1423 MB and free space from 7799 MB to 25563 MB — 17 GB, with the basis still WARM, because the dependency closure survives and only this crate is relinked.

Neither existing tier can reach it, and that is structural rather than an oversight

  • The retention cannot: two copies is exactly what keep = 2 asks it to leave. It is succeeding.
  • The escalation cannot: the hedge copy is not a [[prune.regrowable]] root. It is live build output by policy.

So a run that is short of disk drops incremental — making the next cargo build fully cold, the most expensive thing this module can do — while ~11 GB of artifacts nothing will read sits untouched beside it. The escalation reaches for the costly lever because the cheap one is not wired up.

Why this is not its neighbours

  • CLOUD-1240 widened what the escalation can see — the cross-target trees, 2.1 GB. Correct, landed, and about 11% of this. I read it as the pole and it is not.
  • CLOUD-1244 fixed when the escalation opens. Also correct; it cannot help when the thing that would clear the floor is not a candidate at all.
  • CLOUD-766 is the exhaustion this is one cause of.
  • CLOUD-1211 owns size: it adopted arm C ([profile.dev.package."*"] debug = 0), which strips the dependency closure and deliberately leaves workspace debuginfo for readable backtraces. Its own census measured arm A at 1.60 GB linked against 15.11 GB. That term is real and is not in scope here — this row is about the multiplier, which needs no profile change and no protected edit.

Refinement — Ready

Mechanism (§1) — a computable predicate: when the floor is still breached after the cheap regrowable tier, the retention is re-applied at keep = 1 and the second generation goes. reclaim_superseded(root, 1) already exists and already does exactly this; nothing calls it.

Scope (§2) — in: escalate in crates/batten/src/prune.rs, and the report line that says what it did. Out, deliberately: [prune] keep in batten.toml, which is protected and must keep its declared value — this changes what an ESCALATION may do under pressure, never the standing policy; [[prune.regrowable]], untouched; [profile.dev], which is CLOUD-1211's size term; and the (stem, kind) grouping, which is CLOUD-1157's and is what makes a per-kind retention safe to tighten at all.

Change (§3) — a third tier, placed between the two that exist, and the placement is the argument rather than a detail. Order is cheapest-cost-first: a regrowable root costs its own next run; the hedge copy costs a rebuild only on a rebase that reverts; a basis-moving root costs a full cold rebuild of everything. So the hedge goes **before **incremental, never after — after is too late, because the basis has already moved and the lap is already judged against the cold floor.

It is repo-agnostic by construction: 1 is the schema's own declared minimum for keep, so no new config key, no consumer name, and a consumer that declares keep = 1 already sees this tier do nothing.

Output (§7) — one line, pointer-only: the count of artifacts dropped and that the retention was tightened. Reported separately from the regrowable-cache line, because the two cost different things and a reader deciding whether to worry needs to know which happened. No paths.

Commit / bump (§6)fix(prune); bump: patch. Outcome is already #[non_exhaustive] as of CLOUD-1246, so the new report field is additive and not a break.

Blockers (§8) — none. relatedTo CLOUD-1155 (the retirement unit that owns land, where the lap-cost measurement lives), CLOUD-766 (the exhaustion), CLOUD-1240 and CLOUD-1244 (the two escalation fixes this completes), CLOUD-1211 (the size term, deliberately out of scope), CLOUD-418 (shown able to fail).

Acceptance

  • A tree below the floor after the cheap tier, holding two generations per stem, keeps one and the run says so.
  • The basis stays warm: dropping a superseded copy is not a basis-moving reclaim, and a lap that only tightened retention is still judged against the warm floor.
  • Shown able to fail (CLOUD-418): with the tier removed, the same fixture keeps both generations and the run reports no tightening — and the case reds on the retained count, not merely on a message.
  • The tier is inert above the floor: a tree with headroom keeps its hedge, so the undo the header bought is only spent under pressure.
  • A consumer declaring keep = 1 is unaffected, because there is no second generation to take.
  • The costly tier still runs when tightening was not enough, so nothing that used to be reclaimable stops being reclaimable.

Review in Linear

@coderabbitai

coderabbitai Bot commented Aug 31, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 23 minutes.

View limit details

Limit details: You’ve used the included review currently available.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Free

Run ID: 8fe5c016-e891-4d95-a49c-806d7e0c92a5

📥 Commits

Reviewing files that changed from the base of the PR and between 0683ce5 and 53e9673.

📒 Files selected for processing (2)
  • crates/batten/src/prune.rs
  • crates/batten/tests/target_prune.rs

Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Pro by visiting https://app.coderabbit.ai/settings/billing.

Comment @coderabbitai help to get the list of available commands.

@wenzowski wenzowski changed the title fix(prune): reclaim a nested cargo build tree, which no consumer can name fix(prune): the escalation cannot see a nested build tree, and a cold lap is recorded as a warm one Aug 31, 2026
@wenzowski wenzowski changed the title fix(prune): the escalation cannot see a nested build tree, and a cold lap is recorded as a warm one fix(prune): four defects behind one refusal — the escalation could not see, could not open, mis-recorded, and could not forget Aug 31, 2026
@wenzowski wenzowski changed the title fix(prune): four defects behind one refusal — the escalation could not see, could not open, mis-recorded, and could not forget fix(prune): the escalation could not see, could not open, and could not forget Aug 31, 2026
…name

The escalation knew names and not shapes, so every cross-compilation target
tree was outside it entirely. Measured on this container while lapping #775,
from a completely cleared `target/`:

  debug                   6429 MB   partly reachable (`incremental`, `build`)
  semver-checks           1548 MB   declared by name
  aarch64-apple-darwin    1378 MB   UNREACHABLE
  release                  872 MB   UNREACHABLE
  x86_64-pc-windows-gnu    721 MB   UNREACHABLE
  perf                     562 MB   declared by name

Nothing supersedes those trees, so the retention rule cannot reach them either.
They are caches of `incremental`'s exact kind: regrowable, unbounded, and
superseded by nothing.

WHY THIS IS THE ENGINE'S AND NOT TWO MORE `[[prune.regrowable]]` ROWS. The
module header argued that which directories a build tree grows is a fact about
the consumer's project, so the list belongs in config. That is true of a NAME
and not of a SHAPE, and the distinction is the whole of this change:
`semver-checks`, `perf` and `flycheck*` are names somebody chose, so they stay
declared. `target/<triple>/` is not — it is what cargo lays down for every
`--target`, in every project, and a nested `CARGO_TARGET_DIR` has the identical
shape. Recognising that shape compiles in no consumer identifier and matches
nothing at all in a project that never cross-compiles, which is what
non-negotiable rule 1 is actually about. The header now says so rather than
arguing the opposite.

The derived roots join the WARM tier and never move the basis, on the file's own
existing test: dropping a nested tree makes only *that* build full and leaves
the host cargo build warm — the identical reasoning already recorded for
`semver-checks` and `perf`. They are taken after the declared pass, and a
directory a declared row already removed is skipped, because `remove_dir_all` on
an absent path would count a reclaim that freed nothing.

SHOWN ABLE TO FAIL (CLOUD-418), because this function ends in `remove_dir_all`
and the one case that matters is `target/debug`. A predicate matching it would
delete the host build every time the floor was breached and report it as a
reclaim. It is excluded twice — structurally (a real `target/debug` holds
`deps/`, `build/`, `incremental/`, never a nested profile directory) and by
name — and the redundancy is deliberate. Proved rather than asserted: removing
the name guard reds
`the_host_profile_directories_are_never_taken_as_nested_trees` on the second
assertion, and the guard was restored from a backup taken before the mutation.

Seven cases: the cross-target recognition, the host exclusion above, a
directory with no profile directory inside it, one-level-only so a tree's own
`deps/` is not itself a tree, the warm-tier reclaim leaving the basis warm, the
cold tier never taking a derived root, and a tree a declared row already took
not being counted twice.

WHAT THIS DOES NOT FIX, stated rather than implied. The lap still costs
11684 MB and the floor still ratchets up off the worst lap on record — measured
this session, a cold lap raised the observed warm floor from 9970 MB to
10997 MB, so the next lap needs ~22.7 GB free at open. This closes 2.1 GB of
that gap. `target/release` is deliberately left alone: `perf-gate` rebuilds it
partway through the same lap, so reclaiming it would cause a rebuild inside the
lap rather than a reclaim from it.

Refs: CLOUD-1240, CLOUD-1157, CLOUD-1155, CLOUD-766, CLOUD-418
…ration

`prune` gated its reclaim on `config.warm.mb` while the refusal that follows is
judged against `declared.max(observed)`. Those are one number only until a
ratchet observation stands above the declaration. From then on there is a BAND
between the two, and inside it a run refuses without ever attempting the reclaim
that would have cleared it.

MEASURED, ACROSS ONE SESSION, AND EVERY REFUSAL IN IT FELL IN THE BAND. Declared
7264MB; in force 9970MB and then 10997MB. Four refusals at 7896 / 8777 / 9064 /
10931MB free — all four above the declaration, all four below the floor they were
judged by — and zero escalations. Each one ended with a human deleting
directories by hand.

The reclaim was never unable, it was never asked: ballasting the same tree to
6600MB, below the DECLARATION, escalated immediately and reclaimed 6061MB
without moving the basis. Those gigabytes were reachable at every one of the
four refusals.

Both tiers now open on `warm_floor_in_force`. The costly tier matters as much as
the cheap one: it re-reads free space to decide whether the cheap pass was
enough, so comparing that reading against the declaration inherits the identical
gap one step later.

The WARM standing is what opens both, and never the cold one, even where the
cold floor is what will judge the lap. This gate admits a pass whose rows cost
only their own next run; the costly tier sits behind its own re-read. Opening on
the cold standing would make a basis-moving drop the entry condition for a pass
whose whole contract is that it does not move the basis.

SHOWN ABLE TO FAIL (CLOUD-418):
`the_declaration_alone_would_have_refused_every_one_of_them_without_trying`
asserts the predecessor's reading over the same four numbers — against
`config.warm.mb` alone every one of them is ABOVE the gate and none escalates,
which is the observed behaviour, kept as an assertion so the two numbers cannot
quietly become one again.

Two cases hold the direction: an absent observation must not invent a floor (the
fresh clone, where this is inert), and an observation BELOW the declaration must
not lower it — the declaration is a lower bound, which the reporting half
already states and the gate now agrees with.

Refs: CLOUD-1244, CLOUD-1241, CLOUD-1240, CLOUD-766, CLOUD-418
The reading that recorded a cold lap against the warm basis was corrected. The
number it wrote was not, and could not be: `Ratchet::raise` only ever climbs, so
a corrected engine reads a figure the corrected reading would never have
produced and treats it as measured fact.

MEASURED ON THIS CONTAINER, AFTER THE FIX HAD LANDED. `[prune.warm]` declares
7264MB. The journal held a warm observation of 10997MB, written by a lap that
was cold. `land` was refused at `verify`'s precondition with 8356MB free —
1092MB ABOVE the declaration — by the artifact of the bug it had just fixed.

The only remedy was deleting a file under `$GIT_DIR` by hand. No gate named it,
no output mentioned it, and the refusal's own remedy line said "free space
outside ./target, or start a fresh session" — advice to work around a number
that should have been discarded rather than worked around.

A ratchet is a memory of a MEASUREMENT, and a measurement is only as good as the
instrument that took it. The journal recorded no instrument, so nothing could
tell an observation that is still true from one whose reading has been retired,
and every clone that ever ran a pre-fix binary carries the poisoned floor for
life.

The journal now carries `taken_by`, and a journal some other reading took is
discarded at read — the ratchet AND the open lap, because a lap opened by the
old reading closes into the new one and its `spent` arithmetic would land in
whichever basis the two engines disagree about, which is the misfiling the
discard exists to undo.

COMPARED FOR EQUALITY, never equal-or-absent. Every journal written before this
key existed carries no stamp, and those are exactly the ones the corrected
reading did not take.

The stamp is bumped BY HAND, only when a fix changes what a reading MEANS —
which is why it is not the crate version: a patch release that changes nothing
about how a lap is measured must not throw away a history that is still true.
Its doc comment is the log of which fix each generation is for, so bumping it
costs writing down why.

NOT DECAY, which stays rejected. Decay is a forgetting policy over time; it
weakens correct observations too and needs a half-life nobody has measured. This
is a statement about the engine: an observation a retired reading took is not a
weaker fact, it is not a fact, and discarding it is exactly as principled as the
existing discard of bytes that will not parse.

SHOWN ABLE TO FAIL (CLOUD-418), over the real file rather than a hand-written
equivalent: `the_journal_that_refused_this_container_does_not_survive_the_read`
carries this container's exact journal bytes, and relaxing the comparison to
equal-or-absent — the one plausible weakening — reds it on precisely the 10997MB
observation that produced the row.

Five cases: those bytes discarded, a journal this reading took read unchanged (a
clone whose history is still its own must be untouched, or the ratchet resets
every run and no floor is ever observed), a stamp from some other reading also
discarded so the mechanism works forwards and the next bump is not inert, the
writer re-stamping so a discard happens once rather than every run, and the
report saying so while carrying no number out of the record it threw away.

Refs: CLOUD-1246, CLOUD-1241, CLOUD-1244, CLOUD-1240, CLOUD-418
`semver` refused the branch: `constructible_struct_adds_field`. Adding
`journal_superseded` to `prune::Outcome` is a break, because `pub mod prune`
exports a struct anyone could build with a literal.

THE ROW SAID OTHERWISE AND THE ROW WAS WRONG. CLOUD-1246 §6 read "no config key
moves and no public item changes shape; `LapJournal` is private" — the second
half true, the first false, and the true half is what made the false one look
right. Corrected on the row rather than only here.

The break is declared rather than dodged. Nothing outside this crate builds an
`Outcome`: `prune` builds it and callers read it, so a struct literal was never
the contract, only an accident of it being available.

Declaring it makes this the LAST break of its kind rather than the second.
`journal_superseded` is the second discard flag the struct has grown, and it
arrived because fixing a reading turned out not to retire what that reading had
written — the next class of observation will do the same. Without
`#[non_exhaustive]` each one is priced at a version bump, which teaches the next
author to fold two claims into one boolean. That is exactly what this struct
must not do: unreadable says the store is damaged, superseded says the store is
intact and its numbers stopped being facts, and a reader who cannot tell those
apart cannot tell a disk problem from an upgrade.

BREAKING CHANGE: `prune::Outcome` is `#[non_exhaustive]` and carries a new
`journal_superseded` field. A struct literal outside this crate no longer
compiles; reading the fields is unchanged. The struct is built by `prune` and
read by its callers, so no supported use is affected.

Refs: CLOUD-1246, CLOUD-1241, CLOUD-1244, CLOUD-1240
CLOUD-1246's five cases all called `LapJournal::read` directly. That tier pins
the predicate and proves nothing about whether the ENGINE routes its answer
anywhere — `.claude/rules/policy-modules.md`'s argument, arriving on a lap
journal instead of a policy module.

The new case runs `batten target prune` over the verbatim bytes that refused
this container: the journal as it stood when CLOUD-1241's fix had landed and
`land` was then refused at 8356MB free against a floor of 10997MB, 1092MB ABOVE
the declaration, by the artifact of the bug it had just fixed. No `taken_by`,
because the key did not exist when those bytes were written.

TWO ASSERTIONS, AND NEITHER ALONE DISCRIMINATES. Without "the run says so", the
case passes over an engine that silently starts a fresh history — the failure
`journal_unreadable` already exists to prevent. Without "10997 binds nothing",
it passes over one that prints the line and obeys the observation anyway.

SHOWN ABLE TO FAIL (CLOUD-418): relaxing the stamp compare to equal-or-absent —
the one plausible weakening, and the reading every pre-key journal would slip
through — reds it on the 10997 assertion.

AND IT REPAIRS A CASE OF TRUNK'S THAT THIS CHANGE BROKE.
`an_observed_floor_names_the_file_that_holds_it` seeds a journal by hand to prove
CLOUD-1218's last acceptance bullet, that a learned floor names the file holding
it. Unstamped, that fixture is now discarded before the assertion can see it, and
the case went red for the right reason: its own comment says the case "would
prove nothing over a number the repair discards", written before a second repair
existed. Stamping the fixture restores what it was asserting.

The literal is duplicated from `prune::JOURNAL_GENERATION` because an integration
test cannot see a private constant. That is a drift hazard with a loud failure
rather than a silent one: the next bump reds both cases with the reason in the
message.

Refs: CLOUD-1246, CLOUD-1218, CLOUD-1244, CLOUD-418
…basis

`prune.rs`'s header states the formula and the risk in the same breath:

  `keep` is 2 rather than 1, and it is a hedge with a stated cost. The newest
  artifact per stem is what the current build reads; the one before it is what a
  rebase that reverts would otherwise rebuild from scratch.

  Retained bytes after a perfectly successful prune are `keep x stems x size`.
  `keep` is 2 and `size` is stable; `stems` is not.

BOTH ARE CORRECT AND TOGETHER THEY ARE THE DEFECT. `stems` was ~41 when CLOUD-766
censused it on 2026-08-20. It is ~130 now — one cargo test target per
`crates/batten/tests/*.rs`, with CLOUD-843's campaign adding one per retired gate
— at 85-106MB each. So the hedge is ~11GB on a container with ~26GB for `target`
plus a lap: it costs more than the lap it is meant to survive.

MEASURED, and neither pass could reach it while both reported success:

  target/debug/deps    18326MB over 3177 files
  everything else       234MB

The retention leaves two copies because that is what `keep = 2` asks. The
escalation skips them because they are not a `[[prune.regrowable]]` root. So a
run short of disk took `incremental` — a full cold rebuild, the most expensive
thing this module can do — with gigabytes nothing will ever read beside it. The
cheap lever was not wired up, so it reached for the expensive one.

THE FIX IS A THIRD TIER AND ITS PLACEMENT IS THE ARGUMENT. Cost order:

  1. a regrowable root that is not the basis  -> that tool's next run
  2. the undo hedge                           -> a rebuild only on a revert
  3. a basis-moving root                      -> a full cold rebuild

Tier 2 goes BEFORE tier 3, never after. After is too late: tier 3 has already
moved the basis, the lap is already judged against the cold floor, and the space
tier 2 would have found arrives after the decision it would have changed.

`1` is the SCHEMA'S OWN MINIMUM for `keep`, so this tightens to the strictest
bound a config could legally have declared rather than inventing a number. No new
key, no consumer name, nothing in `batten.toml` moves — the standing policy is
untouched and this is only what an escalation may do under pressure. A consumer
already at `keep = 1` has no second generation and the tier is a no-op.

NOT BASIS-MOVING, which is why it belongs on this side of tier 3: the copy taken
is by definition not the one the next build reads, so `deps` stays populated and
the lap is still judged warm.

The tightened artifacts are counted as SUPERSEDED rather than as cache, because
that is what they are — the same pass re-run at a stricter bound. A reader seeing
them under "regrowable cache dropped" would think a cache had gone that had not,
and the two lines are exactly the distinction a reader deciding whether to worry
is deciding between. `Outcome::tightened` is a count; the bytes are already in
`reclaimed_mb`.

SHOWN ABLE TO FAIL (CLOUD-418): disabling the tier reds
`a_breached_floor_spends_the_undo_hedge_before_it_takes_the_basis` on the
surviving count AND on `incremental` still existing, and
`spending_the_hedge_leaves_the_basis_warm` on the basis. The inertness case
`a_tree_above_the_floor_keeps_its_undo_hedge` stays GREEN under the same
mutation, which is what makes the pair discriminate rather than merely fire.

Four cases: the tier fires and stops before the basis; the basis stays warm; a
tree with headroom keeps its hedge; and the count lands in the superseded column
rather than the cache one.

Refs: CLOUD-1249, CLOUD-1155, CLOUD-766, CLOUD-1240, CLOUD-1244, CLOUD-418
@wenzowski wenzowski changed the title fix(prune): the escalation could not see, could not open, and could not forget fix(prune): the escalation could not see, could not open, could not forget, and reached for the wrong lever Aug 31, 2026
@sonarqubecloud

Copy link
Copy Markdown

❌ The last analysis has failed.

See analysis details on SonarQube Cloud

@wenzowski
wenzowski marked this pull request as ready for review August 31, 2026 16:08
@wenzowski

Copy link
Copy Markdown
Contributor Author

/fast-forward

@wenzowski
wenzowski merged commit 53e9673 into main Aug 31, 2026
19 of 20 checks passed
@wenzowski
wenzowski deleted the claude/rust-test-performance-c257jj branch August 31, 2026 16:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant