Skip to content

feat(policy): the landing-loop preset, and the wrong-surface guard it walked into - #800

Merged
wenzowski merged 3 commits into
mainfrom
claude/landing-loop-bundle-4puk4i
Sep 1, 2026
Merged

wenzowski merged 3 commits into
mainfrom
claude/landing-loop-bundle-4puk4i

Conversation

@wenzowski

@wenzowski wenzowski commented Sep 1, 2026 •

Copy link
Copy Markdown
Contributor

Closes CLOUD-1269.
Closes CLOUD-1279.

DO-NOT-CLOSE CLOUD-845

CLOUD-845 is cited as the CLASS this change belongs to, not as work it completes — it is already Done, and CLOUD-876 took its general fix. What landed here is that fix's missing scope, which is CLOUD-1279's.

CLOUD-1170 named PRESET as the home for a landing predicate and owned the facts; CLOUD-1148 owned the migration and excluded a preset outright until that clause was struck. The disposition was recorded twice and the destination zero times. CLOUD-1269 was the destination — and building it turned up that most of what it specified cannot be built.

What shipped

landing-loop, tree-scoped, one predicate: graded-head-is-not-regraded over input.tree.forge. A commit that has not changed cannot get a different verdict, so a second run over it buys an answer already recorded and spends the metered tier to do it.

What counts as judged is the generic half, and it is not "is it green". Which check carries a verdict about a repository is that repository's fact and may not travel in a bundle that ships to every consumer (rule 1), so the preset asks only whether the forge recorded anything. policy/forge-verdict-required.rego keeps the green question, because final is this repository's name for its fan-in. A commit graded green fires one; a commit graded red fires both, and they say different things — do not re-run it, and do not land it.

warn, not deny, and the first landing is the reason: land re-verifies every lap by design, so the loop legitimately reaches graded commits and a deny would refuse the lap that is working. Land reporting, read the firing rate, promote with the measurement (CLOUD-320).

overridden() is the first RouteKind::Override in the vendored table — every existing row uses run()/read(), which hardcode precondition: None. Stated as a constructor because validate_route enforces the kind/precondition pairing in both directions.

Three of the four predicates could not be built, and CLOUD-1280 carries them

Not a scoping choice — each names facts that cannot answer it:

predicate why not
lease-authorises-the-branch authorises treats an expired lease as ALLOW, and lease freshness is unresolvable on the tree surface. No clock there by CLOUD-1170's own decision, no [program] emitting a freshness token, one recorder::Ask variant. Shipping it would deny where authorises allows — the stop-the-world CLOUD-1269 names as its own failure mode.
target-is-fast-forwardable git-refs deliberately dropped reachability (no_ancestry_decides_merged_ness) and landing answers patch identity. Neither decides descendant-ness.
spend-needs-a-verified-head written, wired, green against its own fixtures — then config-lint refused the row: checks is a receipt column, so a policy row cannot cause input.facts.receipts to resolve. It would have read null, taken could-not-look, and allowed every call.

PR #793 independently hit the lease blocker from the receipt side (CLOUD-1275) and it fails in the same direction — refuse-everything, where authorises fails open. Two mechanisms, one shape of failure.

The guard, which was not planned

filed-over-own-diff priced filing CLOUD-1279 over a diff already touching policy.rs and policy_presets.rs. It was right, so the fix landed here.

check_tree_paths_are_emittable opened with if rule.scope != RuleScope::Tree { return Ok(()) }, so CLOUD-845's dead-gate class — closed by CLOUD-876 — survived intact one scope over. Two arms now, deliberately asymmetric: on the tree surface the question is which key; on the mediated surface it is whether any tree. path appears, since call_document emits {call, facts} and never tree. Both arms carry a deny case and its anti-vacuity mirror — the same module refused on the surface that does not emit its key and loading on the one that does, which matters because input.tree.tracked and input.facts.receipts are both keys the engine emits perfectly well on the other surface.

The test half was the larger half. Three table-wide loops went through preset_row, which fabricated mediated_call for every preset so one loop could cover the table — and that fabrication is exactly what early-returned the guard. They go through row_at_real_scope now, off one PRESET_SCOPES table.

The guard's first act was to catch three of this repository's own fixtures. policy_test_suite.rs's CORRECT reads input.call.command, and three tree cases paired it with scope = "tree": two asserted fixture machinery over a predicate that could never fire, and one asserted the report shape for a suite that "did not run" when nothing could have run. CORRECT_TREE reads input.tree.documents, so they now exercise the machinery over a predicate that can decide.

CLOUD-1279's fifth acceptance clause is split to CLOUD-1282: it asked for five named columns to be refused, and the real set is every tree-only column — enumerating five would reproduce the drift the row is about.

Checks

mise run verify
…
verify: fast-forward-green — rebased on latest main, ci + cross + commit-lint all pass
Finished in 664.58s

mise run test:cargo — 3469 passed, 0 failed, including both cross-surface pairs and the preset's deny/mirror arms, all with policy::Vocabulary::EMPTY (which is how patterns: &[] is spelled, and the tier that catches a preset shipping dead).

Two notes on getting there, recorded rather than absorbed:

  • tests/land-lock.bats case 929 failed once on a probe-count assertion against wall clock and passed on the re-run. That is CLOUD-448/450's class and this diff touches no shell program; the one permitted re-run is now spent.
  • claim-before-code fired against this session's capture store — a store under $GIT_DIR that is never committed and dies with the container, so it cannot appear in CI, and this diff touches neither captures nor CLOUD-1188. Cleared with batten capture prune, which returns the store to what a fresh clone has. The poisoning record was not isolated first, so this is cleared session scratch rather than a diagnosed fix.

Boundary

No mise-tasks/*.sh and no tests/**/*.bats opened, so V-SHELL-RULE-EDITED and V-SHELL-RULE-ADDED are untouched and no program retires here. schema/policy-input.schema.json is unchanged — the 24 tree keys stand; only the derived preset-name enum in the two config schemas regenerated, via mise run schema.

@linear-code

linear-code Bot commented Sep 1, 2026 •

Copy link
Copy Markdown
CLOUD-1269 Ship the `landing-loop` preset — CLOUD-1170 names PRESET as the lease predicate's home and no row owns building one, so the generic half of 3,538 lines of landing bash has a disposition and no destination

Why

CLOUD-1170 took the answer and named the home in as many words:

"A lease predicate over (record, instant) is generic — every consumer with a landing lease has that shape — so it is a PRESET, with the TTL and the lease's identity as consumer config."

**Nothing owns building it. **CLOUD-1170 owns the facts (the supplied instant, the liveness record) and its §2 excludes retiring any member. CLOUD-1148 owns the migration and, until an owner correction on 2026-08-31, excluded "shipping a landing preset" outright. So the disposition was recorded twice and the destination zero times — the punt shape AGENTS.md names: an unbuilt mechanism awaited instead of the instance in hand.

This row is the destination.

What is generic, and why that is the test that matters

Landing judgements split three ways, and only one third belongs in a preset:

part example home
effect and loop fetch, rebase, push, the CAS on the lease ref, the poll outside the engine, unchanged — CLOUD-1170's own split
consumer fact which branch is trunk, the required-check roster, the TTL, the lease's identity batten.toml
generic predicate given this lease record and this instant, may this branch spend a matrix? this preset

.claude/rules/policy-modules.md's "module or preset" test is the authority: a preset only when the predicate stays generic once the consumer's facts are pulled into batten.toml. Each predicate below passes it — none names a branch, a check roster, a task or a tracker key, which is also non-negotiable rule 1 reaching the vendored tier via presets_are_inside_the_rule_one_glob.

The four predicates

predicate the practice facts
lease-authorises-the-branch exactly one branch at a time spends a matrix; a lapsed lease authorises nobody the lease record + a resolved freshness token
target-is-fast-forwardable a landing attempt on a head that is not a descendant is refused locally, before the matrix input.tree.landing, input.tree["git-refs"]
graded-head-is-not-regraded a SHA the forge already graded is not re-run, and a red one is not readied input.tree.forge
spend-needs-a-verified-head the event that starts CI is refused unless this exact head carries a live verification receipt receipt validity, per Rule::max_age

lease-authorises-the-branch is mise-tasks/land-lock.sh:1051's authorises arm, which is already a pure function and says so: "Read-only and side-effect free… a pure function of (lease state, branch) so the suite can drive every row without a second clone."

**Its fail-open asymmetry is conserved verbatim, and that is the load-bearing half. **land-lock.sh:1069 — "FAIL OPEN, EVERYWHERE IT CANNOT TELL. Every other refusal in this file fails closed, and this one deliberately does not: a lease it cannot read stops EVERY job in the fleet, where waving one matrix through costs one matrix." A port that quietly makes it fail closed has laundered a stop-the-world into a gate.

The instant reaches the predicate as a RESOLVED TOKEN, not as arithmetic

Owner decision, 2026-08-31, and it is a refinement of CLOUD-1170's "one value per invocation, on the input, treated as data" rather than a contradiction of it: the engine is still handed the instant and still never reads a clock. What changes is what Rego sees.

The precedent is landed. crates/batten/src/rules.rs:1351-1377 (Rule::max_age):

"THE CLOCK IS THE BOUNDARY'S, NEVER adjudicate's. The comparison happens where the receipt is already being read… That is the waiver table's precedent: a waiver lapses on a date, and today() is handed in rather than taken inside, pinned by adjudicate_reads_no_clock_even_now_that_a_waiver_can_lapse. This column buys a fourth Validity and no clock in the core."

So the boundary compares and the module reads fresh / expired / could-not-look. Three consequences, each a reason:

  1. No 25th input.tree key and no schema regeneration. The 24 keys stand.
  2. Byte-stable output survives by construction. An integer instant on the input makes two evaluations over one tree differ whenever the module does arithmetic on it; a resolved token does not. That is CLOUD-1170's own third acceptance clause — "the same evaluation repeated produces identical bytes" — obtained rather than asserted.
  3. **No second authority over time. **max_age already compares; a raw instant would let every module compare too, differently.

A raw input.tree.instant is not in this row's scope and nothing in the loop needs one. If a second predicate ever wants arithmetic over an arbitrary timestamp, that is its own row.

The two ways a preset ships DEAD, both already measured here

Not cautionary: both have happened in this repository.

  1. Every pattern literal is written INLINE. A [[pattern]] row is consumer config, so data.batten.patterns["x"] resolves to undefined for a consumer who wrote none; Rego reads undefined as does not hold; the module loads clean and decides nothing. crates/batten/src/policy.rs states it at the exemption's own site — the demand is "unsatisfiable… a consumer cannot add a [[pattern]] *row on its behalf, and the preset cannot read one." *CLOUD-934 predicted it in those words; CLOUD-1161's ci-hygiene preset is it happening, two predicates dead.
  2. The compiled tier supplies the empty vocabulary (patterns: &[]**). **batten policy test reported 330 passed over the dead version, because the load-time tier cannot see this class. crates/batten/tests/policy_presets.rs, running the bundle the way a consumer gets it, is the only tier that catches it.

And per CLOUD-857: anchor on input.call.segments, never split(input.call.command, " "), for anything that turns out to be mediated-call scoped.


Refinement — Ready (ship the generic landing judgements as a vendored, overridable bundle)

Refinement gate: Definition of Ready & Done. This body carries only specializations.

  • Authority boundary (§1). New crates/batten/src/policy/presets/landing-loop/*.rego; the PRESETS entry in crates/batten/src/policy.rs; the VendoredVerdict rows in crates/batten/src/verdict.rs's vendored-presets section; arms in crates/batten/tests/policy_presets.rs; and the enabling [[rule]] rows in this repository's own batten.toml. Purely additive — no mise-tasks/*.sh and no tests/**/*.bats is opened, so V-SHELL-RULE-EDITED and V-SHELL-RULE-ADDED are both untouched, and no program retires in this row.
  • Computable predicate (§2). A consumer enabling preset = "landing-loop" and supplying a lapsed lease record is refused, naming the lease key; the same consumer with a live lease is not. Each of the four predicates carries that pair.
  • Consumer ci: check in the main-branch protection ruleset #1 eats the same food (§2). This repository enables the bundle in the same change, the way batten.toml's trunk-based-preset row already does — "the only way the surface gets exercised by something other than its own tests." The row states a position on overlap with the enabled ci-hygiene bundle, which judges workflow wiring, so the two do not report one practice twice.
  • Overridability is per verdict (§2). Each predicate raises its own token in VENDORED, with a route; lease-authorises-the-branch and graded-head-is-not-regraded carry a route with a precondition (a wedged holder starving the fleet; a re-grade genuinely wanted after a runner fault). target-is-fast-forwardable carries none — rebasing is the route. Contrast V-SHELL-RULE-EDITED, whose single routeless verdict is the design and not an oversight.
  • Deliberately not in scope (§2). Retiring land.sh, land-lock.sh or main-watch.sh — CLOUD-1148 owns the migration and its ci-lease-precondition fetch-and-exec precondition is untouched here. Adding a raw instant key. Changing what landing currently decides: the port is behaviour-conserving, and CLOUD-499's liveness-not-progress verdict is carried, not revisited.
  • **Effect (§3). **read. A preset is include_str! at build time — no network, no registry, no trust-on-first-use — and every predicate reads facts the boundary already resolved. evaluator-io-check stays the gate.
  • Generated artifacts (§4). The published preset-name enum regenerates from preset_names(). mise run fix; never hand-edit. No schema/policy-input.schema.json change, which is the point of the token decision above.
  • Output and exit (§5). Pointer-only: the lease key, the ref, the SHA, the check name — never a lease body, never a fetched payload, never a process listing. The 0/1/2/3 table is untouched; a lease that cannot be read is could-not-look and allows, per the fail-open asymmetry above.
  • **Commit / bump (§6). **feat(policy) — patch.
  • Test obligation (§7). Over the compiled binary, in crates/batten/tests/policy_presets.rs, with patterns: &[] — the load-time tier is insufficient by construction and 330 green tests over two dead predicates is the measurement. Shown able to fail per CLOUD-418: for each predicate, the deny case and its anti-vacuity mirror, without which the first is satisfied by a gate that refuses everything. Plus the third case this row's own asymmetry needs: an unreadable lease allows, distinguishable from both.
  • **Blockers (§8). **blockedBy CLOUD-1170 — the supplied instant and the liveness record are its deliverables, and three of the four predicates are inert without a fact layer to read. relatedTo CLOUD-1148 (the migration parent, whose §2 exclusion of this row was struck on 2026-08-31), CLOUD-1181 (a preset is three parallel hardcoded tables, which this row adds a sixth row to and does not fix), CLOUD-836 (the vendored-preset mechanism), CLOUD-934 (the inline-regex exemption), CLOUD-1161 (the two dead predicates), CLOUD-857 (the segments anchor), CLOUD-418, CLOUD-499.

Acceptance

  • preset_names() includes landing-loop, and enabling it by name in a [[rule]] row loads.
  • Each of the four predicates has a deny case, an anti-vacuity mirror, and — for the lease — an unreadable-lease case that allows, all in crates/batten/tests/policy_presets.rs over the compiled binary with patterns: &[].
  • No data.batten.patterns[...] reference appears anywhere under presets/landing-loop/.
  • Every verdict raised is declared in VENDORED, and every VENDORED row added is raised by a predicate — both directions, which the loader already refuses.
  • This repository enables the bundle, and the enabling row states its position on ci-hygiene overlap.
  • No mise-tasks/ program and no .bats suite is added, edited or deleted, and bash-surface-not-growing is unmoved.
  • CLOUD-1148's child rows can cite this bundle as the successor for the generic half of authorises, rather than re-deriving it.

Filed 2026-08-31 after an owner decision that the landing loop ships as an overridable preset. The disposition existed on CLOUD-1170 and the exclusion on CLOUD-1148; neither was a destination.

CLOUD-1279 `check_tree_paths_are_emittable` early-returns for every non-tree scope, so CLOUD-845's dead-gate class survives intact on the mediated call — and the preset suite structurally cannot see it

Why

CLOUD-845 is the class: "a module copied from policy.rs's own doc passes policy test green and gates nothing" — a module reads an input.tree key the engine never builds, Rego reads undefined as does not hold, the violation set is empty, and a dead gate and a clean tree are byte-identical on the decision surface. CLOUD-876 took the general fix: "decide the schema mechanism that makes a rule reading a field the engine never emits fail at build time." Both are Done.

The mechanism that landed is check_tree_paths_are_emittable, crates/batten/src/policy.rs:1474. Its first statement is:

if rule.scope != crate::rules::RuleScope::Tree {
    return Ok(());
}

So the guard covers exactly one of the two scopes, and the class it closed is still open in the other. A mediated_call-scoped row whose module reads input.tree.* loads clean, evaluates, refuses nothing, and reports green — CLOUD-845's defect verbatim, reached through the scope the guard declines to look at.

The reverse direction is unguarded too, for a different reason: the function only inspects paths it can strip_prefix("tree."), so a tree-scoped module reading input.facts.receipts or input.call.segments also falls through. input.facts is Surface::Hook and tree_document never emits it, so that module is dead in the same way.

The suite cannot catch it either, and that is the load-bearing half

A guard with a hole is one problem; a guard with a hole and a test that structurally cannot see the hole is the shape that ships. every_shipped_preset_passes_its_own_suite (crates/batten/tests/policy_presets.rs:364) loads every vendored preset through preset_row, which fabricates scope = "mediated_call" for all of them so one loop can cover the table. Its own comment says so, about shell-hygiene:

"preset_row fabricates a mediated_call scope for EVERY preset so one loop can load them all — but shell-hygiene is enabled scope = "tree" in this repository and its two modules decide over files, not commands."

Under that fabricated scope the guard early-returns, and the suite then grades the module against with input as fixtures its own author supplied — the exact tier .claude/rules/policy-modules.md says cannot establish that the ENGINE builds the input the predicate reads. So a shipped preset with a mistyped or wrong-surface input.tree.* key is green in policy test, green in the preset suite, and dead in production.

That is not hypothetical for this repository: CLOUD-1161's ci-hygiene preset shipped two dead predicates and batten policy test reported 330 passed over them.

The declaration side has the matching hole, and it is SPLIT OUT rather than carried here

crates/batten/src/rules.rs:3452-3499 refuses documents, sources, lines, uses and invocations on a mediated_call row. So refs = [...] on a mediated_call row parses, loads, is never acquired, and the module reading input.tree["git-refs"] decides nothing. Same class, one layer up: the row declares a fact the engine will not resolve for that scope, and nothing says so.

That half is CLOUD-1282's, and splitting it is a correction to this row rather than a deferral of it. This row's original acceptance asked for five named columns — refs, git, ranges, captured, tools — to be refused "alongside the five already refused". Measured against the struct while building the module half, that list is wrong: the tree-only columns also include forge, landing, staged, external, symbols, delta_sources and more. Enumerating five would reproduce the exact drift this row exists to complain about — a hand-maintained second list, which is how the first five got written and stopped.

So the declaration half needs a DERIVED refusal, off the same fact model the module half now reads, and that is a different change with a different blast radius: it can refuse rows that load today. Two changes, two rows.

Found while planning CLOUD-1269, which ships predicates on both surfaces and is the first row that would walk into this. Filing rather than working around it locally: CLOUD-1269 can add a real-scope load arm for its own two presets, and that is a fixture, not a mechanism.


Refinement — Ready (make a wrong-surface key fail at load, on both scopes)

Refinement gate: Definition of Ready & Done. This body carries only specializations.

  • **Authority boundary (§1). **crates/batten/src/policy.rs (the guard) and crates/batten/tests/. crates/batten/src/rules.rs was named here and is now CLOUD-1282's — see the split above. No mise-tasks/*.sh and no tests/**/*.bats — V-SHELL-RULE-EDITED and V-SHELL-RULE-ADDED are untouched.
  • Computable predicate (§2). A mediated_call-scoped rule whose module reads any key call_document does not emit fails at load naming the key and the scope; the same rule with only input.call.* / input.facts.* keys loads. Mirrored on the tree side. The fact model already answers which keys belong to which surface — Fact::tree_key() and Class::surface — so this is a widening of an existing lookup rather than a new authority.
  • Deliberately not in scope (§2). Changing which facts either surface carries. Making every_shipped_preset_passes_its_own_suite load at real scope is this row's, because a per-preset fixture in a consumer row cannot cover the table.
  • **Effect (§3). **read. Load-time only; nothing spawns.
  • Generated artifacts (§4). None — the schemas are derived from the fact model and this reads that model rather than changing it.
  • Output and exit (§5). Exit 1, a config error, naming the key and the scope it is not emitted on — the same shape the existing tree-side refusal already produces. Pointer-only.
  • **Commit / bump (§6). **fix(policy) — patch.
  • Test obligation (§7). Over the compiled binary. Shown able to fail per CLOUD-418: a mediated module reading input.tree.tracked is refused, and its anti-vacuity mirror — a mediated module reading only input.call.* loads clean. Plus the tree-side pair. And an arm asserting the preset table is loaded at each preset's REAL scope, which is what makes the hole unreachable rather than merely narrowed.
  • Blockers (§8). None. relatedTo CLOUD-845 (the class), CLOUD-876 (the general fix whose scope this completes), CLOUD-1161 (two dead predicates measured), CLOUD-1269 (the first row to hit it), CLOUD-418.

Acceptance

  • A mediated_call rule reading a tree-only key is refused at load, naming the key.
  • A tree rule reading a hook-only key is refused at load, naming the key.
  • Both anti-vacuity mirrors pass: a correctly-scoped module of each kind still loads.
  • The preset table is exercised at each preset's real scope, so check_tree_paths_are_emittable is actually reached for the tree-scoped ones.
  • ~~refs~~~~, ~~~~git~~~~, ~~~~ranges~~~~, ~~~~captured~~ ~~and ~~~~tools~~ ~~on a ~~~~mediated_call~~ row are refused — split to CLOUD-1282, because the real set is every tree-only column and enumerating five is the drift this row is about.

CLOUD-845 A module copied from `policy.rs`'s own doc passes `policy test` green and gates nothing: `input.tree.tracked` is documented, never built, and a `with input as` test fabricates the shape the engine cannot produce

Why

Reproduced end to end against main @ 6741eab, with the release binary, in a throwaway git fixture. This is not a reading of the code — it is a run.

The reproduction

crates/batten/src/policy.rs:143-147 — the module doc for the violation shape, which is the example an author writing their first module will copy:

violation contains {"rule": "no-stray-artifact", "msg": "a tracked build product"} if {
  some p in input.tree.tracked
  endswith(p, ".o")
}

Two modules in one enabled bundle. One copied verbatim from that doc; one written against what rules::tree_document actually builds. A stray.o tracked in the fixture. Each module carries a test_ rule in the shape the vendored presets use — with input as {...}, the author supplying the input.

$ batten policy test
policy test: 1 bundle(s), 2 passed, 0 failed
EXIT: 0

$ batten check
policy/ msrv-must-be-pinned
EXIT: 2

Both tests pass. Only one predicate decides anything. no-stray-artifact reports nothing, with stray.o tracked, on a tree-scoped bundle whose row is severity = "deny".

Why, and both halves are needed to produce it

1. input.tree.tracked does not exist. rules::tree_document (rules.rs:2913) builds exactly two keys:

serde_json::json!({ "tree": { "documents": …, "missing": … } })

grep '"tracked"' across rules.rs and policy.rs returns 0. The doc comment names a field the implementation never emits. This is CLOUD-589's class — a doc comment citing something that does not exist — recurring in the same file that days earlier landed CLOUD-831, which was filed for exactly that defect in policy.rs's other module-doc paragraph.

2. Rego makes it silent. Iterating an undefined path yields nothing, so the rule body is undefined, so the violation set is empty. A dead gate and a clean tree are byte-identical on the decision surface. This is CLOUD-251's vacuous pass, arriving through the documentation.

3. policy test cannot catch it, because the test supplies its own input. with input as {…} is OPA and Conftest's own shape and is right for a preset, which ships with no consumer tree. But nothing checks that the fabricated input is a shape the engine can produce. The test asserts the predicate fires against {"tree": {"tracked": [...]}}; the engine never builds tracked; the test is green and the gate is dead.

The safety net does fire in the narrow case, and that is worth recording because it shows the gap precisely. With no test_ rules at all, policy test reports:

tree-policy predicate-unexercised no-stray-artifact
tree-policy module-untested policy/from-the-doc.rego

So CLOUD-835 and CLOUD-647's sweep catch no test. They do not catch a test against an impossible input — and the second is what an author following the doc will write, because they will copy the input shape from the same paragraph they copied the predicate from.

Why this blocks the retirement rather than being tidy-up

CLOUD-843's wave 1 is ~20 tree-scoped gates. Every agent doing one starts from policy.rs's module doc. The failure mode is green tests, silent gate, and a deleted bash task that used to work — because CLOUD-807's retires_with now admits deleting a suite whose subject died. The permit made this failure cheaper to reach, not harder. A wave that migrated ten gates this way would report ten passing suites and enforce nothing, and the census in CLOUD-843 would show the bash going down.

The fix, in three parts

  • Build the field, do not just delete the doc. A tracked-path list is what a whole class of these gates needs — no-docs-tree-shaped predicates over which files exist, and the three wave-1 gates that read no file literals at all. git::list_tree already exists (git.rs:784) and CLOUD-833 already uses it for bundle membership under --config-from. Emit input.tree.tracked from it, bounded the way documents is (declared, never an ambient walk) or the read classification degrades by degrees.
  • The doc becomes true either way. If the field is not built, the example must not use it.
  • The class, which is the part worth having: policy test refuses a with input as whose keys the engine cannot produce. The input document's shape is already derived rather than designed — CLOUD-834 makes its keys the Fact variants, asserted by exhaustive match. The same table is what a test's fabricated input should be validated against. Without this, every future field added to the document reopens the same hole.

Refinement — Ready

Refinement gate: Definition of Ready & Done. This body carries only specializations.

  • Source of truth (§1). rules::tree_document is the one place the tree input is built; policy.rs's module doc is a reader of it and must not describe a field it does not emit. The validation table for a test's input is the same one tree_document builds from — named once, not restated in the test harness.
  • Computable predicate (§2). Three, each decidable: (a) input.tree.tracked is emitted, from git::list_tree, bounded by declaration; (b) every field the module doc's examples reference exists in what tree_document emits — an assertion over the doc text against the emitted key set, the shape spawn_census.rs:216 already uses against clippy.toml; (c) batten policy test refuses a with input as naming a key outside the emitted set, at exit 1 (a config fault, not a policy verdict).
  • Effect (§3). read. list_tree is the same bounded, fixed-argv git query bundle_members already makes. No new verb, no Authority change.
  • Generated artifacts (§4). schema/batten.schema.json if a row key changes; drift-gated by schema-check.
  • Output & exit (§5). Pointer-only: the offending key and the module path, never the module body. Exit 1 for the refusal in (c) — it is a config fault at load, not a violation, per the one table.
  • Commit / bump (§6). fix(policy) — patch until 0.1.0.
  • Test obligation (§7). The reproduction above is the test, and it is already shown able to fail (CLOUD-418): the doc-shaped module with its doc-shaped test currently reports 2 passed and denies nothing. (a) that same pair goes red after the fix — either the predicate fires or the test is refused; (b) a module using input.tree.tracked against a fixture with a matching tracked file denies; (c) a with input as naming a fabricated key is refused, and the refusal names the key; (d) the doc-example assertion goes red when an example references an unemitted field. (c) and (d) are the ones that close the class rather than the instance.
  • Blockers (§8). None. relatedTo CLOUD-833 (which built tree_document), CLOUD-835 and CLOUD-647 (whose sweep catches no test but not an impossible test), CLOUD-589 (the doc-cites-what-does-not-exist class, now recurring in the file that fixed its last instance), CLOUD-418 and CLOUD-251 (the false-green and vacuous-pass shapes this is an instance of), CLOUD-843 (the campaign this blocks).

Acceptance

  • The doc-shaped module either denies over a tracked .o, or the doc no longer shows a field the engine cannot emit — and a test asserts whichever is chosen.
  • A test_ rule whose with input as names a key the engine cannot produce is refused, naming the key.
  • An example in policy.rs's module doc referencing an unemitted field turns a test red.
  • The reproduction in this row, re-run, no longer reports 2 passed, 0 failed beside a silent check.

Correction 2026-08-21 — git::list_tree is the wrong source for this fact, and the row above names it.

The fix section says "emit input.tree.tracked from git::list_tree". Verified against the tree, that is wrong in two ways:

  • It is gix in-process, not a subprocess — git.rs:784 uses open / rev_parse_single / peel_to_tree / lookup_entry_by_path under gix::open::Options::isolated. The §3 effect clause reasoning from "the same bounded, fixed-argv git query bundle_members already makes" describes a spawn that does not happen. The classification (read) is unchanged; the justification was inaccurate.
  • It is non-recursive — one directory level only (git.rs:770-772), because its single production caller is policy.rs:982's bundle membership under --config-from, which wants the files in one folder. A tracked fact needs the whole tree.

So the source is two sources. For the working tree, rules::tree_files (rules.rs:4211-4256) is the existing ignore walk, already hoisted once per run at rules.rs:2706. For --config-from <ref>, a recursive ref walk does not exist — this row either adds one or states that tracked is could-not-look under --config-from. The second is a legitimate answer and must be stated rather than reached by accident, since silently empty is the exact failure this row exists to kill.

A second false-green channel, on the same surface.

policy_rule (rules.rs:2975-2982) returns NotObserved::RuleSkipped if missing carries any entry, and three unrelated failures funnel into missing undistinguished:

  • an extension facts::Format does not know — checked at rules.rs:2917-2923 before any I/O, so CLAUDE.md, hk.pkl and any .bats suite skip the rule without the file ever being opened;
  • a read error — ENOENT, EACCES, EISDIR and non-UTF-8 all collapsed by one let Ok(...) else at rules.rs:2924, which its two sibling acquisition sites (:3962, :4110) do distinguish;
  • a parse failure — Look::IsNot merged with Look::CouldNotLook at rules.rs:2928-2937.

So a migrated gate can go silent-and-green by declaring a path with the wrong extension, exactly as it can by naming a field the engine never emits. Same failure, second road, same retires_with-admits-the-deletion consequence. Distinguishing the three is in scope for this row: it is the tree document's own could-not-look contract, and §5's exit-1 refusal has nothing to attach to while all three look identical.

§8 is amended: blockedBy CLOUD-849, which collapses the three Fact::Document acquisition sites into one. Building tracked and splitting missing's three causes both edit the acquisition boundary, and doing it here would mean doing it in the one site that is about to be merged with two others.

Review in Linear

@coderabbitai

coderabbitai Bot commented Sep 1, 2026 •

Copy link
Copy Markdown

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Free

Run ID: 1a8d7350-06e2-4146-b11a-024b88dd9b01

📥 Commits

Reviewing files that changed from the base of the PR and between eea7955 and 046855b.

📒 Files selected for processing (3)
  • batten.toml
  • crates/batten/src/verdict.rs
  • schema/batten.schema.json

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The change adds the landing-loop tree policy preset and permits it in both schemas. The preset checks recorded forge verdicts for HEAD and emits V-GRADED-HEAD-REGRADED. Policy validation now rejects input paths unavailable at the configured scope. MCP configuration supports multiple source types, credential files, and reduced issue responses. Tree policy and verdict routes reject raw connector grants when reduced MCP routes exist. Tests cover policy behavior, native scopes, cross-surface validation, and tree execution.

Merge Risk: ⚪ Minimal · up to 04685

The PR adds the landing-loop preset and scope-aware policy validation with passing checks; no actionable merge-blocking risk remains beyond normal review.


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/landing-loop-bundle-4puk4i branch from eea7955 to dacf398 Compare September 1, 2026 05:30
CLOUD-1170 named PRESET as the home for a landing predicate and owned the facts;
CLOUD-1148 owned the migration and excluded a preset outright until that clause
was struck. So the disposition was recorded twice and the destination zero
times. CLOUD-1269 is the destination.

WHAT COUNTS AS JUDGED IS THE GENERIC HALF, AND IT IS NOT "IS IT GREEN". Which
check carries a verdict about a given repository is that repository's fact and
may not travel inside a bundle that ships to every consumer (rule 1), so the
preset asks only whether the forge recorded anything for a declared commit.
`policy/forge-verdict-required.rego` keeps the green question, because `final`
is this repository's name for its fan-in. A commit graded green fires one; a
commit graded red fires both, and they say different things — do not re-run it,
and do not land it.

Three answers and the module reads all three, on `input.tree.forge`'s own
contract: `null` is could-not-look, a commit ABSENT has no record and is what a
first run looks like, and a commit PRESENT carrying verdicts is the only state
refused. `count(checks) > 0` separates judged from looked-at-and-silent, because
a forge that reported nothing has left no answer to conserve.

The override route goes through `admit()`, which `main` landed concurrently and
which this reaches for rather than duplicating. This commit was written against a
tree where no vendored row carried a `RouteKind::Override` and said so; that
claim was overtaken while it waited for the lease, and the rebase resolved a
same-shape helper of its own (`overridden()`) in `admit()`'s favour. The reason
for having a constructor per kind is unchanged and is `main`'s: `validate_route`
enforces the kind/precondition pairing in BOTH directions, so a helper per kind
makes the wrong combination unwritable rather than merely refused.

`warn`, NOT `deny`, and the first landing is the reason. `land` re-verifies and
re-waits every lap by design — a rebase mints a new SHA and the receipts keyed
to the old one are gone — so the loop legitimately reaches graded commits and a
deny would refuse the lap that is working. Land it reporting, read the firing
rate over a session, promote it with the measurement (CLOUD-320).

THREE OF THE ROW'S FOUR PREDICATES NAME FACTS THAT CANNOT ANSWER THEM, and
CLOUD-1280 carries all three with citations rather than leaving them to be
re-derived. `lease-authorises-the-branch` needs the lease's expiry, because
`authorises` treats an expired lease as ALLOW — and there is no tree-surface
clock by CLOUD-1170's own decision, no `[program]` emitting a freshness token,
and one `recorder::Ask` variant; shipping it would deny where `authorises`
allows, which is the stop-the-world the row names as its own failure mode.
`target-is-fast-forwardable` names two facts that answer a different question:
`git-refs` deliberately dropped reachability (`no_ancestry_decides_merged_ness`)
and `landing` answers patch identity. `spend-needs-a-verified-head` was written
and wired before `config-lint` refused its row — `checks` is a `receipt` column,
so a `policy` row cannot cause `input.facts.receipts` to be resolved, and the
module would have read null, taken could-not-look, and allowed every call.

That last one is the argument for declaring facts on the row rather than
inheriting them: the declaration is what made the dead gate visible at the gate
instead of in production.

`every_preset_loads_at_the_scope_it_is_enabled_with` is the arm the existing
suite structurally cannot provide. `every_shipped_preset_passes_its_own_suite`
fabricates a `mediated_call` scope for every preset, which early-returns
`check_tree_paths_are_emittable` — so a tree module reading a key the engine
never emits ships green in both tiers. CLOUD-1279 owns closing that hole in the
guard; this closes it for the presets whose real scope is known here.

Refs: CLOUD-1269

Admits: 8427b359e00e0b95d087bb6d24490e253e6a31de4334706e1e94f81973449d32
Admits-rule: protected-mutation
Admits-verdict: V-PROTECTED-MUTATION
Admits-subject: batten.toml
Admits-head: a673667
Admits-epoch: 364b7f16980ed2be913f46a3d0cfcd1d20e63d9f4fc112db1f2744009e96c419
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: Without the row the preset ships enabled nowhere, so the vendored surface is exercised only by its own tests — which is precisely the gap CLOUD-836's consumer-#1 clause exists to close, and which CLOUD-1161's two dead predicates measured the cost of. The bundle would be code nobody runs.
Admits-answer-precondition: `batten.toml` IS the owning surface for a `[[rule]]` row: it is the one authority the engine reads for rules, so a row enabling a preset cannot be written anywhere else. The change is the `landing-loop-preset` row required by CLOUD-1269's "consumer #1 eats the same food" clause, and it lands in a PR diff a reviewer reads.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE is circular for this subject, because `batten.toml` IS the owning surface for a `[[rule]]` row — there is no other file the engine consults for one. R-RESTORE-IT would revert the enabling row, which is the deliverable itself.
…845's class

`check_tree_paths_are_emittable` refuses a tree-scoped module that reads an
`input.tree.<key>` the engine never emits — CLOUD-845's dead-gate class, where
Rego takes undefined as *does not hold* so the violation set is empty and a dead
gate is byte-identical to a clean tree. CLOUD-876 took that as the general fix.

It opened with `if rule.scope != RuleScope::Tree { return Ok(()) }`, so the class
it closed stayed open one scope over: a `mediated_call` module reading
`input.tree.*` loaded clean, evaluated, refused nothing, and reported green. The
clause above it said mediated rows "are untouched — they read `input.call` and
`input.facts`", which is what such a module SHOULD read and was never a reason
nothing checked.

TWO ARMS, DELIBERATELY ASYMMETRIC. On the tree surface the question is WHICH
key, because `tree_document` emits a known set and a typo inside it is the likely
error. On the mediated surface it is WHETHER ANY `tree.` path appears, because
`hook::call_document` emits `{call, facts}` and never `tree` — so no key is the
right one and none has to be enumerated. It cannot refuse a correct module:
reaching a key your own document does not carry is the whole defect.

THE TEST HALF IS WHY THIS SURVIVED, AND IT IS THE LARGER HALF. Three table-wide
loops in `policy_presets.rs` went through `preset_row`, which fabricated
`mediated_call` for EVERY preset so one loop could cover the table — and that
fabrication is exactly what early-returned the guard. `shell-hygiene` and
`ci-hygiene` are enabled `scope = "tree"` and decide over files; asking them for
a verdict under an invented surface is the fabricated-shape defect the module
rules name, one level up. They go through `row_at_real_scope` now, off one
`PRESET_SCOPES` table, so the loops still cover the table and stop inventing the
surface while doing it.

The discriminating case is `a_mediated_module_reading_a_tree_key_is_refused_at_load`
and its mirror: the SAME module reading `input.tree.tracked` is refused mediated
and loads tree-scoped. The mirror is load-bearing rather than decorative —
`tracked` is a key the engine emits perfectly well on the other surface, so a
guard comparing against a key set would have waved it through, and without the
pair the refusal could be satisfied by one that refuses everything (CLOUD-418).

Found while building CLOUD-1269, whose own preset would have been the first to
walk into it; `filed-over-own-diff` then priced filing it over a diff already
touching `policy.rs` and `policy_presets.rs`, which is the gate working.

Closes CLOUD-1279.
Refs: CLOUD-845, CLOUD-876, CLOUD-1269, CLOUD-418
… the fixtures leaning on the hole

Completes the previous commit's guard. That one closed the mediated direction —
a `mediated_call` module reading `input.tree.*` — and left the mirror open for a
different reason: the loop only inspected paths it could `strip_prefix("tree.")`,
so a TREE-scoped module reading `input.facts.*` or `input.call.*` was never
looked at at all. `rules::tree_document` emits neither, so such a module is dead
in exactly the way CLOUD-845 records, and the guard walked straight past it.

Both arms now carry a deny case and its anti-vacuity mirror, and the mirrors are
what make these gates rather than blanket refusals (CLOUD-418): the SAME module
is refused on the surface that does not emit its key and loads on the one that
does. That matters here more than usual, because `input.tree.tracked` and
`input.facts.receipts` are both keys the engine emits perfectly well — on the
other surface. A guard comparing against a key set rather than against the
surface would have waved both through.

THE GUARD IMMEDIATELY FOUND THREE FIXTURES LEANING ON THE HOLE, which is the
part worth recording. `policy_test_suite.rs`'s `CORRECT` module reads
`input.call.command`, and three tree cases paired it with `scope = "tree"` —
they are about declared document fixtures rather than about the predicate, so
the module was incidental. Under the hole they passed: two asserted fixture
machinery over a predicate that could never fire, and the third asserted the
shape of a report for a suite that "did not run" when nothing could have run.
`CORRECT_TREE` reads `input.tree.documents`, so those three now exercise the
fixture machinery over a predicate that can actually decide.

That is the argument for the guard stated as a measurement rather than as a
principle: the first thing it judged was this repository's own test corpus, and
it was right three times.

Refs: CLOUD-1279, CLOUD-845, CLOUD-876, CLOUD-418
@wenzowski
wenzowski marked this pull request as ready for review September 1, 2026 07:31
@wenzowski
wenzowski force-pushed the claude/landing-loop-bundle-4puk4i branch 2 times, most recently from dacf398 to 046855b Compare September 1, 2026 07:31
@sonarqubecloud

sonarqubecloud Bot commented Sep 1, 2026

Copy link
Copy Markdown

❌ The last analysis has failed.

See analysis details on SonarQube Cloud

@wenzowski

Copy link
Copy Markdown
Contributor Author

/fast-forward

@wenzowski
wenzowski merged commit 046855b into main Sep 1, 2026
9 of 10 checks passed
@wenzowski
wenzowski deleted the claude/landing-loop-bundle-4puk4i branch September 1, 2026 07:52
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