Skip to content

test-loop cost: five rows, from the profile measurement to 144 test targets becoming two - #805

Merged
wenzowski merged 5 commits into
mainfrom
claude/test-loop-cost-bundle-nrwv0k
Sep 1, 2026
Merged

test-loop cost: five rows, from the profile measurement to 144 test targets becoming two#805
wenzowski merged 5 commits into
mainfrom
claude/test-loop-cost-bundle-nrwv0k

Conversation

@wenzowski

@wenzowski wenzowski commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Five rows against the test loop's cost, in the order they had to land: the measurement first, then the two harness changes, then the engine fix, then the mass move that sweeps up every path the others touched.

Closes CLOUD-1289
Closes CLOUD-1291
Closes CLOUD-1290
Closes CLOUD-1288
Closes CLOUD-1210

What each one measured

CLOUD-1289 — split-debuginfo, CLOUD-1211's one deferred arm. Paired A/B, three cold builds back to back in one process, two identical baselines bracketing the candidate. Time is inside the null (the baselines differ by 9.7s on their own, spread 0.957; the candidate sits at 0.956) and that is recorded so nobody re-runs it. Bytes are the finding, on a null of zero width — the two baselines are byte-identical — at 2.04x off the linked binaries. And they leave the disk rather than moving to a sibling: target/debug 13.03 GB → 7.76 GB, the 5.20 GB replaced by 111.8 MB of .dwo. Separately, opt-level = 2 was unpinned: dev_profile.rs had four cases and zero occurrences of the key, while dropping it doubles every suite run.

CLOUD-1291 — the harness re-parsed the committed 356 KB config on every fixture command. Priced through a new examples/ bench rather than a CLI verb (the row's own first estimate was refuted that way): 10.48 ms per call, ten times its guess, of which the file read is 1.2%. Suite 60.26s → 53.64s, 11.0%, against a within-arm null of ±0.4%.

CLOUD-1290 — Fixture::git() spent two processes and base_commit() a third on a rename that already held. Verified on git 2.43.0 that init -q alone leaves main, and that all 100 base_commit() call sites are preceded by .git().

CLOUD-1288 — exec spent PIPE_DRAIN_TIMEOUT twice. One shared budget: a_surviving_grandchild_cannot_hang_exec 20.07s → 10.044s, shown able to fail at 20.148s. The row's zero-remaining premise is corrected rather than adoptedrecv_timeout(Duration::ZERO) returned a ready value 10,000/10,000 here, so the predicted false notice does not reproduce and that test obligation cannot be discharged as written. Said plainly in the code rather than claimed.

CLOUD-1210 — 144 test targets → 2, plus the ratchet that stops it regrowing. Rebuild after a real edit (not a touch) 48.0s → 7.4s/7.3s, null 0.986. Bytes, cold and clean: 147 linked artifacts → 4, 4.99 GB → 234 MB, target/debug 7.76 GB → 2.05 GB. A verify lap now consumes 186–2346 MB where the row recorded 25,403 MB.

Why two and not one. crates/batten/tests/policy_modules.rs deliberately stays its own target: evaluator-io-check probes it with cargo test --test policy_modules, and that task is a governed mise-tasks/*.shshell-retirement gives it exactly two landable shapes, retire it whole or leave it alone, so repointing the probe at the group is not an edit this change may make. It shares the harness by #[path = "it/common/mod.rs"] mod common; rather than by a second copy.

Two things worth a reviewer's attention

The ratchet shipped inverted and its own second tier caught it. policy/test-targets.rego tested count(segments) == 5 where the path splits to four — refusing the grouped module and allowing the new target. The module's load-time test_ rules agreed with the mistake. The compiled-binary tier is what failed, which is exactly why .claude/rules/policy-modules.md calls it not optional.

The move silently empties every glob spelling tests/*.rs. batten's globs use literal_separator(true), so * stops at a /: five committed rows plus one hardcoded in rules.rs would have selected nothing and reported clean. Repointed, and declared as three Weakens: rule-predicate-changed trailers — all three are widenings, not relaxations. mise-tasks/replay.sh needed no edit: its declared_in feeds git grep as a pathspec, where * does cross a /.

Expected consequence

The ratchet will red the open PRs adding new top-level crates/batten/tests/*.rs (#797, #796, #795, #793). That is the ratchet working; the fix on each is one git mv into the group plus a mod line.

Suite: 3607 tests, 3607 passed. No case deleted.

Filed rather than fixed here: CLOUD-1293 (.dwo is a class prune.rs's closed RECLAIMED_KINDS cannot see, so it accumulates per build hash).


Generated by Claude Code

@linear-code

linear-code Bot commented Sep 1, 2026

Copy link
Copy Markdown
CLOUD-1289 `split-debuginfo` is CLOUD-1211's one unmeasured arm, and it closed Done with that clause reading "Outstanding"

Why

CLOUD-1211 measured three debuginfo arms, adopted [profile.dev.package."*"] debug = 0, opt-level = 2, and reached Done with its own acceptance ending:

split-debuginfo = "unpacked" is still unmeasured. Outstanding.

That clause is its §3's second arm and it was never run. A successor rather than a reopen: CLOUD-1211's adopted arm is released and its reasoning should stay closed — what is unfinished is one measurement it explicitly deferred.

Why it still matters, measured 2026-09-01 (this container, 4 cores):

phase cold after touching one src/*.rs no-op
build 277s 48.0s 0.4s
run 139s 92.6s 92.6s

A single test target rebuilds and links in ~1.0s, so the 48s a developer pays per edit is roughly ~10s of lib compile plus 144 targets × ~1.0s at 4-wide ≈ 36s of linking. Linking is where a rebuild goes, and split-debuginfo is the one dial that attacks link time without touching what a backtrace can say.

target/debug/deps was measured at 10.3 GB across 147 linked artifacts, mean 70.0 MB — after CLOUD-1211's adopted arm already took it from 124.3 MB to 54.5 MB per binary. The bytes are still the binding constraint on this container (CLOUD-766, CLOUD-1218).

Refinement — Ready

  • **Source of truth (§1). **[profile.dev] in the workspace Cargo.toml; crates/batten/tests/dev_profile.rs, which pins the profile's current keys; the artifact census in target/debug/deps.

  • Mechanism (§3). Add split-debuginfo = "unpacked" to [profile.dev] and measure it as a paired A/B on one machine, back to back, on perf-pair's design — machine noise is common-mode across a pair measured seconds apart, so it divides out. debug = 1 stays, so workspace backtraces keep their file and line.

  • **This does NOT reopen the rejected arm (§2). **CLOUD-1211 measured whole-profile debug = 0 at 9.7x off the bytes — the largest number it found — and rejected it because a panicking test then reports a backtrace with no line numbers. That rejection stands. This arm changes where debuginfo is stored, not whether it exists.

  • Report the byte delta BESIDE the time delta (§3). They are different acceptance tests. CLOUD-1211 records that its own time column was three unpaired cold runs disagreeing in both directions, and that quoting it as a finding would be wrong; and CLOUD-766's exhaustion means an arm that halves artifacts is worth adopting even if its time delta sits inside the null.

  • The prior art does not transfer, and that is why this is a candidate rather than a known win (§3). The community figures CLOUD-1211 cites (14s→4s, 8.7s→3.0s, Add split-debuginfo profile option rust-lang/cargo#9112) are **macOS **dsymutil cases, and unpacked is the Cargo default on macOS and not on Linux — so the mechanism that paid there is not the one in play on this triple. Treat a null result as the expected outcome and record it as such.

  • Deliberately not in scope (§2). The linker: rust-lld is already the default on this host triple since Rust 1.90, readelf -p .comment over a built artifact reports Linker: LLD 22.1.6, and CLOUD-1211 already withdrew that claim — do not re-propose mold or an ld.lld flag. [profile.release] and [profile.dist], whose lto/strip/codegen-units are decided elsewhere and must not move for a test-loop change. [profile.dev.package."*"], which is CLOUD-1211's released decision.

  • A SECOND, INDEPENDENT FINDING IN THE SAME FILE — opt-level = 2 is unpinned (§3). Measured 2026-09-01: dev_profile.rs contains zero occurrences of opt-level, and all four of its cases cover debug alone — workspace_code_keeps_its_line_tables, the_dependency_closure_carries_no_debuginfo, an_absent_debug_key_is_not_read_as_the_adopted_value, the_shipped_profiles_are_untouched_by_the_test_loop_arm.

    That is the larger half left unguarded. CLOUD-1211 measured [profile.dev.package."*"] opt-level = 2 as its biggest single win — mise run test:cargo warm 100.189s → 48.581s, 2.06x on the whole suite — and dev_profile.rs's own header says it exists precisely to catch a dependency override silently falling back to cargo's default. Dropping opt-level today reds nothing, exits 0, and doubles every suite run.

    It lands here rather than as its own row because it is the same file, the same test and the same commit as the obligation above. This clause is not conditional on the split-debuginfo arm being adopted — if that arm nulls out, this assertion still lands.

  • Output (§7). Per-arm wall clock, ratio against the null, and the deps byte census.

Test obligation

crates/batten/tests/dev_profile.rs gains an assertion for whatever key is adopted, so a later edit that drops it is a finding rather than a silent regression — the shape msrv-pin-agreement uses to hold two authorities together. If no arm is adopted, that file is untouched and the null is recorded here instead.

AND IT WILL NOT FAIL ON ITS OWN — checked 2026-09-01, and the correction matters. An earlier revision of this clause implied adding a profile key would red that file. It will not: dev_profile.rs asserts NAMED keys and values, never an exhaustive key set, so a new key is simply uncovered. The risk this obligation defends against is silent non-coverage, not a red test, and an author who expects a red will conclude the assertion is unnecessary.

Commit / bump (§6): perf(build) — no bump. No shipped artifact's profile is touched and [profile.dist] is out of scope.

Blockers (§8): none. CLOUD-1208, which CLOUD-1211 was blocked by for its null, is Done. relatedTo CLOUD-1211 (the row this finishes), CLOUD-1210 (the independent multiplier — that row cuts how many binaries are linked, this cuts what each link writes), CLOUD-766 and CLOUD-1218 (the byte half), CLOUD-1208 (the harness and the null).

Acceptance

  • The arm's time delta and byte delta are reported separately, against a measured null.
  • It is adopted only if it clears the null on one of those two, and recorded as not distinguishable from noise if it clears neither — so nobody re-runs it.
  • debug = 1 and CLOUD-1211's package."*" block are unchanged either way.
  • opt-level = 2 is asserted in dev_profile.rs, and that assertion is shown able to fail — remove the key locally and watch it red. This clause holds whether or not any split-debuginfo arm is adopted.

CLOUD-1291 The test harness re-reads and re-parses the committed 354 KB `batten.toml` on every spawn, and nothing has priced it

Why

crates/batten/tests/common/mod.rs re-reads and re-parses this repository's own batten.toml354,236 bytes — on every call, in three places:

  • bypass_env_vars() (:155-168) calls batten::config::load(&at_root("batten.toml")), a full parse plus the ~20 validate passes config::parse runs. It is called from batten() (:207), i.e. on every construction of a fixture command.
  • declared_patterns() (:73) reads the file and scans it for [[pattern]] rows.
  • committed_patterns() (:706) reads and parses it again.

Static call sites across crates/batten/tests/*.rs: run(..) 412, run_with_stdin(..) 96, bare batten() 253 — 761, and the real call count is higher because several sit inside loops.

There is no memoization anywhere in the test tree: zero uses of LazyLock, OnceLock, OnceCell or once_cell under crates/batten/tests/. The one in the crate is src/lib.rs's static ROOT: OnceLock<PathBuf>.

THE SAVING IS UNMEASURED, AND THE FIRST ATTEMPT TO MEASURE IT WAS WRONG

This row's own headline number was refuted, and the honest state is "nobody knows" — which is why §1 is a measurement rather than a build, and why closing it as not-worth-doing is a sanctioned outcome.

The first estimate was 22.5 ms per call, from batten config show (28.8 ms) minus batten --help (6.3 ms), assuming the difference was the parse. It is not. Running the same verb from /tmp, where no batten.toml exists so load_authority selects the compiled-in defaults and nothing is parsed at all (hyperfine, -N, 40 runs each):

arm mean
in-repo, 354 KB config 29.1 ms ± 1.2
/tmp, nothing to parse 30.2 ms ± 4.9

Identical within noise. The 22.5 ms is verb startup — clap tree construction, defaults, summary rendering — and is paid whether or not a config file exists. Whatever the 354 KB parse costs, it is under the noise floor of a 29 ms process.

A plausible outcome is that this row should not be built. [profile.dev.package."*"] opt-level = 2 (CLOUD-1211) means the toml parser is compiled optimised, so the parse may genuinely be ~1 ms — and 761 × 1 ms is ~0.8 s of serial work against a warm suite whose serial sum is 370.8 s. That is noise. The measurement decides, and a null is a perfectly good result to record.

What is NOT in doubt is the structure: three unmemoized re-reads of one immutable committed file, on the hottest path in the harness.

Refinement — needs a number before it is Ready

  • **Source of truth (§1). **crates/batten/tests/common/mod.rs at :73, :155-168, :207, :706; crates/batten/src/config.rs::parse (:588) and the ~20 validate calls it makes (:989-1088).
  • §1 is a MEASUREMENT, and it comes first. Price batten::config::load over the committed file directly, in the profile the tests actually run under. Do not price it through a CLI verb: batten config show is measurably insensitive to config size, so it cannot answer this question — that is the mistake above, recorded so it is not repeated.
  • THE VEHICLE ALREADY EXISTS, so §1 names an instrument rather than an aspiration (§3). An earlier revision said "price it directly" and named nothing, which is how a measurement clause becomes unexecutable. The landed precedent is crates/batten/examples/acquisition-bench.rs, driven by [tasks.acquisition-bench] as BENCH_METRIC=... cargo run --quiet --release -p batten --example acquisition-bench (mise.toml:1364). Add an arm on that pattern that calls config::load over the committed file in a loop and reports a per-call figure. That is an examples/ target, so it costs the 144-target census nothing.
  • **GIVE THE ARM ITS OWN METRIC STAMP (§3). **perf-record takes the series stamp from BENCH_METRIC, and .claude/rules/rust.md records why it must be distinct: a reader plotting one stamp would otherwise put this arm beside an invocation-latency series sharing only its unit and read the gap as a step change — the reason acquisition-wall-clock is not wall-clock. crates/batten/tests/acquisition_metric.rs is the shape that asserts the task sets it rather than trusting that it does.
  • Mechanism (§3), if and only if the number justifies it. Wrap each of the three in std::sync::LazyLock. batten.toml is committed and cannot change during a run, so the memoized value is identical by construction — there is no semantic change to argue about.
  • **The property that must survive (§3). **bypass_env_vars reads the hatches out of the committed config rather than listing them, and CLOUD-1227's comment states why: a hand-written list "stops covering the next row somebody adds, silently, in the direction that weakens the suite". Memoizing the RESULT preserves that derivation exactly; replacing it with a literal does not, and is not what this row proposes.
  • Deliberately not in scope (§2). Changing which variables are scrubbed, or which spawns happen at all. Reducing the number of batten child processes — those are the point of an end-to-end harness (.claude/rules/rust.md prefers a test over the compiled binary), and each child's own config load is not removable from the parent.
  • Output (§7). A per-call duration and the resulting suite delta, against a measured null.

Test obligation

None new if the mechanism lands — the memoized values are the same values, and the existing suite covers them. What the row owes instead is the number, recorded here whichever way it comes out, so the next reader does not re-derive it.

Commit / bump (§6): perf(test) — no bump. Entirely under crates/batten/tests/.

Blockers (§8): none. relatedTo CLOUD-1227 (which owns why the bypass list is derived rather than written down), CLOUD-1210 (whose §3 names "a shared expensive fixture is built once per binary rather than once" as a possible third saving, and explicitly declines to price it — this is that clause, and it now has a measurement method), CLOUD-63 (the harness this lives in).

Acceptance

  • A measured per-call cost for config::load over the committed file, taken through an examples/ bench arm carrying its own BENCH_METRIC stamp — never through a CLI verb.
  • Adopted only if the resulting suite delta clears the null; otherwise closed as not-worth-doing with the number recorded, which is a legitimate outcome for this row and not a failure of it.

CLOUD-1290 `Fixture::git()` spends two `git` processes where `init -q` alone is one, and `base_commit()` then spends a third on a rename that already held

Why

crates/batten/tests/common/mod.rs:606-611:

pub(crate) fn git(self) -> Self {
    git_in(&self.dir, &["init", "-q"]);
    git_in(&self.dir, &["branch", "-M", "main"]);
    self
}

git_in spawns a process per call, so every fixture repository costs two before it holds anything. base_commit() (:617-621) then spends a third on the same rename: add, commit, branch -M main, update-ref.

The rename is already redundant today. git_command (:513-533) pins -c init.defaultBranch=main on every invocation, so git init -q already produces main. The branch -M main calls are belt-and-braces against a default this harness controls.

Counted across crates/batten/tests/*.rs on 2026-09-01: .git() at 150 call sites, .base_commit() at 98, plus 426 direct git_in occurrences — on the order of ~1,000 git process spawns across a suite run, beside ~750+ batten spawns.

Folding the two into a single git init -q and dropping the redundant rename from base_commit removes ~250 of them for the same resulting repository state.

This is small and is filed as such. The warm run phase is measured at 4.01x parallel efficiency on 4 cores — serial sum 370.8s against a 92.6s wall — so it is CPU-saturated and the only way to move it is to remove serial work. ~250 process spawns is real serial work, but at a few milliseconds each it is single-digit seconds of serial time and low single-digit percent of the wall. Filed at Low, and it should not be sold as a performance fix: the better argument is that a fixture builder doing in three processes what one does is a harness that teaches the wrong thing 150 times.

Refinement — Ready

  • **Source of truth (§1). **crates/batten/tests/common/mod.rs: git() at :606-611, base_commit() at :617-621, git_in at :493-505, and git_command's -c init.defaultBranch=main at :513-533.

  • **Mechanism (§3), and it is smaller than this row first said. **git_in(&self.dir, &["init", "-q"]) as the single call in git(); drop branch -M main from base_commit(). **No **-b main flag — see the measurement below. No call site changes: both are methods on the builder, so this is entirely inside common/mod.rs.

  • Deliberately not in scope (§2). The 426 direct git_in sites: each is a fixture asserting something specific and folding them is a per-case judgement, not a mechanical one. git_command's config pinning and environment fencing, which is what makes fixtures hermetic (GIT_CONFIG_GLOBAL=/dev/null, GIT_CEILING_DIRECTORIES) — untouched. Reducing batten spawns, which is a different question with a different answer.

  • MEASURED 2026-09-01, and it DELETES this row's own open question (§3). The earlier revision proposed git init -q -b main and then asked whether a git >= 2.28 floor on the developer's machine was acceptable, since there is no [tools] entry for git. That question does not arise. Three arms in a scratch directory on git 2.43.0, each run through git_command's own pinned flags:

    Arm one, today's two-process form (init -q, then branch -M main): resulting branch is main. Arm two, the first proposal (init -q -b main): resulting branch is main. Arm three, init -q alone: resulting branch is main.

    git_command already pins -c init.defaultBranch=main on every invocation, so the default *is *main and -b restates it. The change needs no new flag, no version floor, and nothing mise.lock cannot hold — it deletes a line rather than replacing one.

  • Dropping base_commit()'s rename is safe, and that was checked rather than assumed (§3). Every base_commit() call site across crates/batten/tests/*.rs is preceded by .git() within the same chain — zero counterexamples. So there is no fixture reaching it through some other initialisation whose branch the rename was normalising. A fourth arm confirms the no-op directly: after init -q + a first commit, branch -M main leaves main.

  • Output (§7). None; this is harness-internal.

Test obligation

The existing suite is the test: ~150 fixtures build a repository through this path and assert against it, so a git() that produced a differently-named branch or an uninitialised repo reddens broadly and immediately. No new case earns its place here — but the change must be shown to leave the test count unchanged at 3,493 rather than merely green.

Commit / bump (§6): test(harness) — no bump. Entirely under crates/batten/tests/; no shipped verb, public item or config key moves.

Blockers (§8): none. relatedTo CLOUD-63 (the row that built this one fixture materializer, and whose whole argument was that per-suite copies of it had diverged), CLOUD-1210 (the other harness-shaped cost, though a different one — that row is link time, this is process spawns).

Acceptance

  • git() is one process, spawning git init -q and nothing else, and base_commit() is three.
  • The suite is green at an unchanged 3,493 tests.
  • No change outside crates/batten/tests/common/mod.rs.

CLOUD-1288 `exec` spends PIPE_DRAIN_TIMEOUT twice, serially, so the shipped worst case is 20s where the constant says 10s

Why

exec.rs:253 declares const PIPE_DRAIN_TIMEOUT: Duration = Duration::from_secs(10), and exec.rs:2209 (the_drain_deadline_is_long_enough_to_be_about_a_leak) asserts it is >= 10s. Both read as "a leaked grandchild costs at most ten seconds". It costs twenty.

exec.rs:1635-1636:

let (out_bytes, out_spool) = out_drain.collect(Stream::Stdout, &mut notices)?;
let (err_bytes, err_spool) = err_drain.collect(Stream::Stderr, &mut notices)?;

collect is collect_within(PIPE_DRAIN_TIMEOUT, ..) (:600-602), and collect_within blocks on its own self.outcome.recv_timeout(deadline) (:615). So a grandchild holding both pipes open spends a full 10s on stdout and then a fresh 10s on stderr.

THE TEE THREADS ARE ALREADY CONCURRENT, AND SAYING SO IS THE POINT OF THIS ROW. Drain::spawn (:571-592) starts one detached thread per pipe at spawn time, and .claude/rules/rust.md's concurrency table already records "exec.rs drain threads (one per pipe) | OS threads | stays". Nothing here is a concurrency defect and nothing needs a runtime. What is serial is the deadline accounting: two independent budgets where the constant describes one.

This was first written up as "drain them concurrently", which would have changed code that is already correct and left the 20s in place. Recorded so the wrong remedy is not re-derived.

Measured, 2026-09-01, this container, cargo nextest run --workspace warm: process_group::a_surviving_grandchild_cannot_hang_exec takes 20.07s21.7% of the entire 92.6s warm wall clock, in one unparallelizable case. It is the single slowest test in the suite by a factor of 2.5 (next is cli at 7.96s).

That test is not the defect, it is the sensor: it asserts took < Duration::from_mins(1), so it passes at 10s and at 20s alike and has never had an opinion about which.

Refinement — Ready

  • Source of truth (§1). The file crates/batten/src/exec.rs — the constant at :253, the two collect calls at :1635-1636, collect_within at :609-624, and the existing floor assertion at :2209.

  • Mechanism (§3), and the seam already exists. The function collect_within takes the deadline as a parameter, and its own doc says "Production has exactly one caller, above, and it passes the constant." So the fix is at the two call sites, not in Drain: take Instant::now() once before the first collect, pass PIPE_DRAIN_TIMEOUT to it, and pass PIPE_DRAIN_TIMEOUT.saturating_sub(elapsed) to the second. One shared budget, so the constant means what both it and its floor assertion say.

  • The saturating subtraction is load-bearing (§3). If stdout consumes the whole budget, stderr's remaining deadline is zero, and recv_timeout(Duration::ZERO) must still take whatever bytes already arrived rather than discarding them — collect_within already treats a timeout as not-an-error and clones self.seen regardless (:625-628), so the existing shape is correct and must not be disturbed.

  • THE ZERO-REMAINING CASE HAS A SECOND HALF, AND IT IS AN OUTPUT DEFECT RATHER THAN A DATA ONE (§3). The bytes are safe, as above. What is not safe is the notice. When the remaining budget is zero, collect_within reaches its timed_out branch and writes "<stream> did not reach EOF within <deadline>" — and it will do that even for a stderr whose tee finished cleanly seconds earlier, because a zero-duration recv_timeout can report Timeout without the value ever being examined. That is a false pointer on a stream that had nothing wrong with it, on a channel house-style §6 requires to be byte-stable, and it would fire on exactly the runs this row exists to speed up.

    So the implementation must distinguish "the budget ran out" from "this stream had already finished" before emitting — the obvious spelling is to attempt a non-blocking take first and only report a timeout when nothing is there. The row does not mandate which spelling; it mandates that a cleanly-finished second stream emits no notice.

  • Deliberately not in scope (§2). Changing the VALUE of PIPE_DRAIN_TIMEOUT; the floor assertion at :2209 stays as it is. Making the drain threads concurrent — they already are. Touching the capture/spool sealing, which is what makes a timed-out drain still recoverable.

  • Output (§7). Unchanged: the existing pointer-only notice naming the stream, the deadline and a byte count. The deadline it prints for the second stream will now be the remaining budget rather than the constant, which is the honest number.

Test obligation

Two cases, and the second is the one an implementer will skip.

  1. A command leaking BOTH pipes returns within one PIPE_DRAIN_TIMEOUT plus slack, not two — over the compiled binary, and shown able to fail (it fails today at ~20s). a_surviving_grandchild_cannot_hang_exec cannot serve: its bound is one minute, so it is green on both sides of this change.
  2. A command that leaks stdout only and closes stderr cleanly emits exactly one notice, naming stdout — not two. This is the zero-remaining clause above, and without it the fix trades twenty seconds for a false finding on every leaked run. It must be shown able to fail against a naive saturating_sub implementation.

Commit / bump (§6): fix(exec)patch. Below 0.1.0 release-plz bumps the patch whatever the type says (.claude/rules/commits.md), so the honest type is written here and a patch is what it produces. Consumer-visible: a wrapped command that leaks its pipes returns in half the time it used to, and the second stream's notice reports a smaller deadline.

Blockers (§8): none. Independent of the test-target and profile work — this is engine behaviour, and the suite-time saving is a consequence rather than the reason.

Acceptance

  • A both-pipes-leaked command returns in ~10s, not ~20s, asserted over the compiled binary.
  • PIPE_DRAIN_TIMEOUT and its floor assertion are untouched.
  • Both cases are shown able to fail — case 1 against today's main, case 2 against a naive saturating_sub that emits on any zero-duration timeout.
  • A cleanly-finished second stream produces no notice, so §7's output stays byte-stable on the runs this speeds up.

CLOUD-1210 118 integration test targets link 14.1 GB of near-identical binaries, and the retirement campaign adds one per retired gate

Why

crates/batten/tests/*.rs is 118 tracked files, and crates/batten/Cargo.toml declares no [[test]] entries — so cargo autodiscovers one test target per file and links 118 separate binaries, each against the whole closure (gix, regorus, syn, clap, jsonschema, hyper/rustls).

Measured, this container, 2026-08-30:

  • target/debug/deps holds 122 extension-less artifacts totalling 14.1 GB, averaging ~116 MB each; the directory is 17 GB.
  • Partial rebuild (119 crates against a warm remainder): **309s wall, 142.0s nextest **Summary. A true cold build was never measured and this number must not be quoted as one.
  • Warm, nothing changed: 231s wall, 127.0s Summary, 5.9s freshness check — leaving 97.7s, 42% of the loop, unattributed. That residue is NOT this row's to claim: see the withdrawal below.

An earlier revision of this row quoted a 1376s cold arm and "~90% of it compile and link". Both were derived by guessing when the run started and ended rather than reading it, and both are withdrawn — which is CLOUD-1208's point arriving on its own sibling.

crates/batten/src/prune.rs:262-269 already records this artifact set — 114 binaries, 13411.9 MB, 86.8% of deps, measured 2026-08-20 — but reads it as a disk problem (CLOUD-766). It is the same fact, and nobody has priced it as latency. CLOUD-1158 is a third reading of the same count, as a moving basis under [prune]'s floors.

Prior art, and the mechanism here is adopted rather than invented. matklad, Delete Cargo Integration Tests, is the canonical statement of this defect: "rustc needs to repeatedly re-link the library crate with each of the integration tests", producing "a significant compilation time blow up for tests". Its recommended layout for a large codebase is exactly tests/it/main.rs plus one module per former file — the §3 below. Cargo's own repository did this refactor and measured it: the time to compile the test suite fell 3x and the size of on-disk artifacts fell 5x. A 5x artifact reduction against this tree's 14.1 GB is the same order as the byte problem CLOUD-766 is open on, arriving from the cause rather than the symptom.

A SECOND SAVING THIS ROW CLAIMED AND HAS NOW WITHDRAWN. An earlier revision said the 97.7s warm residue was nextest's per-binary list phase and therefore fell with the target count — making it this row's second measured win. Falsified 2026-08-30. A zero-match filter run (-E 'test(zzz_no_such_test_zzz_)') pays the freshness check AND the full enumeration and then runs nothing: Starting 0 tests across 119 binaries (3167 tests skipped), total wall 1.75s. The list phase is 56x smaller than the residue it was supposed to explain, so consolidating targets does not recover it and this row must not be justified by it. What the residue actually is remains unknown — CLOUD-1208's subject, arriving on its sibling for the second time.

What survives is the prior art, and it is enough on its own: 3x off test compile time and 5x off on-disk artifacts, measured by Cargo's own repository. This row stands on that and on the 14.1 GB census, not on a residue nobody can attribute.

A third saving the prior art names, stated as a possibility rather than a claim. Splitting tests across binaries means a shared expensive fixture is built once per binary rather than once; consolidated, a OnceLock/LazyLock setup is paid once. This tree materialises scratch git repositories per fixture through real git subprocesses (tests/common/mod.rs::git_in), so the cost is live — but whether any of it is actually shareable is a question for the migration, and this row does not price it.

Root cause, and why it gets worse on its own. The convention is one integration target per contract surface, which is locally correct and has no stopping condition — the same shape CLOUD-365 records for case counts. CLOUD-843's retirement campaign adds a target per retired gate, with ~147 shell suites still standing, so the count is monotone in the campaign's own progress. prune.rs's comment records the count moving 110 → 114 → 118 across three readings in ten days.

TWO RETIREMENTS ARE IN FLIGHT AGAINST THIS FILE DOMAIN, so the interaction is live rather than anticipated: CLOUD-1145 (derived-check) and CLOUD-1161 (ci-local-parity), both In Progress on 2026-08-30, and each mandated to add a top-level crates/batten/tests/*.rs. Whoever lands this row rebases across them and re-homes their files into a group as a mod — a mechanical move, not a content conflict, and not something to engineer around.

A THIRD READING, measured 2026-08-31: on this container the loop does not close

The existing readings price this as latency (compile time) and as bytes (deps at
14.1 GB). Both are true and both understate it. Measured across four consecutive
mise run land attempts in one session:

  • **One **verify lap consumes 25403 MB, reported by target-prune's own
    lap-close line: "the lap opened on 43fc2414 consumed 25403MB".
  • The container's writable allowance leaves ~24 GB free after a full purge of
    target/debug/deps + target/tmp.
  • So a lap consumes more than the machine can hold, and target-prune closes it
    below its own warm floor every time: free 5680MB, floor 7264MB.

The consequence is not slowness, it is that verify can run at most once per**
**manual purge and then wedges. Three of the four land attempts in that session
died at target-prune before reaching CI, and each recovery was a hand-rm of
target/debug/deps — which CLOUD-1218 records as inverting the ratchet, so the
remedy is itself a defect.

target-prune's refusal also names the wrong remedy, which is what sent the
first recovery in the wrong direction: it says "Free space outside ./target, or*
*start a fresh session" while 17 of the 20 GB were **inside **./target, in
deps, in artifacts its warm-basis logic declines to reclaim. Following the
advice as written leads to deleting cross-compile roots — measured earlier the
same session at 2.3 GB freed and 8.3 GB spent rebuilding them on the next lap.

This is the same 14.1 GB artifact set the row already counts; what is new is that
it is now larger than the headroom, so the failure mode changed from "slow" to
"cannot complete". Raising to High on that basis: a contributor on this container
cannot run the repository's own required gate twice without hand-purging, which is
the condition CLOUD-1218 exists to prevent.

REOPENED 2026-09-01 — this row reached Done without being built

It was closed on 2026-08-31 and none of §3 exists. The evidence is in the row's own metadata and in the tree:

  • startedAt: null. The row went Backlog → Todo → Backlog → Todo → Done, never In Progress.
  • No PR attachment. Its sibling CLOUD-1211 carries PR perf: the gate tail 84.4s→37.2s and the Rust suite 100.2s→48.6s, each measured against a null #775; this row carries nothing.
  • Checked against origin/main (a673667), not a stale checkout: 144 top-level crates/batten/tests/*.rs, zero [[test]] entries in crates/batten/Cargo.toml, and no crates/batten/tests/*/main.rs. The grouping §3 specifies was never done.
  • **The ratchet was never written. **batten.toml carries tests-not-deleted and bats-tests-not-deleted, both token counts of #[test] occurrences; no policy/*.rego module refuses an added top-level crates/batten/tests/*.rs.

It was a sweep, not a decision. Six rows carry a completedAt inside a 1.7-second window on 2026-08-31: CLOUD-699 (14:19:59.748), CLOUD-1211 (14:19:59.865), CLOUD-1210 (14:20:00.294), CLOUD-592 (14:20:00.588), CLOUD-1208 (14:20:00.756), CLOUD-1216 (14:20:01.463). No human sets six statuses in 1.7s.

The count grew while this was being re-groomed

In the 8 commits between one container's clone (1a79d8a) and origin/main (a673667), top-level test targets went 142 → 144: commit_admission.rs and mediated_admission.rs added, none deleted. That is this row's own "the campaign adds one per retired gate" observed live, over hours rather than the ten days prune.rs recorded — and it is a stronger argument for the ratchet than the 110 → 114 → 118 series the row was filed on.

§8 is discharged, and the measurement it was blocked for now exists

CLOUD-1208 is Done, so the blocker is cleared. Measured 2026-09-01, this container, 4 cores, cargo nextest run --workspace:

phase cold after touching one src/*.rs no-op
build 277s 48.0s 0.4s
run 139s 92.6s 92.6s

One test target rebuilds and links in ~1.0s (touched tests/walker.rs alone: 1.03s cargo). So the 48s decomposes as ~10s lib compile plus 144 targets × ~1.0s at 4-wide ≈ 36s of linking — the link step is up to ~75% of every rebuild, and a rebuild is what a developer pays per edit.

Read the 75% as an upper bound, not a point estimate. It was measured with touch, which changes mtime with identical content: cargo fingerprints on mtime so all 144 targets relink either way (the ~36s stands), but incremental codegen has nothing new to do, so the lib-compile half is understated and a real edit shifts the ratio toward compile. The paired before/after this row's acceptance requires must use a real edit, not a touch.

What this measurement does NOT support, stated because the row has withdrawn two over-claims already: it says nothing about the 92.6s run phase, which is separately measured at 4.01x parallel efficiency on 4 cores and has no scheduling headroom left. Consolidation is a build-time win only, exactly as §2 already says.

Refinement — Ready

  • **Source of truth (§1). **git ls-tree -r --name-only <ref> -- crates/batten/tests/, filtered on ^crates/batten/tests/[^/]+\.rs$, against the artifact census in target/debug/deps. NOT git ls-files 'crates/batten/tests/*.rs', which this clause specified until 2026-09-01: git's default pathspec * matches /, so that form also matches crates/batten/tests/common/mod.rs — a file common/mod.rs:19-21 states is deliberately not a target — and reports one target too many. prune.rs:262-269 is the prior reading and the one this row must not contradict silently.

  • Mechanism (§3). Group the files under crates/batten/tests/<group>/ with a main.rs declaring one mod per former file. Cargo compiles a tests/ subdirectory as a single target exactly when it holds a main.rs — a fact tests/common/mod.rs:19-21 already states and relies on, which is why common/ is not itself a target today. Grouping is mechanical and case-preserving.

  • The ratchet that stops it re-growing (§3), and it counts TARGETS rather than FILES — the distinction is load-bearing. A [[ratchet]] row on the count of cargo test targets, direction non-increasing. ratchet is an existing rule kind (CLOUD-55) and tests-not-deleted is the precedent. Without a ratchet the campaign restores the count one retirement at a time and this row buys a one-off saving instead of a property.

    A FILE-count ratchet would refuse CLOUD-843**'s entire campaign, which is why the spelling is not free. **.claude/rules/toolchain.md's two-shapes rule requires a retirement to land its predicate as a policy/*.rego module **plus a **crates/batten/tests/*.rs tier, and .claude/rules/policy-modules.md states that tier "is crates/batten/tests/*.rs and never a .bats". So every retirement adds a top-level test file by mandate — which is exactly why prune.rs recorded the count moving 110 → 114 → 118 across three readings in ten days. A ratchet over top-level FILES would fire on the next correctly-executed retirement, and the campaign would have to switch it off: the shape a gate does not survive.

    Counting targets removes the collision by construction — a retirement's tier lands as a mod inside an existing group, the target count does not move, and the ratchet never sees it. That is matklad's recommended layout anyway, so the campaign adopts nothing it was not already heading for. But the convention has to be written down when this lands, or the next retirement author creates a new top-level file and trips a gate nobody warned them about.

  • The assertion that makes this safe (§3) — upstream-documented AND asserted here. nextest's design doc states it plainly: "A key factor distinguishing nextest from cargo test is that nextest runs each test in a separate process", giving memory isolation, state isolation and independent termination (why-process-per-test). So consolidation changes the link count and nothing a case can observe. The claim is load-bearing — if it were false this row would be trading isolation for speed — so it ships as a case pinning that a test's visible environment does not depend on its target, rather than resting on the citation alone.

  • What consolidation does NOT buy, stated so it is not over-claimed (§2). It does not reduce the number of test PROCESSES: nextest already spawns one per case, and 3167 of those remain. It does not recover the 97.7s residue — measured, the list phase is 1.75s. The saving is in link count and artifact bytes, and in compile time by the prior art's measurement. Nothing else.

  • Deliberately not in scope (§2). Deleting or merging any test case; tests-not-deleted must stay green across the whole change. Also out of scope: the [prune] floors, which read the same count and are CLOUD-1158's — but that row's basis moves when this lands, so it is named here rather than left to discover it.

  • Output (§7). Counts and target names.

Test obligation

tests-not-deleted green across the change; the target-count ratchet fails on a re-added top-level file; and the isolation case above fails if a grouped test can see something a standalone one cannot.

Commit / bump (§6): test(harness) — no bump. Not breaking for the consumer surface or the library surface: no shipped verb, no public item and no config key moves; the change is entirely under tests/.

Blockers (§8): blocked by CLOUD-1208, because the saving must be measured before and after rather than claimed — the prior art's 3x/5x is a measurement of Cargo's tree and not of this one, and the residue this row once claimed has been falsified. relatedTo CLOUD-766 (same artifacts, disk reading), CLOUD-1158 (same count, as a prune basis), CLOUD-843 (the campaign that grows it), CLOUD-365, CLOUD-352.

The mechanism, RESOLVED (2026-08-30) — this row's last unrefined clause.

What was undecided: §3 names kind = "ratchet", and that kind cannot say this. Its fields are glob, pattern, direction, base, severity, and its own doc defines it as "the total occurrences of pattern across files matching glob" — a token count inside files. Every committed row is that shape: tests-not-deleted counts #[test], bash-surface-not-growing counts #MISE description=. A cargo test-target count is a property of the directory structure plus Cargo's autodiscovery, and no glob+pattern pair computes it.

The answer is a tree-scoped policy module, and the fact it needs already exists. input.tree["base-delta"] is declared in schema/policy-input.schema.json with added, edited, deleted, base-lines, base-date and code-changed; policy/filed-here.rego already reads it. So the predicate is:

refuse when base-delta.added contains a path matching crates/batten/tests/*.rs at the top level — one segment past tests/.

That is the non-increasing target count stated structurally, and it resolves each problem the open question named. No token count — it asks about paths, which is what a target count is a property of. No base-rev position to take — the base delta is the comparison, so the hard half of the ratchet spelling disappears rather than being solved. No spawn — a command row was the other candidate and CLOUD-843's campaign is retiring exactly that shape.

Why the file spelling is sound here when a [[ratchet]] file count was not. Cargo autodiscovery makes top-level tests/*.rs and test targets 1:1, and a file inside a group directory (tests/<group>/foo.rs) is not a target — so "no new top-level crates/batten/tests/*.rs" is "the target count does not grow", exactly. The earlier objection was to a [[ratchet]] counting files, which fires on any added file including a group mod; this fires only on an added TOP-LEVEL one, which is the thing that mints a target. A retirement landing its tier as a mod inside a group is invisible to it, which is the property that keeps CLOUD-843's campaign running.

One trap for whoever writes it. .claude/rules/policy-modules.md's list of input.tree.* keys does not name base-delta (or symbols), though the generated schema declares both and filed-here.rego reads the first. That file states rules-drift holds its lists to the schemas, so the omission is either a drift the gate is not catching or a list that needs updating — worth resolving in the same change, because an author who checks the documented list will conclude the fact does not exist and reach for a worse mechanism. That is exactly the silent-dead-gate class that file is otherwise about.

Acceptance

  • The target count falls and the ratchet holds it.
  • The delta is quoted from CLOUD-1208's harness against its measured null, not from a hand timing.
  • No case is deleted, and the isolation assertion exists and is shown able to fail.

Review in Linear

@coderabbitai

coderabbitai Bot commented Sep 1, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 321 files, which is 171 over the limit of 150.

To get a review, reduce the PR to 150 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to a paid plan to raise the limit.

Usage-priced reviews support at most 300 files.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Free

Run ID: 7b9a54bc-6355-4f48-bf22-52e1b7f9c25c

📥 Commits

Reviewing files that changed from the base of the PR and between 26ae231 and 4d5bd0c.

⛔ Files ignored due to path filters (9)
  • crates/batten/tests/snapshots/snapshots__golden_exit_code_table.snap is excluded by !**/*.snap
  • crates/batten/tests/snapshots/snapshots__golden_json_schema.snap is excluded by !**/*.snap
  • crates/batten/tests/snapshots/snapshots__json_output_is_frozen.snap is excluded by !**/*.snap
  • crates/batten/tests/snapshots/snapshots__pointer_output_is_frozen.snap is excluded by !**/*.snap
  • hk.pkl is excluded by !**/*.pkl
  • crates/batten/tests/it/snapshots/it__snapshots__golden_exit_code_table.snap is excluded by !**/*.snap
  • crates/batten/tests/it/snapshots/it__snapshots__golden_json_schema.snap is excluded by !**/*.snap
  • crates/batten/tests/it/snapshots/it__snapshots__json_output_is_frozen.snap is excluded by !**/*.snap
  • crates/batten/tests/it/snapshots/it__snapshots__pointer_output_is_frozen.snap is excluded by !**/*.snap
📒 Files selected for processing (321)
  • .claude/rules/policy-modules.md
  • .claude/rules/rust.md
  • .claude/rules/scanning.md
  • .claude/rules/toolchain.md
  • Cargo.toml
  • README.md
  • batten.toml
  • crates/batten/src/exec.rs
  • crates/batten/src/facts.rs
  • crates/batten/src/hook.rs
  • crates/batten/src/lib.rs
  • crates/batten/src/outputs.rs
  • crates/batten/src/perf.rs
  • crates/batten/src/policy.rs
  • crates/batten/src/ready.rs
  • crates/batten/src/rules.rs
  • crates/batten/src/taskset.rs
  • crates/batten/src/uses.rs
  • crates/batten/tests/acceptance_corpus.rs
  • crates/batten/tests/acquisition_metric.rs
  • crates/batten/tests/acquisition_sweep.rs
  • crates/batten/tests/admission.rs
  • crates/batten/tests/advisory_drain.rs
  • crates/batten/tests/agent_facts.rs
  • crates/batten/tests/ambient_authority.rs
  • crates/batten/tests/attribution.rs
  • crates/batten/tests/authority_replay.rs
  • crates/batten/tests/baseline.rs
  • crates/batten/tests/bats_invocation.rs
  • crates/batten/tests/board_receipts.rs
  • crates/batten/tests/board_record.rs
  • crates/batten/tests/bundle.rs
  • crates/batten/tests/bypass_scrub.rs
  • crates/batten/tests/call_arguments.rs
  • crates/batten/tests/call_background_flag.rs
  • crates/batten/tests/call_ceiling.rs
  • crates/batten/tests/capture_fidelity.rs
  • crates/batten/tests/captured_facts.rs
  • crates/batten/tests/checks_green.rs
  • crates/batten/tests/ci_hygiene.rs
  • crates/batten/tests/ci_parity.rs
  • crates/batten/tests/ci_suite_lane.rs
  • crates/batten/tests/claim.rs
  • crates/batten/tests/claim_receipt.rs
  • crates/batten/tests/cli.rs
  • crates/batten/tests/commit.rs
  • crates/batten/tests/commit_admission.rs
  • crates/batten/tests/commit_meta_facts.rs
  • crates/batten/tests/common/mod.rs
  • crates/batten/tests/config_authority_boundary.rs
  • crates/batten/tests/config_base_ref_reading.rs
  • crates/batten/tests/config_deprecations.rs
  • crates/batten/tests/config_epoch.rs
  • crates/batten/tests/config_in_directory.rs
  • crates/batten/tests/config_lint.rs
  • crates/batten/tests/config_provenance.rs
  • crates/batten/tests/config_schema.rs
  • crates/batten/tests/config_show.rs
  • crates/batten/tests/config_trust.rs
  • crates/batten/tests/connector_allow_door.rs
  • crates/batten/tests/connector_not_granted.rs
  • crates/batten/tests/connector_verbs.rs
  • crates/batten/tests/contract_drift.rs
  • crates/batten/tests/decision_record.rs
  • crates/batten/tests/defects.rs
  • crates/batten/tests/derived_facts.rs
  • crates/batten/tests/design_audit.rs
  • crates/batten/tests/dev_profile.rs
  • crates/batten/tests/doctor.rs
  • crates/batten/tests/document_facts.rs
  • crates/batten/tests/document_read_count.rs
  • crates/batten/tests/done_not_landed.rs
  • crates/batten/tests/enforce_journal.rs
  • crates/batten/tests/extension_surfaces.rs
  • crates/batten/tests/external_facts.rs
  • crates/batten/tests/extracted_facts.rs
  • crates/batten/tests/facts.rs
  • crates/batten/tests/fail_on_warning.rs
  • crates/batten/tests/filed_here.rs
  • crates/batten/tests/fixture_repos.rs
  • crates/batten/tests/forge_facts.rs
  • crates/batten/tests/fuzz_corpus.rs
  • crates/batten/tests/gh_guard.rs
  • crates/batten/tests/git_facts.rs
  • crates/batten/tests/glob_exclusion.rs
  • crates/batten/tests/guardrail_bypass.rs
  • crates/batten/tests/harness_grant.rs
  • crates/batten/tests/history_facts.rs
  • crates/batten/tests/hk_fix_selection.rs
  • crates/batten/tests/hook_profile.rs
  • crates/batten/tests/hook_worktree_root.rs
  • crates/batten/tests/identity_churn.rs
  • crates/batten/tests/identity_precedence.rs
  • crates/batten/tests/init.rs
  • crates/batten/tests/inverted_board_cases.rs
  • crates/batten/tests/issue_key.rs
  • crates/batten/tests/judge_kind.rs
  • crates/batten/tests/mcp_dispatch.rs
  • crates/batten/tests/mediated_admission.rs
  • crates/batten/tests/mediated_verbs.rs
  • crates/batten/tests/memories.rs
  • crates/batten/tests/memory_injection.rs
  • crates/batten/tests/mise_pin_agreement.rs
  • crates/batten/tests/narrow_adoption.rs
  • crates/batten/tests/perf_pair.rs
  • crates/batten/tests/pinned_programs.rs
  • crates/batten/tests/pipeline_shapes.rs
  • crates/batten/tests/pointer_only.rs
  • crates/batten/tests/policy_engine_count.rs
  • crates/batten/tests/policy_input_narrowing.rs
  • crates/batten/tests/policy_input_schema.rs
  • crates/batten/tests/policy_modules.rs
  • crates/batten/tests/policy_presets.rs
  • crates/batten/tests/policy_severity.rs
  • crates/batten/tests/policy_test_suite.rs
  • crates/batten/tests/policy_tree.rs
  • crates/batten/tests/policy_whole_set.rs
  • crates/batten/tests/pr_watch.rs
  • crates/batten/tests/prebuilt_lint.rs
  • crates/batten/tests/preset_segments.rs
  • crates/batten/tests/primitives.rs
  • crates/batten/tests/privileged_lane.rs
  • crates/batten/tests/process_group.rs
  • crates/batten/tests/prose_only.rs
  • crates/batten/tests/prospective_facts.rs
  • crates/batten/tests/provision.rs
  • crates/batten/tests/ratchet.rs
  • crates/batten/tests/ready.rs
  • crates/batten/tests/reference_coverage.rs
  • crates/batten/tests/remedy_authorship.rs
  • crates/batten/tests/retirement_doctrine.rs
  • crates/batten/tests/review_answered.rs
  • crates/batten/tests/rule_cost_census.rs
  • crates/batten/tests/rules_builtin_claims.rs
  • crates/batten/tests/rules_drift.rs
  • crates/batten/tests/run_shape.rs
  • crates/batten/tests/run_shape_guard_door.rs
  • crates/batten/tests/runner_verdict.rs
  • crates/batten/tests/scanner_taxonomy.rs
  • crates/batten/tests/secrets_kind.rs
  • crates/batten/tests/semver_gate.rs
  • crates/batten/tests/shell_retirement.rs
  • crates/batten/tests/shell_write_advisory.rs
  • crates/batten/tests/sinks.rs
  • crates/batten/tests/skill_contract.rs
  • crates/batten/tests/sleep_ban.rs
  • crates/batten/tests/snapshots.rs
  • crates/batten/tests/spawn_ceilings.rs
  • crates/batten/tests/spawn_census.rs
  • crates/batten/tests/staged_facts.rs
  • crates/batten/tests/stop_posture.rs
  • crates/batten/tests/submodule.rs
  • crates/batten/tests/suite_subjects.rs
  • crates/batten/tests/surface.rs
  • crates/batten/tests/symbols.rs
  • crates/batten/tests/target_prune.rs
  • crates/batten/tests/task_prose.rs
  • crates/batten/tests/task_receipt.rs
  • crates/batten/tests/todo_promotion.rs
  • crates/batten/tests/tool_selector.rs
  • crates/batten/tests/tool_verdict_facts.rs
  • crates/batten/tests/use_graph.rs
  • crates/batten/tests/verdict_registry.rs
  • crates/batten/tests/waivers.rs
  • crates/batten/tests/walker.rs
  • crates/batten/tests/wiring_reclaim.rs
  • crates/batten/tests/zero_config.rs
  • mise.toml
  • crates/batten/examples/config-load-bench.rs
  • crates/batten/tests/it/acceptance_corpus.rs
  • crates/batten/tests/it/acquisition_metric.rs
  • crates/batten/tests/it/acquisition_sweep.rs
  • crates/batten/tests/it/admission.rs
  • crates/batten/tests/it/advisory_drain.rs
  • crates/batten/tests/it/agent_facts.rs
  • crates/batten/tests/it/ambient_authority.rs
  • crates/batten/tests/it/attribution.rs
  • crates/batten/tests/it/authority_replay.rs
  • crates/batten/tests/it/baseline.rs
  • crates/batten/tests/it/bats_invocation.rs
  • crates/batten/tests/it/board_receipts.rs
  • crates/batten/tests/it/board_record.rs
  • crates/batten/tests/it/bundle.rs
  • crates/batten/tests/it/bypass_scrub.rs
  • crates/batten/tests/it/call_arguments.rs
  • crates/batten/tests/it/call_background_flag.rs
  • crates/batten/tests/it/call_ceiling.rs
  • crates/batten/tests/it/capture_fidelity.rs
  • crates/batten/tests/it/captured_facts.rs
  • crates/batten/tests/it/checks_green.rs
  • crates/batten/tests/it/ci_hygiene.rs
  • crates/batten/tests/it/ci_parity.rs
  • crates/batten/tests/it/ci_suite_lane.rs
  • crates/batten/tests/it/claim.rs
  • crates/batten/tests/it/claim_receipt.rs
  • crates/batten/tests/it/cli.rs
  • crates/batten/tests/it/commit.rs
  • crates/batten/tests/it/commit_admission.rs
  • crates/batten/tests/it/commit_meta_facts.rs
  • crates/batten/tests/it/common/mod.rs
  • crates/batten/tests/it/config_authority_boundary.rs
  • crates/batten/tests/it/config_base_ref_reading.rs
  • crates/batten/tests/it/config_deprecations.rs
  • crates/batten/tests/it/config_epoch.rs
  • crates/batten/tests/it/config_in_directory.rs
  • crates/batten/tests/it/config_lint.rs
  • crates/batten/tests/it/config_provenance.rs
  • crates/batten/tests/it/config_schema.rs
  • crates/batten/tests/it/config_show.rs
  • crates/batten/tests/it/config_trust.rs
  • crates/batten/tests/it/connector_allow_door.rs
  • crates/batten/tests/it/connector_not_granted.rs
  • crates/batten/tests/it/connector_verbs.rs
  • crates/batten/tests/it/contract_drift.rs
  • crates/batten/tests/it/decision_record.rs
  • crates/batten/tests/it/defects.rs
  • crates/batten/tests/it/derived_facts.rs
  • crates/batten/tests/it/design_audit.rs
  • crates/batten/tests/it/dev_profile.rs
  • crates/batten/tests/it/doctor.rs
  • crates/batten/tests/it/document_facts.rs
  • crates/batten/tests/it/document_read_count.rs
  • crates/batten/tests/it/done_not_landed.rs
  • crates/batten/tests/it/enforce_journal.rs
  • crates/batten/tests/it/extension_surfaces.rs
  • crates/batten/tests/it/external_facts.rs
  • crates/batten/tests/it/extracted_facts.rs
  • crates/batten/tests/it/facts.rs
  • crates/batten/tests/it/fail_on_warning.rs
  • crates/batten/tests/it/filed_here.rs
  • crates/batten/tests/it/fixture_repos.rs
  • crates/batten/tests/it/forge_facts.rs
  • crates/batten/tests/it/fuzz_corpus.rs
  • crates/batten/tests/it/gh_guard.rs
  • crates/batten/tests/it/git_facts.rs
  • crates/batten/tests/it/glob_exclusion.rs
  • crates/batten/tests/it/guardrail_bypass.rs
  • crates/batten/tests/it/harness_grant.rs
  • crates/batten/tests/it/history_facts.rs
  • crates/batten/tests/it/hk_fix_selection.rs
  • crates/batten/tests/it/hook_profile.rs
  • crates/batten/tests/it/hook_worktree_root.rs
  • crates/batten/tests/it/identity_churn.rs
  • crates/batten/tests/it/identity_precedence.rs
  • crates/batten/tests/it/init.rs
  • crates/batten/tests/it/inverted_board_cases.rs
  • crates/batten/tests/it/issue_key.rs
  • crates/batten/tests/it/judge_kind.rs
  • crates/batten/tests/it/main.rs
  • crates/batten/tests/it/mcp_dispatch.rs
  • crates/batten/tests/it/mediated_admission.rs
  • crates/batten/tests/it/mediated_verbs.rs
  • crates/batten/tests/it/memories.rs
  • crates/batten/tests/it/memory_injection.rs
  • crates/batten/tests/it/mise_pin_agreement.rs
  • crates/batten/tests/it/narrow_adoption.rs
  • crates/batten/tests/it/perf_pair.rs
  • crates/batten/tests/it/pinned_programs.rs
  • crates/batten/tests/it/pipeline_shapes.rs
  • crates/batten/tests/it/pointer_only.rs
  • crates/batten/tests/it/policy_engine_count.rs
  • crates/batten/tests/it/policy_input_narrowing.rs
  • crates/batten/tests/it/policy_input_schema.rs
  • crates/batten/tests/it/policy_presets.rs
  • crates/batten/tests/it/policy_severity.rs
  • crates/batten/tests/it/policy_test_suite.rs
  • crates/batten/tests/it/policy_tree.rs
  • crates/batten/tests/it/policy_whole_set.rs
  • crates/batten/tests/it/pr_watch.rs
  • crates/batten/tests/it/prebuilt_lint.rs
  • crates/batten/tests/it/preset_segments.rs
  • crates/batten/tests/it/primitives.rs
  • crates/batten/tests/it/privileged_lane.rs
  • crates/batten/tests/it/process_group.rs
  • crates/batten/tests/it/prose_only.rs
  • crates/batten/tests/it/prospective_facts.rs
  • crates/batten/tests/it/provision.rs
  • crates/batten/tests/it/ratchet.rs
  • crates/batten/tests/it/ready.rs
  • crates/batten/tests/it/reference_coverage.rs
  • crates/batten/tests/it/remedy_authorship.rs
  • crates/batten/tests/it/retirement_doctrine.rs
  • crates/batten/tests/it/review_answered.rs
  • crates/batten/tests/it/rule_cost_census.rs
  • crates/batten/tests/it/rules_builtin_claims.rs
  • crates/batten/tests/it/rules_drift.rs
  • crates/batten/tests/it/run_shape.rs
  • crates/batten/tests/it/run_shape_guard_door.rs
  • crates/batten/tests/it/runner_verdict.rs
  • crates/batten/tests/it/scanner_taxonomy.rs
  • crates/batten/tests/it/secrets_kind.rs
  • crates/batten/tests/it/semver_gate.rs
  • crates/batten/tests/it/shell_retirement.rs
  • crates/batten/tests/it/shell_write_advisory.rs
  • crates/batten/tests/it/sinks.rs
  • crates/batten/tests/it/skill_contract.rs
  • crates/batten/tests/it/sleep_ban.rs
  • crates/batten/tests/it/snapshots.rs
  • crates/batten/tests/it/spawn_ceilings.rs
  • crates/batten/tests/it/spawn_census.rs
  • crates/batten/tests/it/staged_facts.rs
  • crates/batten/tests/it/stop_posture.rs
  • crates/batten/tests/it/submodule.rs
  • crates/batten/tests/it/suite_subjects.rs
  • crates/batten/tests/it/surface.rs
  • crates/batten/tests/it/symbols.rs
  • crates/batten/tests/it/target_consolidation.rs
  • crates/batten/tests/it/target_prune.rs
  • crates/batten/tests/it/task_prose.rs
  • crates/batten/tests/it/task_receipt.rs
  • crates/batten/tests/it/test_targets.rs
  • crates/batten/tests/it/todo_promotion.rs
  • crates/batten/tests/it/tool_selector.rs
  • crates/batten/tests/it/tool_verdict_facts.rs
  • crates/batten/tests/it/use_graph.rs
  • crates/batten/tests/it/verdict_registry.rs
  • crates/batten/tests/it/waivers.rs
  • crates/batten/tests/it/walker.rs
  • crates/batten/tests/it/wiring_reclaim.rs
  • crates/batten/tests/it/zero_config.rs
  • policy/test-targets.rego

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


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 Essentials by visiting https://app.coderabbit.ai/settings/billing.

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

@wenzowski
wenzowski force-pushed the claude/test-loop-cost-bundle-nrwv0k branch 4 times, most recently from 5401b6d to 94571b1 Compare September 1, 2026 09:26
… was unpinned

CLOUD-1211 reached Done with its acceptance clause reading "split-debuginfo = unpacked is still unmeasured. Outstanding." This measures it, and finds a second, independent hole in the same file.

MEASURED as a paired A/B: three cold builds back to back in one process so machine noise is common-mode, two identical baselines bracketing the candidate.

  arm         cold wall   artifacts   linked bytes      mean
  base-1          223.4s         147      10.19 GB   69.3 MB
  unpacked        208.9s         147       4.99 GB   34.0 MB
  base-2          213.7s         147      10.19 GB   69.3 MB

TIME IS INSIDE THE NULL and is not a finding. The two identical baselines differ by 9.7s on their own (spread 0.957) and the candidate sits at 0.956 against their mean. That is the expected result: the prior art is macOS dsymutil, and unpacked is already cargo's default there and not on Linux.

BYTES ARE THE FINDING, on a null of zero width — the two baselines are byte-identical at 10193048624. 2.04x off the linked binaries, adopted on that column alone, which is what the row asks for.

AND THEY LEAVE THE DISK rather than moving to a sibling, which the linked census alone cannot answer: deps 10.97 GB -> 5.87 GB, target/debug 13.03 GB -> 7.76 GB, the 5.20 GB replaced by 111.8 MB of .dwo. Those .dwo files are a class prune.rs's closed RECLAIMED_KINDS cannot see, so they accumulate per build hash; filed as CLOUD-1293 rather than fixed here, since the fix is in prune.rs and out of this row's scope.

debug = 1 is untouched, so this changes where debuginfo is stored and never whether it exists.

SEPARATELY, opt-level = 2 was unpinned. dev_profile.rs carried four cases and zero occurrences of the key, yet it is CLOUD-1211's biggest single win — test:cargo warm 100.189s to 48.581s, 2.06x. Dropping it reds nothing and doubles every suite run. Asserted now, on a shared dependency_override() lookup, with the anti-vacuity case that pins the panicking read: the dev profile's opt-level default is 0 rather than an absent key, so a defaulting lookup would report the unoptimised build as satisfied.

Both new assertions shown able to fail — removing each key reds the suite (exit 100, 101).

Refs: CLOUD-1289, CLOUD-1211, CLOUD-766, CLOUD-1293
The harness re-read and re-parsed this repository's own 356 KB batten.toml on every fixture command it built — bypass_env_vars() from batten(), plus declared_patterns() and committed_patterns() — across 761 static call sites with no memoization anywhere in the test tree.

MEASURED FIRST, because this row's own estimate was refuted before it was written: taking the cost through `batten config show` measures the same to within noise from a directory holding no config at all, so a 29 ms verb cannot resolve it. The new `mise run config-load-bench` times the function instead.

  arm=load  p50=10.48 p95=11.67 mean=10.64 runs=200
  arm=parse p50=10.35 p95=12.11 mean=10.63 runs=200
  arm=null  p50=10.49 p95=11.85 mean=10.67 runs=200
  ratio=parse/load value=0.988

10.48 ms per call, ten times the row's ~1 ms guess. parse/load at 0.988 says the READ is 1.2% of it; the cost is the parse and its validate passes.

SUITE DELTA, paired on one machine, two timed runs per arm after a discarded warmup, on nextest's own reported duration so the forced rebuild is excluded:

  base      60.159s  60.367s  mean 60.263s  3512 tests
  memoized  53.536s  53.746s  mean 53.641s  3512 tests

Ratio 0.890 — 11.0%, 6.62s — against a within-arm null of 1.0035 and 1.0039, so ~28x the noise. Identical test count, both arms fully green.

A first attempt at that half was unreadable and the correction is the interesting part: two IDENTICAL base runs measured 77.3s and 60.9s, a null spread of 0.788, because run one pays the page cache for 147 freshly linked binaries.

What is memoized is the RESULT of reading the committed config, never a hand-written list of hatch names — CLOUD-1227's derivation is why that distinction matters, and it is untouched. Every signature is unchanged, so no caller had to know.

The bench lives in perf.rs for the reason acquisition-bench does: Record is a contract perf-compare parses and perf-gate greps, and the percentile convention behind p50 has to have one author. summarise() is extracted from record() rather than duplicated beside it. The arm takes its own BENCH_METRIC stamp, and acquisition_metric.rs is generalised to a table asserting every bench task carries one and that no two collide — a property one task alone could not have.

Refs: CLOUD-1291, CLOUD-1227, CLOUD-1211, CLOUD-1210
…t already held

git() spawned two git processes before a fixture repository held anything — init -q, then branch -M main — and base_commit() spent a third on the same rename. Across 150 .git() and 100 .base_commit() call sites that is ~250 process spawns for a repository state that was already correct without them.

The rename was redundant, not merely cheap: git_command pins -c init.defaultBranch=main on every invocation, so the default already IS main. Verified through those same pinned flags on git 2.43.0 — init -q alone leaves main, and it is still main after the first commit.

DELETED RATHER THAN REPLACED BY -b main, which is what keeps this free of a version floor. -b arrived in git 2.28 and nothing in [tools] pins git, so the flag would put a requirement on the developer's machine that mise.lock cannot hold, to restate a default this harness already controls.

Dropping base_commit()'s rename was checked rather than assumed: all 100 call sites are preceded by .git() in the same statement chain, zero counterexamples, so no fixture reaches it through an initialisation whose branch the rename was normalising.

The existing suite is the test — ~150 fixtures build a repository through this path — and it is green at 3512 tests, the same count as before the change rather than merely passing.

Refs: CLOUD-1290, CLOUD-63
…ne each

PIPE_DRAIN_TIMEOUT is 10s and the_drain_deadline_is_long_enough_to_be_about_a_leak asserts that floor. Both read as "a leaked grandchild costs at most ten seconds". It cost twenty: the two drains were collected one after the other, each passed the full constant, so a grandchild holding both pipes open spent it twice.

The tee threads were never the problem and are untouched — one per pipe, started at spawn time, already concurrent, and rust.md's concurrency table records that row as staying. What was serial is the DEADLINE ACCOUNTING. Instant::now() is taken once before the first stream and the second gets PIPE_DRAIN_TIMEOUT minus what the first spent.

Measured over the compiled binary: a_surviving_grandchild_cannot_hang_exec 20.07s -> 10.044s. That case leaks BOTH pipes already, so it is the both-leaked scenario the fix is about; its one-minute bound simply could not tell ten seconds from twenty. Tightened to 15s rather than adding a second test, which would have to leak both pipes to mean anything and would cost another ten seconds of wall clock for coverage this fixture already has. Shown able to fail: reverting only the two call sites reds it at 20.148s.

The zero-remaining half is handled and the row's premise for it is CORRECTED rather than quietly adopted. CLOUD-1288 predicted a false "did not reach EOF" notice on a cleanly-finished second stream, on the premise that recv_timeout(Duration::ZERO) can report Timeout without examining a value already in the channel. Measured on this toolchain, that does not reproduce: 10,000 trials over a ready channel returned the value 10,000 times and timed out zero.

collect_within asks the two questions separately anyway — try_recv for "has this stream already finished", and only then the remaining budget for "how long may I wait" — because answering the first through the second leaves a byte-stable channel depending on an mpsc detail std does not promise. a_finished_stream_emits_no_notice_when_the_budget_is_gone pins the property, and its doc says plainly that it does not discriminate the two spellings here rather than claiming a failure it was not shown.

PIPE_DRAIN_TIMEOUT and its floor assertion are untouched. Drain::collect is removed rather than annotated: with the budget shared it had no production caller left.

Refs: CLOUD-1288
@wenzowski
wenzowski force-pushed the claude/test-loop-cost-bundle-nrwv0k branch from 94571b1 to 92ba3d6 Compare September 1, 2026 09:41
…et the count

Cargo autodiscovered one test target per top-level crates/batten/tests/*.rs and
rustc relinked the whole closure — gix, regorus, syn, clap, jsonschema,
hyper/rustls — into each of 144 binaries. matklad's Delete Cargo Integration
Tests states the defect and the layout; Cargo's own repository measured 3x off
test compile time and 5x off artifacts making the same move.

MEASURED HERE, paired on one machine, with a REAL edit rather than a touch — the
row is explicit that touch changes mtime with identical content, so every target
relinks but incremental codegen has nothing new to do, which understates the
lib-compile half:

  rebuild after editing one src/*.rs, 144 targets:  48.0s
  rebuild after editing one src/*.rs, 2 targets:     7.4s / 7.3s

6.5x, against a within-arm null of 0.986. Bytes, cold and clean, against the same
container's 144-target census taken under the same adopted split-debuginfo
profile:

                     144 targets     2 targets
  linked artifacts           147             4
  linked bytes           4.99 GB        234 MB
  deps total             5.87 GB       1.09 GB
  target/debug           7.76 GB       2.05 GB

21x off the linked binaries and 3.8x off target/debug — the reading CLOUD-1210's
third section needed: a verify lap that consumed more than the container could
hold now fits several times over.

The run phase is untouched at 3519 tests, exactly as the row's §2 says: this is a
build-time and bytes change and nothing else.

THE RATCHET IS THE POINT, not the one-off saving. policy/test-targets.rego
refuses an ADDED path that is a top-level crates/batten/tests/*.rs — four path
segments — because cargo autodiscovery makes that 1:1 with a target. A file one
segment deeper is a module and costs nothing, which is what keeps CLOUD-843's
campaign able to land its mandated tier without the gate having to be switched
off.

ITS DEPTH TEST SHIPPED WRONG FIRST AND THE SECOND TIER CAUGHT IT. The count was
written as 5 where the path splits to 4, exactly inverted: it refused the grouped
module and allowed the new target. The module's own test_ rules agreed with the
mistake, because a with-input case is only as right as its author.
crates/batten/tests/it/test_targets.rs over the compiled engine is what failed —
the class .claude/rules/policy-modules.md calls the second tier not optional for,
and it is now the first #MUTANT row.

ONE FILE STAYS A TARGET, AND THE REASON IS A GATE RATHER THAN A COMPROMISE.
policy_modules.rs keeps its own target because `evaluator-io-check` probes it
with `cargo test --test policy_modules`, and that task is a governed
mise-tasks/*.sh: shell-retirement gives it exactly two shapes — retire it
whole, or leave it alone — so repointing the probe is not an edit this change
may make. Measured, not assumed: the edit was attempted and batten-check
refused it as shell-rule-retired. One extra link is the cheaper side of that
trade against a gate that stays live, and the ratchet is unaffected — the file
exists at base, so it is never in base-delta.added.

THE MOVE BREAKS EVERY GLOB THAT SPELLED tests/*.rs, and that is the silent half.
batten's globs compile with literal_separator(true), so * stops at a /: five
committed rows plus one hardcoded in rules.rs would have selected NOTHING and
reported clean over an empty set. Repointed to **/*.rs. Four nextest filtersets
named binaries that are now modules; --no-tests=fail is what made those loud.
mise-tasks/replay.sh needs no edit — its declared_in feeds git grep as a
PATHSPEC, where * does cross a /, so the governed file is untouched.

AND IT MOVES [prune]'S BASIS, which CLOUD-1210's §2 predicted and scoped out.
The floors were measured against a tree where a tracked test FILE was a proxy for
a linked STEM — exact while cargo autodiscovered one target per file, and no
longer a series at all once 144 became 2. target-prune refused on it (declared
140, live 152, tolerance 10), so count and measured move together as that block
instructs. The FLOORS deliberately do not move: both are now far above what the
tree needs, which is the safe direction on the block's own terms, and re-deriving
them downward needs the independent measurement it names — CLOUD-1158's.

target_consolidation.rs asserts what makes this safe rather than citing it:
nextest runs each test in its own process, so isolation survives the target
boundary going away.

Also repointed: insta's four snapshots (payloads byte-identical, only the source
header moved), relative include! paths, the retirement ledger's carried arms, and
the doc citations README and .claude/rules/* make.

Refs: CLOUD-1210, CLOUD-766, CLOUD-1158, CLOUD-843, CLOUD-55

Admits: ffdf8396b47739f87822037710ac7635da092135ea0d60a06a35be196015e8a0
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: 04389bfcbc45e8c84742656a351835cfd5cf25cd
Admits-epoch: 6653a7e618ce30bcce12361e2d99a731c68d00143157f0e3af4be10ca574ba0b
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: CLOUD-1210's ratchet cannot be registered at all. The grouping would buy a one-off saving instead of a property: cargo autodiscovery mints a target per top-level crates/batten/tests/*.rs, CLOUD-843's retirement campaign adds one per retired gate by mandate, and the count was measured climbing 142 to 144 in eight commits. Without the row the 144-to-1 consolidation regrows one retirement at a time and nothing reds.
Admits-answer-precondition: batten.toml is the one committed authority for [[rule]] and [[verdict]] rows (house-style §8), so a new policy rule and the verdict token it raises have no other surface to be expressed on: a module raising a token no row declares fails to LOAD, and a row nothing raises fails the load too, so both halves must land in this file or the change cannot exist. The write is CLOUD-1210's registration of policy/test-targets.rego, and it lands in the diff of a pull request a reviewer reads.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because this file IS the owning surface for rule and verdict registration; there is no other one to route to. R-RESTORE-IT does not apply because nothing was damaged to restore — this is an addition of two rows, not a repair of an edit that should not have happened.
Admits: 8e8a4fd331608067ab55d5c6d020737b55aa7066d575634f9ac6cb95c653ae99
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: 04389bfcbc45e8c84742656a351835cfd5cf25cd
Admits-epoch: 08c3d9251f7d28897e729d704e02166eeda5345cbe38ff8dc7205bcebc200360
Admits-author: alec@wenzowski.com
Admits-prev: ffdf8396b47739f87822037710ac7635da092135ea0d60a06a35be196015e8a0
Admits-answer-lost: policy/test-targets.rego would sit in the tree registered by nothing and decide nothing, while V-TEST-TARGET-ADDED would be a declared verdict no rule raises — which fails the load in the other direction. CLOUD-1210's grouping would then hold no property: the 144-to-1 consolidation regrows one retirement at a time, unrefused.
Admits-answer-precondition: The second half of the same registration: policy/test-targets.rego needs a [[rule]] row to be loaded at all, and batten.toml is the one committed authority for rules (house-style §8). A module no row registers is never evaluated — a dead gate that loads clean, which is the exact failure class .claude/rules/policy-modules.md exists to warn about. The verdict half landed under the previous admission; this is the row that makes it reachable, in the same pull request diff.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because batten.toml IS the owning surface for [[rule]] registration; there is nowhere else to route a rule row. R-RESTORE-IT does not apply because nothing was damaged — this adds a row rather than repairing an edit that should not have happened.
Admits: 49aafbb7b01db2868770e47436354525049360ca541e4fe11a0624549039cdac
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: 04389bfcbc45e8c84742656a351835cfd5cf25cd
Admits-epoch: 29cd0ab87cc14f65ac2eb49fc3fafc5aaefbaa196d5e3e29903e0779ff844021
Admits-author: alec@wenzowski.com
Admits-prev: 8e8a4fd331608067ab55d5c6d020737b55aa7066d575634f9ac6cb95c653ae99
Admits-answer-lost: Five gates go silently dead: the shell-retirement ledger's declared_in, the line_sources for its arms, an exact-path row, and two rows globbing the test tier. Each would then report clean over an empty file set, which is worse than a wrong answer because a gate that found nothing looks exactly like a gate that passed.
Admits-answer-precondition: CLOUD-1210 moves every top-level test file into the tests/it/ group, and batten's globs compile with globset literal_separator(true) — so an asterisk stops at a slash and five committed rows would match NOTHING afterwards. A glob that selects nothing is a gate passing on emptiness, the silent-dead-gate class this repository is most exposed to. Those rows live in this file and nowhere else, so repointing them is only expressible here, in the same pull request diff as the move a reviewer reads.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because this file IS the owning surface for a rule's globs; there is nowhere else to route them. R-RESTORE-IT does not apply because nothing is being restored — the paths these rows name are moving, and the rows must follow or they select nothing.
Admits: 2899d8657165ec05ec52be34efeefcdcd23242ddf7c5cb1bc4052a500c2f06a2
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: policy/test-targets.rego
Admits-head: 04389bfcbc45e8c84742656a351835cfd5cf25cd
Admits-epoch: 7f31bd1e3f27a45f279c38462b59167c4592ef855b219bf5904a60aaf6a5bdba
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The ratchet ships backwards. It would refuse every retirement's tier landing correctly inside the group, and allow the new top-level file that mints a second cargo test target, so CLOUD-1210's 144-to-1 consolidation would regrow while the gate reported clean and blocked the campaign it was designed to survive.
Admits-answer-precondition: Fixing a defect in the module I am landing under CLOUD-1210, found by its own compiled-binary tier. The depth test was written as count(segments)==5, which is exactly inverted: crates/batten/tests/x.rs splits to FOUR segments, so the rule refused the grouped module and allowed the new target — the one direction that fails silently. A module's predicate is only expressible in the module, so this write is the only route, and it lands in the pull request diff a reviewer reads.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because this file IS the owning surface for its own predicate. R-RESTORE-IT does not apply because there is nothing to restore — the file is new in this branch and has never been correct; this is the fix, not a revert.
Admits: eb9e5bb3a4314b722a1e01e445201182b23e80ca40b64cbdd544827434f6935b
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: policy/test-targets.rego
Admits-head: 04389bfcbc45e8c84742656a351835cfd5cf25cd
Admits-epoch: 7f31bd1e3f27a45f279c38462b59167c4592ef855b219bf5904a60aaf6a5bdba
Admits-author: alec@wenzowski.com
Admits-prev: 2899d8657165ec05ec52be34efeefcdcd23242ddf7c5cb1bc4052a500c2f06a2
Admits-answer-lost: The gate cannot land: mutant-census exits 1, so verify and CI refuse the change. And the substantive loss it is pointing at is real — a gate covered by nothing stronger than its own green suite is the vacuity CLOUD-418 measured four times, so the discriminating mutations have to be written down even where no runner can drive them yet.
Admits-answer-precondition: mutant-census refuses the new module as uncovered, and its own remedy is a directive that can only live inside the module: a #MUTANT row declaring a discriminating mutation, plus a #MUTANT-EXEMPT naming the issue, exactly as policy/ci-parity.rego and policy/ci-suite-lane.rego carry them. There is no other surface for a comment directive in a .rego file, and the write lands in the same pull request diff a reviewer reads.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE does not apply because a #MUTANT directive is resolved from the module file itself; there is no owning surface elsewhere. R-RESTORE-IT does not apply because nothing is being restored — this adds the coverage declaration the census asks for.
Weakens: rule-predicate-changed rule[bats-tests-not-deleted].conserves
Weakens: rule-predicate-changed rule[no-key-leaves-the-schema-unannounced].glob
Weakens: rule-predicate-changed rule[shell-retirement].line_sources
@wenzowski
wenzowski force-pushed the claude/test-loop-cost-bundle-nrwv0k branch from 92ba3d6 to 4d5bd0c Compare September 1, 2026 09:47
@sonarqubecloud

sonarqubecloud Bot commented Sep 1, 2026

Copy link
Copy Markdown

❌ The last analysis has failed.

See analysis details on SonarQube Cloud

@wenzowski wenzowski changed the title test-loop cost: five rows, from the profile measurement to 144 test targets becoming one test-loop cost: five rows, from the profile measurement to 144 test targets becoming two Sep 1, 2026
@wenzowski
wenzowski marked this pull request as ready for review September 1, 2026 10:00
@wenzowski

Copy link
Copy Markdown
Contributor Author

/fast-forward

@wenzowski
wenzowski merged commit 4d5bd0c into main Sep 1, 2026
19 of 20 checks passed
@wenzowski
wenzowski deleted the claude/test-loop-cost-bundle-nrwv0k branch September 1, 2026 10:29
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