diff --git a/crates/batten/tests/it/verify_unprovisioned.rs b/crates/batten/tests/it/verify_unprovisioned.rs index 7d98c2ec6..366ddfc5d 100644 --- a/crates/batten/tests/it/verify_unprovisioned.rs +++ b/crates/batten/tests/it/verify_unprovisioned.rs @@ -318,6 +318,84 @@ fn the_extracted_verify_body_is_the_task_and_not_the_whole_file() { ); } +/// CLOUD-1702. THE BASE A SPECULATIVE LAP IS JUDGED AGAINST. +/// +/// `verify:gated` arms two gates with a base ref, and both used to name +/// `origin/main` unconditionally on a premise the body itself states: that +/// `verify` has already refused any branch not rebased on current trunk, so the +/// task is a function of (commit, current trunk). A speculative lap breaks that +/// premise — `land` replays the branch onto the lease holder's UNLANDED commits — +/// and judging that tree against trunk charges the holder's weakenings and the +/// holder's commits to the BORROWER. +/// +/// It refuses rather than merely misreports, because `lint::groom` keys the claim +/// receipt on the branch name: the borrower carries one of its own, minted for +/// its own row and naming no weakening, which is `Groom::Read({})` — a refusal by +/// design (CLOUD-841). The holder's `Weakens:` trailer cannot admit it. +/// +/// Both halves are asserted, because arming only one leaves the other charging +/// the same borrowed commits: `commit-lint`'s instance is already in this +/// repository's cost record as CLOUD-1775's lost lap, *28 commits claim no +/// CLOUD-N issue*. +/// +/// Fails by: restoring either literal, which is the state five laps over four +/// borrowed tips were measured in on 2026-09-25/26. +#[test] +fn both_base_armed_gates_read_the_base_the_lap_actually_borrowed() { + let body = task_body("verify:gated"); + assert!( + body.contains("mise run config-lint"), + "the extraction found the gate set, not a neighbouring table" + ); + for (armed, gate) in [ + ("CONFIG_LINT_BASE", "mise run config-lint"), + ("BASE_SHA", "mise run commit-lint"), + ] { + let line = arming(&body, armed, gate); + assert!( + line.contains("BATTEN_SPEC_BASE"), + "{armed} must read the base the lap borrowed, or a speculative lap \ + charges the holder's diff to this branch: {line}" + ); + } +} + +/// The line that ARMS `gate` with `armed`, never the prose that discusses it. +/// +/// Both halves of the pair are required, for the reason [`task_body`]'s own +/// comment records one layer up: this body explains each arming in a comment +/// above it, so a bare `find` for the variable name returns *"absent +/// `CONFIG_LINT_BASE` the task runs exactly ..."* — a sentence that will never +/// contain the expansion, so every assertion over it fails for the wrong reason. +/// Requiring the invocation on the same line is what picks the executable one. +fn arming<'a>(body: &'a str, armed: &str, gate: &str) -> &'a str { + body.lines() + .find(|line| line.contains(armed) && line.contains(gate)) + .unwrap_or_else(|| panic!("{armed} arms {gate} in this body")) +} + +/// CLOUD-1702's MIRROR, and without it the case above is satisfied by dropping +/// the base entirely — which is the dead-gate class, not a fix. +/// +/// An unspeculated lap and CI both leave `BATTEN_SPEC_BASE` unset, so the +/// expansion has to fall back to trunk. A bare `$BATTEN_SPEC_BASE` would arm +/// `config lint` with an empty ref there and judge nothing at all, which is +/// exactly the silence house style §8 arms this gate against. +#[test] +fn an_unspeculated_lap_still_falls_back_to_trunk() { + let body = task_body("verify:gated"); + for (armed, gate) in [ + ("CONFIG_LINT_BASE", "mise run config-lint"), + ("BASE_SHA", "mise run commit-lint"), + ] { + let line = arming(&body, armed, gate); + assert!( + line.contains("${BATTEN_SPEC_BASE:-origin/main}"), + "the fallback is the whole reason this is a no-op off a bet: {line}" + ); + } +} + /// CLOUD-1683. The load-bearing ordering. An unprovisioned tree has to stop /// BEFORE the receipt question, because that question is asked through a compile /// entry point: on a tree with no toolchain it fails for the wrong reason and is diff --git a/mise.toml b/mise.toml index ca57ab1c6..bd4ef36e0 100644 --- a/mise.toml +++ b/mise.toml @@ -4411,7 +4411,12 @@ if ! mise run test:bats; then echo "::error:: verify: the shell suite failed. No receipt written." >&2 exit 1 fi -if ! BASE_SHA="$(git rev-parse origin/main)" HEAD_SHA="$(git rev-parse HEAD)" mise run commit-lint; then +# THE SAME BOUND, AND THIS HALF IS ALREADY IN THE COST RECORD (CLOUD-1702). +# CLOUD-1775's body tabulates a lap lost to `commit-lint` reporting "28 commits +# claim no CLOUD- issue" — those 28 were the lease holder's, charged here +# because `BASE_SHA` named trunk while the tree sat on the holder's base. See the +# `config-lint` paragraph below for why the borrowed base is the honest one. +if ! BASE_SHA="$(git rev-parse "${BATTEN_SPEC_BASE:-origin/main}")" HEAD_SHA="$(git rev-parse HEAD)" mise run commit-lint; then echo "::error:: verify: commit-lint failed. No receipt written." >&2 exit 1 fi @@ -4424,10 +4429,38 @@ fi # CI confirms what this proved. The first arming ran in CI alone and a # ref-namespace mistake was unreachable by any local run — it cost a matrix. # -# `origin/main` is the right base HERE and is not the property-of-the-world -# mistake the `hk` gate must avoid: `verify` has already refused this branch -# unless it is rebased on current `origin/main` (see the guard in `verify`), so -# this task is a function of (commit, current trunk) before this line runs. The +# `origin/main` IS THE RIGHT BASE ONLY WHEN THE LAP DID NOT SPECULATE, and the +# paragraph below used to say it unconditionally (CLOUD-1702). Its reasoning — +# `verify` has already refused this branch unless it is rebased on current +# `origin/main`, so this task is a function of (commit, current trunk) — holds +# for an ordinary run and is FALSE on a speculative lap: `land` replays the +# branch onto the lease holder's UNLANDED commits, "the main that is about to +# exist", so the tree is a function of (commit, the holder's base) instead. +# +# Judging that tree against trunk charges the holder's weakenings to the +# BORROWER. `lint::groom` keys the claim receipt on the branch name, and a +# borrower has one of its own, minted for its own row and naming no weakening — +# `Groom::Read({})`, which refuses by design (CLOUD-841: evidence of absence, not +# absence of evidence). The holder's `Weakens:` trailer cannot admit it, because +# the borrower's groom has already answered. Measured 2026-09-25/26: five laps +# over four different borrowed tips, every one refusing +# `recorder[board-issue-created] recorder-changed` and its sibling, while +# `mise run test:cargo` over the identical integrated commit was 6057/6057 green. +# +# `BATTEN_SPEC_BASE` is what `land` already publishes for exactly this question +# (`speculation::PUBLISHED_AS`, CLOUD-748), and `land.rs`'s own +# `a_body_gate_is_told_which_base_the_lap_borrowed` asserts it arrives. Using it +# is the same bound CLOUD-1711 put on `race::authored_log`, which read the whole +# branch history where the shell read only the commits the branch authored. +# +# NOTHING ESCAPES JUDGEMENT, which is the property that makes this a fix rather +# than a relaxation: every commit is still judged once, on its own branch, +# against the base it actually sits on. The holder's weakenings are adjudicated +# on the holder's own lap, against trunk, under the holder's groom. +# +# A no-op wherever no bet is live — an ordinary local lap and CI both leave the +# variable unset, so the expansion is `origin/main` and the class is unchanged. +# The # pre-commit step stays single-tree, where that reasoning does not hold. # # Unarmed callers are unaffected: absent `CONFIG_LINT_BASE` the task runs exactly @@ -4445,7 +4478,7 @@ if ! mise run record-verdicts; then echo "::error:: verify: the validator verdicts could not be recorded, so the rows that read them would decide over a record nothing wrote. No receipt written." >&2 exit 1 fi -if ! CONFIG_LINT_BASE=origin/main mise run config-lint; then +if ! CONFIG_LINT_BASE="${BATTEN_SPEC_BASE:-origin/main}" mise run config-lint; then echo "::error:: verify: config-lint refused this tree — either batten.toml carries a policy smell, or this branch weakens policy against origin/main. No receipt written." >&2 exit 1 fi