Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .claude/rules/toolchain.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,32 @@ Where the dying suite's declared `# subject:` is still standing at head, the
ledger must name it too (CLOUD-1130): the arms say where the CASES went, and a
surviving subject that no arm mentions is a program left alive and untested.

**"A policy surface" is three shapes, and picking between them is the whole
anti-laundering rule.** `has_policy_surface` admits a consumer module
(`policy/*.rego`), a preset, or engine source (`crates/batten/src/*.rs`). It cannot
tell which one a retirement SHOULD have taken — the arm is byte-checkable and the
choice is not — so this is where CLOUD-1176's scope creep enters the core, and the
order below is the default rather than a menu:

1. **Consumer module** — the predicate names THIS repo's facts: a check roster,
branch names, tracker keys, path conventions. Deny-only, so a consumer can raise
it and never weaken it (house-style §8).
2. **Preset** — the predicate stays generic once the consumer's facts are pulled
out into `batten.toml`. `.claude/rules/policy-modules.md`'s "module or preset"
section owns that test; don't re-derive it here.
3. **Engine source** — **mechanism only**: a parser, a projection, a fact the
boundary acquires, a verb's behaviour. Never a judgement. The test is
non-negotiable rule 1 — a predicate naming a consumer fact may not land here.

**The measured pressure runs toward the core, which is why the order is stated as
a default.** Over the landed ledger, **0 of 609 arms name a preset** and 110 name
engine source, 18 of those retiring a whole file. That is not a taste for the core
so much as a gate that had one: until `has_policy_surface` grew a preset arm the
DRY home was **unspellable**, so an author who chose it was refused and an author
who chose engine source was not. The arm exists now; the habit it shaped does not
undo itself, and a campaign retiring ~130 more programs will make that choice ~130
more times.

**One edit is admitted, and only one**, so it is not rediscovered as an exception:
`only_drops_a_retired_reference` — a sibling file cleaning up after a path this
same change retires. Retiring a program requires editing whatever declares and
Expand Down
27 changes: 26 additions & 1 deletion .serena/memories/workflow/board-states.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,32 @@ The tell that you are in this case: the prescription names a mechanical edit
("does not paper over", "no scope moves"). A mechanical edit can always be
performed; whether it produces the property is the question the edit does not ask.

## Two things that trip agents up
## Three things that trip agents up

0. **A row's STATE decides whether its body is a spec or a record — so it decides
whether you fix the row or file a new one.** This follows from the table above,
and nothing here used to say it.

| state | the body is | a wrong design is fixed by |
| ----------------------- | ------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| Backlog, **Todo** | a **spec** — nothing has been built from it | **editing the row in place.** Correct it fully; that is what grooming is |
| In Progress | a spec somebody is executing | editing, but tell the assignee — they may already have built from it |
| **In Review**, **Done** | a **record** — the code is on `main` (In Review) or released (Done) | **a NEW row**, with a superseded reference on the old one. Never a rewrite |

The cut is **landed-ness, not readership.** In Review means already merged (item
2 below), so its body describes shipped behaviour: rewriting it makes the record
disagree with the code, and the disagreement is invisible. A Todo row has shipped
nothing, so its body is still the only statement of intent, and editing it is the
whole point of the ready queue.

**The failure mode is treating a Todo row as untouchable.** Measured on CLOUD-1152
(Urgent, Todo, unclaimed, 2026-08-30): a session found its central premise false,
declined to correct it, and planned a spin-off row plus a supersede note —
inventing a second authority over a design nothing had built yet, and leaving the
ready queue holding the wrong spec. It reached for that shape twice before the
rule was stated. The opposite error is the more familiar one and is already
covered by the CLOUD-994 class: quietly rewriting a shipped row so the record
matches whatever got built.

1. **"Ready" is not a status.** It is the **Ready block** — text inside the issue
authored during refinement, "the mechanism specified as a computable
Expand Down
56 changes: 56 additions & 0 deletions crates/batten/tests/retirement_doctrine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,62 @@ fn the_rules_say_what_a_retirement_owes_and_which_edit_is_admitted() {
}
}

/// "A policy surface" is three shapes, and the prose has to say which is which.
///
/// `has_policy_surface` admits a consumer module, a preset, or engine source, and
/// the arm is byte-checkable where the CHOICE between them is not — so the only
/// thing standing between CLOUD-843's ~130 remaining programs and CLOUD-1176's
/// scope creep is a reader knowing the order. A page that says "a policy surface"
/// and stops reads as "a Rego module", which makes every retirement onto an
/// existing verb look unspellable; that misreading cost this session two wrong
/// turns in one turn.
///
/// Both directions, for `the_rules_carry_the_edit_versus_deletion_asymmetry`'s
/// reason: the prose names the three homes, and the module still declares three
/// arms. Dropping the preset arm — the one no landed row has used — is the failure
/// this pins, because it is the arm whose absence pushes an author toward the core.
#[test]
fn the_rules_name_the_three_homes_and_the_module_admits_them() {
let text = squashed(&rules_text());
for clause in [
"Consumer module",
"**Preset**",
"Engine source",
"mechanism only",
] {
assert!(
text.contains(&squashed(clause)),
"{RULES} must name `{clause}` — `has_policy_surface` admits three \
successor shapes and cannot decide which one a retirement SHOULD have \
taken, so a page naming fewer than three leaves that choice to be \
guessed at exactly the moment it is being made"
);
}

let module = squashed(&fs::read_to_string(at_root(MODULE)).expect("the module is committed"));
assert_eq!(
module.matches("has_policy_surface(path) if {").count(),
3,
"{MODULE} must declare all three `has_policy_surface` arms — a preset lives \
at `crates/batten/src/policy/presets/**` and is a `.rego`, so it fails the \
module arm on the prefix and the engine-source arm on the suffix. Without \
its own arm the one generic-by-construction home cannot be spelled, while \
the core can, and the gate's incentive runs toward the thing {RULES} tells \
an author to avoid"
);
assert!(
module.contains("crates/batten/src/policy/presets/"),
"{MODULE}'s preset arm must match the path presets actually live at"
);
}

/// Whitespace-insensitive, because these clauses are prose: `mise run fmt` runs
/// prettier over Markdown and a reflow that moved a line break would otherwise
/// turn a live assertion into a false failure.
fn squashed(text: &str) -> String {
text.split_whitespace().collect::<Vec<_>>().join(" ")
}

/// The sentence the measured session cost, which is the one a reader acts on at
/// the moment the mistake is available to them.
#[test]
Expand Down
38 changes: 38 additions & 0 deletions policy/shell-retirement.rego
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,12 @@ arm_markers := ["// carried:", "// subsumed:", "// changed:", "// withdrawn:"]
# something which does not hold the predicate — the false `subsumed` in this
# module's vocabulary.
#
# "A POLICY SURFACE" IS THREE SHAPES, not a Rego module — a consumer module, a
# preset, or engine source, per `has_policy_surface` below. Reading it as "a
# module" makes every retirement onto an existing verb look unspellable, which is
# a wrong turn this file's own prose has caused; `.claude/rules/toolchain.md` is
# where the choice between the three is argued.
#
# So this arm trades those two obligations for two others, and it is strictly
# narrower than a `[[waiver]]` over the path, because it is spent one file at a
# time and only once the subject went with it.
Expand Down Expand Up @@ -878,6 +884,24 @@ has_policy_surface(path) if {
endswith(name, ".rs")
}

# THE PRESET ARM, AND IT WAS MISSING RATHER THAN DECLINED. A preset lives at
# `crates/batten/src/policy/presets/**` and is a `.rego`, so it failed the first
# arm on the prefix and the second on the suffix — the ONE successor shape
# `.claude/rules/policy-modules.md` calls generic-by-construction could not be
# spelled at all.
#
# That is not a cosmetic gap, because the gate's incentive ran the wrong way.
# Measured over the landed ledger at `5d38e0c2`: of 609 arms, **0** name a preset
# while 110 name engine source, 18 of those retiring a whole file. An author who
# picked the DRY home was refused and one who picked the core was not — which is
# CLOUD-1176's scope creep arriving through the mechanism meant to bound it, and
# it is the reason those 110 are a habit rather than 110 considered choices.
has_policy_surface(path) if {
some name in path_successors_for(path)
startswith(name, "crates/batten/src/policy/presets/")
endswith(name, ".rego")
}

has_binary_test(path) if {
some name in path_successors_for(path)
startswith(name, "crates/batten/tests/")
Expand Down Expand Up @@ -1140,6 +1164,20 @@ test_mapping_without_a_binary_test_is_refused if {
}}
}

# THE POSITIVE HALF OF THE PRESET ARM, and it is the case the arm was added for. A
# preset satisfies neither of the other two arms — wrong prefix for the module arm,
# wrong suffix for the engine-source one — so before this arm existed a retirement
# onto the one generic-by-construction home was refused, and the author's only
# passing move was to name the core instead. `test_mapping_without_a_policy_surface_is_refused`
# above is the mirror that keeps this from being satisfied by an arm admitting
# anything.
test_a_mapping_naming_only_a_preset_is_admitted if {
count(violation) == 0 with input as {"tree": {
"base-delta": {"added": [], "edited": [], "deleted": ["mise-tasks/old-gate.sh"]},
"lines": {"crates/batten/tests/old_gate.rs": ["// carried: mise-tasks/old-gate.sh crates/batten/src/policy/presets/hygiene/old-gate.rego crates/batten/tests/old_gate.rs"]},
}}
}

# --- the fourth arm (CLOUD-1080) --------------------------------------------
#
# The positive case: the subject dies in the same delta, the row carries a reason,
Expand Down