From 252b6651271b177a310d84015ca8f1768a41f0f8 Mon Sep 17 00:00:00 2001 From: Alec Wenzowski Date: Wed, 9 Sep 2026 09:07:00 +0000 Subject: [PATCH 01/23] feat(verdict): a rule id parses through the class grammar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first of CLOUD-1638's three arms, landable alone: `check_rule_id` and `normalise_rule_id`, not yet wired to load. Wiring them is the enforcement and cannot precede the migration of the 136 ids they will judge. THE SAME CHECKER, DELIBERATELY. A rule id and a class token are two names a reader meets in one line, so this is `check_name` with the id normalised first, not a fork of it. A second grammar would be a second thing to learn for no gain. Three spellings in, one out. An id reaches the engine from a `[[rule]] id`, a module's `"rule":` literal, a `//MUTANT` row and a `policy rule` argument, and those surfaces do not share a house style; accepting `-`, `_` and space at the boundary and storing only the space form is what keeps the id ONE name. Two spellings of an id would be the same two-names-for-one-thing this row exists to remove, one level down. The space form is also the cheap one — measured over the declared set, space 3.01 tokens against snake 3.94 and hyphen 4.41. Four cases: three spellings normalise to one; an undeclared word is refused; wrong arity is refused in both directions, since a two-word and a four-word id fail for the same reason and only one is obvious; doubled separators collapse rather than minting an empty word that would fail the arity arm naming the wrong defect. Refs: CLOUD-1638 --- crates/batten/src/verdict.rs | 88 ++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/crates/batten/src/verdict.rs b/crates/batten/src/verdict.rs index 067b79ad8..1138ba3a5 100644 --- a/crates/batten/src/verdict.rs +++ b/crates/batten/src/verdict.rs @@ -859,6 +859,43 @@ fn check_name( Ok(()) } +/// Normalise a rule id to the space form, accepting the three spellings. +/// +/// `-` and `_` are accepted at the boundary and NOWHERE stored: a name has one +/// spelling on every emitted surface, because two spellings of one id is the +/// same two-names-for-one-thing this row exists to remove, one level down. +/// Measured over the declared set (`o200k_base`, leading space): space **3.01** +/// tokens, snake **3.94**, hyphen **4.41** — so the space form is the cheap one +/// as well as the canonical one. +/// +/// This does not decide whether the words are declared; [`check_rule_id`] does. +#[must_use] +pub fn normalise_rule_id(id: &str) -> String { + id.split(['-', '_', ' ']) + .filter(|part| !part.is_empty()) + .collect::>() + .join(" ") +} + +/// A rule id is three vocabulary words, in the same grammar a class token is +/// (CLOUD-1638). +/// +/// **The same checker, deliberately.** A rule id and a class token are two names +/// for things a reader meets in the same line, and a second grammar would be a +/// second thing to learn for no gain — so this is [`check_name`] with the id +/// normalised first, not a fork of it. +/// +/// # Errors +/// +/// When the id is not exactly [`SLOTS`] words, or a word is not declared in the +/// vocabulary slot its position names. +pub fn check_rule_id(id: &str, vocabulary: &Vocabulary) -> anyhow::Result { + let normalised = normalise_rule_id(id); + let mut used = BTreeSet::new(); + check_name("rule", &normalised, vocabulary, &mut used)?; + Ok(normalised) +} + /// The per-entry half of [`validate`]. fn validate_one( verdict: &DeclaredVerdict, @@ -2277,6 +2314,57 @@ mod tests { .expect("a consumer that has not adopted the grammar still loads"); } + // --- the rule-id grammar (CLOUD-1638) ----------------------------------- + + #[test] + fn a_rule_id_is_accepted_in_three_spellings_and_stored_in_one() { + // THE POINT OF NORMALISING RATHER THAN PICKING ONE SPELLING: a rule id + // reaches this from a `[[rule]] id`, a module's `"rule":` literal, a + // `//MUTANT` row and a `policy rule` argument, and those surfaces do not + // share a house style. Accepting all three at the boundary and storing + // one is what keeps the id ONE name — two spellings of an id would be + // the same two-names-for-one-thing this row removes, a level down. + for spelling in ["task read first", "task-read-first", "task_read_first"] { + let back = check_rule_id(spelling, &vocab()) + .unwrap_or_else(|error| panic!("`{spelling}` is a legal id: {error}")); + assert_eq!( + back, "task read first", + "every spelling stores as the space form" + ); + } + } + + #[test] + fn a_rule_id_outside_the_vocabulary_is_refused() { + // The membership arm, which is what makes the id a NAME rather than + // free-text kebab prose — the defect this row is about. + assert!( + check_rule_id("task read undeclared", &vocab()).is_err(), + "a word no slot declares is refused in an id exactly as in a class" + ); + } + + #[test] + fn a_rule_id_of_the_wrong_arity_is_refused() { + // BOTH directions, because a two-word id and a four-word id fail for the + // same reason and only one of them is the obvious one. + for wrong in ["task read", "task read first second", "task"] { + assert!( + check_rule_id(wrong, &vocab()).is_err(), + "`{wrong}` is not three words and must be refused" + ); + } + } + + #[test] + fn normalising_an_id_collapses_repeated_separators_rather_than_minting_empty_words() { + // A doubled separator would otherwise yield an empty word, which reads + // as a four-word name and fails the arity arm with a message naming the + // wrong defect. Cheap to get right, confusing to leave. + assert_eq!(normalise_rule_id("task--read__first"), "task read first"); + assert_eq!(normalise_rule_id(" task read first "), "task read first"); + } + #[test] fn a_duplicate_token_is_refused() { assert!( From 0e72d2e22863764e34353f7c06a2fecb40740b2e Mon Sep 17 00:00:00 2001 From: Alec Wenzowski Date: Wed, 9 Sep 2026 09:17:00 +0000 Subject: [PATCH 02/23] feat(config)!: put all 136 rule ids in the three-word grammar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The migration arm: every `[[rule]] id`, every `[[waiver]] rule`, every module `rules contains` and `"rule":` literal naming one of them, and every `--rule ` invocation that reaches a row by name. TWENTY OF THEM COLLAPSE. Where a row raises exactly one class AND that class has exactly one raiser, the id IS the class token — `leased-push` becomes `branch write unsafe`, `task-substitution` becomes `task run loose`. The other 116 keep a distinct name, because they do not name one thing: 39 policy rows raise more than one class (`ci-parity` raises 21), and the 70 native-kind rows share their kind's class, where the id is the only discriminator. STRUCTURAL POSITIONS ONLY, and the first attempt is recorded because it was wrong. Rewriting every quoted or backticked occurrence touched 274 files and 1248 sites — including prose in `mise-tasks/perf.sh`, where `perf-assert` is ALSO a task name, so the comment came out saying something false. Reverted and redone against the positions that actually hold a rule id. `--rule` is one of those positions and was missed on the first pass; `hooks-wiring-check` caught it by refusing `--rule harness-wiring` against a config that no longer declares it. The task's own comment predicted exactly that: "a renamed row breaks this loudly instead of silently passing." The new spelling is quoted so the space form stays one argv word. Names are chosen, not derived: none of the 116 fell out of its old kebab id mechanically, so each was read off the row's own `reason`, `pattern` or module and validated for arity, slot membership, uniqueness, and — the arm that caught three — not colliding with a class token some other row raises. `config-lint` and `hooks-wiring-check` green over the renamed set. The grammar is not yet ENFORCED at load; that is the next commit, and it could not precede the ids it judges. Refs: CLOUD-1638 Admits: 3b29b9d6b3a25f1cacbdf6f5734437a9af3be5a51a744b238d1bde867e7823da Admits-rule: protected-mutation Admits-verdict: path write refused Admits-subject: batten.toml Admits-anchor: call:f043185a81ad41756e920956a002aa2fb4c409fb Admits-epoch: 722afc3686cbfec9fb85f978ef0c201c81c8be4e416a819645b95c032e612ac1 Admits-author: alec@wenzowski.com Admits-prev: c1f1b8a7b0c252247eb7ee2d335d5e7666e54898a462a86f0e6ec42b9825746d Admits-answer-lost: CLOUD-1638 cannot land at all. Its whole subject is the id grammar, and the ids live in batten.toml — there is no other file to change. Admits-answer-precondition: No surface can express this change: `config read first` reads batten.toml and `patch run first` restores it, and neither writes. CLOUD-1638 requires every `[[rule]] id` to move into the three-word grammar, which is a rewrite of 136 declarations in the authority itself; the write is a reviewed diff in the PR closing CLOUD-1638. Admits-answer-rejected-route: I rejected `patch run first`: it restores the authority rather than editing it, so it undoes the change instead of making it. `config read first` does not apply either — it is a READ of the file, and I have already read it to derive the census (136 rows, 20 collapsible); it leaves every id unmigrated. --- batten.toml | 282 +++++++++--------- crates/batten/tests/it/cli.rs | 4 +- crates/batten/tests/it/prose_only.rs | 2 +- crates/batten/tests/it/rules_drift.rs | 2 +- .../batten/tests/it/shell_write_advisory.rs | 2 +- mise.toml | 16 +- policy/ancestry-decides-nothing.rego | 4 +- policy/bats-invocation.rego | 24 +- policy/cfg-gated-test.rego | 2 +- policy/ci-suite-lane.rego | 6 +- policy/claim-before-code.rego | 4 +- policy/claim-order-is-stated.rego | 10 +- policy/command-task-defined.rego | 6 +- policy/connector-not-granted.rego | 4 +- policy/denials-outlive-the-turn.rego | 4 +- policy/egress-fencing.rego | 16 +- policy/forge-verdict-required.rego | 4 +- policy/harness-grant.rego | 6 +- policy/harness-wiring.rego | 8 +- policy/hk-fix-selection.rego | 10 +- policy/hook-profile.rego | 8 +- policy/hook-skip-local.rego | 4 +- policy/leased-push.rego | 4 +- policy/memories.rego | 12 +- policy/mise-pin-agreement.rego | 10 +- policy/module-layering.rego | 8 +- policy/mutation-declared-case.rego | 4 +- policy/opa-compliance.rego | 18 +- policy/pr-partition-restated.rego | 6 +- policy/privileged-lane.rego | 8 +- policy/prose-only.rego | 4 +- policy/release-tag-shape.rego | 4 +- policy/review-dispatched.rego | 4 +- policy/shell-retirement.rego | 4 +- policy/spawn-adapters.rego | 8 +- policy/spawn-widening.rego | 10 +- policy/stop-posture.rego | 4 +- policy/suite-subject-retirable.rego | 10 +- policy/task-substitution.rego | 4 +- policy/validator-verdict-clean.rego | 4 +- policy/verdict-routes-resolve.rego | 6 +- policy/weakens-declared.rego | 4 +- policy/workspace-dep-referenced.rego | 8 +- policy/worktree-registration.rego | 6 +- 44 files changed, 289 insertions(+), 289 deletions(-) diff --git a/batten.toml b/batten.toml index 504c8e5a4..da11f62a8 100644 --- a/batten.toml +++ b/batten.toml @@ -593,7 +593,7 @@ sha256 = "b2c822742e8cbf355ba0cb4cc690c3cd8fdc9ec1916c8148f27bd9098cb7aee4" # like every other row. The column, the per-row lookup and the suppression all # ship; what waits is this repository's own use of them. [[rule]] -id = "gh-pr-merge" +id = "commit ship other" kind = "shape" scope = "mediated_call" severity = "deny" @@ -609,7 +609,7 @@ discarding the exact objects CI tested. Use `mise run land`, which comments \ # matches — `contains` looks at the segment as written, which is the only reason # this key exists. [[rule]] -id = "gh-pr-comment-fast-forward" +id = "review ship early" kind = "shape" scope = "mediated_call" severity = "deny" @@ -622,7 +622,7 @@ lands. Use `mise run land` (backgrounded): it comments, then blocks until the \ PR is MERGED or the fast-forward bot refuses.""" [[rule]] -id = "gh-pr-checks" +id = "check watch loose" kind = "shape" scope = "mediated_call" severity = "deny" @@ -671,7 +671,7 @@ api`, `gh run view` are NOT blocked.)""" # is not a mediated call, so no `mediated_call` row can reach it — and it passes # the explicit form anyway, which this allows. [[rule]] -id = "leased-push" +id = "branch write unsafe" kind = "policy" scope = "mediated_call" module = "policy/leased-push.rego" @@ -681,14 +681,14 @@ severity = "deny" # already gates over the workflow files; the two read different surfaces and do # not overlap. [[rule]] -id = "hook-skip-local" +id = "hook skip unseen" kind = "policy" scope = "mediated_call" module = "policy/hook-skip-local.rego" severity = "deny" [[rule]] -id = "gh-run-watch" +id = "job watch loose" kind = "shape" scope = "mediated_call" severity = "deny" @@ -800,7 +800,7 @@ it rather than holding a foreground poll open.""" # DEFAULT strictness, including an arm asserting this row refuses without # `--fail-on-warning`, so both a deleted row and a silently lowered one redden. [[rule]] -id = "rebase-not-hand-stepped" +id = "patch run loose" kind = "shape" scope = "mediated_call" severity = "deny" @@ -848,7 +848,7 @@ conflict stop is a limit this row names rather than one it can soften.""" # compiling silently against the wrong compiler — measured 2026-08-09, three # gates disagreeing with a green local run. [[rule]] -id = "no-bare-cargo" +id = "cargo run loose" kind = "shape" scope = "mediated_call" severity = "deny" @@ -904,7 +904,7 @@ mise.toml, so a green local run can mean nothing. Use `mise run ` (\ # `RuleScope::Tree` alone. It lands as a `tree`-scoped `command` rule under # `batten enforce`, which is why `mise-tasks/issue-guard.sh` still exists. [[rule]] -id = "pr-names-an-issue" +id = "review name unnamed" kind = "shape" scope = "mediated_call" severity = "deny" @@ -922,7 +922,7 @@ the work lands with nothing recording that it did.""" # are two — the alternative would be a `patterns` list whose only user is this # pair. [[rule]] -id = "ready-names-an-issue" +id = "review open unnamed" kind = "shape" scope = "mediated_call" severity = "deny" @@ -953,7 +953,7 @@ own push. A PR that names no issue cannot move the board when it merges.""" # stale-main — so the three refusals `ready-guard` wrote by hand survive the # move without this file restating them. [[rule]] -id = "ready-needs-receipts" +id = "check read unread" kind = "receipt" scope = "mediated_call" severity = "deny" @@ -1193,7 +1193,7 @@ counts = "." # about a specific head — a review is of one set of bytes — and `branch` is what a # fact about the WORK takes, which is why `claim-needs-receipt` below takes it. [[rule]] -id = "ready-needs-an-answered-review" +id = "review answer unread" kind = "receipt" scope = "mediated_call" severity = "deny" @@ -1219,7 +1219,7 @@ on 89, so the usual outcome is that there is something to read.""" # added ones are satisfiable in an environment whose proxy refuses the command # above. [[rule]] -id = "ready-needs-the-threads-answered" +id = "review answer partial" kind = "receipt" scope = "mediated_call" severity = "deny" @@ -1247,7 +1247,7 @@ answer needs another read.""" # name the read that satisfies THAT check. A shared remedy had to describe both # reads and could not tell a reader which one was missing. [[rule]] -id = "ready-needs-a-review-to-exist" +id = "review list unread" kind = "receipt" scope = "mediated_call" severity = "deny" @@ -1264,7 +1264,7 @@ that was checked out when you read, so a push needs another read.""" # The DID-YOU-ANSWER-IT half. The module's own header carries the measurement, # the count reformulation, and why the thread ids cannot be in the message. [[rule]] -id = "review-answered" +id = "review judge missing" kind = "policy" scope = "mediated_call" module = "policy/review-answered.rego" @@ -1301,7 +1301,7 @@ severity = "deny" # task's write side is untouched by this row: the engine reads the file it already # writes. [[rule]] -id = "claim-needs-receipt" +id = "claim read unread" kind = "receipt" scope = "mediated_call" severity = "deny" @@ -2753,7 +2753,7 @@ zero-is-a-count = true # The receipt is minted by `mise run issue-search-check`, whose write side this # row does not touch — the engine reads the file that task already writes. [[rule]] -id = "filing-needs-a-search" +id = "issue list unread" kind = "receipt" scope = "mediated_call" severity = "deny" @@ -2821,7 +2821,7 @@ gated.""" # board can change after the read and `save_issue` offers no if-match precondition. # Unbounded, one adjudication would authorise every later sweep in the clone. [[rule]] -id = "a-move-to-in-review-owes-an-adjudication" +id = "review judge unread" kind = "receipt" scope = "mediated_call" severity = "deny" @@ -2906,7 +2906,7 @@ different owner and is not gated here.""" # the agent is entitled to use is the false-positive rate that gets a guard # bypassed rather than satisfied. [[rule]] -id = "a-todo-promotion-owes-a-ready-verdict" +id = "plan grade unread" kind = "receipt" scope = "mediated_call" severity = "deny" @@ -2975,7 +2975,7 @@ gated here.""" # named in its own notice, so the receipt attests the bytes the server actually # returned rather than a field subset. [[rule]] -id = "an-update-owes-a-recent-read" +id = "issue read stale" kind = "receipt" scope = "mediated_call" severity = "deny" @@ -3033,7 +3033,7 @@ is never gated by this row (that is `filing-needs-a-search`).""" # landing. What replaced it is an actor rather than a louder claim — `pr-unsubscribed # drop` makes the call from a task, where no permission control applies. [[rule]] -id = "no-pr-activity-subscription" +id = "review watch refused" kind = "shape" scope = "mediated_call" severity = "deny" @@ -3046,7 +3046,7 @@ A webhook's silence is not success. Fetching CI on demand is fine; the ban is on the timer.""" [[rule]] -id = "no-scheduled-self-wakeup" +id = "timer mint refused" kind = "shape" scope = "mediated_call" severity = "deny" @@ -3059,7 +3059,7 @@ session against 2 that changed a decision (CLOUD-821). Background the long comma and act on its exit.""" [[rule]] -id = "no-scheduled-trigger" +id = "event mint refused" kind = "shape" scope = "mediated_call" severity = "deny" @@ -3097,7 +3097,7 @@ clock nothing reads. Background the task and act on its exit.""" # file that goes on to read forty passes here and always will. Capping what is # written into the prompt is not a claim about what the agent does with it. [[rule]] -id = "a-spawn-names-few-artifacts" +id = "spawn count wrong" kind = "shape" scope = "mediated_call" severity = "deny" @@ -3123,7 +3123,7 @@ count is what the spawn can actually be made to read.""" # one (CLOUD-925 §1): one authority for what a ceiling is, so a per-call cap does # not arrive with its own arithmetic. [[rule]] -id = "a-spawn-prompt-stays-in-budget" +id = "prompt measure wrong" kind = "shape" scope = "mediated_call" severity = "deny" @@ -3189,7 +3189,7 @@ always-loaded surface — and let the prompt name the step it is for.""" # (CLOUD-929 — this sentence used to say "137 programs", which was wrong by # eleven within two days of being written). [[rule]] -id = "shell-hygiene" +id = "shell spelling wrong" kind = "policy" scope = "tree" preset = "shell-hygiene" @@ -3251,7 +3251,7 @@ exits 0, so a stale reference goes quiet rather than failing.""" # command line; and anything outside the repository, which keeps the # `>/tmp/x.log` form the row above MANDATES allowed. [[rule]] -id = "no-tool-substitution" +id = "tool select other" kind = "pipeline" scope = "mediated_call" severity = "deny" @@ -3283,7 +3283,7 @@ a pipe these same utilities are filters over another command's output and are \ not refused.""" [[rule]] -id = "verdict-not-discarded" +id = "verdict guard missing" kind = "pipeline" scope = "mediated_call" severity = "deny" @@ -3432,7 +3432,7 @@ any_argument = true # whole ruleset naming `batten enforce`. `mise run batten-check` already calls # `enforce` for the same reason `no-conflict-markers` made it necessary. [[rule]] -id = "no-secrets" +id = "source carry unsafe" kind = "secrets" glob = "**" severity = "deny" @@ -3440,7 +3440,7 @@ scope = "tree" no_fix_reason = "rotate the credential and purge it from history; removing the line leaves it in every clone and every reflog, so no command can make this right" [[rule]] -id = "no-conflict-markers" +id = "source carry broken" kind = "command" glob = "crates/**" check = "hk util check-merge-conflict --assume-in-merge {{files}}" @@ -3476,7 +3476,7 @@ no_fix_reason = "resolve the conflict by hand: choosing a side is a judgement no # hit, carrying the offending line into the finding; a `forbid` finding is # `path:line` and a rule id (non-negotiable rule 4). [[rule]] -id = "no-appeal-to-authority" +id = "prose point other" kind = "forbid" glob = "**" regex = "jdx|semgrep|opengrep|ast-grep" @@ -3548,7 +3548,7 @@ no_fix_reason = "state the rule and the evidence in this repo's terms; naming wh # could match one. The glob excludes this file, so neither rule matches its own # definition. [[rule]] -id = "no-consumer-account-literal" +id = "fact name other" kind = "forbid" glob = "crates/**" pattern = "etaxbc" @@ -3557,7 +3557,7 @@ scope = "tree" no_fix_reason = "delete the literal; a consumer fact belongs in that consumer's own batten.toml, never in the core" [[rule]] -id = "no-consumer-entity-path" +id = "path name other" kind = "forbid" glob = "crates/**" pattern = "entities/" @@ -3626,7 +3626,7 @@ no_fix_reason = "delete the literal; a consumer fact belongs in that consumer's # this glob, and `policy/**` carries none at all because a module reads # `data.batten.patterns["ready-issue-key"]` by id — the registry doing its job. [[rule]] -id = "issue-key-derivations-not-growing" +id = "issue key duplicate" kind = "ratchet" glob = "mise-tasks/**" # A LITERAL SUBSTRING, NOT A REGEX, and the distinction is a dead gate away. @@ -3664,7 +3664,7 @@ no_fix_reason = "read the key from `[[pattern]] ready-issue-key`, or let the pro # rule 1 asks for, and `tests/it/claim_race.rs` and `src/race.rs` are right to # carry it. [[rule]] -id = "no-tracker-key-in-core" +id = "issue name other" kind = "forbid" glob = "crates/**" regex = 'CLOUD-(\[0-9\]|\\d|\[\\d\])' @@ -3692,7 +3692,7 @@ no_fix_reason = "declare it as a `[[pattern]]` row and read it by id; a tracker' # author to write the literal inline. This glob names the in-repo module tree, # which is exactly where the exemption does not apply. [[rule]] -id = "no-tracker-key-in-modules" +id = "pattern name other" kind = "forbid" glob = "policy/**" regex = 'CLOUD-(\[0-9\]|\\d|\[\\d\])' @@ -3715,7 +3715,7 @@ no_fix_reason = "read the key from `[[pattern]] ready-issue-key` by id; a module # the four markers anchor it at the start of a line, so nothing but a ledger row # can reach the exemption. [[rule]] -id = "no-consumer-repo-name" +id = "source name other" kind = "forbid" glob = "crates/**" regex = "(?i)complian" @@ -3733,7 +3733,7 @@ scope = "tree" # workspace manifest, the toolchain config, a release workflow and the # generated changelog, but nowhere under crates/batten/tests/. [[rule]] -id = "no-origin-literal-in-fixtures" +id = "forge name other" kind = "forbid" glob = "crates/batten/tests/**" pattern = "button-inc" @@ -3769,7 +3769,7 @@ no_fix_reason = "rewrite the fixture so it does not name where this repository i # (CLOUD-208), never `severity = "allow"`: switching the rule off records nothing # and lapses never, which is the undesigned hatch the waiver surface replaced. [[rule]] -id = "no-source-built-tool" +id = "pin add unsafe" kind = "forbid" glob = "mise.toml" pattern = "\"cargo:" @@ -3804,7 +3804,7 @@ no_fix_reason = "pin an attested binary in mise.toml instead; what to pin is a s # comment mentioning `mise` is prose about the ecosystem, and `\bmise\b` in a # sentence is exactly the false positive that gets a gate switched off. [[rule]] -id = "install-does-one-thing" +id = "program add other" kind = "forbid" glob = "install.sh" regex = '^\s*(mise|cargo|apt|apt-get|npm|pipx|uv|brew|rustup)\s|>>\s*"?\$?\{?HOME\}?/\.(bashrc|profile|zshrc)' @@ -3831,7 +3831,7 @@ no_fix_reason = "the installer installs the binary and nothing else; a toolchain # trailing `__*`, because the failure is the character and not its position, and # a row catching one spelling would teach the next author to write the other. [[rule]] -id = "mcp-grant-matches-something" +id = "grant name loose" kind = "forbid" glob = ".claude/settings.json" regex = '"mcp__[^"]*\*' @@ -3840,7 +3840,7 @@ scope = "tree" no_fix_reason = "an MCP permission entry names a server (`mcp__serena`) or one tool (`mcp__serena__read_memory`) and takes no wildcard; enumerate the tools, which is also the only spelling a reader can check against the server's own list. Measured twice in one session: the wildcard granted nothing, and the bare server form granted the reads and left every write still prompting" [[rule]] -id = "no-cargo-install-in-ci" +id = "cargo add loose" kind = "forbid" glob = ".github/workflows/*.yml" pattern = "cargo install" @@ -3901,7 +3901,7 @@ no_fix_reason = "pin the tool in mise.toml so CI installs exactly what the lockf # literals they explain, for the same reason the agnosticism rules above do not # embed theirs: a substring rule over a directory fires on its own explanation. [[rule]] -id = "no-gnu-sed-z" +id = "shell parse unsafe" kind = "forbid" glob = "mise-tasks/**" pattern = "sed -zE" @@ -3912,7 +3912,7 @@ scope = "tree" # no-suffix spelling from `sed -i.bak`, which BSD accepts and which # `tests/helpers.bash` uses. [[rule]] -id = "no-gnu-sed-in-place" +id = "shell edit unsafe" kind = "forbid" glob = "mise-tasks/**" pattern = "sed -i " @@ -3920,7 +3920,7 @@ severity = "deny" scope = "tree" [[rule]] -id = "no-bash4-mapfile" +id = "shell read unsafe" kind = "forbid" glob = "mise-tasks/**" pattern = "mapfile " @@ -3928,7 +3928,7 @@ severity = "deny" scope = "tree" [[rule]] -id = "no-gnu-xargs-r" +id = "shell list unsafe" kind = "forbid" glob = "mise-tasks/**" pattern = "xargs -r" @@ -3939,7 +3939,7 @@ scope = "tree" # standing half: the refusal that fired before every other gate on a Mac was # correct behaviour over a Linux-only primitive. [[rule]] -id = "no-util-linux-flock" +id = "shell guard unsafe" kind = "forbid" glob = "mise-tasks/**" pattern = "flock " @@ -3959,7 +3959,7 @@ scope = "tree" # `tests/verified.bats`'s moved-main case fail, because it checks that other # branch out. [[rule]] -id = "no-branch-f-main" +id = "branch edit unsafe" kind = "forbid" glob = "tests/**/*.bats" pattern = "branch -f main" @@ -3989,7 +3989,7 @@ scope = "tree" # spelling the same prescription is NOT caught here — it is caught at the commit, # by the gate this row exists to stop anyone arguing with. [[rule]] -id = "no-denied-identity-prescribed" +id = "remedy carry refused" kind = "forbid" glob = "**/*.md" pattern = "user.email noreply@anthropic.com" @@ -4554,7 +4554,7 @@ fields = [ # no route at all. Recorded on CLOUD-1122 and CLOUD-1261; the deny lands when a # write succeeds through the verb. [[rule]] -id = "no-raw-issue-read" +id = "issue read loose" kind = "shape" scope = "mediated_call" severity = "deny" @@ -4790,7 +4790,7 @@ max_repeats = 1 # an expiry — never `severity = "allow"`, which switches the row off forever and # tells nobody. [[rule]] -id = "tests-not-deleted" +id = "test count dropped" kind = "ratchet" glob = "crates/**/*.rs" pattern = "#[test]" @@ -4812,7 +4812,7 @@ no_fix_reason = "restore the tests, or waive the reduction deliberately; which o # because the narrow glob was only correct while every bats suite sat directly in # `tests/` — this one covers a nested one the day it is written. [[rule]] -id = "bats-tests-not-deleted" +id = "bats count dropped" kind = "ratchet" glob = "tests/**/*.bats" pattern = "@test \"" @@ -4921,7 +4921,7 @@ declared_in = "crates/batten/tests/**/*.rs" # a mention does not. (`budget.rs` anchors its HTML-comment scan for the same # reason.) [[rule]] -id = "no-new-ignores" +id = "test skip refused" kind = "ratchet" glob = "crates/**/*.rs" pattern = "\n#[ignore]" @@ -4962,7 +4962,7 @@ no_fix_reason = "delete the new ignore, or waive it deliberately; an ignore is a # CLOUD-807 gave for the sibling: retirement is the steady state of a repo # folding its bash into its own engine, not an exception. [[rule]] -id = "bash-surface-not-growing" +id = "shell count ahead" kind = "ratchet" glob = "mise-tasks/**" pattern = "#MISE description=" @@ -5044,7 +5044,7 @@ no_fix_reason = "migrate the predicate onto a rule kind, or declare `# stays-bas # real bash (`8b234af`, `fba3f78`). A row that denies its own campaign's wins is # worse than the hole it closes. [[rule]] -id = "inline-task-bodies-not-growing" +id = "task carry other" kind = "ratchet" glob = "mise.toml" pattern = "run = '''" @@ -5093,7 +5093,7 @@ no_fix_reason = "migrate the predicate onto a rule kind, or waive the increase d # argument applies to every ban in that list, and a row that named only the sleep # would leave the multi-thread-runtime ban waivable at will for the same reason. [[rule]] -id = "delay-waivers-not-growing" +id = "waiver add refused" kind = "ratchet" glob = "crates/batten/src/**/*.rs" pattern = "clippy::disallowed_methods" @@ -5106,7 +5106,7 @@ no_fix_reason = "a waiver is not a fix: either the delay has a bound it can exit # The other spelling of the same concept — see the block above for why it is a # second row rather than a second pattern on the first. [[rule]] -id = "inline-task-bodies-not-growing-basic" +id = "task write other" kind = "ratchet" glob = "mise.toml" pattern = 'run = """' @@ -5128,7 +5128,7 @@ no_fix_reason = "migrate the predicate onto a rule kind, or waive the increase d # `rules/*.md` would count as a program. Zero today; if one lands, the # answer is CLOUD-1058's line-anchored `regex`, not a per-file exception. [[rule]] -id = "claude-shell-not-growing" +id = "shell add other" kind = "ratchet" glob = ".claude/**" pattern = "#!" @@ -5173,7 +5173,7 @@ no_fix_reason = "migrate the predicate onto a rule kind, or declare `# stays-bas # handing it every path in the repository would make it read the engine's own # source to answer a question about fixtures. [[rule]] -id = "fixture-forks" +id = "test fix duplicate" kind = "policy" scope = "tree" base = "origin/main" @@ -5206,7 +5206,7 @@ severity = "deny" # which file answers the question — is the module's and is unchanged. # Recorded on CLOUD-1320, which owns the general form. [[rule]] -id = "landing-roster-guarded" +id = "check read never" kind = "policy" scope = "tree" line_sources = [".github/workflows/*.yml"] @@ -5218,7 +5218,7 @@ severity = "deny" # a refactor legitimately merges assertions. `--fail-on-warning` and # `--strictness strict` promote it when a run wants the stricter reading. [[rule]] -id = "assertions-not-gutted" +id = "test grade dropped" kind = "ratchet" glob = "crates/batten/tests/**/*.rs" pattern = "assert" @@ -5260,7 +5260,7 @@ no_fix_reason = "restore the assertions, or waive the reduction deliberately" # rendezvous is documented in comments that say what it replaced, and a rule that # forbade the words would delete the explanation along with the construct. [[rule]] -id = "no-bash4-wait-n" +id = "shell run unsafe" kind = "forbid" glob = "mise-tasks/**" regex = "^[ \\t]*wait -n" @@ -5286,7 +5286,7 @@ scope = "tree" # otherwise make row 2 pass by never asking anything (non-negotiable rule 2, and # `sbom-empty`'s lesson one layer up). [[rule]] -id = "sbom-ntia-precondition" +id = "manifest check unread" kind = "command" glob = "Cargo.lock" check = "mise run ntia-check --precondition" @@ -5336,7 +5336,7 @@ no_fix_reason = "install the pinned checker (`mise install pipx:ntia-conformance # a tuning question. Taking the rate to zero is what this change does; the `deny` # is what keeps it there. [[rule]] -id = "sbom-ntia-conformance" +id = "manifest cover partial" kind = "command" glob = "Cargo.lock" check = "mise run ntia-check" @@ -5379,7 +5379,7 @@ no_fix_reason = "the missing fields do not exist in a cargo lockfile, so no comm # the file the row exists to run is what makes the trigger and the mechanism the # same object rather than a coincidence. [[rule]] -id = "release-attestation-precondition" +id = "release check unread" kind = "command" glob = "mise-tasks/attestation-check.sh" check = "mise run attestation-check --precondition" @@ -5419,7 +5419,7 @@ no_fix_reason = "install the pinned `gh` (`mise install aqua:cli/cli`) or supply # ambient environment the way an earlier `attestation-check` did inside fixture # repositories. The only way it reds is the workflow actually losing its wiring. [[rule]] -id = "release-tracking-check" +id = "release wire missing" kind = "command" glob = "mise-tasks/release-tracking-check.sh" check = "mise run release-tracking-check" @@ -5473,7 +5473,7 @@ no_fix_reason = "the missing node is in the workflow, not in this file; add the # A tree-scoped row would hand it a document it has no predicate for and read as # a configured gate that never fires. [[rule]] -id = "trunk-based-preset" +id = "branch table missing" kind = "policy" scope = "mediated_call" preset = "trunk-based" @@ -5521,7 +5521,7 @@ severity = "deny" # manifest here is refused at load and would put a consumer's filenames in a row # that ships to every consumer. [[rule]] -id = "mise-preset" +id = "task table missing" kind = "policy" scope = "mediated_call" preset = "mise" @@ -5553,7 +5553,7 @@ severity = "warn" # document; the lines are read to place a finding at a job, and a pointer that # cannot be placed costs the line rather than the finding. [[rule]] -id = "mise-preset-tree" +id = "task table other" kind = "policy" scope = "tree" preset = "mise" @@ -5566,7 +5566,7 @@ line_sources = [".github/workflows/*.yml", ".github/workflows/*.yaml"] severity = "deny" [[rule]] -id = "pinned-toolchain-preset" +id = "pin table other" kind = "policy" scope = "mediated_call" preset = "pinned-toolchain" @@ -5590,7 +5590,7 @@ severity = "warn" # split is non-negotiable rule 1: a preset reaches every consumer, so this # repository's job names may not travel inside one. [[rule]] -id = "ci-hygiene" +id = "job spelling wrong" kind = "policy" scope = "tree" preset = "ci-hygiene" @@ -5638,7 +5638,7 @@ severity = "deny" # stated rather than hidden: at `warn` the refusal is advice, so a re-grade this # row exists to stop still runs. [[rule]] -id = "landing-loop-preset" +id = "lane declare missing" kind = "policy" scope = "tree" preset = "landing-loop" @@ -5680,7 +5680,7 @@ severity = "warn" # is worth asking, and asking on a change to the gate itself is the case that # must never be skipped. [[rule]] -id = "evaluator-closure-io-free" +id = "layer reach unsafe" kind = "command" glob = "mise-tasks/evaluator-closure-check.sh" check = "mise run evaluator-closure-check" @@ -5712,7 +5712,7 @@ no_fix_reason = "an IO crate reaching the evaluator is closed where it was enabl # schema). A gate that would have refused past releases fires on work nobody can # now fix; this one would not have. [[rule]] -id = "no-key-leaves-the-schema-unannounced" +id = "config retire unnamed" kind = "command" glob = "crates/batten/tests/it/config_deprecations.rs" check = "mise run test:config-deprecations" @@ -5771,14 +5771,14 @@ regex = "^(-[A-Za-z]*F|--file)$" # performs it is precisely what that gate refuses. So the row keeps its name and # this comment carries the correction; the module file is the honest label. [[rule]] -id = "commit-message-obtainable" +id = "commit read missing" kind = "policy" scope = "mediated_call" module = "policy/run-shape.rego" severity = "deny" [[rule]] -id = "privileged-lane-tests-origin" +id = "lane guard other" kind = "policy" scope = "tree" sources = [".github/workflows/*.yml", ".github/workflows/*.yaml"] @@ -5795,7 +5795,7 @@ severity = "deny" # fails loudly the first time it runs; a ROUTE naming one fails when a reader has # just been refused, has been handed the one thing to run, and runs it. [[rule]] -id = "verdict-routes-resolve" +id = "route resolve missing" kind = "policy" scope = "tree" sources = ["batten.toml", "mise.toml"] @@ -5818,7 +5818,7 @@ severity = "deny" # policy at all — which is why `crates/batten/tests/remedy_authorship.rs` drives # the binary rather than trusting the schema. [[rule]] -id = "remedy-authorship" +id = "remedy own other" kind = "policy" scope = "tree" line_sources = ["mise-tasks/*.sh"] @@ -5849,7 +5849,7 @@ severity = "deny" # `perf-gate` is the sensor on whether that stays inside the budget, and the # number is reported on the PR rather than asserted here. [[rule]] -id = "shell-retirement" +id = "shell retire partial" kind = "policy" scope = "tree" base = "origin/main" @@ -5931,7 +5931,7 @@ severity = "deny" # `BATTEN_FILED_HERE_OVERLAP`: the point of the admission mechanism is that the # bare variable stops working, and this is the row that variable served. [[rule]] -id = "filed-here" +id = "issue file other" kind = "policy" scope = "tree" base = "origin/main" @@ -5966,7 +5966,7 @@ severity = "deny" # `delta_sources` is the whole tree because the subject is the recorded # obligation set rather than any path this branch happens to touch. [[rule]] -id = "obligations-bound" +id = "test name undefined" kind = "policy" scope = "tree" base = "origin/main" @@ -5989,7 +5989,7 @@ severity = "deny" # somebody renames a case in the SUITE, which is a different file from the one # carrying the promise. [[rule]] -id = "mutation-declared-case" +id = "marker name undefined" kind = "policy" scope = "tree" line_sources = [ @@ -6003,7 +6003,7 @@ module = "policy/mutation-declared-case.rego" severity = "deny" [[rule]] -id = "plan-complete" +id = "plan cover partial" kind = "policy" scope = "tree" base = "origin/main" @@ -6012,7 +6012,7 @@ module = "policy/plan-complete.rego" severity = "deny" [[rule]] -id = "test-targets" +id = "test place wrong" kind = "policy" scope = "tree" base = "origin/main" @@ -6033,7 +6033,7 @@ severity = "deny" # can live, and handing the module every path in the repository would make it # read `batten.toml` looking for Rust attributes. [[rule]] -id = "cfg-gated-test" +id = "test cover missing" kind = "policy" scope = "tree" base = "origin/main" @@ -6043,7 +6043,7 @@ module = "policy/cfg-gated-test.rego" severity = "deny" [[rule]] -id = "stop-posture" +id = "prose report duplicate" kind = "policy" scope = "mediated_call" module = "policy/stop-posture.rego" @@ -6067,14 +6067,14 @@ severity = "deny" # does, and a registered gate emitting into a channel nobody reads is the defect # that issue exists inside. [[rule]] -id = "shell-write-advisory" +id = "shell edit early" kind = "policy" scope = "mediated_call" module = "policy/shell-write-advisory.rego" severity = "warn" [[rule]] -id = "prose-only" +id = "diff ship early" kind = "policy" scope = "tree" base = "origin/main" @@ -6123,7 +6123,7 @@ severity = "deny" # claim of tree-wide reach. A path no glob names is unread -- `absent` in the # could-not-look channel, which the module deliberately stays silent on. [[rule]] -id = "pr-partition-restated" +id = "review state other" kind = "policy" scope = "tree" line_sources = [ @@ -6138,7 +6138,7 @@ severity = "deny" reason = "AGENTS.md is the one authority on how work lands: one branch, one commit per row, and a single request over all of them. A second statement of that decision in the opposite direction is not a duplicate, it is a competing rule -- and prose in this tree is read by an agent that then acts without re-deriving, so it carries the authority of the file it sits in. Measured 2026-09-02: one such line, quoting a single issue's own arrangement, was adopted as convention and produced a plan partitioned eight ways. Delete the sentence or paraphrase it so it states no rate." [[rule]] -id = "ancestry-decides-nothing" +id = "patch judge wrong" kind = "policy" scope = "tree" invocation_sources = ["crates/batten/src/*.rs"] @@ -6215,7 +6215,7 @@ severity = "deny" # that loads at the trigger. Both arms are therefore required: either half alone # is a reader who learns half the trap. [[rule]] -id = "claim-order-is-stated" +id = "claim declare dropped" kind = "policy" scope = "tree" line_sources = ["AGENTS.md", "rules/toolchain.md"] @@ -6242,7 +6242,7 @@ severity = "deny" # rows ask nothing about the registry never opens the common dir's `worktrees/` # directory, which is what keeps `Cost::Read` honest. [[rule]] -id = "worktree-registration-live" +id = "registry read missing" kind = "policy" scope = "tree" git = ["worktrees"] @@ -6250,7 +6250,7 @@ module = "policy/worktree-registration.rego" severity = "deny" [[rule]] -id = "forge-verdict-required" +id = "forge check red" kind = "policy" scope = "tree" forge = ["HEAD"] @@ -6284,7 +6284,7 @@ severity = "deny" # refuses only on a real count, because reading null as "nothing was stranded" # is the false green CLOUD-990 measured costing a session an hour. [[rule]] -id = "denials-outlive-the-turn" +id = "turn deny held" kind = "policy" scope = "mediated_call" module = "policy/denials-outlive-the-turn.rego" @@ -6331,7 +6331,7 @@ count = "hook-denials" # states are null and every one differs from an extractor that ran and counted # zero, so the module refuses only on a real count. [[rule]] -id = "repetition-without-progress" +id = "turn run loose" kind = "policy" scope = "mediated_call" module = "policy/repetition-without-progress.rego" @@ -6361,7 +6361,7 @@ count = "agent-turn-run" # guard comparing a call against an empty task table would refuse every command # this project runs. [[rule]] -id = "task-substitution" +id = "task run loose" kind = "policy" scope = "mediated_call" module = "policy/task-substitution.rego" @@ -6404,7 +6404,7 @@ node = "tasks" # not a negative, so this row is inert on a checkout whose store holds no # matching response. [[rule]] -id = "claim-before-code" +id = "claim mint absent" kind = "policy" scope = "tree" module = "policy/claim-before-code.rego" @@ -6435,7 +6435,7 @@ reduce = "present" # particular prompt ran" a checkable claim rather than an intention, and what # stops a consumer satisfying the gate by pointing it at an easier one. [[rule]] -id = "review-dispatched" +id = "prompt run never" kind = "policy" scope = "tree" base = "origin/main" @@ -6549,7 +6549,7 @@ subject = "tracker-body" # that gets a guard switched off. One row cannot carry two keyings, and the two # questions genuinely have different answers. [[rule]] -id = "ready-needs-review" +id = "review ask missing" kind = "receipt" scope = "mediated_call" severity = "deny" @@ -6598,7 +6598,7 @@ looked at it.""" # checkout gets if the producer was skipped or died. The row that wants a verdict # to be REQUIRED is `forge-verdict-required`'s shape and is not this one. [[rule]] -id = "validator-verdict-clean" +id = "tool judge dirty" kind = "policy" scope = "tree" module = "policy/validator-verdict-clean.rego" @@ -6638,7 +6638,7 @@ input = "renovate.json5" # a FILE rather than of a plan — whether the hook still passes `--profile '!slow'` # on a line that actually runs — so it reads `input.tree.lines` and needs no record. [[rule]] -id = "hook-profile" +id = "hook declare other" kind = "policy" scope = "tree" module = "policy/hook-profile.rego" @@ -6674,7 +6674,7 @@ input = "hk.pkl" # is inert on a checkout whose globs never fired. Present-and-EMPTY is a finding, # because every count below would otherwise pass over an absent key. [[rule]] -id = "sbom-inventory" +id = "manifest list wrong" kind = "policy" scope = "tree" module = "policy/sbom-inventory.rego" @@ -6737,7 +6737,7 @@ input = "Cargo.lock" # pinned producer writes under a different key and does not answer here, which is # the same property stated from the tool's side. [[rule]] -id = "perf-assert" +id = "path measure wrong" kind = "policy" scope = "tree" lines = ["README.md"] @@ -6772,7 +6772,7 @@ input = "target/release/batten" # empty document. `crates/batten/tests/it/nextest_slow.rs` drives the engine over a # tree with the file deleted, because that is the only tier that can tell. [[rule]] -id = "nextest-slow" +id = "suite grade late" kind = "policy" scope = "tree" lines = [".config/nextest.toml"] @@ -6806,7 +6806,7 @@ severity = "deny" # it re-runs whenever the record set grows. Measured 2026-09-05: 6 trial rows, 13 # required keys, 78 mutations, 78 fired, 0 false positives. [[rule]] -id = "agentic-experiment-record" +id = "fact file missing" kind = "policy" scope = "tree" documents = ["bench/agentic/trials.toml", "bench/agentic/method.toml"] @@ -6814,7 +6814,7 @@ module = "policy/agentic-experiment-record.rego" severity = "deny" [[rule]] -id = "release-tag-shape" +id = "tag mint wrong" kind = "policy" scope = "tree" module = "policy/release-tag-shape.rego" @@ -6873,7 +6873,7 @@ tags = "v*" # install` REWRITES `mise.lock` behind the author's back, and nothing writes a # workflow file but a person. [[rule]] -id = "lock-complete" +id = "lock cover partial" kind = "policy" scope = "tree" staged = ["mise.lock", "mise.toml"] @@ -6902,7 +6902,7 @@ severity = "deny" # restraint — and an `attribution` predicate that needs `%B` therefore does not # migrate and keeps its verb, which is the verdict CLOUD-1193 records. [[rule]] -id = "weakens-declared" +id = "commit declare empty" kind = "policy" scope = "tree" commits = ["origin/main..HEAD"] @@ -6910,7 +6910,7 @@ module = "policy/weakens-declared.rego" severity = "deny" [[rule]] -id = "harness-wiring" +id = "hook wire missing" kind = "policy" scope = "tree" documents = [ @@ -6967,7 +6967,7 @@ path = ".gemini/settings.json" # `documents` rather than `external`: this one IS repo-rooted, so it needs no # declared id and no root variable. [[rule]] -id = "harness-grant" +id = "grant carry missing" kind = "policy" scope = "tree" documents = [".claude/settings.json"] @@ -6982,7 +6982,7 @@ reason = "`permissions.allow` is not the layer that decides: Claude Code's auto- # two modules read the same document for different questions, which is why they # are two rules rather than one with two predicates. [[rule]] -id = "connector-not-granted" +id = "connector grant loose" kind = "policy" scope = "tree" documents = [".claude/settings.json"] @@ -7028,7 +7028,7 @@ reason = "a `[[mcp.result]]` reduction decides nothing while the raw tool is gra # consumer's choice: `no_artifact_name_reaches_the_core` refuses the engine # learning this manifest's name (non-negotiable rule 1). [[rule]] -id = "command-task-defined" +id = "task bind undefined" kind = "policy" scope = "tree" sources = ["batten.toml", "mise.toml"] @@ -7071,7 +7071,7 @@ severity = "deny" # matters. The opposite mistake — a name that matches no step — runs the step in # both jobs, which shows up in the bill and needs no gate. [[rule]] -id = "ci-suite-lane" +id = "job select missing" kind = "policy" scope = "tree" sources = [".github/workflows/ci.yml"] @@ -7097,7 +7097,7 @@ severity = "deny" # program refused in its own words. It stays a mise task, where mise answers for # its own task graph. [[rule]] -id = "ci-parity" +id = "job check other" kind = "policy" scope = "tree" sources = [ @@ -7145,7 +7145,7 @@ severity = "deny" # line in `mise.toml`, and declaring one would acquire a document for a pointer # no finding carries. [[rule]] -id = "ci-cache-declared" +id = "job carry missing" kind = "policy" scope = "tree" sources = [".github/workflows/*.yml", "mise.toml"] @@ -7168,7 +7168,7 @@ severity = "deny" # structure — a matrix's `target` values and a platform table's keys — never a # line, so declaring a line surface would acquire bytes nothing decides over. [[rule]] -id = "release-provision-parity" +id = "release check partial" kind = "policy" scope = "tree" sources = [".github/workflows/release-artifacts.yml", "batten.toml"] @@ -7177,7 +7177,7 @@ severity = "deny" no_fix_reason = "a platform a provisioned tool cannot reach is not fixable in this tree: either the upstream artifact exists and the row should pin it, or it does not and the gap is declared with its consequence — and which of the two is a supply-chain decision, not a rewrite" [[rule]] -id = "bats-invocation" +id = "bats run wrong" kind = "policy" scope = "tree" sources = ["mise.toml", ".github/workflows/ci.yml"] @@ -7221,7 +7221,7 @@ severity = "deny" # shipped convention template by path, rather than this row trying to subtract # them -- `line_sources` is a union of includes with no negation. [[rule]] -id = "memory-graph" +id = "memory point missing" kind = "policy" scope = "tree" line_sources = [ @@ -7245,7 +7245,7 @@ module = "policy/memories.rego" severity = "deny" [[rule]] -id = "suite-subject-retirable" +id = "suite retire unclear" kind = "policy" scope = "tree" line_sources = ["tests/*.bats", "tests/**/*.bats"] @@ -7282,7 +7282,7 @@ severity = "deny" # Tree-scoped and spawning, which is the right side of the CLOUD-170 split: it # runs under `enforce` and the hk gate, never on the mediated path. [[rule]] -id = "fix-selection-complete" +id = "gate fix missing" kind = "command" glob = "hk.pkl" check = "mise run fix-selection-check" @@ -7316,7 +7316,7 @@ no_fix_reason = "add the missing step to `fixers` in hk.pkl, or remove the one t # rule 1: `crates/batten` names no step of anybody's, and the module reads the # list out of the fact it is handed. [[rule]] -id = "hk-plan-required" +id = "plan require missing" kind = "policy" scope = "tree" module = "policy/hk-plan-required.rego" @@ -7332,7 +7332,7 @@ hook = "check" required = ["batten-check", "test", "policy-test"] [[rule]] -id = "hk-contract-drift" +id = "gate table other" kind = "command" glob = "hk.pkl" check = "mise run hk-drift" @@ -7341,7 +7341,7 @@ scope = "tree" no_fix_reason = "regenerate the projection with `mise run hk-contract` and read the diff: which of an added, removed, reordered or regrouped step is intended is a decision, and a gate that applied it would be absorbing the change nobody reviewed" [[rule]] -id = "hk-fix-selection" +id = "gate select wrong" kind = "policy" scope = "tree" sources = ["mise.toml"] @@ -7350,7 +7350,7 @@ module = "policy/hk-fix-selection.rego" severity = "deny" [[rule]] -id = "workspace-dep-referenced" +id = "workspace list unused" kind = "policy" scope = "tree" sources = ["Cargo.toml", "crates/*/Cargo.toml"] @@ -7358,7 +7358,7 @@ module = "policy/workspace-dep-referenced.rego" severity = "deny" [[rule]] -id = "module-layering" +id = "layer place wrong" kind = "policy" scope = "tree" use_sources = ["crates/batten/src/*.rs"] @@ -7376,7 +7376,7 @@ severity = "deny" # crate the analyser compiles; a glob here would select files the module does not # read and skip the row when the glob matched nothing. [[rule]] -id = "spawn-adapters" +id = "adapter place missing" kind = "policy" scope = "tree" symbols = true @@ -7408,7 +7408,7 @@ severity = "deny" # shape. A spawn that genuinely belongs is a decision for a groomed row and a # human, never an annotation an agent writes about its own work. [[rule]] -id = "spawn-widening" +id = "spawn add other" kind = "policy" scope = "tree" module = "policy/spawn-widening.rego" @@ -7472,7 +7472,7 @@ line_sources = ["crates/batten/src/**/*.rs", "policy/spawn-adapters.rego"] # healthy, and no rule looked at the tree at all. `doctor egress` catches a bad # container; this catches the commit. [[rule]] -id = "egress-fencing" +id = "provision guard missing" kind = "policy" scope = "tree" documents = ["mise.toml", "batten.toml"] @@ -7481,7 +7481,7 @@ severity = "deny" reason = "Two surfaces fence the resolver host out of the agent proxy, and the tree owns both: mise.toml's [env] for what mise spawns, and batten.toml's [[provision.env]] for the wrapper that fences mise's own resolver (CLOUD-1455). Removing or narrowing either restores the 403 the fence exists for. Both spellings are checked on both, because every client in this class reads the lower-case name first." [[rule]] -id = "mise-pin-agreement" +id = "pin declare wrong" kind = "policy" scope = "tree" documents = [".mcp.json", "mise.toml"] @@ -7490,7 +7490,7 @@ severity = "deny" reason = "mise.toml owns the pin and .mcp.json's copy is a reference to it: move the version in mise.toml and repeat it in the scoped `mise exec`, never the other way round. A launch that names no tool before `--` provisions the whole toolchain and dies with any one of it (CLOUD-316)." [[rule]] -id = "opa-tracks-regorus-compliance" +id = "version check stale" kind = "policy" scope = "tree" sources = ["mise.toml", "Cargo.toml"] @@ -7517,7 +7517,7 @@ severity = "deny" # share one binding and a per-file rule would report the other as a false # positive. `.regal/rules/.../every_package_binds_input.rego` carries the reasoning. [[rule]] -id = "policy-modules-bind-input" +id = "module bind missing" kind = "command" # `glob` NAMES THE LINTER'S OWN CONFIG, not the corpus it reads (CLOUD-614), and # this row is where that idiom stopped being a style note. Globbed at @@ -7552,7 +7552,7 @@ no_fix_reason = "add a `# METADATA schemas:` block binding `input` to the schema # annotation requirement is not decoration beside this row; it is the half that # makes this row mean anything, and it is the Regal aggregate rule's job. [[rule]] -id = "policy-modules-type-check" +id = "module read undefined" kind = "command" # The schema directory rather than the corpus, for the reason spelled out on # `policy-modules-bind-input` above: a glob over `policy/**` matches inside the @@ -7583,7 +7583,7 @@ no_fix_reason = "the module names an input path the engine does not emit; fix th # other. Without this row the suite would run nowhere and the rule would be a # linter guarded by nothing. [[rule]] -id = "policy-lint-rule-tests" +id = "module judge red" kind = "command" glob = ".regal/rules/**/*.rego" check = "mise exec -- regal test .regal/rules" @@ -7592,7 +7592,7 @@ scope = "tree" no_fix_reason = "a custom Regal rule's own tests failed; `mise exec -- regal test .regal/rules` names the failing case. Every arm is paired on purpose — if only the negative arm still passes, the predicate has stopped discriminating" [[rule]] -id = "no-rego-metadata" +id = "module reach undefined" kind = "forbid" glob = "policy/**/*.rego" pattern = "rego.metadata." @@ -7601,7 +7601,7 @@ scope = "tree" no_fix_reason = "regorus implements no `rego.metadata.*` builtin, so the rule would pass `opa check` and behave differently under the shipped engine. Carry whatever the annotation held as an explicit fact on the row or in the module itself." [[rule]] -id = "claim-not-raced" +id = "claim mint twice" kind = "command" glob = "crates/batten/src/race.rs" check = "mise run claim-race" @@ -7637,13 +7637,13 @@ no_fix_reason = "a raced claim is resolved on the board, not in this tree: take # LAPSES. The expiry is the mechanism — when it does, someone re-reads the line # rather than inheriting a decision nobody remembers making. [[waiver]] -rule = "no-secrets" +rule = "source carry unsafe" path = ".github/workflows/release-plz.yml" reason = "ripsecrets reads `${GH_PAT}` in a git-config URL as an embedded credential; it is a shell expansion of an Actions secret, so nothing is committed. Re-check against the pinned scanner's next version." expires = "2026-11-14" [[waiver]] -rule = "tests-not-deleted" +rule = "test count dropped" reason = "CLOUD-780 retires four `git.rs` primitives, the pileup predicate and the `worktree reclaim` verb, and every deleted case named a deleted symbol (1850 -> 1834): `git.rs`'s worktree-listing and snapshot cases, `worktree.rs`'s pileup and reclaim cases, and `tests/cli.rs`'s whole pileup block. Deleting them with their subject is the Ready block's own §7 obligation — weakening them instead would leave assertions about a surface that no longer exists. What must NOT fall is the coverage beside them, and it does not: `worktree status`'s four surviving categories keep every case green with no assertion change, which is what proves the drop was surgical." expires = "2026-09-13" @@ -7684,7 +7684,7 @@ expires = "2026-09-13" # floor becomes 32 on its own and this row suppresses nothing. It exists to get # one commit past the gate, not to stand. [[waiver]] -rule = "inline-task-bodies-not-growing" +rule = "task carry other" reason = "CLOUD-1265's `[tasks.record-verdicts]` is a producer's EFFECT, not a predicate: the predicate it replaces migrated to `policy/validator-verdict-clean.rego`, and section 5 leaves no rule kind for a body that must spawn a validator. `mise-tasks/pkl-check.sh` and its suite leave the tree in the same commit, so the bash surface falls while this row's `mise.toml`-only count rises by one." expires = "2026-10-31" @@ -7844,7 +7844,7 @@ reason = "CLOUD-1551 defeats this rule's override route the same way it defeats expires = "2026-10-11" [[waiver]] -rule = "claim-before-code" +rule = "claim mint absent" reason = "CLOUD-1387: `captured::reduce` selects the first capture whose bytes MENTION the key rather than the one it is the subject of, so a response quoting the row shadows its real payload and `present` answers false over a row that is on a project. Fix is open in PR #842; remove this waiver with it." expires = "2026-10-04" @@ -13399,7 +13399,7 @@ target = "policy/spawn-adapters.rego" # documents, which is what lets predicate 3 do `jq`'s recursive descent as # `walk` rather than as a line scan over JSON. [[rule]] -id = "rules-drift" +id = "rule watch other" kind = "policy" scope = "tree" module = "policy/rules-drift.rego" @@ -13739,7 +13739,7 @@ target = "policy/cfg-gated-test.rego" # the case that keying exists to separate. `apply_admissions` runs inside `check`, # so it runs on the runner too, over a store that cannot be there. A spend # therefore suppresses on the host that minted it and nowhere else: the finding -# below was admitted locally, `batten check --rule cfg-gated-test` read exit 0, +# below was admitted locally, `batten check --rule 'test cover missing'` read exit 0, # and CI's `batten-check` raised # `crates/batten/src/provision.rs platform-gated-test-added` on the same commit. # diff --git a/crates/batten/tests/it/cli.rs b/crates/batten/tests/it/cli.rs index ce96741e7..209e1f9fd 100644 --- a/crates/batten/tests/it/cli.rs +++ b/crates/batten/tests/it/cli.rs @@ -12174,8 +12174,8 @@ fn a_tracked_instruction_may_not_prescribe_the_denied_commit_identity() { // this config outright — it carries a spawning kind — but `--rule` selects // the row BEFORE that refusal is reached, and `no-denied-identity-prescribed` // is not one of the three `kind = "command"` rows. `mise.toml` already relies - // on this against these same committed bytes: `check --rule prose-only`, - // `--rule filed-here`, `--rule memory-graph`. + // on this against these same committed bytes: `check --rule 'diff ship early'`, + // `--rule 'issue file other'`, `--rule 'memory point missing'`. // // This comment used to say `enforce`, "and every sibling test over the // committed bytes takes the same verb for the same reason". True of an diff --git a/crates/batten/tests/it/prose_only.rs b/crates/batten/tests/it/prose_only.rs index d1d1dac39..d942064db 100644 --- a/crates/batten/tests/it/prose_only.rs +++ b/crates/batten/tests/it/prose_only.rs @@ -383,7 +383,7 @@ fn an_unresolvable_base_says_nothing_rather_than_refusing() { /// gate could not fire over it. /// /// Measured 2026-09-04: a 121→71-comment trim of a policy module, no other -/// change in the branch, `batten check --rule prose-only` exit 0, a full +/// change in the branch, `batten check --rule 'diff ship early'` exit 0, a full /// required matrix spent. That is exactly the instance CLOUD-827 exists to /// price, and it walked past because the classifier had never been told what a /// `.rego` comment looks like. diff --git a/crates/batten/tests/it/rules_drift.rs b/crates/batten/tests/it/rules_drift.rs index 60c2006b9..cff80cf9c 100644 --- a/crates/batten/tests/it/rules_drift.rs +++ b/crates/batten/tests/it/rules_drift.rs @@ -72,7 +72,7 @@ // changed: "unreadable wiring is refused rather than reporting every event unwired" policy/rules-drift.rego conditioned on a wiring claim existing: an unreadable `.claude/settings.json` is `drift read unread` when some sentence claims a wiring, and silent when none does // changed: "unreadable schemas are refused rather than reporting every key unemittable" policy/rules-drift.rego same conditioning, plus a READ-BUT-EMPTY arm the predecessor did not need: this build of regorus has no `walk`, so the recursive descent became one fixed path, and a schema whose shape moved parses fine and yields nothing — invisible to `input.tree.missing`, so `schema_vacuous` covers it // changed: "unreadable policy source is refused rather than reporting every name unqueried" policy/rules-drift.rego same conditioning, on a named fixed rule existing -// changed: "the gate is wired into the hk gate, so a drift reddens a commit" policy/rules-drift.rego the assertion moves from the suite to the wiring itself: hk's `rules-drift` step now runs `mise run rules-drift`, which is an inline `batten check --rule rules-drift`, so the step name and the rule id are one object rather than two that a grep held together +// changed: "the gate is wired into the hk gate, so a drift reddens a commit" policy/rules-drift.rego the assertion moves from the suite to the wiring itself: hk's `rules-drift` step now runs `mise run rules-drift`, which is an inline `batten check --rule 'rule watch other'`, so the step name and the rule id are one object rather than two that a grep held together // // ONE PREDICATE NARROWED, and it is recorded here rather than absorbed into the // carried arm above it. The predecessor took `head -n1` of grep order when a diff --git a/crates/batten/tests/it/shell_write_advisory.rs b/crates/batten/tests/it/shell_write_advisory.rs index ef3c35d4b..6bcfa96e2 100644 --- a/crates/batten/tests/it/shell_write_advisory.rs +++ b/crates/batten/tests/it/shell_write_advisory.rs @@ -223,7 +223,7 @@ fn a_call_carrying_no_write_target_is_silent() { /// passing its own suite. /// /// So this drives one corpus through both surfaces and requires the same answer. -/// `batten check --rule shell-retirement` is the tree authority; the advisory is +/// `batten check --rule 'shell retire partial'` is the tree authority; the advisory is /// the mediated one. The corpus deliberately includes the paths where the two /// predicates are known to differ for a REASON — a `mise-tasks/` file with no /// shebang is governed for deletion and not for edit — so the assertion is over diff --git a/mise.toml b/mise.toml index 9f7f786da..0103e5122 100644 --- a/mise.toml +++ b/mise.toml @@ -1292,7 +1292,7 @@ description = "Gate: a branch whose whole diff is comment lines buys a CI matrix # # `--rule` naming no declared row is a usage error rather than a clean run, so a # renamed row breaks this loudly instead of silently passing. -run = "cargo run --quiet -p batten -- check --rule prose-only" +run = "cargo run --quiet -p batten -- check --rule 'diff ship early'" [tasks."ci-drift"] description = "Gate: the committed [ci] table still matches the host ruleset (CLOUD-54)" @@ -1331,7 +1331,7 @@ description = "Gate: batten is registered exactly once on every hook surface (do # the seam between them. `doctor hooks` decides the DERIVATION half — batten # registered exactly once on every event a harness emits, no matcher, no drift — # and has decided it in-process since CLOUD-777; the shell only re-rendered its -# findings. `check --rule harness-wiring` decides the CONSUMER half, which the +# findings. `check --rule 'hook wire missing'` decides the CONSUMER half, which the # engine structurally cannot: `doctor.rs` reports a sibling COUNT and never a # NAME, because a command line carries a path (rule 4) and whether a hook beside # batten's is legitimate is this repository's judgement (rule 1). @@ -1342,7 +1342,7 @@ description = "Gate: batten is registered exactly once on every hook surface (do # ONE LINE, DELIBERATELY, for the reason the task below gives: the multi-line # spellings are where a predicate can hide, and there is no decision here to hide # — both are the engine's. -run = "cargo run --quiet -p batten -- doctor hooks && cargo run --quiet -p batten -- check --rule harness-wiring" +run = "cargo run --quiet -p batten -- doctor hooks && cargo run --quiet -p batten -- check --rule 'hook wire missing'" [tasks."config-lint"] description = "Gate: batten.toml carries no policy smell, and no weakening the groom never saw (arm with CONFIG_LINT_BASE)" @@ -1637,7 +1637,7 @@ description = "Gate: every row this branch put on the board was refined at creat # `stop-guard`'s callers, and `stop-guard` is retired in this same change: those # questions are asked at `Stop` by the engine's own hook now, over the same # module, so there is still exactly one implementation of the intersection. -run = "cargo run --quiet -p batten -- check --rule filed-here" +run = "cargo run --quiet -p batten -- check --rule 'issue file other'" [tasks.claim-race] description = "Gate: no other OPEN pull request already claims a key this branch claims (asks the forge; pointer-only)" @@ -2018,7 +2018,7 @@ cargo run --quiet -p batten -- record tool perf-p95 <"$records" # latency gate would report every other tree rule through it. [tasks.perf-assert] description = "Gate: every measured invocation path is inside its latency budget, and README publishes the budget this gate enforces" -run = "cargo run --quiet -p batten -- check --rule perf-assert" +run = "cargo run --quiet -p batten -- check --rule 'path measure wrong'" # `[tasks.msrv]` IS RETIRED (CLOUD-593), replaced by # `mise-tasks/msrv-pin-agreement.sh`. @@ -2395,7 +2395,7 @@ description = "Gate: every mem: reference in tracked markdown resolves to a real # under the branch, the installed binary predated it, and BOTH wrappers refused # with a config parse error that named a section neither gate reads. Nothing in # CI installs `batten` either, so the bare form had never been CI-verified. -run = "cargo run --quiet -p batten -- check --rule memory-graph" +run = "cargo run --quiet -p batten -- check --rule 'memory point missing'" # CLOUD-843 / CLOUD-1150, and the same wrapper shape one member over: `hk.pkl`'s # `rules-drift` step runs `mise run rules-drift` and still does, so the caller @@ -2412,7 +2412,7 @@ run = "cargo run --quiet -p batten -- check --rule memory-graph" # together across two files, and there is one name here. [tasks.rules-drift] description = "Gate: a value `.claude/rules/*.md` restates still agrees with the mechanism that owns it (CLOUD-506)" -run = "cargo run --quiet -p batten -- check --rule rules-drift" +run = "cargo run --quiet -p batten -- check --rule 'rule watch other'" # --------------------------------------------------------------------------- # The session-start provisioning steps, one task each (CLOUD-312 row 10). # @@ -2693,7 +2693,7 @@ run = "mise run reclaim-census record-boot >/dev/null 2>&1 || true; v=$(mise run # predecessor's own scope stated as a flag. [tasks.lock-complete] description = "Gate: mise.lock has no partial or bogus platform entry — a pure function of the committed lockfile, no network, no write" -run = "cargo run --quiet -p batten -- check --rule lock-complete" +run = "cargo run --quiet -p batten -- check --rule 'lock cover partial'" [tasks.batten-check] description = "Consumer #1: evaluate the committed batten.toml with batten's own engine against this repository" diff --git a/policy/ancestry-decides-nothing.rego b/policy/ancestry-decides-nothing.rego index 55bec4560..ffd1af2e6 100644 --- a/policy/ancestry-decides-nothing.rego +++ b/policy/ancestry-decides-nothing.rego @@ -41,7 +41,7 @@ package batten.ancestry import rego.v1 -rules contains "ancestry-decides-nothing" +rules contains "patch judge wrong" # The reachability-ANSWER surface, spelled plainly. A Rego string is not Rust # source, so this file is not its own corpus and needs none of the scan's @@ -57,7 +57,7 @@ reachability_answers := { } violation contains { - "rule": "ancestry-decides-nothing", + "rule": "patch judge wrong", # The site first, then the token that gave it away: the fix is at the line, # and the token is what a reader searches for once there. "verdict": "patch judge wrong", diff --git a/policy/bats-invocation.rego b/policy/bats-invocation.rego index c6e5f6ce2..3f8e0acc9 100644 --- a/policy/bats-invocation.rego +++ b/policy/bats-invocation.rego @@ -61,7 +61,7 @@ package batten.bats_invocation import rego.v1 -rules contains "bats-invocation" +rules contains "bats run wrong" # --- what is being judged, and whether there is anything to judge ------------- @@ -135,7 +135,7 @@ cost_markers := { # --- A: the run is serial, or is about to be --------------------------------- violation contains { - "rule": "bats-invocation", + "rule": "bats run wrong", "verdict": "suite run late", "subjects": [{"path": "mise.toml"}, {"artifact": marker}], } if { @@ -145,7 +145,7 @@ violation contains { } violation contains { - "rule": "bats-invocation", + "rule": "bats run wrong", "verdict": "suite run late", "subjects": [{"path": "mise.toml"}, {"artifact": spelling}], } if { @@ -166,7 +166,7 @@ nproc_mentions := [line | ] violation contains { - "rule": "bats-invocation", + "rule": "bats run wrong", "verdict": "suite run late", "subjects": [{"path": "mise.toml"}, {"count": count(nproc_mentions)}], } if { @@ -178,7 +178,7 @@ violation contains { # defaults to probing for GNU parallel, which is not in the mise registry and is # pinned nowhere here. violation contains { - "rule": "bats-invocation", + "rule": "bats run wrong", "verdict": "suite run late", "subjects": [{"path": "mise.toml"}, {"artifact": "aqua:shenwei356/rush"}], } if { @@ -212,7 +212,7 @@ ci_install_args := [args | ] violation contains { - "rule": "bats-invocation", + "rule": "bats run wrong", "verdict": "suite run late", "subjects": [ {"path": ".github/workflows/ci.yml"}, @@ -229,7 +229,7 @@ violation contains { # --- B: the run proves less than it appears to ------------------------------- violation contains { - "rule": "bats-invocation", + "rule": "bats run wrong", "verdict": "suite count missing", "subjects": [{"path": "mise.toml"}, {"artifact": marker}], } if { @@ -239,7 +239,7 @@ violation contains { } violation contains { - "rule": "bats-invocation", + "rule": "bats run wrong", "verdict": "suite count missing", "subjects": [{"path": "mise.toml"}, {"artifact": spelling}], } if { @@ -251,7 +251,7 @@ violation contains { # --- C: nothing compares what the run cost against a record ------------------- violation contains { - "rule": "bats-invocation", + "rule": "bats run wrong", "verdict": "suite measure missing", "subjects": [{"path": "mise.toml"}, {"artifact": marker}], } if { @@ -279,7 +279,7 @@ measured_date(line) := date if { } violation contains { - "rule": "bats-invocation", + "rule": "bats run wrong", "verdict": "suite measure missing", "subjects": [ {"path": "mise.toml"}, @@ -302,7 +302,7 @@ violation contains { # CLOUD-352's whole scope; demanding a fresh date for one would be asking for a # number nobody measured. violation contains { - "rule": "bats-invocation", + "rule": "bats run wrong", "verdict": "suite measure missing", "subjects": [{"path": ".github/workflows/ci.yml", "line": index + 1}], } if { @@ -321,7 +321,7 @@ violation contains { # in order. (CLOUD-1049: the engine half does not populate `missing` for a parse # failure yet, so this clause is right and the channel is not yet filled.) violation contains { - "rule": "bats-invocation", + "rule": "bats run wrong", "verdict": "bats parse unread", "subjects": [{"path": path}], } if { diff --git a/policy/cfg-gated-test.rego b/policy/cfg-gated-test.rego index c353f3474..4ce89d0a1 100644 --- a/policy/cfg-gated-test.rego +++ b/policy/cfg-gated-test.rego @@ -124,7 +124,7 @@ attribute_or_doc(line) if { # reasoned. The first was `every index, line in lines { block_ok(…) }`, which # walks the whole file for every candidate `(cfg, test)` pair. The second kept # the pairing and narrowed the inner test to `not gap_dirty`, which stops at the -# first breaking line. `batten check --rule cfg-gated-test` took **1099s** over +# first breaking line. `batten check --rule 'test cover missing'` took **1099s** over # this tree on the second spelling, where the same-`delta_sources` # `test-targets` takes **1s**. So the PAIRING is the cost and no inner test # removes it: `exec.rs` alone is ~30 `cfg` lines against ~60 `#[test]` lines over diff --git a/policy/ci-suite-lane.rego b/policy/ci-suite-lane.rego index c8ab356bc..f93723a4d 100644 --- a/policy/ci-suite-lane.rego +++ b/policy/ci-suite-lane.rego @@ -51,7 +51,7 @@ package batten.ci_suite_lane import rego.v1 -rules contains "ci-suite-lane" +rules contains "job select missing" # --- what is being judged, and whether there is anything to judge ------------- @@ -110,7 +110,7 @@ runs_task(name) if { # --- the refusal --------------------------------------------------------------- violation contains { - "rule": "ci-suite-lane", + "rule": "job select missing", "verdict": "gate skip unseen", "subjects": [{"path": workflow_path}, {"artifact": name}], } if { @@ -126,7 +126,7 @@ violation contains { # boundary tried and failed. Spelling those the same way is how a gate reports # green over a file it never read. violation contains { - "rule": "ci-suite-lane", + "rule": "job select missing", "verdict": "workflow read unread", "subjects": [{"path": path}], } if { diff --git a/policy/claim-before-code.rego b/policy/claim-before-code.rego index fc14d01cb..bd368d260 100644 --- a/policy/claim-before-code.rego +++ b/policy/claim-before-code.rego @@ -57,7 +57,7 @@ package batten.claim_before_code import rego.v1 -rules contains "claim-before-code" +rules contains "claim mint absent" # Every declared id whose captured payload carries no project. # @@ -79,7 +79,7 @@ refused contains id if { } violation contains { - "rule": "claim-before-code", + "rule": "claim mint absent", "verdict": "claim mint absent", "subjects": [{"count": count(refused)}], } if { diff --git a/policy/claim-order-is-stated.rego b/policy/claim-order-is-stated.rego index 88ae86922..1548009b1 100644 --- a/policy/claim-order-is-stated.rego +++ b/policy/claim-order-is-stated.rego @@ -48,7 +48,7 @@ package batten.claim_order_is_stated import rego.v1 -rules contains "claim-order-is-stated" +rules contains "claim declare dropped" # The always-loaded file. `CLAUDE.md` is a symlink to this; the TRACKED path is # the one judged, because the symlink is not what the budget counts. @@ -100,7 +100,7 @@ rules_carry_both_directions if { } violation contains { - "rule": "claim-order-is-stated", + "rule": "claim declare dropped", "verdict": "claim declare dropped", "subjects": [{"path": index_path}], } if { @@ -110,7 +110,7 @@ violation contains { } violation contains { - "rule": "claim-order-is-stated", + "rule": "claim declare dropped", "verdict": "claim declare dropped", "subjects": [{"path": index_path}], } if { @@ -120,7 +120,7 @@ violation contains { } violation contains { - "rule": "claim-order-is-stated", + "rule": "claim declare dropped", "verdict": "claim declare dropped", "subjects": [{"path": rules_path}], } if { @@ -132,7 +132,7 @@ violation contains { # reported rather than left absent, or this gate comes back green over a file it # never opened. violation contains { - "rule": "claim-order-is-stated", + "rule": "claim declare dropped", "verdict": "claim declare dropped", "subjects": [{"path": path}], } if { diff --git a/policy/command-task-defined.rego b/policy/command-task-defined.rego index 090f4e9d0..909653803 100644 --- a/policy/command-task-defined.rego +++ b/policy/command-task-defined.rego @@ -49,7 +49,7 @@ package batten.command_task_defined import rego.v1 -rules contains "command-task-defined" +rules contains "task bind undefined" # Tasks the manifest declares under `[tasks]`. defined contains name if { @@ -118,7 +118,7 @@ mise_task(command) := task if { } violation contains { - "rule": "command-task-defined", + "rule": "task bind undefined", # The row first, then the task it names: the fix is on the row. "verdict": "task name undefined", "subjects": [{"artifact": row.id}, {"artifact": row.task}], @@ -141,7 +141,7 @@ violation contains { # judged — which must not be spelled the same way as a config whose every task # resolves. violation contains { - "rule": "command-task-defined", + "rule": "task bind undefined", "verdict": "config parse broken", "subjects": [{"path": path}], } if { diff --git a/policy/connector-not-granted.rego b/policy/connector-not-granted.rego index 1e4bc84a4..4cf68884f 100644 --- a/policy/connector-not-granted.rego +++ b/policy/connector-not-granted.rego @@ -40,7 +40,7 @@ package batten.connector_not_granted import rego.v1 -rules contains "connector-not-granted" +rules contains "connector grant loose" # The settings file's permission allow list, or nothing. # @@ -72,7 +72,7 @@ granted contains entry if { # entries themselves — a finding that listed them would be restating the grant it # refuses, and rule 4's subject vocabulary has a `count` for exactly this. violation contains { - "rule": "connector-not-granted", + "rule": "connector grant loose", "verdict": "connector grant loose", "subjects": [{"path": ".claude/settings.json"}, {"count": count(granted)}], } if { diff --git a/policy/denials-outlive-the-turn.rego b/policy/denials-outlive-the-turn.rego index 8fda8bea6..de0480985 100644 --- a/policy/denials-outlive-the-turn.rego +++ b/policy/denials-outlive-the-turn.rego @@ -38,7 +38,7 @@ package batten.denials_outlive_the_turn import rego.v1 -rules contains "denials-outlive-the-turn" +rules contains "turn deny held" # The turn ended with refusals recorded and the agent stopping again. # @@ -47,7 +47,7 @@ rules contains "denials-outlive-the-turn" # with refusals still in the session's record is the shape `finding-sink-check` # exists to catch — a finding produced and then left behind. violation contains { - "rule": "denials-outlive-the-turn", + "rule": "turn deny held", "verdict": "turn deny held", "subjects": [{"count": input.facts.extracted.denials}], } if { diff --git a/policy/egress-fencing.rego b/policy/egress-fencing.rego index 8471d5581..5633dacfb 100644 --- a/policy/egress-fencing.rego +++ b/policy/egress-fencing.rego @@ -66,7 +66,7 @@ package batten.egress_fencing import rego.v1 -rules contains "egress-fencing" +rules contains "provision guard missing" # --------------------------------------------------------------------------- # The authority, bound through a rule so every predicate below is UNDEFINED @@ -87,7 +87,7 @@ spellings := {"NO_PROXY", "no_proxy"} # --------------------------------------------------------------------------- violation contains { - "rule": "egress-fencing", + "rule": "provision guard missing", "verdict": "task declare dropped", "subjects": [{"path": "mise.toml"}, {"artifact": key}], } if { @@ -106,7 +106,7 @@ violation contains { # --------------------------------------------------------------------------- violation contains { - "rule": "egress-fencing", + "rule": "provision guard missing", "verdict": "task declare partial", "subjects": [{"path": "mise.toml"}, {"artifact": key}], } if { @@ -123,7 +123,7 @@ violation contains { # --------------------------------------------------------------------------- violation contains { - "rule": "egress-fencing", + "rule": "provision guard missing", "verdict": "task read unread", "subjects": [{"path": path}], } if { @@ -169,7 +169,7 @@ fences_resolver(key) if { # C: no provision row declares the spelling at all. violation contains { - "rule": "egress-fencing", + "rule": "provision guard missing", "verdict": "provision declare dropped", "subjects": [{"path": "batten.toml"}, {"artifact": key}], } if { @@ -180,7 +180,7 @@ violation contains { # D: a row declares it and no longer prepends the host it exists for. violation contains { - "rule": "egress-fencing", + "rule": "provision guard missing", "verdict": "provision declare partial", "subjects": [{"path": "batten.toml"}, {"artifact": key}], } if { @@ -204,7 +204,7 @@ violation contains { # spelling is what went missing; here nothing is left to name one against, and # inventing one would point a reader at a key the file never had. violation contains { - "rule": "egress-fencing", + "rule": "provision guard missing", "verdict": "provision declare dropped", "subjects": [{"path": "batten.toml"}], } if { @@ -216,7 +216,7 @@ violation contains { # Could not look, for the second surface. violation contains { - "rule": "egress-fencing", + "rule": "provision guard missing", "verdict": "provision read unread", "subjects": [{"path": path}], } if { diff --git a/policy/forge-verdict-required.rego b/policy/forge-verdict-required.rego index 1122c76c2..2b5cd40f3 100644 --- a/policy/forge-verdict-required.rego +++ b/policy/forge-verdict-required.rego @@ -34,7 +34,7 @@ package batten.forge_verdict import rego.v1 -rules contains "forge-verdict-required" +rules contains "forge check red" # The check that carries this repository's verdict. # @@ -70,7 +70,7 @@ passed(checks) if { } violation contains { - "rule": "forge-verdict-required", + "rule": "forge check red", "verdict": "forge check red", "subjects": [{"count": count(refused)}], } if { diff --git a/policy/harness-grant.rego b/policy/harness-grant.rego index f31733c6d..44d81e7dd 100644 --- a/policy/harness-grant.rego +++ b/policy/harness-grant.rego @@ -37,7 +37,7 @@ package batten.harness_grant import rego.v1 -rules contains "harness-grant" +rules contains "grant carry missing" # The settings file's auto-mode allow list, or nothing. # @@ -70,7 +70,7 @@ keeps_the_defaults if { # The grant is gone, so this repository's own binary is refused by the layer that # actually decides. violation contains { - "rule": "harness-grant", + "rule": "grant carry missing", "verdict": "grant declare absent", "subjects": [{"path": ".claude/settings.json"}], } if { @@ -80,7 +80,7 @@ violation contains { # The grant is present and every built-in safety rule was discarded with it. violation contains { - "rule": "harness-grant", + "rule": "grant carry missing", "verdict": "default carry dropped", "subjects": [{"path": ".claude/settings.json"}], } if { diff --git a/policy/harness-wiring.rego b/policy/harness-wiring.rego index 42a7e52dc..bd69e8e36 100644 --- a/policy/harness-wiring.rego +++ b/policy/harness-wiring.rego @@ -54,7 +54,7 @@ package batten.harness_wiring import rego.v1 -rules contains "harness-wiring" +rules contains "hook wire missing" # The program every hook registration must resolve to. # @@ -190,7 +190,7 @@ stray(command) if { # basename for that reason while its committed half did not. The file is a # tracked path, so the pointer is honest and opening it shows the entry. violation contains { - "rule": "harness-wiring", + "rule": "hook wire missing", "verdict": "hook wire loose", "subjects": [{"path": path}, {"count": count(strays_in(path))}], } if { @@ -209,7 +209,7 @@ strays_in(path) := {command | # that may be emitted, and the declared id would only tell a reader which # home-relative file to open on their own machine. violation contains { - "rule": "harness-wiring", + "rule": "hook wire missing", "verdict": "hook wire duplicate", "subjects": [{"count": count(merged_strays)}], } if { @@ -235,7 +235,7 @@ merged_strays contains command if { # existed to prevent. A surface that EXISTS and will not parse is a host reading # nothing at all, and nobody can tell that from a clean wiring without this. violation contains { - "rule": "harness-wiring", + "rule": "hook wire missing", "verdict": "hook wire unread", "subjects": [{"count": count(unreadable)}], } if { diff --git a/policy/hk-fix-selection.rego b/policy/hk-fix-selection.rego index f6c561c76..68e1f12f7 100644 --- a/policy/hk-fix-selection.rego +++ b/policy/hk-fix-selection.rego @@ -63,7 +63,7 @@ package batten.hk_fix_selection import rego.v1 -rules contains "hk-fix-selection" +rules contains "gate select wrong" # --- what is being judged, and whether there is anything to judge ------------- @@ -97,7 +97,7 @@ declares(marker) if { fmt_description := input.tree.documents["mise.toml"].tasks.fmt.description violation contains { - "rule": "hk-fix-selection", + "rule": "gate select wrong", "verdict": "task state wrong", "subjects": [{"path": "mise.toml"}, {"artifact": "Run every fixer over the tree"}], } if { @@ -107,7 +107,7 @@ violation contains { } violation contains { - "rule": "hk-fix-selection", + "rule": "gate select wrong", "verdict": "task state wrong", "subjects": [ {"path": "rules/toolchain.md"}, @@ -138,7 +138,7 @@ fixer_tasks := {name | } violation contains { - "rule": "hk-fix-selection", + "rule": "gate select wrong", "verdict": "task select missing", "subjects": [{"path": "hk.pkl"}, {"artifact": task}], } if { @@ -155,7 +155,7 @@ violation contains { # (CLOUD-1049: the engine half does not populate `missing` for a parse failure # yet, so this clause is right and the channel is not yet filled.) violation contains { - "rule": "hk-fix-selection", + "rule": "gate select wrong", "verdict": "gate parse unread", "subjects": [{"path": path}], } if { diff --git a/policy/hook-profile.rego b/policy/hook-profile.rego index 924eb7d32..db23f11e3 100644 --- a/policy/hook-profile.rego +++ b/policy/hook-profile.rego @@ -40,7 +40,7 @@ package batten.hook_profile import rego.v1 -rules contains "hook-profile" +rules contains "hook declare other" # The status a step selected by the `check` hook carries in hk's plan. included := "included" @@ -67,7 +67,7 @@ stray contains name if { } violation contains { - "rule": "hook-profile", + "rule": "hook declare other", "verdict": "step declare missing", "subjects": [{"count": count(stray)}], } if { @@ -81,7 +81,7 @@ violation contains { # Told apart from ABSENT by `is_object` plus the count: an id nothing recorded # never binds `plan` at all, and that is could-not-look rather than a finding. violation contains { - "rule": "hook-profile", + "rule": "hook declare other", "verdict": "tier list empty", "subjects": [{"artifact": "hk-plan"}], } if { @@ -107,7 +107,7 @@ flagged contains line if { } violation contains { - "rule": "hook-profile", + "rule": "hook declare other", "verdict": "hook declare missing", "subjects": [{"path": hook}], } if { diff --git a/policy/hook-skip-local.rego b/policy/hook-skip-local.rego index 183ad97be..d60ea0df0 100644 --- a/policy/hook-skip-local.rego +++ b/policy/hook-skip-local.rego @@ -70,7 +70,7 @@ package batten.hook_skip_local import rego.v1 -rules contains "hook-skip-local" +rules contains "hook skip unseen" # The declared carve, which is CI's and is judged by `ci-suite-lane` instead. # @@ -86,7 +86,7 @@ ci_lane if { } violation contains { - "rule": "hook-skip-local", + "rule": "hook skip unseen", "verdict": "hook skip unseen", "subjects": [{"count": 1}], } if { diff --git a/policy/leased-push.rego b/policy/leased-push.rego index 2a1451f58..58b165410 100644 --- a/policy/leased-push.rego +++ b/policy/leased-push.rego @@ -49,10 +49,10 @@ package batten.leased_push import rego.v1 -rules contains "leased-push" +rules contains "branch write unsafe" violation contains { - "rule": "leased-push", + "rule": "branch write unsafe", "verdict": "branch write unsafe", "subjects": [{"count": 1}], } if { diff --git a/policy/memories.rego b/policy/memories.rego index 825c49ca3..a3ba33e2d 100644 --- a/policy/memories.rego +++ b/policy/memories.rego @@ -52,7 +52,7 @@ package batten.memories import rego.v1 -rules contains "memory-graph" +rules contains "memory point missing" # --- the graph, as the tree holds it ------------------------------------------ @@ -91,7 +91,7 @@ name_of(path) := trim_suffix(trim_prefix(path, memories_dir), ".md") # minimal repository is unshippable. A repository with no memory directory is not # a repository with a broken memory graph. violation contains { - "rule": "memory-graph", + "rule": "memory point missing", "verdict": "memory resolve missing", "subjects": [{"path": root_memory}], } if { @@ -105,7 +105,7 @@ violation contains { # because the tooling strips one extension and the reference matcher stops at the # first foreign character. violation contains { - "rule": "memory-graph", + "rule": "memory point missing", "verdict": "memory name duplicate", "subjects": [{"path": path}], } if { @@ -114,7 +114,7 @@ violation contains { } violation contains { - "rule": "memory-graph", + "rule": "memory point missing", "verdict": "memory name unseen", "subjects": [{"path": path}], } if { @@ -140,7 +140,7 @@ referrer(path) if { } violation contains { - "rule": "memory-graph", + "rule": "memory point missing", "verdict": "memory point stale", "subjects": [{"path": path, "line": number}], } if { @@ -161,7 +161,7 @@ violation contains { # here it is the exact failure the predecessor could not have: a shell `grep` over # an unreadable file is loud, and an absent map key is silent. violation contains { - "rule": "memory-graph", + "rule": "memory point missing", "verdict": "memory read unread", "subjects": [{"path": path}], } if { diff --git a/policy/mise-pin-agreement.rego b/policy/mise-pin-agreement.rego index b53dc9c72..843f9d26d 100644 --- a/policy/mise-pin-agreement.rego +++ b/policy/mise-pin-agreement.rego @@ -85,7 +85,7 @@ package batten.mise_pin_agreement import rego.v1 -rules contains "mise-pin-agreement" +rules contains "pin declare wrong" # --------------------------------------------------------------------------- # The two documents, bound through rules so every predicate below is UNDEFINED @@ -134,7 +134,7 @@ reference contains {"server": server, "ref": arg, "tool": tool, "want": want} if # --------------------------------------------------------------------------- violation contains { - "rule": "mise-pin-agreement", + "rule": "pin declare wrong", "verdict": "pin declare other", "subjects": [{"path": ".mcp.json"}, {"artifact": entry.server}, {"artifact": entry.ref}], } if { @@ -149,7 +149,7 @@ violation contains { # --------------------------------------------------------------------------- violation contains { - "rule": "mise-pin-agreement", + "rule": "pin declare wrong", "verdict": "pin declare missing", "subjects": [{"path": ".mcp.json"}, {"artifact": entry.server}, {"artifact": entry.ref}], } if { @@ -170,7 +170,7 @@ violation contains { # --------------------------------------------------------------------------- violation contains { - "rule": "mise-pin-agreement", + "rule": "pin declare wrong", "verdict": "call run loose", "subjects": [{"path": ".mcp.json"}, {"artifact": server}], } if { @@ -212,7 +212,7 @@ terminator(args) := count(args) if { # --------------------------------------------------------------------------- violation contains { - "rule": "mise-pin-agreement", + "rule": "pin declare wrong", "verdict": "pin read unread", "subjects": [{"path": path}], } if { diff --git a/policy/module-layering.rego b/policy/module-layering.rego index 26491e634..f1dc6e204 100644 --- a/policy/module-layering.rego +++ b/policy/module-layering.rego @@ -57,7 +57,7 @@ package batten.module_layering import rego.v1 -rules contains "module-layering" +rules contains "layer place wrong" # Every module this table has placed. A module in the judged set and absent here # is refused rather than allowed. @@ -703,7 +703,7 @@ module_of(path) := name if { # names. Never the source line — `to` and `item` are path segments, and the text # that produced them stays on the engine's side. violation contains { - "rule": "module-layering", + "rule": "layer place wrong", "verdict": "layer reach refused", "subjects": [{"path": path, "line": edge.line}, {"artifact": edge.to}], } if { @@ -720,7 +720,7 @@ violation contains { # A module nobody placed. An implicit allow here is the hole this table exists to # close, so it is a finding rather than silence. violation contains { - "rule": "module-layering", + "rule": "layer place wrong", "verdict": "module place missing", "subjects": [{"path": path}], } if { @@ -733,7 +733,7 @@ violation contains { # reporting "no violations" is CLOUD-251's failure mode, and the reason a gate # can be switched off by deletion without anything going red. violation contains { - "rule": "module-layering", + "rule": "layer place wrong", "verdict": "layer table dead", } if { count(forbidden) == 0 diff --git a/policy/mutation-declared-case.rego b/policy/mutation-declared-case.rego index e30cfa690..1fbcf3891 100644 --- a/policy/mutation-declared-case.rego +++ b/policy/mutation-declared-case.rego @@ -53,7 +53,7 @@ package batten.mutation_declared_case import rego.v1 -rules contains "mutation-declared-case" +rules contains "marker name undefined" # The declared documents, or nothing. ABSENT IS NOT EMPTY: a run acquiring no # lines has no key here, Rego reads that as *does not hold*, and this module is @@ -128,7 +128,7 @@ resolves(entry) if { # what a reader opens to fix it; the suite follows as the place the case was # looked for. violation contains { - "rule": "mutation-declared-case", + "rule": "marker name undefined", "verdict": "marker name undefined", "subjects": [{"path": entry.path}, {"path": entry.suite}], } if { diff --git a/policy/opa-compliance.rego b/policy/opa-compliance.rego index 38f468bb2..88da08a92 100644 --- a/policy/opa-compliance.rego +++ b/policy/opa-compliance.rego @@ -51,14 +51,14 @@ import rego.v1 # A gate outside $MUTANT_GATES with no row here fails `mise run mutant-census`. #MUTANT-EXEMPT CLOUD-845|no compiled-binary tier names this module at all, so there is no suite a declared mutation could redden. That is not the `tests/$gate.bats` hole CLOUD-1267 closed — a suite may now be DECLARED — it is that none exists to declare, and what is owed is the tier -rules contains "opa-tracks-regorus-compliance" +rules contains "version check stale" # A file this build could not parse lands in `input.tree.missing` rather than in # `documents` (CLOUD-845). Without this clause an unparseable manifest is simply # absent from every rule below and the module reports GREEN over a file it never # read — a vacuous pass, indistinguishable from a real one. violation contains { - "rule": "opa-tracks-regorus-compliance", + "rule": "version check stale", "verdict": "source parse broken", "subjects": [{"path": path}], } if { @@ -68,7 +68,7 @@ violation contains { # The checker and the evaluator naming different OPA release lines. violation contains { - "rule": "opa-tracks-regorus-compliance", + "rule": "version check stale", "verdict": "version pin ahead", "subjects": [{"artifact": pin}, {"artifact": declared}], } if { @@ -81,7 +81,7 @@ violation contains { # this commit resolves. The number may still be right; nothing here has checked, # and that is the point. violation contains { - "rule": "opa-tracks-regorus-compliance", + "rule": "version check stale", "verdict": "claim state stale", "subjects": [{"artifact": recorded_for}, {"artifact": regorus_pin}], } if { @@ -144,7 +144,7 @@ in_this_workspace if input.tree.documents["Cargo.toml"] # an absent document is already reported above, and reporting it twice would # name the caller's parse failure as four separate findings. violation contains { - "rule": "opa-tracks-regorus-compliance", + "rule": "version check stale", "verdict": "claim declare absent", "subjects": [{"artifact": "opa"}], } if { @@ -154,7 +154,7 @@ violation contains { } violation contains { - "rule": "opa-tracks-regorus-compliance", + "rule": "version check stale", "verdict": "claim declare absent", "subjects": [{"artifact": "REGORUS_OPA_COMPLIANCE"}], } if { @@ -164,7 +164,7 @@ violation contains { } violation contains { - "rule": "opa-tracks-regorus-compliance", + "rule": "version check stale", "verdict": "claim declare absent", "subjects": [{"artifact": "REGORUS_OPA_COMPLIANCE_FOR"}], } if { @@ -174,7 +174,7 @@ violation contains { } violation contains { - "rule": "opa-tracks-regorus-compliance", + "rule": "version check stale", "verdict": "claim declare absent", "subjects": [{"artifact": "regorus"}], } if { @@ -187,7 +187,7 @@ violation contains { # undefined propagates into the comparisons as silence — the same vacuous pass # one level in. `"1"` against a declared `1.2.0` was measured passing. violation contains { - "rule": "opa-tracks-regorus-compliance", + "rule": "version check stale", "verdict": "version read unread", "subjects": [{"artifact": entry.key}, {"artifact": entry.owner}], } if { diff --git a/policy/pr-partition-restated.rego b/policy/pr-partition-restated.rego index eddc9ac7a..9c0356685 100644 --- a/policy/pr-partition-restated.rego +++ b/policy/pr-partition-restated.rego @@ -58,7 +58,7 @@ package batten.pr_partition_restated import rego.v1 -rules contains "pr-partition-restated" +rules contains "review state other" # A line of declared prose asserting the partition AGENTS.md refuses. # @@ -67,7 +67,7 @@ rules contains "pr-partition-restated" # and would itself be a tracked copy of the phrase the moment anyone pasted the # report into the tree. violation contains { - "rule": "pr-partition-restated", + "rule": "review state other", "verdict": "prose state other", "subjects": [{"path": path, "line": number}], } if { @@ -100,7 +100,7 @@ violation contains { # records two live instances of, arriving on the module written to close that # class. violation contains { - "rule": "pr-partition-restated", + "rule": "review state other", "verdict": "source parse refused", "subjects": [{"count": count(unreadable)}], } if { diff --git a/policy/privileged-lane.rego b/policy/privileged-lane.rego index 10799d01b..004de2668 100644 --- a/policy/privileged-lane.rego +++ b/policy/privileged-lane.rego @@ -64,7 +64,7 @@ import rego.v1 #MUTANT resolution-conjunct-dropped|s@^\tresolves_head(body)$@\ttrue@|an_outsider_reachable_writer_that_resolves_no_outside_head_is_not_a_subject #MUTANT-SUITE crates/batten/tests/it/privileged_lane.rs -rules contains "privileged-lane-tests-origin" +rules contains "lane guard other" # A file this build could not parse lands in `input.tree.missing` rather than in # `documents` (CLOUD-845). Without this clause a workflow that fails to parse is @@ -72,7 +72,7 @@ rules contains "privileged-lane-tests-origin" # never read — a vacuous pass, which is worse than a wrong answer because it is # indistinguishable from a real one. violation contains { - "rule": "privileged-lane-tests-origin", + "rule": "lane guard other", "verdict": "workflow parse broken", "subjects": [{"path": path}], } if { @@ -86,7 +86,7 @@ violation contains { # # Arm one: the EVENT carries the head, so the test is on the event. violation contains { - "rule": "privileged-lane-tests-origin", + "rule": "lane guard other", "verdict": "lane guard missing", "subjects": [{"path": path}, {"artifact": job}], } if { @@ -108,7 +108,7 @@ violation contains { # would get two remedies for one fix. Arm one wins that overlap deliberately: its # remedy is the earlier of the two, applied before the lookup happens at all. violation contains { - "rule": "privileged-lane-tests-origin", + "rule": "lane guard other", "verdict": "lane resolve missing", "subjects": [{"path": path}, {"artifact": job}], } if { diff --git a/policy/prose-only.rego b/policy/prose-only.rego index 048e6ea24..018be52cf 100644 --- a/policy/prose-only.rego +++ b/policy/prose-only.rego @@ -65,7 +65,7 @@ package batten.prose_only import rego.v1 -rules contains "prose-only" +rules contains "diff ship early" # Every path this branch touched, whichever way it moved. changed := array.concat( @@ -99,7 +99,7 @@ touches_a_test if { # the second it fires on every change, without the third it blocks the doc # rewrite that ships with its own test. violation contains { - "rule": "prose-only", + "rule": "diff ship early", "verdict": "diff ship early", "subjects": [{"count": count(changed)}], } if { diff --git a/policy/release-tag-shape.rego b/policy/release-tag-shape.rego index 99aaa3c53..7404ac9ea 100644 --- a/policy/release-tag-shape.rego +++ b/policy/release-tag-shape.rego @@ -34,7 +34,7 @@ package batten.release_tag_shape import rego.v1 -rules contains "release-tag-shape" +rules contains "tag mint wrong" # Every tag the declared glob matched. # @@ -47,7 +47,7 @@ shipped contains tag if { } violation contains { - "rule": "release-tag-shape", + "rule": "tag mint wrong", "verdict": "tag mint wrong", "subjects": [{"count": count(malformed)}], } if { diff --git a/policy/review-dispatched.rego b/policy/review-dispatched.rego index 5c80b7cc9..c8f5f956b 100644 --- a/policy/review-dispatched.rego +++ b/policy/review-dispatched.rego @@ -44,7 +44,7 @@ package batten.review_dispatched import rego.v1 -rules contains "review-dispatched" +rules contains "prompt run never" # The reviews this repository declares it will not land without. # @@ -107,7 +107,7 @@ undispatched contains id if { } violation contains { - "rule": "review-dispatched", + "rule": "prompt run never", "verdict": "prompt run never", "subjects": [{"artifact": id}], } if { diff --git a/policy/shell-retirement.rego b/policy/shell-retirement.rego index b47890915..0c3167650 100644 --- a/policy/shell-retirement.rego +++ b/policy/shell-retirement.rego @@ -239,7 +239,7 @@ violation contains { # `shell add refused` offered two routes, and the second -- "declare that it # stays bash" -- DID NOT CLEAR THE VERDICT THAT OFFERED IT. Measured 2026-08-28: # prepending `# stays-bash: ` to `tests/wiring-reclaim.bats` and -# re-running `batten check --rule shell-retirement` left the finding byte- +# re-running `batten check --rule 'shell retire partial'` left the finding byte- # identical. # # The reason was structural rather than a typo. `admits_with = "# stays-bash:"` @@ -704,7 +704,7 @@ assigned_value(line) := value if { # spelling). # # MEASURED, and it is a non-termination rather than a slowdown: `batten check -# --rule shell-retirement` spun for three hours on a delta editing +# --rule 'shell retire partial'` spun for three hours on a delta editing # `tests/land-lock.bats` (~2200 lines) and `mise-tasks/land-lock.sh` (~1800), # the two largest governed files in the tree. Bisected — `origin/main` returns in # under a second, and reverting both edited files returns the same tree to under diff --git a/policy/spawn-adapters.rego b/policy/spawn-adapters.rego index 6a67805bf..f65e5fe71 100644 --- a/policy/spawn-adapters.rego +++ b/policy/spawn-adapters.rego @@ -57,7 +57,7 @@ package batten.spawn_adapters import rego.v1 -rules contains "spawn-adapters" +rules contains "adapter place missing" # The placed adapters, each named with what it delegates to. Measured against the # tree rather than imagined: this is exactly the resolved set on the commit that @@ -191,7 +191,7 @@ module_of(path) := name if { # The analyser's diagnostic and the source line it quoted are not in the fact at # all, so there is nothing here for this module to leak even by mistake. violation contains { - "rule": "spawn-adapters", + "rule": "adapter place missing", "verdict": "spawn place missing", "subjects": [{"path": site.path, "line": site.line}, {"artifact": module_of(site.path)}], } if { @@ -218,7 +218,7 @@ violation contains { no_census if not input.tree.symbols.sites violation contains { - "rule": "spawn-adapters", + "rule": "adapter place missing", "verdict": "symbol count absent", } if { no_census @@ -227,7 +227,7 @@ violation contains { # THE VACUITY GUARD. A table placing nothing decides nothing, and a rule that # cannot refuse is off. violation contains { - "rule": "spawn-adapters", + "rule": "adapter place missing", "verdict": "adapter table empty", } if { count(adapters) == 0 diff --git a/policy/spawn-widening.rego b/policy/spawn-widening.rego index 062d18982..a28e429e1 100644 --- a/policy/spawn-widening.rego +++ b/policy/spawn-widening.rego @@ -50,7 +50,7 @@ package batten.spawn_widening import rego.v1 -rules contains "spawn-widening" +rules contains "spawn add other" # `input.tree["base-delta"]` is NULL when the base rev did not resolve, and # `null` is not `undefined` — `not input.tree["base-delta"]` would be FALSE for @@ -155,7 +155,7 @@ engine_source(path) if { } violation contains { - "rule": "spawn-widening", + "rule": "spawn add other", "verdict": "spawn write refused", "subjects": [{"path": path}], } if { @@ -177,7 +177,7 @@ violation contains { # or may not be. violation contains { - "rule": "spawn-widening", + "rule": "spawn add other", "verdict": "adapter add refused", "subjects": [{"path": placements_module}], } if { @@ -195,7 +195,7 @@ violation contains { # arm below is what makes the engine record that it could not look. violation contains { - "rule": "spawn-widening", + "rule": "spawn add other", "verdict": "diff read absent", } if { not delta @@ -205,7 +205,7 @@ violation contains { # parse, and a module iterating only `lines` reports green over a file it never # read. `.claude/rules/policy-modules.md`: write the clause. violation contains { - "rule": "spawn-widening", + "rule": "spawn add other", "verdict": "source parse dead", "subjects": [{"path": path}], } if { diff --git a/policy/stop-posture.rego b/policy/stop-posture.rego index e248592de..f9d8a4745 100644 --- a/policy/stop-posture.rego +++ b/policy/stop-posture.rego @@ -59,7 +59,7 @@ package batten.stop_posture import rego.v1 -rules contains "stop-posture" +rules contains "prose report duplicate" # The turn's final text, or the empty string when this is not a Stop. # @@ -114,7 +114,7 @@ hits := count(regex.find_n(data.batten.patterns["hedged-flag-framing"], scrubbed # here rather than decorative: handing the matched prose back would make this a # mirror, and a mirror is cleared by restating it, which is the double-write. violation contains { - "rule": "stop-posture", + "rule": "prose report duplicate", "verdict": "prose report duplicate", "subjects": [{"count": hits}], } if { diff --git a/policy/suite-subject-retirable.rego b/policy/suite-subject-retirable.rego index 8dd0a8b2b..6a89bf219 100644 --- a/policy/suite-subject-retirable.rego +++ b/policy/suite-subject-retirable.rego @@ -85,7 +85,7 @@ package batten.suite_subject_retirable import rego.v1 -rules contains "suite-subject-retirable" +rules contains "suite retire unclear" # --- what the campaign can actually delete ------------------------------------ @@ -212,7 +212,7 @@ exempt := { # --- A: an immortal subject nobody declared ------------------------------------ violation contains { - "rule": "suite-subject-retirable", + "rule": "suite retire unclear", "verdict": "suite retire never", "subjects": [{"path": path}, {"path": subject}], } if { @@ -230,7 +230,7 @@ violation contains { # OUT of this gate — and a suite with no subject is not retirable either, because # `SubjectFacts::died` would have nothing to decide over. violation contains { - "rule": "suite-subject-retirable", + "rule": "suite retire unclear", "verdict": "suite declare missing", "subjects": [{"path": path}], } if { @@ -260,7 +260,7 @@ violation contains { # present, which is decidable everywhere. The cost is stated rather than hidden: a # retired suite leaves a spent row until someone reads the table. violation contains { - "rule": "suite-subject-retirable", + "rule": "suite retire unclear", "verdict": "suite admit stale", "subjects": [{"path": path}], } if { @@ -277,7 +277,7 @@ violation contains { # module iterating only `lines` reports green over a file it never opened, which # is the class `rules/policy-modules.md` records for this channel. violation contains { - "rule": "suite-subject-retirable", + "rule": "suite retire unclear", "verdict": "suite parse unread", "subjects": [{"path": path}], } if { diff --git a/policy/task-substitution.rego b/policy/task-substitution.rego index e367c084c..af25aa1a0 100644 --- a/policy/task-substitution.rego +++ b/policy/task-substitution.rego @@ -34,7 +34,7 @@ package batten.task_substitution import rego.v1 -rules contains "task-substitution" +rules contains "task run loose" # Every declared task this call is a WEAKER SPELLING of. # @@ -112,7 +112,7 @@ weaker_than_program(entry, argv) if { } violation contains { - "rule": "task-substitution", + "rule": "task run loose", "verdict": "task run loose", "subjects": [{"artifact": task}], } if { diff --git a/policy/validator-verdict-clean.rego b/policy/validator-verdict-clean.rego index 313bd5516..6227c7507 100644 --- a/policy/validator-verdict-clean.rego +++ b/policy/validator-verdict-clean.rego @@ -40,7 +40,7 @@ package batten.validator_verdict import rego.v1 -rules contains "validator-verdict-clean" +rules contains "tool judge dirty" # The key a producer writes when the validator had nothing to say. # @@ -98,7 +98,7 @@ findings(verdict) := {key | } violation contains { - "rule": "validator-verdict-clean", + "rule": "tool judge dirty", "verdict": "tool judge dirty", "subjects": [{"count": count(refused)}], } if { diff --git a/policy/verdict-routes-resolve.rego b/policy/verdict-routes-resolve.rego index 9bdb3bdb7..ce52b354b 100644 --- a/policy/verdict-routes-resolve.rego +++ b/policy/verdict-routes-resolve.rego @@ -67,7 +67,7 @@ package batten.verdict_routes import rego.v1 -rules contains "verdict-routes-resolve" +rules contains "route resolve missing" # --------------------------------------------------------------------------- # The routes, flattened out of the registry. @@ -128,7 +128,7 @@ mise_task(command) := task if { # --------------------------------------------------------------------------- violation contains { - "rule": "verdict-routes-resolve", + "rule": "route resolve missing", "verdict": "route name undefined", "subjects": [{"artifact": entry.verdict}, {"artifact": entry.route}, {"artifact": entry.task}], } if { @@ -147,7 +147,7 @@ violation contains { # tree that could not read it has judged no route at all — which must not be # spelled the same way as a registry whose every route resolves. violation contains { - "rule": "verdict-routes-resolve", + "rule": "route resolve missing", "verdict": "config parse broken", "subjects": [{"path": path}], } if { diff --git a/policy/weakens-declared.rego b/policy/weakens-declared.rego index 27dc6ce96..96531233b 100644 --- a/policy/weakens-declared.rego +++ b/policy/weakens-declared.rego @@ -35,7 +35,7 @@ package batten.weakens_declared import rego.v1 -rules contains "weakens-declared" +rules contains "commit declare empty" # Every `Weakens:` trailer on every commit in every declared range. # @@ -51,7 +51,7 @@ weakens contains value if { } violation contains { - "rule": "weakens-declared", + "rule": "commit declare empty", "verdict": "commit declare empty", "subjects": [{"count": count(empty)}], } if { diff --git a/policy/workspace-dep-referenced.rego b/policy/workspace-dep-referenced.rego index a6f983623..7082b3335 100644 --- a/policy/workspace-dep-referenced.rego +++ b/policy/workspace-dep-referenced.rego @@ -49,7 +49,7 @@ package batten.workspace_dep_referenced import rego.v1 -rules contains "workspace-dep-referenced" +rules contains "workspace list unused" # The root manifest's declared keys. declared contains key if { @@ -83,7 +83,7 @@ referenced contains key if { # line of either file. A document finding carries no line number by construction, # so the message is where the pointer lives. violation contains { - "rule": "workspace-dep-referenced", + "rule": "workspace list unused", "verdict": "workspace declare unused", "subjects": [{"artifact": key}], } if { @@ -94,7 +94,7 @@ violation contains { # COULD NOT LOOK, NEVER A SILENT PASS. A manifest the engine could not parse is # in `missing`, and without this the walk above simply does not see it. violation contains { - "rule": "workspace-dep-referenced", + "rule": "workspace list unused", "verdict": "manifest parse broken", "subjects": [{"path": path}], } if { @@ -107,7 +107,7 @@ violation contains { # CLOUD-251 shape the whole row is an instance of, closed on the module's own # input rather than assumed away. violation contains { - "rule": "workspace-dep-referenced", + "rule": "workspace list unused", "verdict": "workspace table absent", } if { count(declared) == 0 diff --git a/policy/worktree-registration.rego b/policy/worktree-registration.rego index 911da3b33..94ea7ca97 100644 --- a/policy/worktree-registration.rego +++ b/policy/worktree-registration.rego @@ -42,7 +42,7 @@ package batten.worktree import rego.v1 -rules contains "worktree-registration-live" +rules contains "registry read missing" # THE FINDING. A registration whose directory is gone, and which nobody locked. # @@ -53,7 +53,7 @@ rules contains "worktree-registration-live" # silent when its input changes shape is the recoverable failure; one that starts # denying everything is how a guard gets switched off. violation contains { - "rule": "worktree-registration-live", + "rule": "registry read missing", "verdict": "registry name absent", # `artifact` rather than `path`, because the registration's id is not a # repository path and the fact deliberately carries no path at all: a linked @@ -92,7 +92,7 @@ violation contains { # carries the array — EMPTY when there is nothing to report, which is defined, so # `not` is false and a repository with only its main checkout stays clean. violation contains { - "rule": "worktree-registration-live", + "rule": "registry read missing", "verdict": "registry list unread", "subjects": [{"count": 0}], } if { From 793ce75f57f5c0f494cf2f8eee9b223800e391d5 Mon Sep 17 00:00:00 2001 From: Alec Wenzowski Date: Wed, 9 Sep 2026 09:37:57 +0000 Subject: [PATCH 03/23] feat(config)!: enforce the id grammar at load, and collapse where two names are one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The enforcement and collapse arms. The grammar could not be wired before the ids it judges, so it lands after the migration rather than with it. GRAMMAR, at parse. Every `[[rule]] id` and `[[waiver]] rule` is normalised to the space form before validation, so each later reader — a gate, the emitted line, `policy rule`, a receipt, a re-emitted config — sees one spelling. Then each is held to the same vocabulary a class token is. The opt-out is inherited rather than restated: a consumer declaring no word lists cannot satisfy membership, so it is exempt exactly as `verdict::validate` exempts it, and the two names cannot diverge on the trees that have adopted neither. COLLAPSE, and the predicate is a property of the PAIR in both directions. A rule id equals a class token iff the rule raises exactly one class AND that class has exactly one raiser. Both are refused: a row satisfying both whose id differs (two names for one thing), and a row not satisfying both whose id equals a class (one name for two things, which is worse — a reader who dereferences it is answered about the other one). A native-kind row raises no module class, so the second clause covers it without a special case. The issue asked for the first direction only, on a census that counted classes-with-one-raiser and read it as rules-with-one-class. Measured here: 20 policy rows raise exactly one class, 39 raise more, `ci-parity` raises 21. The one-directional rule would have demanded `ci-parity` carry 21 ids at once. THE TWO ARMS REFUSED EACH OTHER FIRST. Collapse renames the trunk-based preset row to `trunk push forced`, and `trunk` is a VENDORED word no consumer vocabulary declares — so the grammar arm rejected the name the collapse arm required, and no tree could satisfy both. A collapsed id is governed by the class registry, not the consumer list, because re-checking it here would be a second authority over one name. Getting the exemption right took two runs: the token set has THREE sources and a set built from the consumer table and the native sites alone still refused the preset's. The gate then found two rows my own census missed, both preset rows binding a bundle rather than a module: `trunk-based-preset` and `mise-preset` are each the sole raiser of one class and now carry it. It reports every violation rather than the first, because a migration is what it fires on and one-at-a-time turns a single reading into N builds. Refs: CLOUD-1638 Admits: 904fd90a4c98168655f1f1a127d4ad1d1c07bfa94f24b49946d487ceadbfb69c Admits-rule: protected-mutation Admits-verdict: path write refused Admits-subject: batten.toml Admits-anchor: call:7e850c102db8da9a99304a8fc5fe033e90347855 Admits-epoch: e16b9ebedf77d2752b2146f85cc6232be3dee087a2a33e84c38e1d762a755b0c Admits-author: alec@wenzowski.com Admits-prev: 3b29b9d6b3a25f1cacbdf6f5734437a9af3be5a51a744b238d1bde867e7823da Admits-answer-lost: CLOUD-1638's collapse arm cannot load its own tree. The gate I just wrote refuses the config until the two rows carry their class tokens, so the branch is unbuildable without this write. Admits-answer-precondition: No surface can express this change: `config read first` reads batten.toml and `patch run first` restores it, and neither writes. The collapse predicate this branch adds refused two rows at load — `branch table missing` and `task table missing` are each the sole raiser of one class — and the only fix the gate accepts is renaming those rows in the authority. The write is a reviewed diff in the PR closing CLOUD-1638. Admits-answer-rejected-route: I rejected `patch run first`: restoring batten.toml reverts the migration rather than completing it. `config read first` does not apply — I have already read it, and reading is what produced the census that found these two rows; it leaves both unrenamed and the gate still refusing. --- batten.toml | 4 +- crates/batten/src/config.rs | 68 +++++++++++ crates/batten/src/policy.rs | 110 +++++++++++++++++- .../it__snapshots__json_output_is_frozen.snap | 8 +- ...__snapshots__pointer_output_is_frozen.snap | 4 +- 5 files changed, 184 insertions(+), 10 deletions(-) diff --git a/batten.toml b/batten.toml index da11f62a8..a32757d81 100644 --- a/batten.toml +++ b/batten.toml @@ -5473,7 +5473,7 @@ no_fix_reason = "the missing node is in the workflow, not in this file; add the # A tree-scoped row would hand it a document it has no predicate for and read as # a configured gate that never fires. [[rule]] -id = "branch table missing" +id = "trunk push forced" kind = "policy" scope = "mediated_call" preset = "trunk-based" @@ -5521,7 +5521,7 @@ severity = "deny" # manifest here is refused at load and would put a consumer's filenames in a row # that ships to every consumer. [[rule]] -id = "task table missing" +id = "task reach loose" kind = "policy" scope = "mediated_call" preset = "mise" diff --git a/crates/batten/src/config.rs b/crates/batten/src/config.rs index 16aa5bb8a..85a2cdbfd 100644 --- a/crates/batten/src/config.rs +++ b/crates/batten/src/config.rs @@ -1708,6 +1708,58 @@ fn validate_tables(config: &Config, text: &str, source: &str) -> Result<()> { Native::VerdictTableRefused, crate::verdict::validate(&config.verdicts, &config.vocabulary), )?; + // THE RULE-ID GRAMMAR (CLOUD-1638), here rather than in `rules::validate` + // because this is where both halves are in scope: the ids are on the rule + // table and the words they must be drawn from are on the vocabulary, and a + // validator holding one cannot decide the other. + // + // Under `RuleTableRefused` rather than `VerdictTableRefused`: what is being + // refused is a `[[rule]]` row, and the class the fault is filed under is + // read by a consumer trying to find the row to fix. + // + // The opt-out is the vocabulary's own, inherited rather than restated — a + // consumer that declares no lists cannot satisfy membership, so demanding + // it would be a refusal with no fix available. That is the same exemption + // `verdict::validate` grants a registry with no vocabulary, and it must be + // the same one, or the two names diverge on exactly the trees that have + // adopted neither. + if !config.vocabulary.is_empty() { + // A COLLAPSED ID IS GOVERNED BY THE CLASS REGISTRY, NOT BY THIS LIST. + // + // Where the id IS a class token, the class's own validation already + // holds it to a grammar, and checking it again here would be a second + // authority over one name — the defect this row exists to remove, + // reintroduced by the row's own gate. It is not hypothetical: the + // collapse arm renames the trunk-based preset's row to `trunk push + // forced`, and `trunk` is a VENDORED word that no consumer vocabulary + // declares. Held to this list, the two arms of CLOUD-1638 would refuse + // each other and no tree could satisfy both. + // ALL THREE SOURCES OF A CLASS TOKEN, and the third is the one that + // matters here: `trunk push forced` is neither a consumer row nor a + // native site but a VENDORED PRESET's, and a set built from the first + // two refuses it — measured, twice, before this line was written. + let vendored = crate::preset::verdict_rows(); + let declared: std::collections::BTreeSet<&str> = config + .verdicts + .iter() + .map(|entry| entry.id.as_str()) + .chain(vendored.iter().map(|entry| entry.id.as_str())) + .chain(crate::verdict::native_tokens().iter().copied()) + .collect(); + under(Native::RuleTableRefused, { + let mut first = Ok(()); + for rule in &config.rules { + if declared.contains(rule.id.as_str()) { + continue; + } + if let Err(error) = crate::verdict::check_rule_id(&rule.id, &config.vocabulary) { + first = Err(error); + break; + } + } + first + })?; + } under( Native::RedirectTableRefused, crate::redirect::validate(&config.redirects), @@ -3083,6 +3135,22 @@ fn parse_ungated(text: &str, source: &str) -> Result { config.version ))); } + // NORMALISE BEFORE VALIDATING, so every later reader — the gates, the + // emitted line, `policy rule`, a receipt — sees one spelling (CLOUD-1638). + // A consumer may write `task-read-first` or `task_read_first`; nothing + // downstream should have to know that, and `emit` round-trips the space + // form, so the canonical spelling is what a re-emitted config carries too. + // + // Unconditional, and cheap: a config that has already adopted the space + // form is rewritten to itself. Doing it here rather than in the validator + // is what makes it a parse-time property rather than a check somebody can + // run late. + for rule in &mut config.rules { + rule.id = crate::verdict::normalise_rule_id(&rule.id); + } + for waiver in &mut config.waivers { + waiver.rule = crate::verdict::normalise_rule_id(&waiver.rule); + } validate_tables(&config, text, source)?; Ok(config) } diff --git a/crates/batten/src/policy.rs b/crates/batten/src/policy.rs index a2532a761..5bc7822ab 100644 --- a/crates/batten/src/policy.rs +++ b/crates/batten/src/policy.rs @@ -705,6 +705,12 @@ pub fn load( // fix available. let registry = registry_for(verdicts)?; let mut emitted: BTreeSet = BTreeSet::new(); + // What each policy row raises, kept PER ROW as well as unioned into + // `emitted` (CLOUD-1638). The union answers "is every declared class + // raised"; the collapse predicate needs the other direction — how many + // classes THIS row raises — and a set that has already been merged cannot + // answer it. + let mut per_rule: BTreeMap> = BTreeMap::new(); let mut bundles = Vec::new(); // Keyed on the scope's WORD rather than the enum, so this set does not oblige // `RuleScope` to carry `Ord` for one local lookup — the derive would be a @@ -850,7 +856,12 @@ pub fn load( check_tree_paths_are_emittable(rule, &bundle, source_key)?; check_no_inline_regex(rule, &bundle, &declared_patterns, source_key)?; check_verdicts_are_declared(rule, &bundle, ®istry, source_key)?; - emitted.extend(emitted_verdicts(&bundle)); + let raised = emitted_verdicts(&bundle); + per_rule + .entry(rule.id.clone()) + .or_default() + .extend(raised.iter().cloned()); + emitted.extend(raised); } claim_ids(&mut ids, &declared, source_key)?; bundles.push(bundle.with_severity(rule)); @@ -879,7 +890,12 @@ pub fn load( check_tree_paths_are_emittable(rule, &bundle, where_it_came_from)?; check_no_inline_regex(rule, &bundle, &declared_patterns, where_it_came_from)?; check_verdicts_are_declared(rule, &bundle, ®istry, where_it_came_from)?; - emitted.extend(emitted_verdicts(&bundle)); + let raised = emitted_verdicts(&bundle); + per_rule + .entry(rule.id.clone()) + .or_default() + .extend(raised.iter().cloned()); + emitted.extend(raised); } claim_ids(&mut ids, &declared, where_it_came_from)?; @@ -889,6 +905,17 @@ pub fn load( if checks == ModuleChecks::Run { check_registry_is_exhausted(verdicts, &emitted)?; + let tokens: BTreeSet = verdicts + .iter() + .filter(|entry| !entry.retired()) + .map(|entry| entry.id.clone()) + .chain( + crate::verdict::native_tokens() + .iter() + .map(|t| (*t).to_owned()), + ) + .collect(); + check_collapse(rules, &per_rule, &tokens)?; } Ok(bundles) } @@ -1603,6 +1630,85 @@ fn check_tree_paths_are_emittable(rule: &Rule, bundle: &Bundle, source: &str) -> /// # Errors /// /// A [`UsageError`] (exit `1`) naming the unraised tokens. +/// One name where a rule and a class name one thing (CLOUD-1638). +/// +/// # The predicate is a property of the PAIR, in both directions +/// +/// A rule's `id` equals a class token **iff** the rule raises exactly one class +/// **and** that class has exactly one raiser. Both clauses are refused: +/// +/// * a row satisfying both whose id differs from that class — two names for one +/// thing, which is the cost this row removes; +/// * a row NOT satisfying both whose id equals any class token — one name for +/// two things, which is worse, because a reader who dereferences it gets an +/// answer about the other one. +/// +/// # Why both directions, measured +/// +/// The issue's Ready block asked only for the first, on a census that counted +/// classes-with-one-raiser (170 of 174) and read it as rules-with-one-class. +/// Those are opposite directions. Measured on this tree: **20** policy rows +/// raise exactly one class and **39** raise more — `ci-parity` raises 21, +/// `shell-retirement` 13, `lock-complete` 10. The one-directional rule would +/// have demanded `ci-parity` carry twenty-one ids at once, so it was +/// unsatisfiable and could not have shipped in that form. +/// +/// A row this function never sees a class for — every native-kind row, whose +/// class the ENGINE picks rather than a module — is correctly handled by the +/// second clause alone: it raises no module class, so it is not collapsible, +/// so its id must not be a class token. +/// +/// # Errors +/// +/// A [`UsageError`] (exit `1`) naming the row and which direction it broke. +fn check_collapse( + rules: &[Rule], + per_rule: &BTreeMap>, + registry: &BTreeSet, +) -> Result<()> { + let raisers: BTreeMap<&str, usize> = { + let mut counts: BTreeMap<&str, usize> = BTreeMap::new(); + for classes in per_rule.values() { + for class in classes { + *counts.entry(class.as_str()).or_default() += 1; + } + } + counts + }; + // EVERY VIOLATION, NOT THE FIRST. A migration is the case this fires on, + // and one-at-a-time turns a single reading into N builds — measured on + // CLOUD-1638's own migration, where the first two runs each bought one + // rename. Reporting the set is also what lets a reader see whether the + // config disagrees with the modules in one place or systematically. + let mut findings: Vec = Vec::new(); + for rule in rules { + let empty = BTreeSet::new(); + let classes = per_rule.get(&rule.id).unwrap_or(&empty); + let sole = match (classes.len(), classes.iter().next()) { + (1, Some(class)) if raisers.get(class.as_str()) == Some(&1) => Some(class.as_str()), + _ => None, + }; + match sole { + Some(class) if rule.id != class => findings.push(format!( + "rule `{}` is the only raiser of `{class}` and raises nothing else, so the two \ + are one thing and owe one name: rename the row to `{class}`", + rule.id + )), + None if registry.contains(&rule.id) => findings.push(format!( + "rule `{}` is spelled as a class token but is not that class's sole raiser, so \ + the name answers for two different things — give the row a distinct three-word \ + id", + rule.id + )), + _ => {} + } + } + if !findings.is_empty() { + return Err(UsageError::raise(findings.join("\n"))); + } + Ok(()) +} + fn check_registry_is_exhausted( verdicts: &[crate::verdict::DeclaredVerdict], emitted: &BTreeSet, diff --git a/crates/batten/tests/it/snapshots/it__snapshots__json_output_is_frozen.snap b/crates/batten/tests/it/snapshots/it__snapshots__json_output_is_frozen.snap index b82e88dd2..8e8b0bd7d 100644 --- a/crates/batten/tests/it/snapshots/it__snapshots__json_output_is_frozen.snap +++ b/crates/batten/tests/it/snapshots/it__snapshots__json_output_is_frozen.snap @@ -6,24 +6,24 @@ expression: stdout_of(&output) "fail_on_warning": false, "findings": [ { - "rule": "no-todo", + "rule": "no todo", "path": "a.rs", "line": 1, "severity": "deny", "report": "fail", "identity": { - "fingerprint": "6010ecbf984d1bab0ef56ac4959473e3e209146d402d86aa13532c016932fba2", + "fingerprint": "d9f86609ab986e2fd8b795a15c0d7b6c69d8bac7455046ba3b0889d9a8990169", "version": "code:2026-08-06" } }, { - "rule": "no-todo", + "rule": "no todo", "path": "b.rs", "line": 1, "severity": "deny", "report": "fail", "identity": { - "fingerprint": "11a067cdf556f9397aaf4746713629abff4994983289de7bd613d7930d5922b2", + "fingerprint": "062895ed8666aa9fc5e3b743176424f375694c4fec6f1930135112f51c42d3dc", "version": "code:2026-08-06" } } diff --git a/crates/batten/tests/it/snapshots/it__snapshots__pointer_output_is_frozen.snap b/crates/batten/tests/it/snapshots/it__snapshots__pointer_output_is_frozen.snap index 2276ee2bf..cd97fd353 100644 --- a/crates/batten/tests/it/snapshots/it__snapshots__pointer_output_is_frozen.snap +++ b/crates/batten/tests/it/snapshots/it__snapshots__pointer_output_is_frozen.snap @@ -2,5 +2,5 @@ source: crates/batten/tests/it/snapshots.rs expression: stdout_of(&output) --- -a.rs:1 no-todo -b.rs:1 no-todo +a.rs:1 no todo +b.rs:1 no todo From 08cf53176ef9cc7079c3128fe7a6afb25d9db1cc Mon Sep 17 00:00:00 2001 From: Alec Wenzowski Date: Wed, 9 Sep 2026 09:52:59 +0000 Subject: [PATCH 04/23] feat(refusal)!: render the rule id only where it differs from the class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The line arm and the tests the other three arms owe. LINE. A collapsed row renders one token: where the row IS its class's sole raiser the two names are one, and `validate` refuses any other spelling at load, so appending the id would print the same three words twice. The 116 rows that are not collapsed still carry it — for them it is the only discriminator. Decided from the strings rather than a flag, because the load-time predicate has already made them equal exactly when they name one thing. `//MUTANT identity-arm-dropped` sits on the collapse arm — in `policy.rs`, where the behaviour is, rather than in `verdict.rs` where the issue guessed it would be. This enrols `engine-policy` in `$MUTANT_GATES`. THE MUTANT IS PROVEN CAUGHT, and the first attempt to prove it was vacuous: the marker line embeds its own sed pattern, so a naive replace hit the comment and the tree was never mutated — the case passed on clean source and the marker would have shipped as decoration. Re-applied at the code site only, confirmed the declared case fails, reverted. Four cases in `rules_drift.rs`, the declared suite: a sole raiser whose id differs is refused; one carrying its class token loads (without which the first passes over a predicate that refuses every policy row); a word no slot declares is refused; a hyphen-spelled id is the same row as the space form, so normalisation is asserted at the LOAD path and not only under it. The fixture took three corrections worth recording, each a real property: every declared word must be spent by some name, every name must be spelled from declared words — including the ROUTE id, which needs `config` and `first` — and a module may only read an emitted `input.tree` key. The spelling table asserts the ORDER, not the figures. Absolute means drift with the table's contents and would make it a snapshot nobody can update honestly; space < snake < hyphen is the property the canonical form rests on, recomputed from the committed table on every run. Schemas regenerate to no change: the id grammar is a constraint on a value, not a change to a shape. Refs: CLOUD-1638 --- crates/batten/src/policy.rs | 2 + crates/batten/src/refusal.rs | 10 ++ crates/batten/tests/it/rules_drift.rs | 154 +++++++++++++++++++ crates/batten/tests/it/verdict_vocabulary.rs | 81 ++++++++++ mise.toml | 2 +- 5 files changed, 248 insertions(+), 1 deletion(-) diff --git a/crates/batten/src/policy.rs b/crates/batten/src/policy.rs index 5bc7822ab..aaa6afd7b 100644 --- a/crates/batten/src/policy.rs +++ b/crates/batten/src/policy.rs @@ -1661,6 +1661,8 @@ fn check_tree_paths_are_emittable(rule: &Rule, bundle: &Bundle, source: &str) -> /// # Errors /// /// A [`UsageError`] (exit `1`) naming the row and which direction it broke. +//MUTANT-SUITE crates/batten/tests/it/rules_drift.rs +//MUTANT identity-arm-dropped|s@ Some(class) if rule.id != class => findings.push(format!(@ Some(class) if false \&\& rule.id != class => findings.push(format!(@|a_sole_raiser_whose_id_differs_from_its_class_is_refused_at_load fn check_collapse( rules: &[Rule], per_rule: &BTreeMap>, diff --git a/crates/batten/src/refusal.rs b/crates/batten/src/refusal.rs index 8d2414ce6..4bf8f7c12 100644 --- a/crates/batten/src/refusal.rs +++ b/crates/batten/src/refusal.rs @@ -644,6 +644,15 @@ impl Refusal { /// Concision is bought with a class a reader can look up; where there is no /// class there is nothing to buy it with, and the long form is the honest /// answer rather than a fallback. + /// **A COLLAPSED ROW RENDERS ONE TOKEN** (CLOUD-1638). Where the row IS its + /// class's sole raiser the two names are one, and `validate` refuses any + /// other spelling at load — so appending the id would print the same three + /// words twice. The 116 rows that are not collapsed still carry it, for the + /// reason above: it is their only discriminator. + /// + /// Decided from the strings rather than from a flag, because the load-time + /// predicate has already made them equal exactly when they name one thing, + /// and re-deriving the condition here would be a second authority over it. #[must_use] pub fn line(&self) -> String { match self.verdict() { @@ -651,6 +660,7 @@ impl Refusal { // token plus pointers — so this is a projection rather than a second // renderer. Composing the line here from the token and the subject // would be a second authority over a string the composer built. + Some(token) if token == self.rule => self.reason.clone(), Some(_) => format!("{} {}", self.reason, self.rule), None => self.render(), } diff --git a/crates/batten/tests/it/rules_drift.rs b/crates/batten/tests/it/rules_drift.rs index cff80cf9c..ecbeb6f6d 100644 --- a/crates/batten/tests/it/rules_drift.rs +++ b/crates/batten/tests/it/rules_drift.rs @@ -865,3 +865,157 @@ fn the_two_anchors_this_gate_keys_on_are_still_one_line_in_the_committed_files() `schema-key-undocumented` silently stops judging it" ); } + +// --- the rule-id grammar and the collapse predicate (CLOUD-1638) ------------ +// +// Anchored in this suite because it is `//MUTANT identity-arm-dropped`'s +// declared `//MUTANT-SUITE`, and because the question is the same one the rest +// of the file asks: does a name in the tree agree with the mechanism that +// judges it. These ask it of the two names a refusal line carries. +// +// They use their own runner rather than `judge`: that one names rules-drift's +// own rule and reads stdout, and what is under test here is whether the config +// LOADS at all, which is a usage error on stderr. + +/// A config declaring one class, one module raising it, and one `policy` row +/// whose id the caller chooses. +/// +/// **Every declared word is spent by a name in the file, and every name is +/// spelled from it.** Both directions bite: `validate` refuses an orphan word — +/// a vocabulary entry no name uses — and it refuses a name using a word no slot +/// declares. The first draft failed all four cases on the first, then two more +/// on the second, because the ROUTE id `config read first` is a name too and +/// needs `config` and `first` declared alongside the class's own words. +fn collapse_fixture(name: &str, rule_id: &str, extra_words: &str) -> std::path::PathBuf { + let dir = scratch(name); + write( + &dir, + "policy/collapse-probe.rego", + r#"# METADATA +# description: | +# One class, so a row binding this module is its sole raiser. +# THIS BLOCK IS YAML AND MUST STAY THE LAST COMMENT BLOCK BEFORE `package`. +# schemas: +# - input: schema["policy-input.schema"] +package batten.collapse_probe + +import rego.v1 + +rules contains "collapse-probe" + +violation contains { + "rule": "collapse-probe", + "verdict": "probe read absent", + "subjects": [{"count": 1}], +} if { + input.tree.tracked +} +"#, + ); + write( + &dir, + "batten.toml", + &format!( + r#"version = 1 + +[vocabulary] +tokenizer = "o200k_base" +tokenizer_source = "https://github.com/openai/tiktoken" +tokenizer_retrieved = "2026-09-01" +subject = [{{ word = "probe", gloss = "the fixture subject" }}, {{ word = "config", gloss = "the committed authority" }}{subject}] +action = [{{ word = "read", gloss = "a read of the subject" }}{action}] +condition = [{{ word = "absent", gloss = "the subject is not there" }}, {{ word = "first", gloss = "the subject comes first" }}{condition}] + +[[verdict]] +id = "probe read absent" +gloss = "the fixture class the collapse predicate is exercised over" +class = """ +A fixture class, raised by exactly one module, so a row binding that module is +its sole raiser and the collapse predicate has a decision to make. +""" + +[[verdict.route]] +id = "config read first" +kind = "document" +target = "batten.toml" + +[[rule]] +id = "{rule_id}" +kind = "policy" +scope = "tree" +module = "policy/collapse-probe.rego" +severity = "deny" +"#, + subject = if extra_words.is_empty() { + "" + } else { + r#", { word = "task", gloss = "a declared task" }"# + }, + action = if extra_words.is_empty() { + "" + } else { + r#", { word = "run", gloss = "a run of the subject" }"# + }, + condition = "", + ), + ); + git_in(&dir, &["init", "-q"]); + dir +} + +/// `check` over the fixture's own config, reading the channel a usage error uses. +fn load(dir: &Path) -> (Option, String) { + let out = run(dir, &["check"]); + (out.status.code(), common::stderr(&out)) +} + +/// THE COLLAPSE ARM, first direction: two names for one thing is refused. +#[test] +fn a_sole_raiser_whose_id_differs_from_its_class_is_refused_at_load() { + let dir = collapse_fixture("collapse-differs", "task run first", "extra"); + let (code, text) = load(&dir); + assert_eq!( + code, + Some(1), + "the row and the class name one thing: {text}" + ); + assert!( + text.contains("probe read absent"), + "and the refusal names the token the row owes: {text}" + ); +} + +/// THE SAME ARM, satisfied. Without this the case above passes over a predicate +/// that refuses every policy row, which would make the arm decorative. +#[test] +fn a_sole_raiser_carrying_its_class_token_loads() { + let dir = collapse_fixture("collapse-agrees", "probe read absent", ""); + let (code, text) = load(&dir); + assert_ne!( + code, + Some(1), + "a row spelled as the class it solely raises is the shape the arm WANTS: {text}" + ); +} + +/// THE GRAMMAR ARM: a word no slot declares is refused, id or class alike. +#[test] +fn a_rule_id_outside_the_vocabulary_is_refused_at_load() { + let dir = collapse_fixture("grammar-undeclared", "task run undeclared", "extra"); + let (code, text) = load(&dir); + assert_eq!(code, Some(1), "`undeclared` is in no slot: {text}"); +} + +/// NORMALISATION reaches the load path, not just the unit under it: a +/// hyphen-spelled id is the same row as the space form, so it satisfies the +/// collapse arm the space form satisfies. +#[test] +fn a_hyphen_spelled_id_is_the_same_row_as_the_space_form() { + let dir = collapse_fixture("grammar-hyphen", "probe-read-absent", ""); + let (code, text) = load(&dir); + assert_ne!( + code, + Some(1), + "`probe-read-absent` normalises to `probe read absent`: {text}" + ); +} diff --git a/crates/batten/tests/it/verdict_vocabulary.rs b/crates/batten/tests/it/verdict_vocabulary.rs index 46fa71896..2196615f7 100644 --- a/crates/batten/tests/it/verdict_vocabulary.rs +++ b/crates/batten/tests/it/verdict_vocabulary.rs @@ -254,3 +254,84 @@ fn the_measured_list_and_the_declared_table_are_the_same_set() { "declared-but-unmeasured: {unmeasured:?}; measured-but-undeclared: {undeclared:?}" ); } + +/// The spelling table (CLOUD-1638): what a name costs in each separator. +/// +/// # Why this is a case rather than a paragraph +/// +/// CLOUD-1638 moves 136 rule ids from unconstrained kebab prose into this +/// grammar, and the argument for doing so is a measurement: a rule id averaged +/// **4.93** tokens (max 13) against a three-word name's **3.01**. A number that +/// lives only in an issue body decays the moment the vocabulary changes; here it +/// is recomputed from the committed table on every run, so a word that stops +/// being one token, or a spelling that stops being the cheap one, reddens. +/// +/// The ORDER is the assertion, not the absolute figures. Absolute means drift +/// with the table's contents and would make this a snapshot nobody can update +/// honestly; the ranking — space cheapest, then snake, then hyphen — is the +/// property the grammar's canonical form rests on, and it is what would have to +/// be false for the space form to be the wrong choice. +#[test] +fn the_space_form_is_the_cheapest_spelling_of_a_name() { + let bpe = tiktoken_rs::o200k_base().expect("the pinned encoding is vendored with the crate"); + let root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../.."); + let text = + std::fs::read_to_string(root.join("batten.toml")).expect("the authority is readable"); + let config: toml::Value = toml::from_str(&text).expect("the authority parses"); + + // Every name the grammar governs: the class tokens and the rule ids, which + // after this row are drawn from one vocabulary and must price the same. + let mut names: Vec = Vec::new(); + for table in ["verdict", "rule"] { + let Some(rows) = config.get(table).and_then(toml::Value::as_array) else { + continue; + }; + names.extend( + rows.iter() + .filter_map(|row| row.get("id")) + .filter_map(toml::Value::as_str) + .filter(|id| id.split(' ').count() == 3) + .map(ToOwned::to_owned), + ); + } + assert!( + names.len() > 100, + "the scan must actually find the declared names: {}", + names.len() + ); + + // A LEADING SPACE, for the reason the case above gives: it is what a name + // inside a rendered line actually costs. + let mean = |spell: &dyn Fn(&str) -> String| -> f64 { + let total: usize = names + .iter() + .map(|name| { + bpe.encode_with_special_tokens(&format!(" {}", spell(name))) + .len() + }) + .sum(); + #[expect( + clippy::cast_precision_loss, + reason = "a count of names and of tokens, both far below 2^53; the ratio is \ + reported to two decimals and the assertion below is an ORDERING" + )] + let mean = total as f64 / names.len() as f64; + mean + }; + + let space = mean(&|name: &str| name.to_owned()); + let snake = mean(&|name: &str| name.replace(' ', "_")); + let hyphen = mean(&|name: &str| name.replace(' ', "-")); + + assert!( + space < snake && snake < hyphen, + "the space form must be the cheapest spelling and hyphen the dearest — \ + space {space:.2}, snake {snake:.2}, hyphen {hyphen:.2} over {} names under {PIN}", + names.len() + ); + assert!( + space < 4.0, + "a three-word name drawn from one-token words costs about three tokens; \ + {space:.2} means a word in the table stopped being one token" + ); +} diff --git a/mise.toml b/mise.toml index 0103e5122..0cc7a1044 100644 --- a/mise.toml +++ b/mise.toml @@ -617,7 +617,7 @@ CI_FANIN_WORKFLOW = ".github/workflows/ci.yml" BATS_TEST_TIMEOUT = "300" REGORUS_OPA_COMPLIANCE = "1.2.0" REGORUS_OPA_COMPLIANCE_FOR = "0.11" -MUTANT_GATES = "mise,attestation-check,engine-checks-green,engine-config,engine-doctor,engine-landed,engine-perf,engine-mcp,engine-pinned,engine-ready,engine-verdict,engine-wiring,engine-surface,agentic-experiment-record,awk-regex-check,bats-invocation,batten-glob-check,board-diff-overlap,board-payloads,board-sweep,branch-age-check,cap-drift,cfg-gated-test,ci-cache-declared,ci-hygiene,ci-parity,ci-slow-needed,ci-suite-lane,ci-tools-check,claim-before-code,claim-order-is-stated,claimed-keys,closing-key-check,coderabbit-config-check,commit-hygiene,connector-allow-guard,connector-allow-resolve,container-preflight,darwin-link,deferral-check,denials-outlive-the-turn,digest-major-agreement,doctor,done-check,done-pr-check,duplicate-close-check,egress-fencing,evaluator-closure-check,evaluator-io-check,filed-here,finding-sink-check,fixture-forks,forge-verdict-required,graph-check,harness-grant,harness-wiring,hk-fix-selection,hk-plan-required,hook-pin-check,hook-skip-local,in-progress-drain,install-check,land-divergence-assert,landed-check,landing-loop,landing-roster-guarded,leased-push,license-table-check,linear-check,lock-complete,macos-link-check,mcp-allow-check,mcp-attach-check,mcp-timeout-budget,merged-pr-keys,mise-action-floor,mise-pin-agreement,module-map-check,msrv-pin-agreement,mutation-declared-case,no-doctests,nonverdict-assert,ntia-check,obligations-bound,perf-assert,pinned-toolchain,pipefail-grep-check,plan-complete,pr-partition-restated,pr-unsubscribed,privileged-lane,prose-only,publish-credential-check,ready-cites-check,ready-lint,reclaim-census,release-assets-check,release-due,release-provision-parity,release-tag-shape,release-tracking-check,released,remedy-authorship,repetition-without-progress,report-only-check,review-answered,review-dispatched,run-shape,rust-paths-check,sbom,sbom-inventory,serena-mcp,shell-hygiene,shell-retirement,shell-write-advisory,signing-posture,sonar-gate,spec-ref-check,stop-posture,stop-posture-check,suite-bench-check,suite-subject-retirable,task-substitution,test-targets,timeout-check,token-bench-check,transcript-corpus-check,tree-clean,trunk-based,validator-verdict-clean,verdict-routes-resolve,weakens-declared,worktree-registration,spawn-widening,nextest-slow,engine-lease,engine-handler,engine-speculation,engine-pipeline" +MUTANT_GATES = "mise,attestation-check,engine-checks-green,engine-config,engine-doctor,engine-landed,engine-perf,engine-mcp,engine-pinned,engine-ready,engine-verdict,engine-wiring,engine-surface,agentic-experiment-record,awk-regex-check,bats-invocation,batten-glob-check,board-diff-overlap,board-payloads,board-sweep,branch-age-check,cap-drift,cfg-gated-test,ci-cache-declared,ci-hygiene,ci-parity,ci-slow-needed,ci-suite-lane,ci-tools-check,claim-before-code,claim-order-is-stated,claimed-keys,closing-key-check,coderabbit-config-check,commit-hygiene,connector-allow-guard,connector-allow-resolve,container-preflight,darwin-link,deferral-check,denials-outlive-the-turn,digest-major-agreement,doctor,done-check,done-pr-check,duplicate-close-check,egress-fencing,evaluator-closure-check,evaluator-io-check,filed-here,finding-sink-check,fixture-forks,forge-verdict-required,graph-check,harness-grant,harness-wiring,hk-fix-selection,hk-plan-required,hook-pin-check,hook-skip-local,in-progress-drain,install-check,land-divergence-assert,landed-check,landing-loop,landing-roster-guarded,leased-push,license-table-check,linear-check,lock-complete,macos-link-check,mcp-allow-check,mcp-attach-check,mcp-timeout-budget,merged-pr-keys,mise-action-floor,mise-pin-agreement,module-map-check,msrv-pin-agreement,mutation-declared-case,no-doctests,nonverdict-assert,ntia-check,obligations-bound,perf-assert,pinned-toolchain,pipefail-grep-check,plan-complete,pr-partition-restated,pr-unsubscribed,privileged-lane,prose-only,publish-credential-check,ready-cites-check,ready-lint,reclaim-census,release-assets-check,release-due,release-provision-parity,release-tag-shape,release-tracking-check,released,remedy-authorship,repetition-without-progress,report-only-check,review-answered,review-dispatched,run-shape,rust-paths-check,sbom,sbom-inventory,serena-mcp,shell-hygiene,shell-retirement,shell-write-advisory,signing-posture,sonar-gate,spec-ref-check,stop-posture,stop-posture-check,suite-bench-check,suite-subject-retirable,task-substitution,test-targets,timeout-check,token-bench-check,transcript-corpus-check,tree-clean,trunk-based,validator-verdict-clean,verdict-routes-resolve,weakens-declared,worktree-registration,spawn-widening,nextest-slow,engine-lease,engine-handler,engine-speculation,engine-pipeline,engine-policy" # --- GitHub reachability behind an egress proxy (Claude Code web sandbox etc.) --- # mise resolves every tool's release through GitHub's *API* host, api.github.com. From b80cbc6036952342cfd83ce6f3f4dbec1bbdf608 Mon Sep 17 00:00:00 2001 From: Alec Wenzowski Date: Wed, 9 Sep 2026 12:09:03 +0000 Subject: [PATCH 05/23] fix(tests): carry the migration through every surface that names a rule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The migration's fallout, and each item is a place a rule id lives that the first pass did not reach. 431 failures at the start of this, 0 at the end. **Test expectations.** 521 lines across 128 suites assert on the emitted id. Rewritten structurally, excluding two contexts the first attempt got wrong: a `policy/.rego` PATH is a filename, not an id — the naive pass turned `include_str!("policy/validator-verdict-clean.rego")` into a path with spaces in it — and `PRESET_SCOPES` lists preset DIRECTORIES, so `shell-hygiene` there is a preset name that survived a rename of the row that enables it. **Two arms were scoped, and both scopings are the same exemption.** Normalisation and the collapse predicate applied to every config the engine loads, including fixtures that never adopted the grammar. A tree declaring no vocabulary spells its ids however it likes — `no-todo` is that consumer's whole name for the row, not a spelling of a three-word one — so rewriting it renamed a row nobody asked to rename (22 cases), and refusing it demanded a fixture name itself after a class it invented (49 cases). Collapse is now scoped to rows already in the grammar: it decides WHICH of two grammar names a row carries, and a row with one name is not in that conversation. Not a hole — `validate_tables` refuses a non-grammar id outright once a vocabulary is declared, so the arms compose. **Surfaces the structural pass missed**: the shipped `batten.example.toml` and `starter.toml`, the built-in `default_rules()` the zero-config path serves, a `resolve.rs` fixture that redefines the default rule to prove the clamp fires, and `rules/{scanning,commits,README}.md`, which name rules by id as pointers. **Two assertions changed shape rather than value.** Both recovered the id by taking the last WHITESPACE token of the head, which is now one word of three. The head is ` ` with the id present only when it differs, so the last three words are the id on a discriminating row and the first three are on a collapsed one, where the class IS the id. **One assertion lost a property and got it back through the hop.** `todo_promotion` asserted `text.contains("ready")` and passed because the row was called `a-todo-promotion-owes-a-ready-verdict` — prose inside an id. A vocabulary id cannot carry an arbitrary state name, which is this row's point rather than a regression, so the assertion now reads it from `policy rule`, proving the dereference works instead of assuming it. Golden snapshots regenerated, never hand-edited. Refs: CLOUD-1638 --- batten.example.toml | 4 +- crates/batten/src/config.rs | 20 +- crates/batten/src/policy.rs | 15 ++ crates/batten/src/resolve.rs | 2 +- crates/batten/src/starter.toml | 2 +- crates/batten/tests/it/acquisition_sweep.rs | 4 +- crates/batten/tests/it/admission.rs | 24 +- crates/batten/tests/it/admission_narrowing.rs | 4 +- crates/batten/tests/it/agent_facts.rs | 12 +- crates/batten/tests/it/agentic_record.rs | 2 +- crates/batten/tests/it/ambient_authority.rs | 2 +- .../batten/tests/it/attribution_provenance.rs | 2 +- crates/batten/tests/it/bats_invocation.rs | 10 +- crates/batten/tests/it/board_receipts.rs | 32 +-- crates/batten/tests/it/board_record.rs | 2 +- crates/batten/tests/it/bypass_precondition.rs | 2 +- crates/batten/tests/it/cfg_gated_test.rs | 6 +- crates/batten/tests/it/ci_cache_declared.rs | 2 +- crates/batten/tests/it/ci_hygiene.rs | 2 +- crates/batten/tests/it/ci_parity.rs | 4 +- crates/batten/tests/it/ci_suite_lane.rs | 4 +- crates/batten/tests/it/claim.rs | 2 +- crates/batten/tests/it/claim_order.rs | 2 +- crates/batten/tests/it/claim_receipt.rs | 6 +- crates/batten/tests/it/cli.rs | 227 ++++++++++-------- .../batten/tests/it/commit_arm_sequencing.rs | 4 +- crates/batten/tests/it/config_skew.rs | 2 +- .../batten/tests/it/connector_allow_door.rs | 6 +- .../batten/tests/it/connector_not_granted.rs | 8 +- crates/batten/tests/it/connector_verbs.rs | 34 ++- crates/batten/tests/it/container_health.rs | 2 +- crates/batten/tests/it/contract_drift.rs | 4 +- crates/batten/tests/it/document_read_count.rs | 2 +- crates/batten/tests/it/egress_fencing.rs | 4 +- crates/batten/tests/it/enforce_journal.rs | 16 +- crates/batten/tests/it/fact_record_keying.rs | 2 +- crates/batten/tests/it/filed_here.rs | 10 +- crates/batten/tests/it/fixture_forks.rs | 2 +- crates/batten/tests/it/forced_push.rs | 8 +- crates/batten/tests/it/forge_facts.rs | 2 +- crates/batten/tests/it/gh_guard.rs | 52 ++-- crates/batten/tests/it/harness_grant.rs | 8 +- crates/batten/tests/it/harness_wiring.rs | 18 +- crates/batten/tests/it/hk_contract.rs | 2 +- crates/batten/tests/it/hk_fix_selection.rs | 8 +- crates/batten/tests/it/hk_plan.rs | 4 +- crates/batten/tests/it/hook_profile.rs | 8 +- crates/batten/tests/it/hook_skip_local.rs | 6 +- crates/batten/tests/it/identity_precedence.rs | 4 +- crates/batten/tests/it/install_web.rs | 2 +- .../batten/tests/it/inverted_board_cases.rs | 2 +- crates/batten/tests/it/issue_key.rs | 14 +- crates/batten/tests/it/land_entry_gates.rs | 2 +- crates/batten/tests/it/land_hand_stepping.rs | 6 +- crates/batten/tests/it/land_lap.rs | 4 +- crates/batten/tests/it/landed_check.rs | 2 +- crates/batten/tests/it/landing_roster.rs | 4 +- crates/batten/tests/it/lock_complete.rs | 6 +- crates/batten/tests/it/mcp_reduce_array.rs | 2 +- crates/batten/tests/it/mediated_verbs.rs | 10 +- crates/batten/tests/it/memories.rs | 2 +- crates/batten/tests/it/minted_facts.rs | 2 +- crates/batten/tests/it/mise_pin_agreement.rs | 8 +- crates/batten/tests/it/mise_preset.rs | 4 +- .../batten/tests/it/mutation_declared_case.rs | 6 +- crates/batten/tests/it/nextest_slow.rs | 8 +- crates/batten/tests/it/obligations_bound.rs | 6 +- crates/batten/tests/it/perf_assert.rs | 10 +- crates/batten/tests/it/perf_compare.rs | 2 +- crates/batten/tests/it/perf_pair.rs | 12 +- crates/batten/tests/it/pipeline_shapes.rs | 14 +- crates/batten/tests/it/plan_complete.rs | 6 +- crates/batten/tests/it/pointer_only.rs | 6 +- crates/batten/tests/it/policy_presets.rs | 4 +- crates/batten/tests/it/policy_severity.rs | 2 +- .../batten/tests/it/pr_partition_restated.rs | 8 +- crates/batten/tests/it/prebuilt_lint.rs | 20 +- crates/batten/tests/it/preset_segments.rs | 4 +- crates/batten/tests/it/privileged_lane.rs | 6 +- crates/batten/tests/it/prose_only.rs | 12 +- crates/batten/tests/it/prospective_facts.rs | 2 +- crates/batten/tests/it/ratchet.rs | 18 +- crates/batten/tests/it/raw_tracker_read.rs | 6 +- crates/batten/tests/it/ready.rs | 8 +- crates/batten/tests/it/rebase.rs | 4 +- crates/batten/tests/it/receipt_verified.rs | 4 +- crates/batten/tests/it/reclaim_report_once.rs | 2 +- crates/batten/tests/it/refusal_ceiling.rs | 8 +- .../tests/it/release_provision_parity.rs | 4 +- crates/batten/tests/it/remedy_authorship.rs | 6 +- crates/batten/tests/it/repaired_arms.rs | 2 +- crates/batten/tests/it/repetition.rs | 2 +- crates/batten/tests/it/retirement_doctrine.rs | 2 +- crates/batten/tests/it/review_answered.rs | 38 +-- crates/batten/tests/it/review_dispatched.rs | 4 +- .../batten/tests/it/review_receipt_delta.rs | 4 +- crates/batten/tests/it/rule_cost_census.rs | 2 +- crates/batten/tests/it/rules_drift.rs | 10 +- crates/batten/tests/it/run_shape.rs | 14 +- .../batten/tests/it/run_shape_guard_door.rs | 4 +- crates/batten/tests/it/runner_verdict.rs | 2 +- crates/batten/tests/it/sbom_inventory.rs | 4 +- crates/batten/tests/it/scanner_taxonomy.rs | 6 +- crates/batten/tests/it/secret_redaction.rs | 2 +- crates/batten/tests/it/secrets_kind.rs | 22 +- crates/batten/tests/it/semver_gate.rs | 4 +- .../batten/tests/it/session_provisioning.rs | 8 +- crates/batten/tests/it/shell_retirement.rs | 2 +- .../batten/tests/it/shell_retirement_cost.rs | 2 +- .../batten/tests/it/shell_write_advisory.rs | 18 +- crates/batten/tests/it/sleep_ban.rs | 2 +- .../it__snapshots__json_output_is_frozen.snap | 8 +- ...__snapshots__pointer_output_is_frozen.snap | 4 +- crates/batten/tests/it/spawn_ceilings.rs | 13 +- crates/batten/tests/it/spawn_census.rs | 2 +- crates/batten/tests/it/spawn_widening.rs | 12 +- crates/batten/tests/it/staged_facts.rs | 10 +- crates/batten/tests/it/stop_posture.rs | 16 +- crates/batten/tests/it/submodule.rs | 4 +- crates/batten/tests/it/suite_subjects.rs | 2 +- crates/batten/tests/it/surface.rs | 2 +- crates/batten/tests/it/target_prune.rs | 2 +- crates/batten/tests/it/task_prose.rs | 8 +- crates/batten/tests/it/task_receipt.rs | 6 +- crates/batten/tests/it/task_registry.rs | 4 +- crates/batten/tests/it/test_targets.rs | 2 +- crates/batten/tests/it/todo_promotion.rs | 24 +- crates/batten/tests/it/tool_verdict_facts.rs | 12 +- crates/batten/tests/it/trunk_watch.rs | 2 +- crates/batten/tests/it/verdict_registry.rs | 4 +- crates/batten/tests/it/waivers.rs | 2 +- crates/batten/tests/it/wiring_reclaim.rs | 2 +- .../batten/tests/it/worktree_registration.rs | 4 +- crates/batten/tests/it/zero_config.rs | 4 +- crates/batten/tests/policy_modules.rs | 2 +- fuzz/corpus/config_parse/batten.example.toml | 4 +- rules/README.md | 14 +- rules/commits.md | 2 +- rules/scanning.md | 4 +- 139 files changed, 627 insertions(+), 570 deletions(-) diff --git a/batten.example.toml b/batten.example.toml index 5a73f443d..54a62f881 100644 --- a/batten.example.toml +++ b/batten.example.toml @@ -150,7 +150,7 @@ redirect = "append, or write through the surface that owns the file" # default). Scope is never severity: a severity value in the scope # key (or the reverse) is refused with exit 1, not reinterpreted. [[rule]] -id = "no-conflict-markers" +id = "source carry broken" kind = "command" glob = "**/*.rs" check = "hk util check-merge-conflict --assume-in-merge {{files}}" @@ -311,7 +311,7 @@ reason = "set the tool's own severity to deny; do not let a warning ride an exit # A git-ignored batten.local.toml may NOT waive a rule declared here — a waiver # lowers the bar, so the durable tier is the committed authority alone (§8). [[waiver]] -rule = "no-conflict-markers" +rule = "source carry broken" reason = "the vendored tree is being replaced in CLOUD-123; gating it churns the diff" expires = "2026-12-31" path = "vendor/**" diff --git a/crates/batten/src/config.rs b/crates/batten/src/config.rs index 85a2cdbfd..f769fcca4 100644 --- a/crates/batten/src/config.rs +++ b/crates/batten/src/config.rs @@ -3145,11 +3145,19 @@ fn parse_ungated(text: &str, source: &str) -> Result { // form is rewritten to itself. Doing it here rather than in the validator // is what makes it a parse-time property rather than a check somebody can // run late. - for rule in &mut config.rules { - rule.id = crate::verdict::normalise_rule_id(&rule.id); - } - for waiver in &mut config.waivers { - waiver.rule = crate::verdict::normalise_rule_id(&waiver.rule); + // ONLY FOR A CONSUMER THAT HAS ADOPTED THE GRAMMAR, which is the same + // exemption the grammar and collapse arms take and must be the same one. + // A tree declaring no vocabulary spells its ids however it likes — + // `no-todo` is that consumer's whole name for the row, not a spelling of a + // three-word one — and rewriting it to `no todo` would rename a row nobody + // asked to rename. Measured: 22 cases failed on exactly that. + if !config.vocabulary.is_empty() { + for rule in &mut config.rules { + rule.id = crate::verdict::normalise_rule_id(&rule.id); + } + for waiver in &mut config.waivers { + waiver.rule = crate::verdict::normalise_rule_id(&waiver.rule); + } } validate_tables(&config, text, source)?; Ok(config) @@ -3463,7 +3471,7 @@ pub fn defaults() -> Config { /// validator every committed rule passes. fn default_rules() -> Vec { vec![Rule { - id: "no-conflict-markers".to_owned(), + id: "source carry broken".to_owned(), kind: crate::rules::RuleKind::Forbid, // Every path, unlike the starter's `**/*/*`. That narrower glob exists // to keep the rule off the `batten.toml` that declares it — a `forbid` diff --git a/crates/batten/src/policy.rs b/crates/batten/src/policy.rs index aaa6afd7b..942679c03 100644 --- a/crates/batten/src/policy.rs +++ b/crates/batten/src/policy.rs @@ -1684,6 +1684,21 @@ fn check_collapse( // config disagrees with the modules in one place or systematically. let mut findings: Vec = Vec::new(); for rule in rules { + // A ROW OUTSIDE THE GRAMMAR IS NOT IN THIS CONVERSATION. + // + // The collapse rule decides WHICH of two grammar names a row carries; + // a row whose id is not a three-word name carries only one, so there is + // nothing to collapse and nothing to disambiguate. This is not a hole a + // consumer can hide in: `config::validate_tables` refuses a non-grammar + // id outright once they declare a vocabulary, so adopting the grammar is + // what brings a row into scope here, and the two arms compose. + // + // Measured: without this, 49 cases across fifteen suites failed — every + // fixture that declares a `policy` row raising one class, none of which + // has adopted the vocabulary and none of which the rule is about. + if rule.id.split(' ').count() != 3 { + continue; + } let empty = BTreeSet::new(); let classes = per_rule.get(&rule.id).unwrap_or(&empty); let sole = match (classes.len(), classes.iter().next()) { diff --git a/crates/batten/src/resolve.rs b/crates/batten/src/resolve.rs index 57bf2754c..05f5a4c85 100644 --- a/crates/batten/src/resolve.rs +++ b/crates/batten/src/resolve.rs @@ -2684,7 +2684,7 @@ mod tests { "resolve-zero-config-local-redefine", "version = 1\n", Some( - "version = 1\n\n[[rule]]\nid = \"no-conflict-markers\"\nkind = \"forbid\"\n\ + "version = 1\n\n[[rule]]\nid = \"source carry broken\"\nkind = \"forbid\"\n\ glob = \"nothing/**\"\npattern = \"x\"\nseverity = \"deny\"\n", ), ); diff --git a/crates/batten/src/starter.toml b/crates/batten/src/starter.toml index a9df49a60..3e4ede37d 100644 --- a/crates/batten/src/starter.toml +++ b/crates/batten/src/starter.toml @@ -83,7 +83,7 @@ redirect = "append, or write through the surface that owns the file" # that declares the rule. Narrow it to your source tree (`src/**`, `lib/**`) once # you know the layout; a glob is include-only, so `!` does not work here. [[rule]] -id = "no-conflict-markers" +id = "source carry broken" kind = "forbid" glob = "**/*/*" pattern = "<<<<<<< " diff --git a/crates/batten/tests/it/acquisition_sweep.rs b/crates/batten/tests/it/acquisition_sweep.rs index 599962c38..eeb387fc4 100644 --- a/crates/batten/tests/it/acquisition_sweep.rs +++ b/crates/batten/tests/it/acquisition_sweep.rs @@ -34,14 +34,14 @@ //! //! `bench/acquisition/sweep.py` is retired here under CLOUD-1229. It was 327 lines //! of Python driven by a one-line task, and its own header argued the shape was -//! forced by `shell-retirement` refusing an added shell rule. A second author read +//! forced by `shell retire partial` refusing an added shell rule. A second author read //! that argument and added a third helper for the identical stated reason //! (CLOUD-1208). The campaign's subject is authored SHELL because that is what it //! was built to retire — a statement about its reach, never a licence for what //! sits beside it. //! //! It carries **no** `// changed:` marker, and that absence is the point rather -//! than an omission. Those arms are `shell-retirement`'s and `[rule.conserves]`'s +//! than an omission. Those arms are `shell retire partial`'s and `[rule.conserves]`'s //! ledger over a governed file's death, and the deleted path was governed by //! neither — not under `mise-tasks/`, not a `.bats` suite, watched by nothing. //! Writing an arm for it would put a row in a ledger whose subject it never was. diff --git a/crates/batten/tests/it/admission.rs b/crates/batten/tests/it/admission.rs index 12c587581..65f77dbec 100644 --- a/crates/batten/tests/it/admission.rs +++ b/crates/batten/tests/it/admission.rs @@ -116,7 +116,7 @@ fn answers(reason: &str) -> BTreeMap { /// read `Spent`. Every caller below passes a distinct reason. fn binding(subject: &str, anchor: &str, epoch: &str, reason: &str) -> Binding { Binding { - rule: "prose-only".to_owned(), + rule: "diff ship early".to_owned(), verdict: "diff ship early".to_owned(), subject: subject.to_owned(), anchor: batten::admission::Anchor::Call { @@ -132,7 +132,7 @@ fn binding(subject: &str, anchor: &str, epoch: &str, reason: &str) -> Binding { /// The situation the binding above was minted for. fn situation<'a>(subject: &'a str, anchor: &'a str, epoch: &'a str) -> Situation<'a> { Situation { - rule: "prose-only", + rule: "diff ship early", verdict: "diff ship early", subject, anchor, @@ -329,7 +329,7 @@ fn a_cycle_cannot_be_constructed_without_breaking_an_address() { } let situation = Situation { - rule: "prose-only", + rule: "diff ship early", verdict: "diff ship early", subject: "a.rs", anchor: "call:head1", @@ -396,7 +396,7 @@ fn two_concurrent_consumes_resolve_to_exactly_one_winner() { &root, &issued, &Situation { - rule: "prose-only", + rule: "diff ship early", verdict: "diff ship early", subject: "a.rs", anchor: "call:head1", @@ -462,7 +462,7 @@ fn an_unanswered_question_yields_no_admission_and_prints_what_to_answer() { "override", "request", "--rule", - "prose-only", + "diff ship early", "--verdict", "diff ship early", "--subject", @@ -530,7 +530,7 @@ fn an_undeclared_class_is_refused_naming_the_registry_size() { "override", "request", "--rule", - "prose-only", + "diff ship early", "--verdict", "no such class", "--subject", @@ -562,7 +562,7 @@ fn a_correctly_answered_override_completes_end_to_end() { "override", "request", "--rule", - "prose-only", + "diff ship early", "--verdict", "diff ship early", "--subject", @@ -635,7 +635,7 @@ fn issued_through_the_verb(root: &Path, subject: &str) -> String { "override", "request", "--rule", - "prose-only", + "diff ship early", "--verdict", "diff ship early", "--subject", @@ -670,7 +670,7 @@ fn the_verb_spends_a_legitimate_admission_and_reports_it() { "--admission", &admission, "--rule", - "prose-only", + "diff ship early", "--verdict", "diff ship early", "--subject", @@ -734,7 +734,7 @@ fn the_verb_refuses_a_replay_with_the_policy_code() { "--admission", admission.as_str(), "--rule", - "prose-only", + "diff ship early", "--verdict", "diff ship early", "--subject", @@ -775,7 +775,7 @@ fn the_verb_refuses_an_admission_presented_for_another_subject() { "--admission", &admission, "--rule", - "prose-only", + "diff ship early", "--verdict", "diff ship early", "--subject", @@ -1247,7 +1247,7 @@ fn admits_fixture_with_predicate(name: &str) -> PathBuf { /// /// Measured before the fix, on this repository: two admissions for /// `filed-over-own-diff` and `filed-and-left-open` — both predicates of the -/// `filed-here` row — were issued, spent, committed, and honoured by neither +/// `issue file other` row — were issued, spent, committed, and honoured by neither /// gate. `batten-check` reported both findings unchanged afterwards. /// /// # Why the existing cases could not see it diff --git a/crates/batten/tests/it/admission_narrowing.rs b/crates/batten/tests/it/admission_narrowing.rs index fcb64d342..1c0af85f0 100644 --- a/crates/batten/tests/it/admission_narrowing.rs +++ b/crates/batten/tests/it/admission_narrowing.rs @@ -5,7 +5,7 @@ //! //! `admission_anchor` re-runs the rule a refusal named so it can recover that //! finding's fingerprint and bind the admission to it. `--rule` carries a -//! PREDICATE id — `filed-here` publishes `filed-over-own-diff` — so filtering +//! PREDICATE id — `issue file other` publishes `filed-over-own-diff` — so filtering //! `declared.id == rule` selected nothing, the scan produced no finding, and the //! mint silently took the `head()` fallback: an admission answered, spent, and //! queried by nothing (CLOUD-1087, CLOUD-1125). @@ -214,7 +214,7 @@ fn the_committed_bundles_publish_no_engine_side_rule_name() { assert!( policy::publishers_of(&bundles, "filed-over-own-diff") .into_iter() - .eq(["filed-here"]), + .eq(["issue file other"]), "the committed tree still publishes a predicate under a differently-named \ row — the shape the whole narrowing exists to handle" ); diff --git a/crates/batten/tests/it/agent_facts.rs b/crates/batten/tests/it/agent_facts.rs index e48de92b4..fb19cdeab 100644 --- a/crates/batten/tests/it/agent_facts.rs +++ b/crates/batten/tests/it/agent_facts.rs @@ -371,8 +371,8 @@ fn the_record_is_keyed_on_the_fact_and_on_its_rows_declared_subject() { // here: a record minted under one subject is simply absent under another, and // `facts::sourced` already turns absence into could-not-look. assert_ne!( - facts::sourced_path(git, "review-answered", "0f1e2d3"), - facts::sourced_path(git, "review-answered", "claude/some-branch") + facts::sourced_path(git, "review judge missing", "0f1e2d3"), + facts::sourced_path(git, "review judge missing", "claude/some-branch") ); // Neither component may escape the directory: a fact name may carry a `/` and // a branch name routinely does. @@ -559,7 +559,7 @@ fn no_byte_of_a_mismatched_buffer_is_available_to_the_verdict() { /// A row that counts `counts`'s elements satisfying `clauses`. fn counting(counts: &str, clauses: &[(&str, facts::Literal)], returns: Returns) -> facts::Declared { facts::Declared { - name: "review-answered".to_owned(), + name: "review judge missing".to_owned(), command: None, tool: Some("pull_request_read".to_owned()), counts: Some(counts.to_owned()), @@ -799,7 +799,7 @@ fn a_named_path_beside_a_json_array_contract_is_refused_at_load() { panic!("a named `counts` path beside `json-array` must not load"); }; let rendered = error.to_string(); - assert!(rendered.contains("review-answered"), "got: {rendered}"); + assert!(rendered.contains("review judge missing"), "got: {rendered}"); assert!(rendered.contains("json-array"), "got: {rendered}"); // THE DISCRIMINATING HALF, and it is what keeps this from being a ban on // `json-array` beside `counts` altogether: the root spelling is the one path a @@ -916,7 +916,7 @@ fn counts_beside_an_opaque_contract_is_refused_at_load() { panic!("`counts` beside `opaque` must not load"); }; let rendered = error.to_string(); - assert!(rendered.contains("review-answered"), "got: {rendered}"); + assert!(rendered.contains("review judge missing"), "got: {rendered}"); assert!(rendered.contains("opaque"), "got: {rendered}"); // And the shapes it CAN carry load, or the conjunct would be a ban on // counting rather than on the contradiction. `json-array` takes the root @@ -1072,7 +1072,7 @@ fn blocking_without_counts_is_refused_at_load() { panic!("`blocking` with no `counts` must not load"); }; let rendered = error.to_string(); - assert!(rendered.contains("review-answered"), "got: {rendered}"); + assert!(rendered.contains("review judge missing"), "got: {rendered}"); assert!(rendered.contains("blocking"), "got: {rendered}"); } diff --git a/crates/batten/tests/it/agentic_record.rs b/crates/batten/tests/it/agentic_record.rs index ac6a5d753..e72eee61d 100644 --- a/crates/batten/tests/it/agentic_record.rs +++ b/crates/batten/tests/it/agentic_record.rs @@ -101,7 +101,7 @@ fn config() -> String { r#"version = 1 [[rule]] -id = "agentic-experiment-record" +id = "fact file missing" kind = "policy" scope = "tree" documents = ["{TRIALS}", "{METHOD}"] diff --git a/crates/batten/tests/it/ambient_authority.rs b/crates/batten/tests/it/ambient_authority.rs index 5d5139dce..3ae687b38 100644 --- a/crates/batten/tests/it/ambient_authority.rs +++ b/crates/batten/tests/it/ambient_authority.rs @@ -51,7 +51,7 @@ use common::{Fixture, at_root, rust_sources, stderr}; /// are **compile errors** rather than lint findings — stronger than the /// `clippy.toml` rows that name them, which is why those rows now carry /// `allow-invalid` with that reason recorded. -/// * `perf-assert` holds the mediated path to CLOUD-689's ceiling, which is what +/// * `path measure wrong` holds the mediated path to CLOUD-689's ceiling, which is what /// a runtime on that path would break and what a manifest scan never measured. /// /// The other clients stay listed. Nothing in this tree may reach the network diff --git a/crates/batten/tests/it/attribution_provenance.rs b/crates/batten/tests/it/attribution_provenance.rs index a9cff2e6b..5494fd624 100644 --- a/crates/batten/tests/it/attribution_provenance.rs +++ b/crates/batten/tests/it/attribution_provenance.rs @@ -29,7 +29,7 @@ //! //! # The declared mutation, and why the row is in THIS file //! -//! `obligations-bound` reads the declared file's own lines for a row beginning +//! `test name undefined` reads the declared file's own lines for a row beginning //! `#MUTANT |`, and its `line_sources` covers `crates/batten/tests/**` and //! not `crates/batten/src/**` — so the row lives here even though the expression //! it applies belongs to `decision.rs`'s degradation. A block comment because diff --git a/crates/batten/tests/it/bats_invocation.rs b/crates/batten/tests/it/bats_invocation.rs index 1b9f0443b..b57650131 100644 --- a/crates/batten/tests/it/bats_invocation.rs +++ b/crates/batten/tests/it/bats_invocation.rs @@ -5,9 +5,9 @@ //! //! The successor to `tests/test-bats-parallel.bats`, whose subject is //! `mise.toml`'s `[tasks."test:bats"]`. The suite was an authored bats file, so -//! `shell-retirement`'s arm B refused maintaining it in place; the predicate +//! `shell retire partial`'s arm B refused maintaining it in place; the predicate //! moved into the module and the classification of the task body moved into fact -//! acquisition, which is the same split `command-task-defined` already makes. +//! acquisition, which is the same split `task bind undefined` already makes. //! //! # Why this tier and not the module's own rules //! @@ -51,7 +51,7 @@ // carried: "the parallel backend is named explicitly rather than left to bats' default probe" policy/bats-invocation.rego // carried: "the parallel backend is a pinned tool, so the fast path cannot depend on the host" policy/bats-invocation.rego // carried: "CI installs the parallel backend — an absent rush is a missing TOOL, not a slow suite" policy/bats-invocation.rego -// changed: "the test:bats invocation was found at all — this suite is not passing vacuously" crates/batten/tests/it/bats_invocation.rs the suite asserted its own subject exists, which a module cannot: a tree with no `test:bats` task is not-applicable rather than in violation, or the row fires on every fixture that copies this config (`command-task-defined` measured seven such findings). The property survives as `this_repository_is_clean_today` plus `a_tree_with_no_such_task_is_not_judged`, which together say the same thing about THIS tree without claiming it about every tree +// changed: "the test:bats invocation was found at all — this suite is not passing vacuously" crates/batten/tests/it/bats_invocation.rs the suite asserted its own subject exists, which a module cannot: a tree with no `test:bats` task is not-applicable rather than in violation, or the row fires on every fixture that copies this config (`task bind undefined` measured seven such findings). The property survives as `this_repository_is_clean_today` plus `a_tree_with_no_such_task_is_not_judged`, which together say the same thing about THIS tree without claiming it about every tree // CLOUD-1268's fifth arm, and the first ledger block in this tree to use it. The // four above describe a subject that went with its suite; `tests/helpers.bash` is @@ -84,7 +84,7 @@ use batten::rules::{self, Rule}; /// the same column census a consumer's config does. fn row() -> Rule { serde_json::from_value(serde_json::json!({ - "id": "bats-invocation", + "id": "bats run wrong", "kind": "policy", "scope": "tree", "sources": ["mise.toml", ".github/workflows/ci.yml"], @@ -351,7 +351,7 @@ fn a_sweep_that_names_no_hardware_is_refused() { #[test] fn a_tree_with_no_such_task_is_not_judged() { - // `command-task-defined`'s measured lesson, one row over: an unguarded module + // `task bind undefined`'s measured lesson, one row over: an unguarded module // reported seven findings against a fixture that copies this config without a // task namespace, including against a case named "this repository is clean // today". diff --git a/crates/batten/tests/it/board_receipts.rs b/crates/batten/tests/it/board_receipts.rs index fd058e6d6..9e96add5d 100644 --- a/crates/batten/tests/it/board_receipts.rs +++ b/crates/batten/tests/it/board_receipts.rs @@ -71,7 +71,7 @@ //! translation is stated rather than assumed — the shell body denies by printing a //! decision document and exiting 0, the engine denies with exit 2 (§7). //! -// replay-call: tests/issue-search-guard.bats 8e0acf1 mise-tasks/issue-search-guard.sh filing-needs-a-search deny=2 allow=0 +// replay-call: tests/issue-search-guard.bats 8e0acf1 mise-tasks/issue-search-guard.sh issue list unread deny=2 allow=0 //! //! ─── CLOUD-908's MAPPING, row 2 ────────────────────────────────────────────── //! @@ -116,7 +116,7 @@ //! //! ─── CLOUD-909's REPLAY, row 2 ─────────────────────────────────────────────── //! -// replay-call: tests/issue-read-guard.bats 1dbad05 mise-tasks/issue-read-guard.sh an-update-owes-a-recent-read deny=2 allow=0 +// replay-call: tests/issue-read-guard.bats 1dbad05 mise-tasks/issue-read-guard.sh issue read stale deny=2 allow=0 //! //! ─── CLOUD-908's MAPPING, the two MINTERS (CLOUD-1024) ─────────────────────── //! @@ -214,7 +214,7 @@ //! //! ─── CLOUD-909's REPLAY, row 3 ─────────────────────────────────────────────── //! -// replay-call: tests/board-move-guard.bats 66d9d8f mise-tasks/board-move-guard.sh a-move-to-in-review-owes-an-adjudication deny=2 allow=0 +// replay-call: tests/board-move-guard.bats 66d9d8f mise-tasks/board-move-guard.sh review judge unread deny=2 allow=0 // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] @@ -238,7 +238,7 @@ fn repo(name: &str) -> PathBuf { // name, and staged BEFORE the commit so they are tracked like the config is. // // Enumerated because naming them would put a consumer's policy filenames in - // `crates/**`, which is non-negotiable rule 1 — and `no-consumer-repo-name` + // `crates/**`, which is non-negotiable rule 1 — and `source name other` // computes that rather than trusting a reader to notice, which is how the // first draft of this file was caught. It is also the more robust half: a // module added to `policy/` needs no edit here, where a list would silently @@ -339,7 +339,7 @@ fn filing_without_a_search_is_refused_and_with_one_is_allowed() { "the refusal must name the receipt that is absent: {text}" ); assert!( - text.contains("filing-needs-a-search"), + text.contains("issue list unread"), "and the row that refused, so a reader can find it in the config: {text}" ); @@ -376,18 +376,18 @@ fn an_update_is_not_row_ones_business() { // THE REFUSING ROW IS THE ONLY ROW ID ON THE LINE, which is what CLOUD-1286 // changed here and it changed it for the better. This case used to have to // read attribution off the `Refused by ` PREFIX, because a bare - // `contains("filing-needs-a-search")` matched row 2's own reason — which + // `contains("issue list unread")` matched row 2's own reason — which // ENDS by naming row 1, "Creating an issue is never gated by this row (that - // is `filing-needs-a-search`)". That cross-reference is prose, so it now + // is `issue list unread`)". That cross-reference is prose, so it now // lives behind `batten policy explain` with the rest of it, and the id on // the emitted line is the engine's own attribution and nothing else. The // negative assertion is what keeps that claim honest. assert!( - !text.contains("filing-needs-a-search"), + !text.contains("issue list unread"), "an update names an id, so the row that gates FILING must stay silent: {text}" ); assert!( - text.contains("an-update-owes-a-recent-read"), + text.contains("issue read stale"), "and the row that does answer an edit is the one that spoke: {text}" ); } @@ -557,7 +557,7 @@ fn an_update_with_no_receipt_is_refused() { ); let text = stderr(&refusal); assert!( - text.contains("an-update-owes-a-recent-read"), + text.contains("issue read stale"), "the row that refused, so a reader can find it in the config: {text}" ); // The CALL that mints the receipt is the class's declared route and is one @@ -628,7 +628,7 @@ fn a_read_older_than_the_bound_is_refused() { ); let text = stderr(&refusal); assert!( - text.contains("an-update-owes-a-recent-read"), + text.contains("issue read stale"), "the row that refused: {text}" ); assert!( @@ -685,7 +685,7 @@ fn a_supplied_instant_decides_recency_rather_than_the_clock() { ); let text = stderr(&refusal); assert!( - text.contains("an-update-owes-a-recent-read"), + text.contains("issue read stale"), "the row that refused: {text}" ); } @@ -935,7 +935,7 @@ fn a_create_is_not_row_twos_business() { "a create that asked its question is allowed, with no read receipt anywhere" ); assert!( - !text.contains("Refused by an-update-owes-a-recent-read"), + !text.contains("Refused by issue read stale"), "the row that gates EDITING must stay silent on a filing: {text}" ); } @@ -1105,7 +1105,7 @@ fn a_move_with_no_adjudication_is_refused() { ); let text = stderr(&refusal); assert!( - text.contains("a-move-to-in-review-owes-an-adjudication"), + text.contains("review judge unread"), "the row that refused: {text}" ); // The check whose receipt is missing is the pointer and stays inline; the @@ -1156,7 +1156,7 @@ fn an_adjudication_past_the_bound_is_refused() { ); let text = stderr(&refusal); assert!( - text.contains("a-move-to-in-review-owes-an-adjudication"), + text.contains("review judge unread"), "the row that refused: {text}" ); // The bound the age was measured against travels as a pointer, because it @@ -1318,7 +1318,7 @@ fn row_threes_selectors_are_the_guards() { ), ); assert!( - !stderr(&output).contains("Refused by a-move-to-in-review-owes-an-adjudication"), + !stderr(&output).contains("Refused by review judge unread"), "a create names no row to move, so this row must stay silent: {}", stderr(&output) ); diff --git a/crates/batten/tests/it/board_record.rs b/crates/batten/tests/it/board_record.rs index 5b477ae69..9e8ad542d 100644 --- a/crates/batten/tests/it/board_record.rs +++ b/crates/batten/tests/it/board_record.rs @@ -18,7 +18,7 @@ //! reads, the column arithmetic, and the create/groom boundary. The two real //! programs keep their own suites. //! -//! # RETIREMENT LEDGER, PER PATH — what `shell-retirement` reads +//! # RETIREMENT LEDGER, PER PATH — what `shell retire partial` reads //! //! The successor is `recorder.rs` rather than a module: a recorder decides //! nothing, so there is no predicate for a `.rego` file to hold. It is a policy diff --git a/crates/batten/tests/it/bypass_precondition.rs b/crates/batten/tests/it/bypass_precondition.rs index f68298c97..570aa3687 100644 --- a/crates/batten/tests/it/bypass_precondition.rs +++ b/crates/batten/tests/it/bypass_precondition.rs @@ -39,7 +39,7 @@ use crate::common; use std::path::PathBuf; /// A class that declares an `override` route with a precondition, and is raised -/// on the mediated boundary. `batten.toml`'s `leased-push` row. +/// on the mediated boundary. `batten.toml`'s `branch write unsafe` row. const PRECONDITIONED: &str = "branch write unsafe"; /// The same, spelled as a command this repository refuses. const PRECONDITIONED_CALL: &str = "git push --force-with-lease origin main"; diff --git a/crates/batten/tests/it/cfg_gated_test.rs b/crates/batten/tests/it/cfg_gated_test.rs index 49b7d5eb1..d77f7a14c 100644 --- a/crates/batten/tests/it/cfg_gated_test.rs +++ b/crates/batten/tests/it/cfg_gated_test.rs @@ -38,7 +38,7 @@ use std::path::{Path, PathBuf}; use batten::rules::{self, Rule}; /// The predicate id the module declares — NOT the `[[rule]]` id, which is -/// `cfg-gated-test`. The two differ, and the difference is load-bearing: an +/// `test cover missing`. The two differ, and the difference is load-bearing: an /// admission resolves its anchor by the FINDING's rule, so minting against the /// config id silently produces a `call:` anchor that suppresses nothing. const GATED_ADDED: &str = "platform-gated-test-added"; @@ -58,7 +58,7 @@ const GATED_ADDED: &str = "platform-gated-test-added"; /// /// NO HAND-ROLLED `git init` — [`common::init_repo`] copies the one template the /// whole suite shares. CLOUD-1419 measured 79 forked inits producing 1,819 git -/// processes over one traced run, and `fixture-forks` refused this helper's first +/// processes over one traced run, and `test fix duplicate` refused this helper's first /// spelling at the line that wrote it. fn repo(name: &str, before: &[&str], after: &[&str]) -> PathBuf { let root = common::scratch(name); @@ -99,7 +99,7 @@ fn install_module(root: &Path) { /// no lines and refuses nothing. fn row() -> Rule { serde_json::from_value(serde_json::json!({ - "id": "cfg-gated-test", + "id": "test cover missing", "kind": "policy", "scope": "tree", "base": "origin/main", diff --git a/crates/batten/tests/it/ci_cache_declared.rs b/crates/batten/tests/it/ci_cache_declared.rs index 40d18af22..08a87cb5b 100644 --- a/crates/batten/tests/it/ci_cache_declared.rs +++ b/crates/batten/tests/it/ci_cache_declared.rs @@ -57,7 +57,7 @@ use batten::rules::{self, Rule}; /// the same column census a consumer's config does. fn row() -> Rule { serde_json::from_value(serde_json::json!({ - "id": "ci-cache-declared", + "id": "job carry missing", "kind": "policy", "scope": "tree", "sources": [".github/workflows/*.yml", "mise.toml"], diff --git a/crates/batten/tests/it/ci_hygiene.rs b/crates/batten/tests/it/ci_hygiene.rs index 9e62d7d87..b42d7eac8 100644 --- a/crates/batten/tests/it/ci_hygiene.rs +++ b/crates/batten/tests/it/ci_hygiene.rs @@ -45,7 +45,7 @@ use batten::rules::{self, Rule}; /// the same column census a consumer's config does. fn row() -> Rule { serde_json::from_value(serde_json::json!({ - "id": "ci-hygiene", + "id": "job spelling wrong", "kind": "policy", "scope": "tree", "preset": "ci-hygiene", diff --git a/crates/batten/tests/it/ci_parity.rs b/crates/batten/tests/it/ci_parity.rs index ea58be753..6b1fb911b 100644 --- a/crates/batten/tests/it/ci_parity.rs +++ b/crates/batten/tests/it/ci_parity.rs @@ -37,7 +37,7 @@ //! while that task carries no template. `task read unread` is the arm //! that surfaces the day they stop being. -//! # RETIREMENT LEDGER, PER PATH — what `shell-retirement` reads +//! # RETIREMENT LEDGER, PER PATH — what `shell retire partial` reads //! //! CLOUD-1161. `ci-local-parity` was 54.6s and 1093 lines holding 40 predicates. //! The generic half is the `ci-hygiene` preset, the consumer half is @@ -187,7 +187,7 @@ use batten::rules::{self, Rule}; /// the same column census a consumer's config does. fn row() -> Rule { serde_json::from_value(serde_json::json!({ - "id": "ci-parity", + "id": "job check other", "kind": "policy", "scope": "tree", "sources": [ diff --git a/crates/batten/tests/it/ci_suite_lane.rs b/crates/batten/tests/it/ci_suite_lane.rs index 3eb5ef554..8f836ad51 100644 --- a/crates/batten/tests/it/ci_suite_lane.rs +++ b/crates/batten/tests/it/ci_suite_lane.rs @@ -28,7 +28,7 @@ //! half a plan cannot see and the reason the two are not one gate. //! //! Whether the surviving job installs what the suite needs is -//! `bats-invocation`'s, whose `install_args` clause derives the job from the +//! `bats run wrong`'s, whose `install_args` clause derives the job from the //! same `mise run test:bats` reading this file exercises. //! //! # The measurement behind the row @@ -57,7 +57,7 @@ use batten::rules::{self, Rule}; /// the same column census a consumer's config does. fn row() -> Rule { serde_json::from_value(serde_json::json!({ - "id": "ci-suite-lane", + "id": "job select missing", "kind": "policy", "scope": "tree", "sources": [".github/workflows/ci.yml"], diff --git a/crates/batten/tests/it/claim.rs b/crates/batten/tests/it/claim.rs index 33bccd1a2..232619414 100644 --- a/crates/batten/tests/it/claim.rs +++ b/crates/batten/tests/it/claim.rs @@ -31,7 +31,7 @@ //! identical in every one — the same payloads are refused, the same gaps are //! gaps — and stating the remapping once here beats burying it in 76 entries. //! -//! # RETIREMENT LEDGER, PER PATH — what `shell-retirement` reads +//! # RETIREMENT LEDGER, PER PATH — what `shell retire partial` reads //! // carried: mise-tasks/claim-check.sh crates/batten/src/claim.rs kind:verb crates/batten/tests/it/claim.rs // carried: tests/claim-check.bats crates/batten/src/claim.rs kind:verb crates/batten/tests/it/claim.rs diff --git a/crates/batten/tests/it/claim_order.rs b/crates/batten/tests/it/claim_order.rs index 151ca2f88..f968d6b4d 100644 --- a/crates/batten/tests/it/claim_order.rs +++ b/crates/batten/tests/it/claim_order.rs @@ -44,7 +44,7 @@ use batten::rules::{self, Rule}; /// the same column census a consumer's config does. fn row() -> Rule { serde_json::from_value(serde_json::json!({ - "id": "claim-order-is-stated", + "id": "claim declare dropped", "kind": "policy", "scope": "tree", "line_sources": ["AGENTS.md", "rules/toolchain.md"], diff --git a/crates/batten/tests/it/claim_receipt.rs b/crates/batten/tests/it/claim_receipt.rs index a1c456b1b..b871058f6 100644 --- a/crates/batten/tests/it/claim_receipt.rs +++ b/crates/batten/tests/it/claim_receipt.rs @@ -38,7 +38,7 @@ use common::{Fixture, git_in, run, run_with_stdin, stderr, stdout, write}; const POLICY: &str = r#"version = 1 [[rule]] -id = "claim-needs-receipt" +id = "claim read unread" kind = "receipt" scope = "mediated_call" severity = "deny" @@ -151,7 +151,7 @@ fn the_refusal_names_the_check_and_the_keying() { &write_payload("src/tracked.rs"), )); assert!( - refusal.contains("claim-needs-receipt"), + refusal.contains("claim read unread"), "names the rule: {refusal}" ); assert!(refusal.contains("branch"), "names the keying: {refusal}"); @@ -468,7 +468,7 @@ fn a_detached_head_cannot_answer_and_says_so_rather_than_refusing() { const ALTERNATION: &str = r#"version = 1 [[rule]] -id = "claim-needs-receipt" +id = "claim read unread" kind = "receipt" scope = "mediated_call" severity = "deny" diff --git a/crates/batten/tests/it/cli.rs b/crates/batten/tests/it/cli.rs index 209e1f9fd..3a5bee9d2 100644 --- a/crates/batten/tests/it/cli.rs +++ b/crates/batten/tests/it/cli.rs @@ -34,7 +34,7 @@ use common::{ /// no-authority case". It has none, and `hook` resolves its authority upward to /// the git root anyway, so every case below adjudicated against THIS /// repository's own committed policy. Measured 2026-08-29: driven from -/// `crates/batten/`, `gh pr checks 714` came back refused by `gh-pr-checks`, a +/// `crates/batten/`, `gh pr checks 714` came back refused by `check watch loose`, a /// row that exists only in the repository-root `batten.toml`. /// /// What that cost is the failure this file already names one helper down — "a @@ -62,7 +62,7 @@ fn run_hook(name: &str, harness: &str, payload: &str, bypass: bool) -> Output { /// the same defect the decision matrix's own totality test exists to prevent. const GH_POLICY_CONFIG: &str = r#"version = 1 [[rule]] -id = "gh-pr-merge" +id = "commit ship other" kind = "shape" scope = "mediated_call" severity = "deny" @@ -70,7 +70,7 @@ pattern = "gh pr merge" reason = "use `mise run land`" [[rule]] -id = "gh-pr-comment-fast-forward" +id = "review ship early" kind = "shape" scope = "mediated_call" severity = "deny" @@ -79,7 +79,7 @@ contains = "fast-forward" reason = "use `mise run land`" [[rule]] -id = "gh-pr-checks" +id = "check watch loose" kind = "shape" scope = "mediated_call" severity = "deny" @@ -87,7 +87,7 @@ pattern = "gh pr checks" reason = "use `mise run ci-wait`" [[rule]] -id = "gh-run-watch" +id = "job watch loose" kind = "shape" scope = "mediated_call" severity = "deny" @@ -300,7 +300,7 @@ fn committed_budget_surfaces(dir: &Path) { fs::create_dir_all(dir.join(".serena")).expect("create fixture serena dir"); fs::write(dir.join(".serena/project.yml"), "initial_prompt: ''\n") .expect("write fixture project config"); - // The committed `perf-assert` row declares `README.md` as a LITERAL `lines` + // The committed `path measure wrong` row declares `README.md` as a LITERAL `lines` // entry (CLOUD-1321), so it is acquired whether or not this fixture has one // and an absent file reaches the module through `input.tree.missing`. That is // deliberate there — a glob would match nothing in a treeless fixture and the @@ -324,7 +324,7 @@ fn committed_budget_surfaces(dir: &Path) { committed_policy_modules(dir); } -/// Also seeds the scanner the committed `no-secrets` row resolves, and returns +/// Also seeds the scanner the committed `source carry unsafe` row resolves, and returns /// the `HOME` every invocation against this fixture must run under. /// /// Same argument as the `origin/main` ref above, one precondition further out: @@ -2041,12 +2041,24 @@ fn every_hook_policy_table_deny_names_its_fix() { // was always named after: `explain` answers about the CLASS and resolves // a rule id only as a fallback, and the two are different questions // wherever a class has more than one raiser. + // AND THE ID IS THREE WORDS, NOT ONE (CLOUD-1638). Taking the last + // whitespace token grabbed `other` out of `commit ship other`. The head + // is ` ` where the id is present only when it + // DIFFERS from the class, so: the last three words are the id on a + // discriminating row, and on a collapsed row the class token — the first + // three words — is the id, because that is what collapsing means. let head = stderr.split(" — ").next().unwrap_or(&stderr); - let row = head - .split_whitespace() - .next_back() - .expect("a deny names the rule that fired"); - let explained = batten_with(&dir, &["policy", "rule", row], &[]); + let words: Vec<&str> = head.split_whitespace().collect(); + assert!( + words.len() >= 3, + "a deny names the class that fired: {stderr}" + ); + let tail = words[words.len() - 3..].join(" "); + let class = words[..3].join(" "); + let mut explained = batten_with(&dir, &["policy", "rule", &tail], &[]); + if explained.status.code() != Some(0) { + explained = batten_with(&dir, &["policy", "rule", &class], &[]); + } assert_eq!( explained.status.code(), Some(0), @@ -2096,7 +2108,7 @@ fn the_in_band_hosts_carry_the_decision_in_their_document() { "{harness}: the document must carry the class, got: {reason}" ); assert!( - reason.contains("gh-pr-merge"), + reason.contains("commit ship other"), "{harness}: and the rule the hop takes, got: {reason}" ); } @@ -2613,7 +2625,10 @@ fn a_quoted_invocation_denies_on_both_harness_channels() { // The row's id on the line is what says a deny reached this channel // — CLOUD-1286 took the `Refused by` prefix off it, and this case // is about the CHANNEL rather than about the wording. - assert!(stderr.contains("gh-pr-merge"), "{harness}: got {stderr}"); + assert!( + stderr.contains("commit ship other"), + "{harness}: got {stderr}" + ); } } } @@ -3108,22 +3123,22 @@ struct ShapeCase { const SHAPE_CENSUS: &[ShapeCase] = &[ ShapeCase { call: CensusCall::Command("gh pr merge 42"), - rule: "gh-pr-merge", + rule: "commit ship other", site: CensusSite::Checkout, }, ShapeCase { call: CensusCall::Command("gh pr comment 7 --body /fast-forward"), - rule: "gh-pr-comment-fast-forward", + rule: "review ship early", site: CensusSite::Checkout, }, ShapeCase { call: CensusCall::Command("gh pr checks --watch"), - rule: "gh-pr-checks", + rule: "check watch loose", site: CensusSite::Checkout, }, ShapeCase { call: CensusCall::Command("gh run watch 123"), - rule: "gh-run-watch", + rule: "job watch loose", site: CensusSite::Checkout, }, // The landing loop's own hand-stepping (CLOUD-1461), which belongs with the @@ -3146,46 +3161,46 @@ const SHAPE_CENSUS: &[ShapeCase] = &[ // allows, and that file is what stops the row becoming a blanket refusal. ShapeCase { call: CensusCall::Command("git rebase origin/main"), - rule: "rebase-not-hand-stepped", + rule: "patch run loose", site: CensusSite::Checkout, }, ShapeCase { call: CensusCall::Command("cargo test -p batten"), - rule: "no-bare-cargo", + rule: "cargo run loose", site: CensusSite::Checkout, }, ShapeCase { // The wrapper does not launder it: `effective_program` steps past `env` // to reach `cargo`, and the mediator is read from what it stepped over. call: CensusCall::Command("env RUSTFLAGS=-Awarnings cargo build"), - rule: "no-bare-cargo", + rule: "cargo run loose", site: CensusSite::Checkout, }, ShapeCase { call: CensusCall::Command("gh pr create --title 'no key here'"), - rule: "pr-names-an-issue", + rule: "review name unnamed", site: CensusSite::Keyless, }, ShapeCase { call: CensusCall::Command("gh pr ready 42"), - rule: "ready-names-an-issue", + rule: "review open unnamed", site: CensusSite::Keyless, }, // CLOUD-312 row 4. Decided by the tool name alone, under the readable server // spelling — the UUID and bare-name spellings are `connector_verbs.rs`'s. ShapeCase { call: CensusCall::Verb("mcp__Claude_Code_Remote__subscribe_pr_activity"), - rule: "no-pr-activity-subscription", + rule: "review watch refused", site: CensusSite::Checkout, }, ShapeCase { call: CensusCall::Verb("mcp__Claude_Code_Remote__send_later"), - rule: "no-scheduled-self-wakeup", + rule: "timer mint refused", site: CensusSite::Checkout, }, ShapeCase { call: CensusCall::Verb("mcp__Claude_Code_Remote__create_trigger"), - rule: "no-scheduled-trigger", + rule: "event mint refused", site: CensusSite::Checkout, }, // CLOUD-1264. The raw tracker read, under the readable server spelling — the @@ -3193,7 +3208,7 @@ const SHAPE_CENSUS: &[ShapeCase] = &[ // `raw_tracker_read.rs`'s. ShapeCase { call: CensusCall::Verb("mcp__Linear__get_issue"), - rule: "no-raw-issue-read", + rule: "issue read loose", site: CensusSite::Checkout, }, // CLOUD-312 row 6. Four named artifacts against a ceiling of three, over the @@ -3204,7 +3219,7 @@ const SHAPE_CENSUS: &[ShapeCase] = &[ prompt: "read one.txt two.txt three.txt four.txt then act", repeat: 1, }, - rule: "a-spawn-names-few-artifacts", + rule: "spawn count wrong", site: CensusSite::Manifest, }, // The token ceiling reads only the envelope, so no fact about the tree can @@ -3217,7 +3232,7 @@ const SHAPE_CENSUS: &[ShapeCase] = &[ prompt: "x", repeat: 6100, }, - rule: "a-spawn-prompt-stays-in-budget", + rule: "prompt measure wrong", site: CensusSite::Checkout, }, ]; @@ -3322,7 +3337,7 @@ fn render_gaps(config: &str, gaps: &[CensusGap]) -> String { /// before any rule runs — correctly, since a silently absent module is a gate /// that decides nothing. So a fixture missing them fails on that refusal rather /// than on the rule it is about, which is the same precondition the budget -/// surfaces and the `no-secrets` scanner already owe. +/// surfaces and the `source carry unsafe` scanner already owe. /// /// The DIRECTORY is mirrored rather than a list named here. The retirement /// campaign adds a module per migrated gate, and a hand-kept list would make @@ -3459,14 +3474,14 @@ fn the_committed_shape_rules_fire_on_every_banned_shape() { // And the reads it must not refuse, from the same committed rows. // // `gh pr ready` is absent from this list and did not simply become a deny: - // since CLOUD-312 it is *also* gated by the `ready-needs-receipts` row, so + // since CLOUD-312 it is *also* gated by the `check read unread` row, so // against this checkout its verdict depends on whether the tree carries // valid receipts — a property of the world, not of the commit. Its shape row // is censused at the keyless site above, where the shape rows are evaluated - // first and the refusal therefore names `ready-names-an-issue`; the receipt + // first and the refusal therefore names `review open unnamed`; the receipt // row's own case is the one below this. // `mise exec -- cargo test` and `mise run test:cargo` are here because - // `no-bare-cargo` is not a ban on the program (CLOUD-271): the row refuses + // `cargo run loose` is not a ban on the program (CLOUD-271): the row refuses // the ROUTE, and a row that closed the sanctioned route too would ban the // toolchain outright. `mise exec` is looked through, so this pair is the // only thing standing between `require_via` and exactly that. @@ -3536,8 +3551,8 @@ fn the_bare_cargo_refusal_names_the_sanctioned_route() { // this case makes is unchanged and still asserted end to end — a reader must // be able to reach "the program is fine, the route is not" rather than // reading the deny as "cargo is banned". - assert!(stderr.contains("no-bare-cargo"), "got: {stderr}"); - let explained = batten_with(&root, &["policy", "explain", "no-bare-cargo"], &[]); + assert!(stderr.contains("cargo run loose"), "got: {stderr}"); + let explained = batten_with(&root, &["policy", "explain", "cargo run loose"], &[]); assert_eq!(explained.status.code(), Some(0), "the row resolves"); let text = String::from_utf8_lossy(&explained.stdout); assert!(text.contains("mise exec -- cargo"), "got: {text}"); @@ -3700,11 +3715,11 @@ fn the_census_check_refuses_a_case_naming_no_row() { /// checkout happens to have run `verify`. /// /// SEVEN ROWS CAN REFUSE, AND ALL SEVEN ARE PRECONDITIONS — which is the -/// assertion, rather than a widening of it. `ready-needs-receipts` refuses until -/// `verify` has run; `ready-names-an-issue` is a `shape` row carrying +/// assertion, rather than a widening of it. `check read unread` refuses until +/// `verify` has run; `review open unnamed` is a `shape` row carrying /// `requires_key`, which the rules file describes as narrowing the deny "from /// *this command is banned* to *this command is banned unless the work is -/// keyed*"; `ready-needs-an-answered-review` (CLOUD-859) refuses until the +/// keyed*"; `review answer unread` (CLOUD-859) refuses until the /// declared review command has been run for this head. None is the outright ban /// this case exists to refuse, and which one fires first is a property of the /// checkout. @@ -3724,13 +3739,13 @@ fn the_census_check_refuses_a_case_naming_no_row() { /// was correct (CLOUD-661). The fix is to assert what the case means. /// /// AND THE SAME OMISSION RECURRED, which is why the list is the fragile part -/// rather than the wording. `ready-needs-an-answered-review` landed as a third +/// rather than the wording. `review answer unread` landed as a third /// precondition row and was not added here, so this case stayed green only while /// one of the older two ALSO refused. It goes red the moment a branch satisfies /// both — a `verify` receipt present and a key on the commits — which is the /// state every branch reaches just before it readies, and precisely the state /// this case is about. CI never saw it: a fresh checkout has no verify receipt, -/// so `ready-needs-receipts` fires first and masks the gap. Measured 2026-08-26. +/// so `check read unread` fires first and masks the gap. Measured 2026-08-26. /// A fourth precondition row will do this again; the durable form is to select /// the rows by KIND rather than to name them, which needs a surface this test /// does not have today. @@ -3746,9 +3761,9 @@ fn the_committed_policy_gates_ready_on_receipts_rather_than_banning_it() { // the `gh` lifecycle bans, which refuse the command outright. Some(2) => assert!( [ - "ready-needs-receipts", - "ready-names-an-issue", - "ready-needs-an-answered-review", + "check read unread", + "review open unnamed", + "review answer unread", // CLOUD-690's two tool-sourced siblings, each a receipt row over // one check, and the two module predicates that read what those // records found. The module rows belong here for the same reason @@ -3758,8 +3773,8 @@ fn the_committed_policy_gates_ready_on_receipts_rather_than_banning_it() { // the command. Which one fires first is a property of the // checkout — measured, a head with a record carrying unresolved // threads reaches the module rather than any receipt row. - "ready-needs-the-threads-answered", - "ready-needs-a-review-to-exist", + "review answer partial", + "review list unread", "review-unanswered", "review-absent", ] @@ -3793,7 +3808,7 @@ fn hook_denies_a_blocked_shape_in_the_harness_channel() { // CLOUD-1286: the redirect is one hop off the line, so what the channel must // carry is the row that refused — the handle that hop takes. assert!( - stdout.contains("gh-pr-merge"), + stdout.contains("commit ship other"), "the deny must name the row the fixture policy declares, got: {stdout}" ); } @@ -3840,7 +3855,7 @@ fn every_host_denies_the_same_call_through_its_own_channel() { // about the channel, and the hop itself is proven by // `every_hook_policy_table_deny_names_its_fix`. assert!( - stdout.contains("gh-pr-merge"), + stdout.contains("commit ship other"), "{harness}: the deny must name the row that refused, got: {stdout}" ); } @@ -3862,7 +3877,7 @@ fn every_host_denies_the_same_call_through_its_own_channel() { "{harness}: stray stdout on these hosts risks being read as an allow" ); assert!( - common::stderr(&output).contains("gh-pr-merge"), + common::stderr(&output).contains("commit ship other"), "{harness}: the decision travels on stderr here" ); } @@ -4137,7 +4152,7 @@ fn hook_fails_open_on_an_undecodable_payload() { fn hook_honours_the_bypass_hatch() { // THE AUTHORITY HAS TO REFUSE THIS CALL, or the case says nothing (CLOUD-1135). // It used to drive `run_hook`, which now loads an authority declaring no - // rules — an allow the bypass could not have caused. `gh-pr-merge` is a row + // rules — an allow the bypass could not have caused. `commit ship other` is a row // in the same fixture `hook_exit_code_harness_denies_with_exit_2` uses to // assert the deny this suppresses, so the two are the same call twice. let dir = repo_with_gh_policy("bypass-over-a-real-deny"); @@ -4156,7 +4171,7 @@ fn hook_exit_code_harness_denies_with_exit_2() { assert_eq!(output.status.code(), Some(2)); assert!(output.stdout.is_empty()); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("gh-pr-merge"), "got: {stderr}"); + assert!(stderr.contains("commit ship other"), "got: {stderr}"); // A verdict is an answer, not a crash. The host hands this text back to the // model as the deny reason, so it must not wear the binary's error prefix. assert!( @@ -6469,7 +6484,7 @@ fn the_committed_repo_config_gates_a_repository() { let dir = repo_with_config("config-committed", &contents); let home = committed_config_fixture_git(&dir); committed_budget_surfaces(&dir); - // A file the committed no-conflict-markers rule must flag. The marker is + // A file the committed source carry broken rule must flag. The marker is // still assembled at runtime, but for a narrower reason than before // (CLOUD-229): the rule now delegates to `hk util check-merge-conflict`, // which only fires on a marker at the START of a line, so the seven @@ -6497,7 +6512,7 @@ fn the_committed_repo_config_gates_a_repository() { let output = batten() .arg("enforce") .arg("--rule") - .arg("no-conflict-markers") + .arg("source carry broken") .current_dir(&dir) .state_home(&home) .env_remove("BATTEN_STRICTNESS") @@ -6511,7 +6526,7 @@ fn the_committed_repo_config_gates_a_repository() { ); assert_eq!( String::from_utf8_lossy(&output.stdout), - "crates/** no-conflict-markers\n", + "crates/** source carry broken\n", "a command condemns a batch, so the pointer is the glob and carries no line" ); } @@ -6568,7 +6583,7 @@ fn the_committed_delegating_rule_spawns_nothing_when_its_glob_misses() { fs::write(dir.join("notes.txt"), marker).expect("write out-of-glob source"); // `enforce --rule`, not a bare `enforce`. The property is about ONE row — - // `no-conflict-markers`, `kind = "command"`, `glob = "crates/**"` — and the + // `source carry broken`, `kind = "command"`, `glob = "crates/**"` — and the // narrowing is what makes the assertion say so. Unnarrowed, exit 0 and an // empty stdout also claimed that none of the other 103 rows fires on this // fixture, which is incidental to the property and cost 206s of a 1482s @@ -6581,7 +6596,7 @@ fn the_committed_delegating_rule_spawns_nothing_when_its_glob_misses() { let output = batten() .arg("enforce") .arg("--rule") - .arg("no-conflict-markers") + .arg("source carry broken") .current_dir(&dir) .state_home(&home) .env_remove("BATTEN_STRICTNESS") @@ -6607,10 +6622,10 @@ fn the_committed_delegating_rule_spawns_nothing_when_its_glob_misses() { /// drifted between them would leave the discriminator proving nothing about the /// set the other arm ran. const AGNOSTICISM_RULES: [&str; 5] = [ - "no-consumer-account-literal", - "no-consumer-entity-path", - "no-consumer-repo-name", - "no-tracker-key-in-core", + "fact name other", + "path name other", + "source name other", + "issue name other", // CLOUD-761's row, and the only one of the five whose glob is not // `crates/**`: the module tree carries the same class and had no gate at // all. It joins this census rather than getting a fixture of its own, @@ -6618,17 +6633,17 @@ const AGNOSTICISM_RULES: [&str; 5] = [ // committed table — is exactly what a row scoped to a different directory // needs. A separate fixture would assert the row fires and say nothing about // whether it fires where it was aimed. - "no-tracker-key-in-modules", + "pattern name other", ]; /// See [`AGNOSTICISM_RULES`]. const PORTABILITY_RULES: [&str; 6] = [ - "no-gnu-sed-z", - "no-gnu-sed-in-place", - "no-bash4-mapfile", - "no-gnu-xargs-r", - "no-branch-f-main", - "no-util-linux-flock", + "shell parse unsafe", + "shell edit unsafe", + "shell read unsafe", + "shell list unsafe", + "branch edit unsafe", + "shell guard unsafe", ]; #[test] @@ -6694,7 +6709,7 @@ fn the_committed_repo_agnosticism_rules_fire_on_every_banned_shape() { fs::create_dir_all(&src).expect("create fixture source tree"); fs::write(src.join("lib.rs"), &payload).expect("write fixture source"); fs::write(dirty.join("crates/demo/notes.txt"), &payload).expect("write fixture notes"); - // The module tree, which `no-tracker-key-in-modules` is the row for. A + // The module tree, which `pattern name other` is the row for. A // module composing its own key expression instead of reading // `data.batten.patterns` by id is the second authority the registry exists // to make unwritable; the file is a fixture rather than a loadable module, @@ -6728,17 +6743,17 @@ fn the_committed_repo_agnosticism_rules_fire_on_every_banned_shape() { ); assert_eq!( String::from_utf8_lossy(&output.stdout), - "crates/demo/notes.txt:1 no-consumer-account-literal\n\ - crates/demo/notes.txt:2 no-consumer-entity-path\n\ - crates/demo/notes.txt:3 no-consumer-repo-name\n\ - crates/demo/notes.txt:4 no-tracker-key-in-core\n\ - crates/demo/notes.txt:5 no-tracker-key-in-core\n\ - crates/demo/src/lib.rs:1 no-consumer-account-literal\n\ - crates/demo/src/lib.rs:2 no-consumer-entity-path\n\ - crates/demo/src/lib.rs:3 no-consumer-repo-name\n\ - crates/demo/src/lib.rs:4 no-tracker-key-in-core\n\ - crates/demo/src/lib.rs:5 no-tracker-key-in-core\n\ - policy/demo.rego:1 no-tracker-key-in-modules\n", + "crates/demo/notes.txt:1 fact name other\n\ + crates/demo/notes.txt:2 path name other\n\ + crates/demo/notes.txt:3 source name other\n\ + crates/demo/notes.txt:4 issue name other\n\ + crates/demo/notes.txt:5 issue name other\n\ + crates/demo/src/lib.rs:1 fact name other\n\ + crates/demo/src/lib.rs:2 path name other\n\ + crates/demo/src/lib.rs:3 source name other\n\ + crates/demo/src/lib.rs:4 issue name other\n\ + crates/demo/src/lib.rs:5 issue name other\n\ + policy/demo.rego:1 pattern name other\n", "one sorted pointer per banned shape per file, and nothing else" ); @@ -6823,7 +6838,7 @@ fn the_committed_portability_rules_fire_on_every_banned_shape() { .expect("write fixture task"); fs::create_dir_all(dirty.join("tests")).expect("create fixture test dir"); // The `# subject:` header every suite owes since CLOUD-807: the committed - // `bats-tests-not-deleted` row carries `retires_with`, so a suite declaring + // `bats count dropped` row carries `retires_with`, so a suite declaring // no subject is itself a finding. Declared here — pointing at the task seed // this fixture already writes — so this test keeps asserting the PORTABILITY // rules and nothing else, rather than growing a second rule's pointer. It @@ -6837,7 +6852,7 @@ fn the_committed_portability_rules_fire_on_every_banned_shape() { // THE SEEDS ARE COMMITTED INTO THE BASE, for the reason the `# subject:` // header above already records one rule earlier (CLOUD-1059). The committed - // `shell-retirement` row decides over the delta against `origin/main`, and + // `shell retire partial` row decides over the delta against `origin/main`, and // `committed_config_fixture_git` pins that ref at an EMPTY commit — so a seed // written afterwards is an authored shell rule this fixture ADDS, which that // row refuses, and the refusal would grow a second rule's pointer into an @@ -6852,7 +6867,7 @@ fn the_committed_portability_rules_fire_on_every_banned_shape() { git_in(&dirty, &["commit", "-q", "-m", "seed"]); git_in(&dirty, &["update-ref", "refs/remotes/origin/main", "HEAD"]); - // `enforce`, not `check`: the committed ruleset carries `no-conflict-markers`, + // `enforce`, not `check`: the committed ruleset carries `source carry broken`, // a kind that runs a configured command, and the read-effect verb refuses the // whole config rather than silently skipping that one row (exit 1, pinned by // `the_committed_config_refuses_to_run_a_spawning_kind_under_check`). Every @@ -6874,12 +6889,12 @@ fn the_committed_portability_rules_fire_on_every_banned_shape() { ); assert_eq!( String::from_utf8_lossy(&output.stdout), - "mise-tasks/seed.sh:1 no-gnu-sed-z\n\ - mise-tasks/seed.sh:2 no-gnu-sed-in-place\n\ - mise-tasks/seed.sh:3 no-bash4-mapfile\n\ - mise-tasks/seed.sh:4 no-gnu-xargs-r\n\ - mise-tasks/seed.sh:5 no-util-linux-flock\n\ - tests/seed.bats:2 no-branch-f-main\n", + "mise-tasks/seed.sh:1 shell parse unsafe\n\ + mise-tasks/seed.sh:2 shell edit unsafe\n\ + mise-tasks/seed.sh:3 shell read unsafe\n\ + mise-tasks/seed.sh:4 shell list unsafe\n\ + mise-tasks/seed.sh:5 shell guard unsafe\n\ + tests/seed.bats:2 branch edit unsafe\n", "one sorted pointer per banned construct, and nothing else" ); @@ -6912,7 +6927,7 @@ fn the_committed_portability_rules_fire_on_every_banned_shape() { // Committed into the base for the reason the dirty fixture above records // (CLOUD-1059): otherwise these two seeds are files this fixture ADDS, and - // `shell-retirement` refuses an added authored shell rule — which would make + // `shell retire partial` refuses an added authored shell rule — which would make // a tree that is portable by construction exit 2 for a reason that has // nothing to do with portability. git_in(&clean, &["add", "mise-tasks/seed.sh", "tests/seed.bats"]); @@ -6978,7 +6993,7 @@ fn the_committed_example_config_loads_over_the_binary() { ); assert_eq!( String::from_utf8_lossy(&output.stdout), - "**/*.rs no-conflict-markers\n", + "**/*.rs source carry broken\n", "a command condemns a batch, so the pointer is the glob and carries no line" ); } @@ -7032,7 +7047,7 @@ fn the_shipped_starter_config_loads_over_the_binary() { ); assert_eq!( String::from_utf8_lossy(&output.stdout), - "src/main.rs:1 no-conflict-markers\n", + "src/main.rs:1 source carry broken\n", "a forbid rule points at the line, not at the batch a command condemns" ); } @@ -11407,7 +11422,7 @@ fn a_non_string_prompt_reads_as_absent() { /// A policy whose `gh pr create` needs one agent-sourced fact. /// -/// `claim-not-raced` is the worked instance CLOUD-776 names: `issue-guard`'s +/// `claim mint twice` is the worked instance CLOUD-776 names: `issue-guard`'s /// duplicate-claim half could not port to the mediated path because "the /// claimed-key lookup needs a network call the mediated path is barred from" /// (CLOUD-446), so it became a `tree`-scoped row run under `verify` — catching @@ -11421,7 +11436,7 @@ command = "gh pr list --state open --json headRefName" returns = "json-array" [[rule]] -id = "claim-not-raced" +id = "claim mint twice" kind = "receipt" scope = "mediated_call" severity = "deny" @@ -11465,10 +11480,10 @@ fn the_agent_sourced_fact_loop_closes_end_to_end() { // string the record is verified against, so a dereference that lost it would // be a broken loop rather than a shorter line. assert!( - reason.contains("claim-not-raced"), + reason.contains("claim mint twice"), "the deny names the row that refused; got: {reason}" ); - let explained = batten_with(&dir, &["policy", "explain", "claim-not-raced"], &[]); + let explained = batten_with(&dir, &["policy", "explain", "claim mint twice"], &[]); assert_eq!(explained.status.code(), Some(0), "the row resolves"); assert!( String::from_utf8_lossy(&explained.stdout) @@ -12143,7 +12158,7 @@ fn help_leads_with_the_crate_description() { /// violation, and the same tree without it is clean. /// /// `[attribution] identity_deny` refuses what a COMMIT carries and has never -/// failed to. `no-denied-identity-prescribed` refuses what a tracked FILE +/// failed to. `remedy carry refused` refuses what a tracked FILE /// prescribes — the user-level hook's remedy, copied into the tree, where it /// would become a standing second authority telling the next reader to do the /// thing this repository denies. @@ -12172,7 +12187,7 @@ fn a_tracked_instruction_may_not_prescribe_the_denied_commit_identity() { // `check --rule`, not `enforce`. The unnarrowed read-effect verb does refuse // this config outright — it carries a spawning kind — but `--rule` selects - // the row BEFORE that refusal is reached, and `no-denied-identity-prescribed` + // the row BEFORE that refusal is reached, and `remedy carry refused` // is not one of the three `kind = "command"` rows. `mise.toml` already relies // on this against these same committed bytes: `check --rule 'diff ship early'`, // `--rule 'issue file other'`, `--rule 'memory point missing'`. @@ -12189,7 +12204,7 @@ fn a_tracked_instruction_may_not_prescribe_the_denied_commit_identity() { let output = batten() .arg("check") .arg("--rule") - .arg("no-denied-identity-prescribed") + .arg("remedy carry refused") .current_dir(&dirty) .state_home(&home) .env_remove("BATTEN_STRICTNESS") @@ -12203,7 +12218,7 @@ fn a_tracked_instruction_may_not_prescribe_the_denied_commit_identity() { ); assert_eq!( String::from_utf8_lossy(&output.stdout), - "HOWTO.md:2 no-denied-identity-prescribed\n", + "HOWTO.md:2 remedy carry refused\n", "one pointer, and the matched line is never echoed" ); @@ -12227,7 +12242,7 @@ fn a_tracked_instruction_may_not_prescribe_the_denied_commit_identity() { let output = batten() .arg("check") .arg("--rule") - .arg("no-denied-identity-prescribed") + .arg("remedy carry refused") .current_dir(&clean) .state_home(&home) .env_remove("BATTEN_STRICTNESS") @@ -12338,7 +12353,7 @@ name = "claimed-key" command = "gh pr list --state open --json headRefName" [[rule]] -id = "claim-not-raced" +id = "claim mint twice" kind = "receipt" scope = "mediated_call" severity = "deny" @@ -12835,7 +12850,7 @@ fn a_met_precondition_lets_the_rule_run_normally() { fn a_ratchet_declaring_no_precondition_is_untouched_by_the_new_gate() { let dir = Fixture::new("precondition-ratchet") .config( - "version = 1\n\n[[rule]]\nid = \"tests-not-deleted\"\nkind = \"ratchet\"\nglob = \"**/*.rs\"\npattern = \"#[test]\"\ndirection = \"non_decreasing\"\nbase = \"HEAD\"\nseverity = \"deny\"\nscope = \"tree\"\nno_fix_reason = \"restore the tests, or waive the reduction deliberately\"\n", + "version = 1\n\n[[rule]]\nid = \"test count dropped\"\nkind = \"ratchet\"\nglob = \"**/*.rs\"\npattern = \"#[test]\"\ndirection = \"non_decreasing\"\nbase = \"HEAD\"\nseverity = \"deny\"\nscope = \"tree\"\nno_fix_reason = \"restore the tests, or waive the reduction deliberately\"\n", ) .files(&[("lib.rs", "#[test]\nfn a() {}\n")]) .git() @@ -12922,11 +12937,11 @@ fn every_rule_kind_is_classified_and_only_one_approximates() { assert!(!Decidability::Approximating.may_block()); } -// --- ready-guard, retired onto `ready-needs-receipts` (CLOUD-843 / CLOUD-1170) - +// --- ready-guard, retired onto `check read unread` (CLOUD-843 / CLOUD-1170) - // // `mise-tasks/ready-guard.sh` was a `PreToolUse` body denying `gh pr ready` // until `verify` and `linear-check` had both passed against this exact HEAD. -// CLOUD-312 landed its receipt predicate as the `ready-needs-receipts` row in +// CLOUD-312 landed its receipt predicate as the `check read unread` row in // `batten.toml`, and this block is the ledger for the shell half going away. // // **THE PROGRAM WAS WIRED NOWHERE AT HEAD, which is what makes the withdrawal @@ -12973,7 +12988,7 @@ fn every_rule_kind_is_classified_and_only_one_approximates() { // every ready, always. Closing it needs either an engine change or an edit to // `land-lock.sh`, which is governed and out of this change's scope. // -// withdrawn: "gh pr ready --undo is the inverse action and is never gated" CLOUD-237's carve-out inverted under the engine: `pattern = "gh pr ready"` matches the undo too, so the one call that can only SAVE CI minutes is now denied (measured, exit 2, `Refused by ready-needs-receipts`). Not fixable by rewriting the pattern — `regex` carries no lookaround, and anchoring the row would trade this false deny for a false ALLOW on `gh pr ready 42 --json x`. CLOUD-1275 +// withdrawn: "gh pr ready --undo is the inverse action and is never gated" CLOUD-237's carve-out inverted under the engine: `pattern = "gh pr ready"` matches the undo too, so the one call that can only SAVE CI minutes is now denied (measured, exit 2, `Refused by check read unread`). Not fixable by rewriting the pattern — `regex` carries no lookaround, and anchoring the row would trade this false deny for a false ALLOW on `gh pr ready 42 --json x`. CLOUD-1275 // withdrawn: "denies ready when this clone does not hold the landing lease" the lease predicate is not expressible as a receipt row for the `branch_validity` reason above, and the program enforcing it was wired nowhere. CLOUD-1275 // withdrawn: "the lease refusal names the task to run, not merely the refusal" same predicate, same block; the remedy text has no row to live on until the predicate does. CLOUD-1275 // withdrawn: "a LAPSED lease is refused, and the refusal says how long ago" `max_age` reads an mtime and renders `expired`, never an elapsed count, so even once the predicate lands the "how long ago" half is a deliberate loss. CLOUD-1275 @@ -12996,7 +13011,7 @@ fn every_rule_kind_is_classified_and_only_one_approximates() { const READY_RECEIPT_CONFIG: &str = r#"version = 1 [[rule]] -id = "ready-needs-receipts" +id = "check read unread" kind = "receipt" scope = "mediated_call" severity = "deny" @@ -13063,7 +13078,7 @@ fn a_ready_with_no_receipts_is_refused_and_the_refusal_names_the_task() { assert_eq!(output.status.code(), Some(2), "{}", stderr(&output)); let refusal = stderr(&output); assert!( - refusal.contains("ready-needs-receipts"), + refusal.contains("check read unread"), "the refusal must name the row: {refusal}" ); assert!( diff --git a/crates/batten/tests/it/commit_arm_sequencing.rs b/crates/batten/tests/it/commit_arm_sequencing.rs index 413705a21..d72479ba7 100644 --- a/crates/batten/tests/it/commit_arm_sequencing.rs +++ b/crates/batten/tests/it/commit_arm_sequencing.rs @@ -33,7 +33,7 @@ //! # The real commit, replayed — and why it is not a case here //! //! `BASE_SHA='65757c86^' HEAD_SHA=65757c86 mise run commit-check` reports -//! `65757c86 arm-self-authorized bats-tests-not-deleted.withdrawn`, which is +//! `65757c86 arm-self-authorized bats count dropped.withdrawn`, which is //! CLOUD-1402's Done clause satisfied over this repository's own history rather //! than over a fixture. //! @@ -46,7 +46,7 @@ //! //! # The declared mutation, and why the row is in THIS file //! -//! `obligations-bound` binds a §7 obligation by reading the declared file's lines +//! `test name undefined` binds a §7 obligation by reading the declared file's lines //! for a row beginning `#MUTANT |`. Its `line_sources` covers //! `crates/batten/tests/**` and not `crates/batten/src/**`, so the row has to be //! here even though the expression it applies belongs to `commit.rs`'s predicate diff --git a/crates/batten/tests/it/config_skew.rs b/crates/batten/tests/it/config_skew.rs index 5cb011aa9..eec2daa7c 100644 --- a/crates/batten/tests/it/config_skew.rs +++ b/crates/batten/tests/it/config_skew.rs @@ -56,7 +56,7 @@ use common::{batten, stderr}; /// A repository whose committed authority is `config`. /// -/// Built through `Fixture` rather than by hand, and that is `fixture-forks` +/// Built through `Fixture` rather than by hand, and that is `test fix duplicate` /// working rather than a style note: it refused the first draft of this file, /// which ran its own `git init`. Every fixture copies the one template /// `common/mod.rs` builds, so a suite cannot drift into its own repository diff --git a/crates/batten/tests/it/connector_allow_door.rs b/crates/batten/tests/it/connector_allow_door.rs index 0eb2e5d71..177aa0651 100644 --- a/crates/batten/tests/it/connector_allow_door.rs +++ b/crates/batten/tests/it/connector_allow_door.rs @@ -21,7 +21,7 @@ //! real `batten.toml` would let another rule's verdict stand in for this one — //! the exact substitution that hid the defect for the life of the migration. //! -//! **Rust rather than a `.bats` suite** (CLOUD-843): `shell-retirement` refuses +//! **Rust rather than a `.bats` suite** (CLOUD-843): `shell retire partial` refuses //! a new one, correctly. The fixture and the binary are the same either way. //! **UNIX ONLY, and the gate is load-bearing rather than tidy.** Every case here @@ -65,7 +65,7 @@ expires = "2027-02-28" /// THE DENIED VERB IS ONE NO ENGINE ROW COVERS. `send_later` would have been the /// natural fixture and is the wrong one: this repository's own -/// `no-scheduled-self-wakeup` refuses it, so a case built on it passes whether +/// `timer mint refused` refuses it, so a case built on it passes whether /// the handler spoke or not. That substitution is the defect, not a detail. const SETTINGS: &str = r#"{"permissions":{ "allow":["mcp__Claude_Code_Remote__create_session"], @@ -198,7 +198,7 @@ fn the_committed_guard_writes_a_host_document_so_its_verdict_is_dropped() { // and this suite's whole subject is telling those two apart. // // WHY IT IS ASSERTED RATHER THAN FIXED: the repair is one `case` in a - // governed shell file, which `shell-retirement` refuses unless the file is + // governed shell file, which `shell retire partial` refuses unless the file is // retired — and it cannot be, because it reads `/tmp/mcp-config-cse_*.json` // per call, which no Rego module may do and no Rust port may carry into the // core (rule 1). So this case is the finding's durable home, and it FLIPS the diff --git a/crates/batten/tests/it/connector_not_granted.rs b/crates/batten/tests/it/connector_not_granted.rs index 4737e5205..66dd581e2 100644 --- a/crates/batten/tests/it/connector_not_granted.rs +++ b/crates/batten/tests/it/connector_not_granted.rs @@ -1,4 +1,4 @@ -//! `connector-not-granted` over the compiled binary (CLOUD-1260). +//! `connector grant loose` over the compiled binary (CLOUD-1260). //! //! **The question a `with input as` case cannot answer.** The module's own `test_` //! rules pin the predicate and nothing else: they hand it a fabricated @@ -52,7 +52,7 @@ const MODULE: &str = include_str!("../../../../policy/connector-not-granted.rego const CONFIG: &str = r#"version = 1 [[rule]] -id = "connector-not-granted" +id = "connector grant loose" kind = "policy" scope = "tree" documents = [".claude/settings.json"] @@ -130,7 +130,7 @@ fn a_named_raw_grant_is_refused() { Some(2), "a granted raw tool must refuse: the reduction beside it decides nothing\n{answer}{cause}" ); - assert!(answer.contains("connector-not-granted"), "{answer}{cause}"); + assert!(answer.contains("connector grant loose"), "{answer}{cause}"); } #[test] @@ -147,7 +147,7 @@ fn a_globbed_server_grant_is_refused() { Some(2), "a wildcard grant is wider than a named one and must refuse too\n{answer}{cause}" ); - assert!(answer.contains("connector-not-granted"), "{answer}{cause}"); + assert!(answer.contains("connector grant loose"), "{answer}{cause}"); } #[test] diff --git a/crates/batten/tests/it/connector_verbs.rs b/crates/batten/tests/it/connector_verbs.rs index cf4561cd8..cb44c67b5 100644 --- a/crates/batten/tests/it/connector_verbs.rs +++ b/crates/batten/tests/it/connector_verbs.rs @@ -63,7 +63,7 @@ // // `01e9534` is `773a8fc^` — the commit before the one that retired this guard — // and both the dying suite and the dying program are present there, checked. -// replay-call: tests/connector-verb-guard.bats 01e9534 mise-tasks/connector-verb-guard.sh no-pr-activity-subscription deny=2 allow=0 +// replay-call: tests/connector-verb-guard.bats 01e9534 mise-tasks/connector-verb-guard.sh review watch refused deny=2 allow=0 // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] @@ -103,9 +103,9 @@ fn payload(tool: &str) -> String { /// The three verbs, and the rows that decide them. const DECIDED: &[(&str, &str)] = &[ - ("subscribe_pr_activity", "no-pr-activity-subscription"), - ("send_later", "no-scheduled-self-wakeup"), - ("create_trigger", "no-scheduled-trigger"), + ("subscribe_pr_activity", "review watch refused"), + ("send_later", "timer mint refused"), + ("create_trigger", "event mint refused"), ]; /// CARRIES: every "is denied" case, under the readable name, under a UUID, and @@ -237,12 +237,28 @@ fn each_refusal_names_its_own_remedy() { // line" stopped being the rule id — it became the last word of a // sentence. The pointer half is what this reads, and taking it // explicitly says so rather than relying on the route's absence. + // AND THE NAME IS THREE WORDS (CLOUD-1638), so the last WORD is one + // third of it. `explain` answers about the CLASS, which is the head's + // first three words on both arms — a discriminating row appends its id + // after the pointers and a collapsed row's id IS the class, so reading + // the front is right in both cases and reading the back is right in + // neither. let pointer = text.split(" — ").next().unwrap_or(&text); - let row = pointer - .split_whitespace() - .next_back() - .expect("a deny names the rule that fired"); - let explained = run(&repo, &["policy", "explain", row]); + let words: Vec<&str> = pointer.split_whitespace().collect(); + assert!( + words.len() >= 3, + "{verb}: a deny names the class that fired: {text}" + ); + // THE ROW'S remedy, not the class's: what this case asserts is that the + // refusal reaches the row's own `reason`, and `explain` answers about + // the class. The id is the head's last three words on a discriminating + // row; on a collapsed row it IS the class, so the first three resolve. + let tail = words[words.len() - 3..].join(" "); + let class = words[..3].join(" "); + let mut explained = run(&repo, &["policy", "rule", &tail]); + if explained.status.code() != Some(0) { + explained = run(&repo, &["policy", "rule", &class]); + } assert_eq!(explained.status.code(), Some(0), "{verb}: the row resolves"); let explained_text = String::from_utf8_lossy(&explained.stdout); assert!( diff --git a/crates/batten/tests/it/container_health.rs b/crates/batten/tests/it/container_health.rs index 667ad6547..5cdb15772 100644 --- a/crates/batten/tests/it/container_health.rs +++ b/crates/batten/tests/it/container_health.rs @@ -134,7 +134,7 @@ fn advisory(output: &Output) -> Option { /// The measured shape: a declared program nothing can reach is said out loud, at /// the session's first moment, naming the program rather than describing it. /// -/// `no-conflict-markers` is the instance — it declared `hk`, nothing a toolchain +/// `source carry broken` is the instance — it declared `hk`, nothing a toolchain /// manager provides is on bare `PATH`, and the merge-conflict gate had been /// unable to launch for as long as the drift existed while `doctor` reported only /// that SOME declared program was off `PATH`. diff --git a/crates/batten/tests/it/contract_drift.rs b/crates/batten/tests/it/contract_drift.rs index 8dc1aabfc..aadd17bfd 100644 --- a/crates/batten/tests/it/contract_drift.rs +++ b/crates/batten/tests/it/contract_drift.rs @@ -350,7 +350,7 @@ fn each_session_is_told_about_what_moved_under_it_and_not_about_the_rest() { #[test] fn the_notice_names_paths_and_never_a_byte_of_one() { // Assembled rather than written, and the reason is this repo's own gate: - // a credential-shaped literal in a tracked file is what `no-secrets` + // a credential-shaped literal in a tracked file is what `source carry unsafe` // exists to catch, and it caught this one. The planted value is still // secret-shaped where it matters — in the file the fixture writes and in // every string this case then searches — while the SOURCE carries no @@ -527,7 +527,7 @@ fn the_mediation_hatch_does_not_silence_the_advisory() { // CLOUD-908's calibration, and the retirement it calibrates against is the only // one the campaign has actually completed. Every one of the 22 `@test` cases the // deleted suite declared is claimed below by exactly one arm naming a successor -// that resolves. `bats-tests-not-deleted` reads this shape on every future +// that resolves. `bats count dropped` reads this shape on every future // deletion; here it is retroactive, because the deletion already landed and // nothing recorded where the cases went. // diff --git a/crates/batten/tests/it/document_read_count.rs b/crates/batten/tests/it/document_read_count.rs index 9f0419b14..669d1bbe3 100644 --- a/crates/batten/tests/it/document_read_count.rs +++ b/crates/batten/tests/it/document_read_count.rs @@ -100,7 +100,7 @@ fn rows_declaring_one_path_read_it_once() { // THE DEFECT THIS ASSERTS AWAY. `run`'s `for rule in rules` wrapped // `tree_document`'s `for path in documents` with no dedup and no cache, so // two rows declaring one path read and parsed it twice — 79 rules x N - // documents is 79N reads plus 79N parses, on the one surface `perf-assert` + // documents is 79N reads plus 79N parses, on the one surface `path measure wrong` // deliberately budgets no ceiling for. // // Fails by: removing the cache lookup in `acquire_declared`, which makes diff --git a/crates/batten/tests/it/egress_fencing.rs b/crates/batten/tests/it/egress_fencing.rs index a9fc0d11b..9c4c2bdae 100644 --- a/crates/batten/tests/it/egress_fencing.rs +++ b/crates/batten/tests/it/egress_fencing.rs @@ -39,7 +39,7 @@ const AUTHORITY: &str = r#" version = 1 [[rule]] -id = "egress-fencing" +id = "provision guard missing" kind = "policy" scope = "tree" documents = ["mise.toml", "batten.toml"] @@ -221,7 +221,7 @@ fn denied_at(root: &Path, pointer: &str) { // carries neither the token nor the class, so a case asserting one would be // asserting the renderer. Which class fired is the module's own rules' job. assert!( - text.contains("egress-fencing"), + text.contains("provision guard missing"), "the finding names the rule: {text}" ); assert!( diff --git a/crates/batten/tests/it/enforce_journal.rs b/crates/batten/tests/it/enforce_journal.rs index 2082d5745..48f4b2104 100644 --- a/crates/batten/tests/it/enforce_journal.rs +++ b/crates/batten/tests/it/enforce_journal.rs @@ -874,7 +874,7 @@ fn the_read_surface_journals_nothing() { // --- the secret class: journaling, then custody ------------------------------- /// The fragments the synthetic credential is assembled from, split so no -/// contiguous token exists in a committed byte — consumer #1's own `no-secrets` +/// contiguous token exists in a committed byte — consumer #1's own `source carry unsafe` /// rule globs this file. const TOKEN_PARTS: [&str; 5] = ["AKIA", "6RJ4", "MP2T", "V8QX", "L3ZB"]; @@ -914,7 +914,7 @@ fn secrets_config(url: &str, sha: &str) -> String { sha256 = \"{sha}\"\n\ binary = \"ripsecrets\"\n\n\ [[rule]]\n\ - id = \"no-secrets\"\n\ + id = \"source carry unsafe\"\n\ kind = \"secrets\"\n\ glob = \"**/*.conf\"\n\ severity = \"deny\"\n\ @@ -961,7 +961,7 @@ fn secret_env(name: &str) -> Env { fn a_secret_class_finding_reaches_the_store_carrying_no_kind() { let env = secret_env("enforce-journal-secret"); let record = env - .record("no-secrets") + .record("source carry unsafe") .expect("the secrets kind's finding reached the store"); assert_eq!( record["identity"]["version"].as_str().unwrap(), @@ -983,7 +983,7 @@ fn a_secret_class_finding_reaches_the_store_carrying_no_kind() { assert_eq!( env.records() .iter() - .filter(|r| r["rule"] == "no-secrets") + .filter(|r| r["rule"] == "source carry unsafe") .count(), 1 ); @@ -993,7 +993,7 @@ fn a_secret_class_finding_reaches_the_store_carrying_no_kind() { #[test] fn a_lost_key_re_opens_its_findings_loudly_and_re_mints_nothing() { let env = secret_env("enforce-journal-orphan"); - let before = env.record("no-secrets").unwrap(); + let before = env.record("source carry unsafe").unwrap(); let fingerprint = before["identity"]["fingerprint"] .as_str() .unwrap() @@ -1055,7 +1055,7 @@ fn a_lost_key_re_opens_its_findings_loudly_and_re_mints_nothing() { #[test] fn a_rotation_join_moves_the_record_and_keeps_its_disposition() { let env = secret_env("enforce-journal-rotate"); - let old = env.record("no-secrets").unwrap()["identity"]["fingerprint"] + let old = env.record("source carry unsafe").unwrap()["identity"]["fingerprint"] .as_str() .unwrap() .to_owned(); @@ -1088,7 +1088,7 @@ fn a_rotation_join_moves_the_record_and_keeps_its_disposition() { "the ledger carries no key bytes" ); - let moved = env.record("no-secrets").unwrap(); + let moved = env.record("source carry unsafe").unwrap(); let new = moved["identity"]["fingerprint"].as_str().unwrap(); assert_ne!(new, old, "the identity moved with the key"); assert_eq!( @@ -1100,7 +1100,7 @@ fn a_rotation_join_moves_the_record_and_keeps_its_disposition() { assert_eq!( env.records() .iter() - .filter(|r| r["rule"] == "no-secrets") + .filter(|r| r["rule"] == "source carry unsafe") .count(), 1, "one finding, not two — the pre-rotation file is dropped, and it must be, \ diff --git a/crates/batten/tests/it/fact_record_keying.rs b/crates/batten/tests/it/fact_record_keying.rs index 4e52b8691..06de47af4 100644 --- a/crates/batten/tests/it/fact_record_keying.rs +++ b/crates/batten/tests/it/fact_record_keying.rs @@ -193,7 +193,7 @@ fn a_head_keyed_record_cleared_on_one_commit_does_not_satisfy_the_next() { // ported: "ANTI-VACUITY: a branch-keyed record still satisfies the check after a new commit" crates/batten/tests/it/fact_record_keying.rs subject:crates/batten/src/facts.rs #[test] fn anti_vacuity_a_branch_keyed_record_survives_a_new_commit() { - // The case that has to stay green. `claim-needs-receipt` is keyed by branch + // The case that has to stay green. `claim read unread` is keyed by branch // precisely because a claim attests to a decision about an issue that every // commit on the branch continues to serve. A fix that head-keyed every record // would pass the case above and make `claim` demand a re-claim per commit, diff --git a/crates/batten/tests/it/filed_here.rs b/crates/batten/tests/it/filed_here.rs index c41ca3ee2..dcb9e6938 100644 --- a/crates/batten/tests/it/filed_here.rs +++ b/crates/batten/tests/it/filed_here.rs @@ -1,4 +1,4 @@ -//! `filed-here`, over the engine that builds its input (CLOUD-1051). +//! `issue file other`, over the engine that builds its input (CLOUD-1051). //! //! # The tier this is, and why the retired suite could not be it //! @@ -10,11 +10,11 @@ //! cannot reach. The module's own `test_` rules are the other tier and pin the //! predicate; neither replaces the other. //! -//! # RETIREMENT LEDGER, PER PATH — what `shell-retirement` reads +//! # RETIREMENT LEDGER, PER PATH — what `shell retire partial` reads //! //! Two ledgers, two keys, and neither substitutes for the other. CLOUD-908's //! `[rule.conserves]` ledger below is keyed on a quoted CASE TITLE and asks what -//! happened to each assertion. `shell-retirement` is keyed on the RETIRED PATH +//! happened to each assertion. `shell retire partial` is keyed on the RETIRED PATH //! and asks what now holds the predicate at all — so it demands one arm per file //! naming both a policy surface and a compiled-binary test, because either alone //! is satisfiable by a port that does nothing. @@ -195,7 +195,7 @@ fn install_module(root: &Path) { fn row() -> Rule { serde_json::from_value(serde_json::json!({ - "id": "filed-here", + "id": "issue file other", "kind": "policy", "scope": "tree", "base": "origin/main", @@ -842,7 +842,7 @@ fn the_committed_row_is_the_one_these_cases_exercise() { .rules; let declared = committed .iter() - .find(|rule| rule.id == "filed-here") + .find(|rule| rule.id == "issue file other") .expect("the committed config declares the row this suite exercises"); assert_eq!(declared.kind, RuleKind::Policy); assert_eq!(declared.scope, RuleScope::Tree); diff --git a/crates/batten/tests/it/fixture_forks.rs b/crates/batten/tests/it/fixture_forks.rs index a4c57fe91..2cf807d70 100644 --- a/crates/batten/tests/it/fixture_forks.rs +++ b/crates/batten/tests/it/fixture_forks.rs @@ -99,7 +99,7 @@ fn install_module(root: &Path) { /// pass here. fn row() -> Rule { serde_json::from_value(serde_json::json!({ - "id": "fixture-forks", + "id": "test fix duplicate", "kind": "policy", "scope": "tree", "base": "origin/main", diff --git a/crates/batten/tests/it/forced_push.rs b/crates/batten/tests/it/forced_push.rs index 8d7d71df0..8b36960b6 100644 --- a/crates/batten/tests/it/forced_push.rs +++ b/crates/batten/tests/it/forced_push.rs @@ -8,7 +8,7 @@ //! at any point** — the rejection came from git, after the work was written, //! verified and committed. //! -//! `claim-not-raced` asks about a KEY across open pull requests, and both +//! `claim mint twice` asks about a KEY across open pull requests, and both //! sessions served the same one, so it is correctly silent. The claim receipt — //! the one artifact saying *this session is working this branch* — lives under //! `$GIT_DIR`, is never committed, and dies with the container, so no clone can @@ -64,7 +64,7 @@ fn denied_by_this_row(command: &str) { "the committed policy must refuse: {command}\n{out}" ); assert!( - out.contains("leased-push"), + out.contains("branch write unsafe"), "the refusal for `{command}` must come from this row\n{out}" ); } @@ -137,7 +137,7 @@ fn a_grammar_token_does_not_make_another_tool_gits() { #[test] fn the_preset_still_owns_the_bare_forced_spellings() { // Asserted as SOMEBODY refusing rather than as this row's work. If this ever - // starts coming from `leased-push`, the narrowing has been undone and + // starts coming from `branch write unsafe`, the narrowing has been undone and // there are two rules over one object again. denied("git push --force origin main"); denied("git push -f origin main"); @@ -179,7 +179,7 @@ fn the_explicit_expected_value_is_allowed() { fn the_flag_named_in_prose_is_not_a_push() { // ANTI-VACUITY IN THE OTHER DIRECTION. A row keyed on the substring alone // would fire on any command mentioning the flag — including the ones - // documenting this rule, which is how `no-secrets` refused its own + // documenting this rule, which is how `source carry unsafe` refused its own // explanatory comment. `pattern` requires the `git push` shape and `contains` // narrows within it, so a sentence about a leased push is not one. allowed("echo 'never reach for git push --force-with-lease here'"); diff --git a/crates/batten/tests/it/forge_facts.rs b/crates/batten/tests/it/forge_facts.rs index 0dbacc57e..ef776ab4b 100644 --- a/crates/batten/tests/it/forge_facts.rs +++ b/crates/batten/tests/it/forge_facts.rs @@ -210,7 +210,7 @@ fn no_record_at_all_is_could_not_look() { // Every case above plants the record by hand, which is right for asserting what // the READER does with one and cannot show that anything in the tree can write // one. Nothing could: `git grep batten-forge` found this file and `forge.rs`, so -// `forge-verdict-required` — a registered `severity = "deny"` row — resolved +// `forge check red` — a registered `severity = "deny"` row — resolved // `null` on every real checkout and decided nothing from the day it merged. // // These run `batten record forge`. The difference is the same one its sibling diff --git a/crates/batten/tests/it/gh_guard.rs b/crates/batten/tests/it/gh_guard.rs index cec195466..f98dcf7c3 100644 --- a/crates/batten/tests/it/gh_guard.rs +++ b/crates/batten/tests/it/gh_guard.rs @@ -28,7 +28,7 @@ //! # One case asserts a rule id rather than a verdict, and that is not a weakening //! //! `gh pr ready` is ALLOWED by the predicate under test and DENIED by the engine -//! overall, because `ready-needs-receipts` legitimately refuses a ready with no +//! overall, because `check read unread` legitimately refuses a ready with no //! `verify` receipt for this head. The bats case drove `gh-guard-check.sh` //! directly, so it only ever asked the narrower question. Reading the aggregate //! exit code here would make one rule's correct arrival look like this rule's @@ -86,10 +86,10 @@ use common::{run_with_stdin_at_real_root, stdout}; /// THESE rows decide — a refusal from any other row is a different question, and /// the `gh pr ready` case below turns on exactly that distinction. const LIFECYCLE: [&str; 4] = [ - "gh-pr-merge", - "gh-pr-comment-fast-forward", - "gh-pr-checks", - "gh-run-watch", + "commit ship other", + "review ship early", + "check watch loose", + "job watch loose", ]; /// The repository root, whose committed `batten.toml` is the policy under test. @@ -187,17 +187,20 @@ fn allowed_backgrounded(command: &str) { #[test] fn gh_pr_merge_is_refused_however_it_is_spelled() { - denied_by("gh pr merge 42 --rebase", "gh-pr-merge"); + denied_by("gh pr merge 42 --rebase", "commit ship other"); // Behind a flag VALUE, and behind an env prefix. The pairs are adjacent, so // an interposed flag never hides a real match. - denied_by("gh -R example-org/example-repo pr merge 42", "gh-pr-merge"); - denied_by("GH_TOKEN=x gh pr merge 42", "gh-pr-merge"); + denied_by( + "gh -R example-org/example-repo pr merge 42", + "commit ship other", + ); + denied_by("GH_TOKEN=x gh pr merge 42", "commit ship other"); } #[test] fn the_ci_watch_shapes_are_refused() { - denied_by("gh pr checks 63 --watch", "gh-pr-checks"); - denied_by("gh run watch 12345", "gh-run-watch"); + denied_by("gh pr checks 63 --watch", "check watch loose"); + denied_by("gh run watch 12345", "job watch loose"); } /// THE PAREN LANDS ON A MATCHED OPERAND ONLY WHEN THE COMMAND TAKES NO TRAILING @@ -217,24 +220,24 @@ fn the_ci_watch_shapes_are_refused() { /// WHY the first probe passed, rather than leaving that to be rediscovered. #[test] fn a_grouped_lifecycle_command_is_still_refused() { - denied_by("(gh pr merge)", "gh-pr-merge"); - denied_by("(gh run watch)", "gh-run-watch"); - denied_by("(gh pr merge 42)", "gh-pr-merge"); + denied_by("(gh pr merge)", "commit ship other"); + denied_by("(gh run watch)", "job watch loose"); + denied_by("(gh pr merge 42)", "commit ship other"); } #[test] fn a_blocked_verb_in_a_later_segment_is_still_refused() { // CLOUD-857's class: a real agent command is compound most of the time, and // anchoring on the first word of the LINE misses every one of these. - denied_by("gh pr view 63 && gh run watch 1", "gh-run-watch"); - denied_by("echo hi; gh pr checks 63", "gh-pr-checks"); + denied_by("gh pr view 63 && gh run watch 1", "job watch loose"); + denied_by("echo hi; gh pr checks 63", "check watch loose"); } #[test] fn a_hand_typed_fast_forward_comment_is_refused() { denied_by( "gh pr comment 63 --body \"/fast-forward\"", - "gh-pr-comment-fast-forward", + "review ship early", ); } @@ -244,9 +247,12 @@ fn a_wrapped_gh_call_is_judged_by_its_effective_program() { // looks through wrappers, so the wrapper token is never what is judged. In // the web sandbox the wrapper form is often the only working form, so a guard // stopping at the wrapper would see none of the calls that matter. - denied_by("mise exec -- gh pr merge 42", "gh-pr-merge"); - denied_by("mise x node@22 gh pr merge 42", "gh-pr-merge"); - denied_by("env GH_TOKEN=x timeout 30 gh pr checks 42", "gh-pr-checks"); + denied_by("mise exec -- gh pr merge 42", "commit ship other"); + denied_by("mise x node@22 gh pr merge 42", "commit ship other"); + denied_by( + "env GH_TOKEN=x timeout 30 gh pr checks 42", + "check watch loose", + ); } // --- allowed: reads, creates, and verbs with no task wrapper ------------------- @@ -254,7 +260,7 @@ fn a_wrapped_gh_call_is_judged_by_its_effective_program() { #[test] fn gh_pr_ready_is_not_a_lifecycle_refusal() { // The one case that asks about the ROW rather than the verdict. See the - // module header: `ready-needs-receipts` refuses this correctly, and reading + // module header: `check read unread` refuses this correctly, and reading // the aggregate would report that as this rule's regression. assert_no_gh_lifecycle_refusal("gh pr ready 63"); } @@ -262,7 +268,7 @@ fn gh_pr_ready_is_not_a_lifecycle_refusal() { #[test] fn gh_pr_create_is_not_a_lifecycle_refusal() { // THE SAME SHAPE AS `gh pr ready` ABOVE, AND IT SAT ON `allowed` UNTIL - // CLOUD-1384. `pr-names-an-issue` is a `requires_key` row whose evidence is + // CLOUD-1384. `review name unnamed` is a `requires_key` row whose evidence is // the command, the BRANCH NAME, and the subjects on `origin/main..HEAD` — so // a full allow here asks about the developer's branch rather than about any // `gh` lifecycle row. Measured: exit 2 from a landed branch carrying no key, @@ -412,7 +418,7 @@ fn decision_with_env(command: &str, key: &str, value: &str) -> String { /// /// The tree's decision is deliberate and documented; the rules file had drifted /// from it. That is corrected in the same change rather than carried forward, and -/// the drift is reported rather than folded in — `rules-drift` gates values +/// the drift is reported rather than folded in — `rule watch other` gates values /// `.claude/rules/*.md` restates, and it did not catch this one. /// /// **So the shell's own `BATTEN_GH_GUARD_BYPASS` arm is NOT conserved, and that @@ -437,7 +443,7 @@ fn the_engines_hatch_suppresses_the_lifecycle() { fn the_undeclared_row_bypass_does_not_suppress() { let out = decision_with_env("gh pr merge 63", "BATTEN_GH_GUARD_BYPASS", "1"); assert!( - out.contains("gh-pr-merge"), + out.contains("commit ship other"), "no row declares BATTEN_GH_GUARD_BYPASS, so it must not suppress this refusal \ (batten.toml:273, deferred to CLOUD-1027)\n{out}" ); diff --git a/crates/batten/tests/it/harness_grant.rs b/crates/batten/tests/it/harness_grant.rs index 9ae3f5880..2e6326f49 100644 --- a/crates/batten/tests/it/harness_grant.rs +++ b/crates/batten/tests/it/harness_grant.rs @@ -1,4 +1,4 @@ -//! `harness-grant` over the compiled binary (CLOUD-1247). +//! `grant carry missing` over the compiled binary (CLOUD-1247). //! //! **The question a `with input as` case cannot answer.** The module's own //! `test_` rules pin the predicate and nothing else: they hand it a fabricated @@ -54,7 +54,7 @@ const MODULE: &str = include_str!("../../../../policy/harness-grant.rego"); const CONFIG: &str = r#"version = 1 [[rule]] -id = "harness-grant" +id = "grant carry missing" kind = "policy" scope = "tree" documents = [".claude/settings.json"] @@ -142,7 +142,7 @@ fn a_dropped_grant_is_refused() { Some(2), "a settings file naming no mediator must refuse\n{answer}{cause}" ); - assert!(answer.contains("harness-grant"), "{answer}{cause}"); + assert!(answer.contains("grant carry missing"), "{answer}{cause}"); } #[test] @@ -161,7 +161,7 @@ fn a_dropped_sentinel_is_refused() { Some(2), "dropping $defaults must refuse in its own right\n{answer}{cause}" ); - assert!(answer.contains("harness-grant"), "{answer}{cause}"); + assert!(answer.contains("grant carry missing"), "{answer}{cause}"); } #[test] diff --git a/crates/batten/tests/it/harness_wiring.rs b/crates/batten/tests/it/harness_wiring.rs index 7e7dbcf01..d2623795f 100644 --- a/crates/batten/tests/it/harness_wiring.rs +++ b/crates/batten/tests/it/harness_wiring.rs @@ -239,7 +239,7 @@ const ROOT_VAR: &str = "BATTEN_FIXTURE_WIRING_ROOT"; /// directions this module no longer has. What survives them is worth keeping: the /// committed `ready-issue-key` row spells this consumer's tracker prefix, and /// reproducing that expression here would put a specific consumer's vocabulary -/// inside `crates/`, which `no-tracker-key-in-core` refuses. It refused it twice — +/// inside `crates/`, which `issue name other` refuses. It refused it twice — /// the second time was the comment explaining the first fix, which quoted the /// prefix it had just removed, and the gate was right: a grep for a consumer's /// names does not care which side of a `///` the name is on. @@ -289,7 +289,7 @@ fn config() -> String { r#"version = 1 [[rule]] -id = "harness-wiring" +id = "hook wire missing" kind = "policy" scope = "tree" documents = [".claude/settings.json"] @@ -446,7 +446,7 @@ fn a_committed_sibling_beside_the_mediator_is_refused() { let output = check(&repo, Some(&outside)); assert!(!output.status.success(), "a sibling passed"); assert!( - findings(&output).contains(".claude/settings.json harness-wiring"), + findings(&output).contains(".claude/settings.json hook wire missing"), "wrong finding: {}", findings(&output) ); @@ -495,7 +495,7 @@ fn a_stop_sibling_is_refused_too_so_the_scope_is_every_event() { let output = check(&repo, Some(&outside)); assert!(!output.status.success(), "a Stop sibling passed"); assert!( - findings(&output).contains(".claude/settings.json harness-wiring"), + findings(&output).contains(".claude/settings.json hook wire missing"), "wrong finding: {}", findings(&output) ); @@ -508,7 +508,7 @@ fn a_tree_with_no_wiring_surface_is_clean() { // table: a declared row matched nothing in a tree carrying no wiring surface // at all, so the module reported a spent licence over a tree it never looked // at — `cli.rs`'s fixture repos have no `.claude/settings.json` and four of - // its cases went red with `1 harness-wiring` above their own expected finding. + // its cases went red with `1 hook wire missing` above their own expected finding. // // The table is gone and with it the direction that could fire here, so the // property is now structural rather than guarded. The case stays because it @@ -553,7 +553,7 @@ fn a_merged_registration_beside_the_mediator_is_refused() { let output = check(&repo, Some(&outside)); assert!(!output.status.success(), "a merged sibling passed"); assert!( - findings(&output).contains("harness-wiring"), + findings(&output).contains("hook wire missing"), "wrong finding: {}", findings(&output) ); @@ -588,7 +588,7 @@ fn the_launcher_hooks_are_refused_rather_than_tolerated() { // A COUNT AND NO PATH: a merged path is under somebody's home directory and // differs per machine, so rule 4 and §6 byte-stability both forbid it travelling. assert!( - findings(&output).contains("2 harness-wiring"), + findings(&output).contains("2 hook wire missing"), "wrong finding: {}", findings(&output) ); @@ -642,7 +642,7 @@ fn the_committed_half_survives_an_absent_merged_surface() { "the committed half went silent with no merged surface — CLOUD-1307 has been reintroduced by recombining the two rows" ); assert!( - findings(&output).contains(".claude/settings.json harness-wiring"), + findings(&output).contains(".claude/settings.json hook wire missing"), "wrong finding: {}", findings(&output) ); @@ -695,7 +695,7 @@ fn this_repository_is_wired_correctly() { // row a module reads and the config stops declaring. let output = batten() .current_dir(at_root(".")) - .args(["check", "--rule", "harness-wiring"]) + .args(["check", "--rule", "hook wire missing"]) .output() .expect("run batten check"); assert!( diff --git a/crates/batten/tests/it/hk_contract.rs b/crates/batten/tests/it/hk_contract.rs index c4795f8a3..179781cc5 100644 --- a/crates/batten/tests/it/hk_contract.rs +++ b/crates/batten/tests/it/hk_contract.rs @@ -265,7 +265,7 @@ fn the_refusal_is_a_declared_class_with_a_route() { fn a_drifted_contract_exits_two_and_names_the_class() { // `Fixture` rather than a `tempfile`: it is this suite's own scratch // convention and needs no dev-dependency the binary does not link. - let scratch = common::Fixture::new("hk-contract-drift"); + let scratch = common::Fixture::new("gate table other"); let root = scratch.path(); fs::copy(common::at_root("hk.pkl"), root.join("hk.pkl")).expect("the runner config copies"); fs::create_dir_all(root.join("contracts")).expect("the artifact directory"); diff --git a/crates/batten/tests/it/hk_fix_selection.rs b/crates/batten/tests/it/hk_fix_selection.rs index 630982fb1..86e0ebf03 100644 --- a/crates/batten/tests/it/hk_fix_selection.rs +++ b/crates/batten/tests/it/hk_fix_selection.rs @@ -22,7 +22,7 @@ //! # What this row does NOT decide, and where that half lives //! //! Whether hk's `fix` hook selects exactly the gate's fixer-bearing steps is -//! `fix-selection-complete`'s, a `command` row running `mise run +//! `gate fix missing`'s, a `command` row running `mise run //! fix-selection-check`. It is not answerable from lines: it needs the config //! evaluated, and evaluation is where the surprise was — the derived spelling //! evaluates correctly under `pkl` while hk's own evaluator reads it as EMPTY. @@ -64,7 +64,7 @@ use batten::rules::{self, Rule}; /// the same column census a consumer's config does. fn row() -> Rule { serde_json::from_value(serde_json::json!({ - "id": "hk-fix-selection", + "id": "gate select wrong", "kind": "policy", "scope": "tree", "sources": ["mise.toml"], @@ -208,7 +208,7 @@ fn the_fixture_shape_is_clean_too() { // --------------------------------------------------------------------------- // The prose, which is this row's half. What the config DOES is -// `fix-selection-complete`'s, and it is a command rather than a line scan. +// `gate fix missing`'s, and it is a command rather than a line scan. // --------------------------------------------------------------------------- #[test] @@ -308,7 +308,7 @@ run = "hk fix --all" #[test] fn a_tree_with_no_hook_config_is_not_judged() { - // `command-task-defined`'s measured lesson, one row over: an unguarded module + // `task bind undefined`'s measured lesson, one row over: an unguarded module // reported seven findings against a fixture that copies this config without // its subject. A repository that runs no hk hooks has no selection to judge. let root = common::scratch("hk-fix-selection-foreign"); diff --git a/crates/batten/tests/it/hk_plan.rs b/crates/batten/tests/it/hk_plan.rs index 4a3d244fb..1a85d0fda 100644 --- a/crates/batten/tests/it/hk_plan.rs +++ b/crates/batten/tests/it/hk_plan.rs @@ -35,7 +35,7 @@ use batten::hk; /// census a consumer's config does. fn row() -> batten::rules::Rule { serde_json::from_value(serde_json::json!({ - "id": "hk-plan-required", + "id": "plan require missing", "kind": "policy", "scope": "tree", "module": "policy/hk-plan-required.rego", @@ -240,7 +240,7 @@ fn a_row_naming_an_unplannable_surface_is_refused() { #[test] fn this_repositorys_plan_row_is_clean_today() { let root = common::at_root("."); - let output = common::run_at_real_root(&root, &["enforce", "--rule", "hk-plan-required"]); + let output = common::run_at_real_root(&root, &["enforce", "--rule", "plan require missing"]); assert_eq!( output.status.code(), Some(0), diff --git a/crates/batten/tests/it/hook_profile.rs b/crates/batten/tests/it/hook_profile.rs index 972723a45..54048f0cc 100644 --- a/crates/batten/tests/it/hook_profile.rs +++ b/crates/batten/tests/it/hook_profile.rs @@ -6,7 +6,7 @@ //! `input.tree["tool-verdict"]["hk-plan"]` with `with input as`, which passes //! whether or not anything can ever produce that shape — the exact class //! `rules/policy-modules.md` opens with, and the class that let -//! `validator-verdict-clean` ship deciding nothing. This file runs the real +//! `tool judge dirty` ship deciding nothing. This file runs the real //! producer and the real engine. //! //! **The three record states are the whole subject**, and this module reads them @@ -14,7 +14,7 @@ //! EMPTY is the tier having evaporated (a finding), and present-with-a-stray is //! the false green the split can produce. //! -//! # RETIREMENT LEDGER, PER PATH — what `shell-retirement` reads +//! # RETIREMENT LEDGER, PER PATH — what `shell retire partial` reads //! //! `hook-profile-check` ran `hk check --all --plan` twice and adjudicated the two //! plans in shell. The run stays outside — §9's prior art, and §5 makes `check` @@ -72,7 +72,7 @@ fn config() -> String { r#"version = 1 [[rule]] -id = "hook-profile" +id = "hook declare other" kind = "policy" scope = "tree" module = "hook-profile.rego" @@ -193,7 +193,7 @@ fn a_slow_step_missing_from_check_is_refused() { Some(2), "a slow step outside the check plan is a policy verdict\n{answer}{cause}" ); - assert!(answer.contains("hook-profile"), "{answer}{cause}"); + assert!(answer.contains("hook declare other"), "{answer}{cause}"); } #[test] diff --git a/crates/batten/tests/it/hook_skip_local.rs b/crates/batten/tests/it/hook_skip_local.rs index 94f4f0d81..59c94c408 100644 --- a/crates/batten/tests/it/hook_skip_local.rs +++ b/crates/batten/tests/it/hook_skip_local.rs @@ -13,7 +13,7 @@ //! //! # Why the exemption case is the load-bearing one //! -//! `ci-suite-lane` already governs this variable where CI sets it, so the +//! `job select missing` already governs this variable where CI sets it, so the //! DECLARED use is gated and the ad-hoc one was free — a hole shaped exactly like //! the repository's own legitimate use. That shape is what makes the exemption //! assertion matter more than the deny: a row that refused the `ci` job's own line @@ -69,7 +69,7 @@ fn denied_by_this_row(command: &str) { "the committed policy must refuse: {command}\n{out}" ); assert!( - out.contains("hook-skip-local"), + out.contains("hook skip unseen"), "the refusal for `{command}` must come from this row\n{out}" ); } @@ -126,7 +126,7 @@ fn a_step_skip_behind_a_compound_command_is_still_reached() { #[test] fn the_declared_ci_carve_is_not_judged_here() { // THE CASE THAT KEEPS THIS FROM BEING SWITCHED OFF. `.github/workflows/ci.yml` - // hands hk exactly this, and `ci-suite-lane` is the row that governs it. A + // hands hk exactly this, and `job select missing` is the row that governs it. A // guard refusing the repository's own declared invocation gets disabled, and // then it enforces nothing at all. allowed_backgrounded("HK_SKIP_STEPS=test:bats mise run ci"); diff --git a/crates/batten/tests/it/identity_precedence.rs b/crates/batten/tests/it/identity_precedence.rs index fe438c853..d323d660f 100644 --- a/crates/batten/tests/it/identity_precedence.rs +++ b/crates/batten/tests/it/identity_precedence.rs @@ -23,7 +23,7 @@ //! mode for a rule whose whole value is being readable at Stop time. //! //! It does not catch a session that reads the line and complies anyway. The -//! runnable half of that is `no-denied-identity-prescribed`, which refuses the +//! runnable half of that is `remedy carry refused`, which refuses the //! prescription in the tree, and `identity_deny` itself, which refuses the //! commit. Same shape as `scanner_taxonomy.rs`: the prose carries the position, //! and the test keeps the prose from evaporating. @@ -118,7 +118,7 @@ fn the_detail_states_why_the_hooks_predicate_cannot_be_satisfied() { // The standing mechanism is named where the detail lives, so a reader who // arrives here knows the prose is not the only thing holding the line. assert!( - detail.contains("no-denied-identity-prescribed"), + detail.contains("remedy carry refused"), "{DETAIL} must name the row that refuses the prescription in the tree" ); } diff --git a/crates/batten/tests/it/install_web.rs b/crates/batten/tests/it/install_web.rs index bf90c0193..bdfdacd65 100644 --- a/crates/batten/tests/it/install_web.rs +++ b/crates/batten/tests/it/install_web.rs @@ -63,7 +63,7 @@ const TAG: &str = "v9.9.9"; /// The repository the fixture host serves. /// /// Named by the fixture rather than defaulted, so this suite does not spell out -/// where this repository is hosted — `no-origin-literal-in-fixtures` refuses +/// where this repository is hosted — `forge name other` refuses /// that literal anywhere under `crates/batten/tests/**`, and it is right to: a /// fixture that hard-codes the origin is testing this deployment rather than the /// installer. `install.sh` reads `BATTEN_REPO`, so naming one here is the diff --git a/crates/batten/tests/it/inverted_board_cases.rs b/crates/batten/tests/it/inverted_board_cases.rs index fcf222c31..5a61cb97a 100644 --- a/crates/batten/tests/it/inverted_board_cases.rs +++ b/crates/batten/tests/it/inverted_board_cases.rs @@ -1,7 +1,7 @@ //! The mapping ledger for a case a live suite INVERTED rather than lost //! (CLOUD-908's column, this bundle's three). //! -//! `bats-tests-not-deleted` conserves case NAMES, not counts: a case name that +//! `bats count dropped` conserves case NAMES, not counts: a case name that //! disappears between `origin/main` and the head tree owes exactly one arm in //! `crates/batten/tests/*.rs`, whatever the suite's total did. That is the right //! reading and it is what caught this branch — three board-gate cases asserted a diff --git a/crates/batten/tests/it/issue_key.rs b/crates/batten/tests/it/issue_key.rs index 56fa0d7fd..0881e5434 100644 --- a/crates/batten/tests/it/issue_key.rs +++ b/crates/batten/tests/it/issue_key.rs @@ -43,7 +43,7 @@ //! //! # The declared mutation, and why the row is in THIS file //! -//! `obligations-bound` binds a §7 obligation by reading the declared file's +//! `test name undefined` binds a §7 obligation by reading the declared file's //! lines for a row beginning `#MUTANT |`, and its `line_sources` covers //! `crates/batten/tests/**` and not `crates/batten/src/**` — so the row lives //! here even though the expression it applies belongs to `ready.rs`'s reader. @@ -81,7 +81,7 @@ use common::{Fixture, git_in, run, run_with_stdin, scratch_outside_tree, stderr} const POLICY: &str = r#"version = 1 [[rule]] -id = "pr-names-an-issue" +id = "review name unnamed" kind = "shape" scope = "mediated_call" severity = "deny" @@ -91,7 +91,7 @@ base = "origin/main" reason = "name the issue in the branch, a commit, or the body" [[rule]] -id = "ready-names-an-issue" +id = "review open unnamed" kind = "shape" scope = "mediated_call" severity = "deny" @@ -347,7 +347,7 @@ fn the_refusal_names_the_route_and_leaks_no_evidence() { &payload("gh pr create --title 'tidy' --body 'no key'"), )); assert!( - refusal.contains("pr-names-an-issue"), + refusal.contains("review name unnamed"), "names the rule: {refusal}" ); // WHAT IS MISSING IS THE CLASS (CLOUD-1286): `issue name missing` says the @@ -358,7 +358,7 @@ fn the_refusal_names_the_route_and_leaks_no_evidence() { refusal.contains("issue name missing"), "says what is missing rather than that the shape is banned: {refusal}" ); - let explained = run(&dir, &["policy", "explain", "pr-names-an-issue"]); + let explained = run(&dir, &["policy", "explain", "review name unnamed"]); assert_eq!(explained.status.code(), Some(0), "the row resolves"); assert!( String::from_utf8_lossy(&explained.stdout).contains("branch"), @@ -393,7 +393,7 @@ fn a_requires_key_row_without_a_base_is_a_load_error() { .config( "version = 1\n\n\ [[rule]]\n\ - id = \"pr-names-an-issue\"\n\ + id = \"review name unnamed\"\n\ kind = \"shape\"\n\ scope = \"mediated_call\"\n\ severity = \"deny\"\n\ @@ -442,7 +442,7 @@ pub(crate) struct KeyExample { /// The consumer's own key, spelled from parts so this file carries no derivation /// of the token. /// -/// `no-tracker-key-in-core` refuses one anywhere under `crates/**`, and a test is +/// `issue name other` refuses one anywhere under `crates/**`, and a test is /// not exempt from the rule it is testing — the same dodge `tests/it/cli.rs` /// uses to seed the banned shapes it asserts on. pub(crate) fn key(n: u32) -> String { diff --git a/crates/batten/tests/it/land_entry_gates.rs b/crates/batten/tests/it/land_entry_gates.rs index 0c1172507..12467e201 100644 --- a/crates/batten/tests/it/land_entry_gates.rs +++ b/crates/batten/tests/it/land_entry_gates.rs @@ -57,7 +57,7 @@ fn fixture(name: &str) -> std::path::PathBuf { /// /// That is `rules/rust.md`'s "shown able to fail" rule inverted — a case /// asserting a conclusion over a premise the environment never created — and it -/// is worth naming that `cfg-gated-test` does NOT see this shape: the `#[cfg]` +/// is worth naming that `test cover missing` does NOT see this shape: the `#[cfg]` /// was on a block inside this helper rather than on a `#[test]`, so the cases /// compiled and ran on Windows and only their fixture was missing. The remedy is /// to make the premise real on both platforms rather than to narrow the cases. diff --git a/crates/batten/tests/it/land_hand_stepping.rs b/crates/batten/tests/it/land_hand_stepping.rs index dfb3c36d5..0ecc09ed0 100644 --- a/crates/batten/tests/it/land_hand_stepping.rs +++ b/crates/batten/tests/it/land_hand_stepping.rs @@ -13,7 +13,7 @@ //! fix", naming its own remedy, and the session hand-stepped `git fetch origin //! main`, `git rebase origin/main` and `git push` instead of handing the lap //! back to `land`. Nothing refused any of it. A `git push --force-with-lease` -//! minutes earlier WAS refused by `leased-push`, which is what makes this a gap +//! minutes earlier WAS refused by `branch write unsafe`, which is what makes this a gap //! rather than a decision somebody took. //! //! **Judged against the committed `batten.toml`, not a fixture**, on @@ -73,7 +73,7 @@ use crate::common::{run_with_stdin_at_real_root, stdout}; /// The row under test. Named rather than inferred from the verdict, because a /// refusal from any OTHER row is a different question. -const ROW: &str = "rebase-not-hand-stepped"; +const ROW: &str = "patch run loose"; /// The repository root, whose committed `batten.toml` is the policy under test. fn root() -> PathBuf { @@ -328,7 +328,7 @@ was an unterminated comment (E0758). `mutate::subjects()` enumerates the shell gates under `mise-tasks/ *.sh`, the consumer modules under `policy/ *.rego` and the preset directories, never `crates/batten/tests/`, so no sweep reaches this row today — exactly as none -reaches `mcp_dispatch.rs`'s. `obligations-bound` is satisfied and the sweep half +reaches `mcp_dispatch.rs`'s. `test name undefined` is satisfied and the sweep half is not; naming that here keeps the declaration from reading as coverage it does not have. diff --git a/crates/batten/tests/it/land_lap.rs b/crates/batten/tests/it/land_lap.rs index 94af42b6e..3bd8a9300 100644 --- a/crates/batten/tests/it/land_lap.rs +++ b/crates/batten/tests/it/land_lap.rs @@ -10,7 +10,7 @@ //! //! # The per-title rows are not what the gate counts, and that is the point //! -//! `shell-retirement` counts `arms_for(path)` and stops: two arms, one per +//! `shell retire partial` counts `arms_for(path)` and stops: two arms, one per //! deleted path. The 146 rows below are invisible to it. They exist because //! reading titles has produced a live defect once per suite across this whole //! campaign, every one in code written the same session and green under its own @@ -88,7 +88,7 @@ // carried: "THE PR BODY REACHES filed-here-check, or its exemption is inert" crates/batten/src/lib.rs kind:mechanism // carried: "CLOUD-995: a gate that exits before reading stdin is not a refusal" crates/batten/src/land.rs kind:mechanism // carried: "a body that names its issue but never closes it stops before review is asked for" crates/batten/src/lib.rs kind:mechanism -// carried: "a prose-only branch stops before review is asked for" crates/batten/src/lib.rs kind:mechanism +// carried: "a diff ship early branch stops before review is asked for" crates/batten/src/lib.rs kind:mechanism // carried: "a missing verify receipt stops the lap" crates/batten/src/lib.rs kind:mechanism // carried: "red CI stops the lap without asking for the merge" crates/batten/src/land.rs kind:mechanism // carried: "a run CI DECLINED is a stop, not a red — the agent is told to rebase" crates/batten/src/land.rs kind:mechanism diff --git a/crates/batten/tests/it/landed_check.rs b/crates/batten/tests/it/landed_check.rs index fe73bd9e0..52acde0bf 100644 --- a/crates/batten/tests/it/landed_check.rs +++ b/crates/batten/tests/it/landed_check.rs @@ -454,7 +454,7 @@ its assertion really binds. Stated plainly rather than implied: `mutate::subjects()` enumerates the shell programs under `mise-tasks`, the Rego modules under `policy`, and the preset directories — never `crates/batten/tests`, so no sweep reaches these rows today, -exactly as none reaches `mcp_dispatch.rs`'s. `obligations-bound` is satisfied (a +exactly as none reaches `mcp_dispatch.rs`'s. `test name undefined` is satisfied (a tracked file carrying the slug) and the sweep half is not. That gap is the mutation-tooling row's, and naming it here is what keeps the declaration from reading as coverage it does not have. diff --git a/crates/batten/tests/it/landing_roster.rs b/crates/batten/tests/it/landing_roster.rs index 0e8887d6f..a588431d7 100644 --- a/crates/batten/tests/it/landing_roster.rs +++ b/crates/batten/tests/it/landing_roster.rs @@ -92,7 +92,7 @@ fn install_module(root: &Path) { /// pass here. fn row() -> Rule { serde_json::from_value(serde_json::json!({ - "id": "landing-roster-guarded", + "id": "check read never", "kind": "policy", "scope": "tree", "line_sources": [".github/workflows/*.yml"], @@ -194,7 +194,7 @@ jobs: /// the guard's presence rather than on the fixture being a fixture. #[test] fn a_fixture_landing_workflow_that_consults_the_roster_is_clean() { - let root = repo("landing-roster-guarded", Some(GUARDED)); + let root = repo("check read never", Some(GUARDED)); assert!(rules_fired(&root).is_empty()); } diff --git a/crates/batten/tests/it/lock_complete.rs b/crates/batten/tests/it/lock_complete.rs index 1d0200ceb..7dfc730ad 100644 --- a/crates/batten/tests/it/lock_complete.rs +++ b/crates/batten/tests/it/lock_complete.rs @@ -21,7 +21,7 @@ //! `@test` cases. The successor is a consumer module, so no `kind:` field is //! owed. A THIRD path goes with them and owes no arm — //! `policy/lock-entry-complete.rego` is neither a `mise-tasks/` program nor a -//! `.bats` suite, so `shell-retirement` does not govern it; the note below says +//! `.bats` suite, so `shell retire partial` does not govern it; the note below says //! where its predicate went anyway, because a reader looking for it should not //! have to reconstruct that from the absence of a row. // @@ -90,7 +90,7 @@ use std::path::{Path, PathBuf}; use common::{git_in, run, scratch, stdout, write}; /// The rule ids this module publishes, which is also what `--rule` selects. -const RULE: &str = "lock-complete"; +const RULE: &str = "lock cover partial"; /// A lockfile entry for one tool, complete for whichever platforms are named. fn tool_with(name: &str, platforms: &[&str]) -> String { @@ -550,7 +550,7 @@ id = "plain-dotted-version" regex = '^[0-9]+(\.[0-9]+)*$' [[rule]] -id = "lock-complete" +id = "lock cover partial" kind = "policy" scope = "tree" module = "policy/lock-complete.rego" diff --git a/crates/batten/tests/it/mcp_reduce_array.rs b/crates/batten/tests/it/mcp_reduce_array.rs index 0f6781808..ee685c256 100644 --- a/crates/batten/tests/it/mcp_reduce_array.rs +++ b/crates/batten/tests/it/mcp_reduce_array.rs @@ -180,7 +180,7 @@ fn an_empty_page_still_reduces_because_it_is_a_real_answer() { // The other side of the case above, which is what keeps it discriminating: a // genuinely empty array IS a list, so it reduces to an empty projection with // its paging intact rather than to could-not-look. A search that found - // nothing is the answer `filing-needs-a-search` acts on. + // nothing is the answer `issue list unread` acts on. let empty = serde_json::json!({ "hasNextPage": false, "issues": [] }); let payload = mcp::payload(&framed(&empty)); let reduced = mcp::reduce(&row(LIST_ROW), &payload.value).expect("an empty page is an answer"); diff --git a/crates/batten/tests/it/mediated_verbs.rs b/crates/batten/tests/it/mediated_verbs.rs index 2e3595efc..8536ef4b1 100644 --- a/crates/batten/tests/it/mediated_verbs.rs +++ b/crates/batten/tests/it/mediated_verbs.rs @@ -94,7 +94,7 @@ fn assert_allowed(command: &str) { /// Weaker than [`assert_allowed`] on purpose, and only for commands where a /// SECOND row legitimately fires. `sed -n 1p ` is a read as far as the /// `[[verb]]` table is concerned — the property these tests exist to pin — and -/// is also, correctly, a `no-tool-substitution` deny, because printing a range +/// is also, correctly, a `tool select other` deny, because printing a range /// of a tracked file is what `Read(offset, limit)` is for (CLOUD-864). Reading /// the aggregate exit code would make one rule's arrival look like the other /// rule's regression. @@ -240,7 +240,7 @@ fn an_in_place_stream_edit_is_a_write_and_every_other_one_is_a_read() { // The read half, which a row without `requires_flag` would have refused: // every filtering invocation in the repository. assert_allowed("sed --version"); - // A TRANSFORM, and allowed outright: `no-tool-substitution` qualifies its + // A TRANSFORM, and allowed outright: `tool select other` qualifies its // `sed` entry with `-n` precisely so this stays allowed — no first-class // tool applies a substitution expression, so refusing it would state a // reason that does not hold. @@ -428,7 +428,7 @@ fn the_unqualified_rows_still_deny_and_a_read_is_still_allowed() { assert_denied(&format!("tee {GUARDED}")); assert_denied(&format!("cat x > {GUARDED}")); // Reads, and still reads to the VERB TABLE — which is what this case is - // about. `no-tool-substitution` also refuses them now, correctly, since + // about. `tool select other` also refuses them now, correctly, since // `cat`/`grep` over a tracked path is what `Read` and `Grep` are for; the // weaker assertion is what keeps that from reading as a protected-path // regression. @@ -694,7 +694,7 @@ fn an_interpreter_writing_through_a_heredoc_body_is_a_known_gap() { /// enumeration rather than lengthen it. /// NOT `cat` OR `grep`, AND THAT IS THIS REPOSITORY'S OWN CONFIG SPEAKING. Both /// are declared readers, and both are refused here by a DIFFERENT row — -/// `no-tool-substitution`, which routes a text utility over a tracked path to the +/// `tool select other`, which routes a text utility over a tracked path to the /// structured surface. Asserting them allowed would fail for a reason that has /// nothing to do with this gate, and asserting them denied would read as evidence /// about readers when it is evidence about substitution. @@ -962,7 +962,7 @@ fn a_backslash_continuation_is_one_command_and_is_still_refused() { // --- CLOUD-1258: a generic read of a memory names `read_memory` --------------- // // The third face of the object CLOUD-185 and CLOUD-864 closed the other two of. -// `no-tool-substitution` decides over shell argv, so a structured-tool call is +// `tool select other` decides over shell argv, so a structured-tool call is // invisible to it; `protected` crossed with `[[verb]]` enumerates mutations. /// A `Read` tool call naming a path, as a host sends it. diff --git a/crates/batten/tests/it/memories.rs b/crates/batten/tests/it/memories.rs index 863bedae0..a6fd143b6 100644 --- a/crates/batten/tests/it/memories.rs +++ b/crates/batten/tests/it/memories.rs @@ -48,7 +48,7 @@ use std::path::Path; use common::{git_in, run, scratch, stdout, write}; /// The rule's own id, which is also what `--rule` selects. -const RULE: &str = "memory-graph"; +const RULE: &str = "memory point missing"; /// Materialize a repository carrying the committed module and this row. /// diff --git a/crates/batten/tests/it/minted_facts.rs b/crates/batten/tests/it/minted_facts.rs index 1828454f9..7235eb908 100644 --- a/crates/batten/tests/it/minted_facts.rs +++ b/crates/batten/tests/it/minted_facts.rs @@ -39,7 +39,7 @@ use common::{batten, git_in, scratch, stderr, stdout, write}; /// The subject every case reads about. /// /// GENERIC, NEVER THIS CONSUMER'S TRACKER PREFIX. A key shape naming a specific -/// tracker inside `crates/` is non-negotiable rule 1, which `no-tracker-key-in-core` +/// tracker inside `crates/` is non-negotiable rule 1, which `issue name other` /// refuses — and it refuses a comment quoting one just as readily as an expression. const SUBJECT: &str = "ROW-1"; diff --git a/crates/batten/tests/it/mise_pin_agreement.rs b/crates/batten/tests/it/mise_pin_agreement.rs index d157a364c..fabdfeb5b 100644 --- a/crates/batten/tests/it/mise_pin_agreement.rs +++ b/crates/batten/tests/it/mise_pin_agreement.rs @@ -50,7 +50,7 @@ // engine's `2` is the policy verdict, and `2=2` is exactly the carry-over // `replay` refuses. // -// replay: tests/mise-pin-agreement.bats c9aaa5dcc43b159f1bcee7fd5a6f50b6eb0280e3 mise-tasks/mise-pin-agreement.sh mise-pin-agreement 0=0 1=2 +// replay: tests/mise-pin-agreement.bats c9aaa5dcc43b159f1bcee7fd5a6f50b6eb0280e3 mise-tasks/mise-pin-agreement.sh pin declare wrong 0=0 1=2 // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] @@ -74,7 +74,7 @@ id = "mise-tool-reference" regex = '^[a-z0-9]+:.+@.+$' [[rule]] -id = "mise-pin-agreement" +id = "pin declare wrong" kind = "policy" scope = "tree" documents = [".mcp.json", "mise.toml"] @@ -186,14 +186,14 @@ fn denied(root: &Path) { ); // THE RULE AND THE POINTER ARE THE WHOLE OBSERVABLE, and that is rule 4 // rather than a thin assertion. Measured on this tree: `batten check` renders - // `.mcp.json mise-pin-agreement` and `check -J` carries + // `.mcp.json pin declare wrong` and `check -J` carries // `{rule, path, severity, report, identity}` — the verdict TOKEN reaches // neither. So which class fired is pinned by the module's own `test_` rules, // where the class is nameable, and this tier pins that the engine builds the // input at all. A case here asserting a token would have been asserting the // renderer, and it would have been red. assert!( - text.contains("mise-pin-agreement"), + text.contains("pin declare wrong"), "the finding names the rule: {text}" ); assert!( diff --git a/crates/batten/tests/it/mise_preset.rs b/crates/batten/tests/it/mise_preset.rs index a16eeac63..7811dc71c 100644 --- a/crates/batten/tests/it/mise_preset.rs +++ b/crates/batten/tests/it/mise_preset.rs @@ -44,7 +44,7 @@ use batten::rules::{self, Rule}; /// compiles, the suite still runs, and the deny cases silently stop denying. fn row() -> Rule { serde_json::from_value(serde_json::json!({ - "id": "mise-preset-tree", + "id": "task table other", "kind": "policy", "scope": "tree", "preset": "mise", @@ -77,7 +77,7 @@ fn findings(root: &Path) -> Vec<(String, Option)> { // vendored table; its `[[pattern]]` lookups would resolve to undefined for // every real consumer, so a harness that declared any id would supply input // no consumer supplies and the deny cases below would pass for the wrong - // reason — how `ci-hygiene` once shipped two dead predicates under a green + // reason — how `job spelling wrong` once shipped two dead predicates under a green // `batten policy test` reporting 330 passed. rules::run_static( &[row()], diff --git a/crates/batten/tests/it/mutation_declared_case.rs b/crates/batten/tests/it/mutation_declared_case.rs index 053a2ff22..f5eca501f 100644 --- a/crates/batten/tests/it/mutation_declared_case.rs +++ b/crates/batten/tests/it/mutation_declared_case.rs @@ -1,4 +1,4 @@ -//! `mutation-declared-case`, over the engine that builds its input (CLOUD-1355). +//! `marker name undefined`, over the engine that builds its input (CLOUD-1355). //! //! # The seam, and why the module's own suite cannot reach it //! @@ -63,7 +63,7 @@ fn install_module(root: &Path) { /// keep honest. fn row() -> Rule { serde_json::from_value(serde_json::json!({ - "id": "mutation-declared-case", + "id": "marker name undefined", "kind": "policy", "scope": "tree", "line_sources": [ @@ -98,7 +98,7 @@ fn verdicts(root: &Path) -> Vec { .collect() } -const UNDEFINED: &str = "mutation-declared-case"; +const UNDEFINED: &str = "marker name undefined"; /// The declaring file, as a module carrying a suite declaration and one row. fn declaring(suite: &str, case: &str) -> String { diff --git a/crates/batten/tests/it/nextest_slow.rs b/crates/batten/tests/it/nextest_slow.rs index fb42bf5a0..c8c5e14e8 100644 --- a/crates/batten/tests/it/nextest_slow.rs +++ b/crates/batten/tests/it/nextest_slow.rs @@ -9,7 +9,7 @@ //! //! # The absent-file case is the one that earns this file //! -//! `landing-roster-guarded` shipped with a could-not-look arm that could never +//! `check read never` shipped with a could-not-look arm that could never //! fire: its row named a GLOB, and a glob matching zero files leaves the rule //! SKIPPED rather than evaluated over an empty document, so a branch deleting the //! subject passed clean. `.config/nextest.toml` is a literal path rather than a @@ -79,7 +79,7 @@ fn install_module(root: &Path) { /// pass here. fn row() -> Rule { serde_json::from_value(serde_json::json!({ - "id": "nextest-slow", + "id": "suite grade late", "kind": "policy", "scope": "tree", "lines": [CONFIG], @@ -134,7 +134,7 @@ fn the_committed_config_declares_a_terminating_slow_timeout() { } /// THE CASE THIS TIER EXISTS FOR, and the one a `with input as` case cannot -/// decide. `landing-roster-guarded` carried a could-not-look arm that never fired +/// decide. `check read never` carried a could-not-look arm that never fired /// because its row named a glob and a glob matching nothing leaves the rule /// SKIPPED. This row names a literal path; that it therefore still acquires the /// source and still refuses is measured here rather than assumed. @@ -216,7 +216,7 @@ fn a_period_in_an_unconvertible_unit_is_refused() { /// AND A COMMENTED-OUT DECLARATION IS NOT ONE. The ordinary shape of "this was /// flaky, disabling it for now" leaves the text in the file, which is exactly how -/// `landing-roster-guarded`'s first draft was defeated by its own documentation. +/// `check read never`'s first draft was defeated by its own documentation. #[test] fn a_commented_declaration_does_not_arm_the_ban() { let root = repo( diff --git a/crates/batten/tests/it/obligations_bound.rs b/crates/batten/tests/it/obligations_bound.rs index 295abdbac..7a95d7ac8 100644 --- a/crates/batten/tests/it/obligations_bound.rs +++ b/crates/batten/tests/it/obligations_bound.rs @@ -1,4 +1,4 @@ -//! `obligations-bound`, over the engine that builds its input (CLOUD-472). +//! `test name undefined`, over the engine that builds its input (CLOUD-472). //! //! # The seam, and why the module's own suite cannot reach it //! @@ -101,7 +101,7 @@ fn install_module(root: &Path) { /// obligation unbindable for a reason no message would name. fn row() -> Rule { serde_json::from_value(serde_json::json!({ - "id": "obligations-bound", + "id": "test name undefined", "kind": "policy", "scope": "tree", "base": "origin/main", @@ -341,7 +341,7 @@ fn the_committed_row_is_the_one_these_cases_exercise() { .rules; let declared = committed .iter() - .find(|rule| rule.id == "obligations-bound") + .find(|rule| rule.id == "test name undefined") .expect("the committed config declares the row this suite exercises"); assert_eq!(declared.kind, RuleKind::Policy); assert_eq!(declared.scope, RuleScope::Tree); diff --git a/crates/batten/tests/it/perf_assert.rs b/crates/batten/tests/it/perf_assert.rs index 5500b00a5..6069d150c 100644 --- a/crates/batten/tests/it/perf_assert.rs +++ b/crates/batten/tests/it/perf_assert.rs @@ -22,16 +22,16 @@ //! **The producer is RUN, never planted.** `batten record tool` writes the record //! and `batten check` reads it back, so these cases prove the writer and the //! reader compose the SAME key. A hand-written record agrees with the reader by -//! construction, which is how `validator-verdict-clean` shipped resolving `null` +//! construction, which is how `tool judge dirty` shipped resolving `null` //! on every real checkout — the only writer in the tree was a test helper. //! //! The module read here is the COMMITTED one, copied into each scratch tree, and //! the pattern row is derived from the committed table rather than restated: an //! inline copy of either would drift and pass while the real gate was broken. //! -//! # RETIREMENT LEDGER, PER PATH — what `shell-retirement` reads +//! # RETIREMENT LEDGER, PER PATH — what `shell retire partial` reads //! -//! `perf-assert.sh` was a pure function of stdin that adjudicated two questions +//! `path measure wrong.sh` was a pure function of stdin that adjudicated two questions //! over data something else measured. Both move: the measurement verdict onto the //! `perf-p95` `[[rule.tools]]` row, and the README-agreement clause onto the //! module. The MEASUREMENT itself never lived in the program and does not move — @@ -127,7 +127,7 @@ fn pattern_rows() -> String { /// This selected on `starts_with("path measure")`, `("prose state")` and /// `("source read")`, which reads as "the module's families" and is really "every /// class anybody ever names that way". A rebase brought in `prose state other`, -/// raised by an unrelated `pr-partition-restated` row; the prefix swept it into a +/// raised by an unrelated `review state other` row; the prefix swept it into a /// bundle that enables ONE module, nothing there raises it, and the registry's own /// both-directions check failed the config LOAD. The fixture cannot grow a class /// its module does not raise, so the list is the four ids and the coupling to @@ -172,7 +172,7 @@ fn config() -> String { r#"version = 1 [[rule]] -id = "perf-assert" +id = "path measure wrong" kind = "policy" scope = "tree" lines = ["README.md"] diff --git a/crates/batten/tests/it/perf_compare.rs b/crates/batten/tests/it/perf_compare.rs index 048ef28c2..b4a7cd6a5 100644 --- a/crates/batten/tests/it/perf_compare.rs +++ b/crates/batten/tests/it/perf_compare.rs @@ -46,7 +46,7 @@ // // `perf-compare.bats` had 20 cases and `perf-gate.bats` 6, and every title below // is the retired suite's own — read off the deleted files rather than restated -// from the design, which is the error `bats-tests-not-deleted` caught on the +// from the design, which is the error `bats count dropped` caught on the // first draft of this block. // // carried: "a pair within the threshold passes, and names the threshold" crates/batten/tests/it/perf_compare.rs diff --git a/crates/batten/tests/it/perf_pair.rs b/crates/batten/tests/it/perf_pair.rs index a0d64bbcc..9e48cbaee 100644 --- a/crates/batten/tests/it/perf_pair.rs +++ b/crates/batten/tests/it/perf_pair.rs @@ -28,7 +28,7 @@ //! # The retirement ledger //! //! `mise-tasks/perf-pair.sh` and `tests/perf-pair.bats` are retired here under -//! CLOUD-1059. The rows below are what `shell-retirement`'s arm C reads. +//! CLOUD-1059. The rows below are what `shell retire partial`'s arm C reads. //! //! WHY IT WAS MIGRATED AT ALL, which is the campaign working on its author a //! second time (after `semver`). CLOUD-875 is a repair to the SKIP, and making it @@ -58,11 +58,11 @@ // subsumed: "each wired arm runs in its OWN tree, which is what replaced the cd" crates/batten/src/perf.rs kind:verb // subsumed: "failures are not ignored — a broken binary is timeable and must not pass" crates/batten/src/perf.rs kind:verb // -// The arm census. `perf-assert` still budgets the paths, and the port must still +// The arm census. `path measure wrong` still budgets the paths, and the port must still // pair every one of them — but the assertion moved from counting `^pair ` lines // in a shell file to the plan the module builds. // -// carried: "every path perf-assert budgets is paired here" crates/batten/tests/it/perf_pair.rs +// carried: "every path path measure wrong budgets is paired here" crates/batten/tests/it/perf_pair.rs // // The worktree recovery, and this pair is the most interesting entry in the // ledger. Both cases guarded a MEASURED defect (2026-08-14): `git worktree add` @@ -288,10 +288,10 @@ fn the_keyed_base_directory_survives_the_per_run_wipe() { ); } -/// Every path `perf-assert` budgets is still paired. +/// Every path `path measure wrong` budgets is still paired. /// /// CARRIED from the retired suite, and the assertion had to move rather than be -/// dropped: it caught a real hole once already (CLOUD-697, where `perf-assert` +/// dropped: it caught a real hole once already (CLOUD-697, where `path measure wrong` /// budgeted four paths and the pair measured three, so `perf-compare` was blind /// to `wired` — the entry point an agent actually waits on). /// @@ -344,7 +344,7 @@ fn every_path_perf_assert_budgets_is_paired() { for path in budgeted { assert!( module.contains(&format!("\"{path}\"")), - "`perf-assert` budgets `{path}` and the pair does not measure it" + "`path measure wrong` budgets `{path}` and the pair does not measure it" ); } } diff --git a/crates/batten/tests/it/pipeline_shapes.rs b/crates/batten/tests/it/pipeline_shapes.rs index 51a91a331..8b4d60ea6 100644 --- a/crates/batten/tests/it/pipeline_shapes.rs +++ b/crates/batten/tests/it/pipeline_shapes.rs @@ -17,7 +17,7 @@ //! `cargo` row, which is exactly the drift the corpus exists to catch. //! //! Every `cargo` sample is written `mise exec -- cargo …` since CLOUD-271: the -//! committed `no-bare-cargo` row refuses the unmediated route outright, so a +//! committed `cargo run loose` row refuses the unmediated route outright, so a //! bare spelling would make the allows fail and — worse — make the denies pass //! for the wrong row, which is coverage that has stopped testing this predicate. //! The wrapper look-through means the mediated form is still judged as `cargo`, @@ -165,7 +165,7 @@ fn an_and_chain_is_allowed_because_it_cannot_manufacture_a_green() { assert_allowed_backgrounded("mise run fmt && mise run verify"); // THE GIT-FAMILY ARM, AND ITS OPERAND MOVED (CLOUD-1351). This read // `git fetch origin main && git rebase origin/main`, and that command is now - // DENIED — by `rebase-not-hand-stepped`, for hand-stepping a step + // DENIED — by `patch run loose`, for hand-stepping a step // `mise run land` drives, which is a different row answering a different // question. The claim here is about `&&` alone, so an operand another row // independently refuses makes the case ambiguous: it would fail while @@ -293,7 +293,7 @@ fn the_refusal_states_the_principle_rather_than_naming_one_command() { // renders one cause, and this case asserts WHICH row the reader is sent to. let refusal = cause_backgrounded("mise run verify | tail -6"); assert!( - refusal.contains("verdict-not-discarded"), + refusal.contains("verdict guard missing"), "names the rule: {refusal}" ); // The principle now travels as the declared class rather than as a sentence @@ -310,7 +310,7 @@ fn the_refusal_states_the_principle_rather_than_naming_one_command() { // survive the move: `explain` prints the principle in full rather than the // narrower wording an agent complied with literally and then re-broke on the // next command (CLOUD-199). - let explained = run(&root(), &["policy", "explain", "verdict-not-discarded"]); + let explained = run(&root(), &["policy", "explain", "verdict guard missing"]); assert_eq!(explained.status.code(), Some(0), "the row resolves"); assert!( String::from_utf8_lossy(&explained.stdout).contains("run_in_background"), @@ -358,8 +358,8 @@ fn each_shape_renders_its_own_cause() { // The second predicate this kind carries. Same file as the discard family // because they are decided over the same parse and by the same row kind, and // splitting them would hide that the ALLOW half of each is the other's deny: -// a filter downstream of a pipe is refused by `verdict-not-discarded` when its -// producer carries a verdict, and allowed by `no-tool-substitution` always. +// a filter downstream of a pipe is refused by `verdict guard missing` when its +// producer carries a verdict, and allowed by `tool select other` always. #[test] fn a_text_utility_aimed_at_a_repository_path_is_refused() { @@ -452,7 +452,7 @@ fn the_same_utility_downstream_of_a_pipe_is_a_filter_and_is_untouched() { #[test] fn a_target_outside_the_repository_is_not_a_substitution() { - // `>/tmp/.log` is the shape `verdict-not-discarded` MANDATES, so a row + // `>/tmp/.log` is the shape `verdict guard missing` MANDATES, so a row // that refused reading one back would put the two rows in contradiction. assert_allowed("cat /tmp/verify.log"); assert_allowed("tail -20 /tmp/land.log"); diff --git a/crates/batten/tests/it/plan_complete.rs b/crates/batten/tests/it/plan_complete.rs index 42d98a342..46f569405 100644 --- a/crates/batten/tests/it/plan_complete.rs +++ b/crates/batten/tests/it/plan_complete.rs @@ -1,4 +1,4 @@ -//! `plan-complete`, over the engine that builds its input (CLOUD-472). +//! `plan cover partial`, over the engine that builds its input (CLOUD-472). //! //! # The seam this tier owns, and why the module's own suite cannot reach it //! @@ -111,7 +111,7 @@ fn install_module(root: &Path) { fn row() -> Rule { serde_json::from_value(serde_json::json!({ - "id": "plan-complete", + "id": "plan cover partial", "kind": "policy", "scope": "tree", "base": "origin/main", @@ -310,7 +310,7 @@ fn the_committed_row_is_the_one_these_cases_exercise() { .rules; let declared = committed .iter() - .find(|rule| rule.id == "plan-complete") + .find(|rule| rule.id == "plan cover partial") .expect("the committed config declares the row this suite exercises"); assert_eq!(declared.kind, RuleKind::Policy); assert_eq!(declared.scope, RuleScope::Tree); diff --git a/crates/batten/tests/it/pointer_only.rs b/crates/batten/tests/it/pointer_only.rs index 43ba583b0..104539416 100644 --- a/crates/batten/tests/it/pointer_only.rs +++ b/crates/batten/tests/it/pointer_only.rs @@ -1419,7 +1419,7 @@ const CENSUS: &[Verb] = &[ // which is 66 of this config's 128 rows. Verb { path: "policy rule", - args: &["no-bare-cargo"], + args: &["cargo run loose"], stdin: Stdin::Nothing, disposition: Disposition::Echoes( "the answer IS a `[[rule]]` row's declared remedy, echoed back to the caller who \ @@ -1445,7 +1445,7 @@ const CENSUS: &[Verb] = &[ path: "override request", args: &[ "--rule", - "prose-only", + "diff ship early", "--verdict", "path write refused", "--subject", @@ -1467,7 +1467,7 @@ const CENSUS: &[Verb] = &[ "--admission", "0000000000000000000000000000000000000000000000000000000000000000", "--rule", - "prose-only", + "diff ship early", "--verdict", "path write refused", "--subject", diff --git a/crates/batten/tests/it/policy_presets.rs b/crates/batten/tests/it/policy_presets.rs index a633a88a4..d18b5c84f 100644 --- a/crates/batten/tests/it/policy_presets.rs +++ b/crates/batten/tests/it/policy_presets.rs @@ -538,7 +538,7 @@ fn every_shipped_preset_passes_its_own_suite() { // NOT MEDIATED HERE, and the reason is this loop's own bound rather // than a gap (CLOUD-857). `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 + // `shell spelling wrong` is enabled `scope = "tree"` in this repository and its // two modules decide over files, not commands. Asking them whether a // test ever passed a compound COMMAND would be judging a surface this // helper invented, which is the fabricated-shape defect one level up. @@ -598,7 +598,7 @@ fn tree_preset_row(id: &str, preset: &str) -> Rule { /// loads and decides for a consumer who wrote no `[[pattern]]` and no /// `[[verdict]]` row at all. A harness that declared the ids would supply input /// no consumer supplies, and the deny cases would then pass for the wrong reason -/// — which is how CLOUD-1161's `ci-hygiene` shipped two dead predicates under a +/// — which is how CLOUD-1161's `job spelling wrong` shipped two dead predicates under a /// green `batten policy test` reporting 330 passed. fn loaded(name: &str, row: Rule) -> policy::Bundle { let root = scratch(name); diff --git a/crates/batten/tests/it/policy_severity.rs b/crates/batten/tests/it/policy_severity.rs index 705ca3330..9ebfdff31 100644 --- a/crates/batten/tests/it/policy_severity.rs +++ b/crates/batten/tests/it/policy_severity.rs @@ -12,7 +12,7 @@ //! "mediated_call"` policy row **denied exactly as `deny` did**, silently, which //! is the one direction a severity column must never fail in. //! -//! `pinned-toolchain-preset` was live in that state: `batten.toml` declares it +//! `pin table other` was live in that state: `batten.toml` declares it //! `warn` — with a comment explaining that the first landing must not refuse — //! and it refused. //! diff --git a/crates/batten/tests/it/pr_partition_restated.rs b/crates/batten/tests/it/pr_partition_restated.rs index d17df07f0..03e38b1b9 100644 --- a/crates/batten/tests/it/pr_partition_restated.rs +++ b/crates/batten/tests/it/pr_partition_restated.rs @@ -1,4 +1,4 @@ -//! `pr-partition-restated` over the compiled binary. +//! `review state other` over the compiled binary. //! //! **What the module's own `test_` rules cannot answer.** They substitute their //! own vocabulary for `data.batten.patterns` and hand the predicate a fabricated @@ -64,7 +64,7 @@ id = "pr-partition-prose" regex = '{PATTERN}' [[rule]] -id = "pr-partition-restated" +id = "review state other" kind = "policy" scope = "tree" line_sources = ["prose/*.md"] @@ -126,7 +126,7 @@ fn refuses(name: &str, prose: &str) { Some(2), "{name}: this prose must refuse\n{answer}{cause}" ); - assert!(answer.contains("pr-partition-restated"), "{answer}{cause}"); + assert!(answer.contains("review state other"), "{answer}{cause}"); } #[test] @@ -212,7 +212,7 @@ fn an_unreadable_source_is_reported_rather_than_passed() { Some(2), "a declared source that will not read must refuse, never pass\n{answer}{cause}" ); - assert!(answer.contains("pr-partition-restated"), "{answer}{cause}"); + assert!(answer.contains("review state other"), "{answer}{cause}"); } #[test] diff --git a/crates/batten/tests/it/prebuilt_lint.rs b/crates/batten/tests/it/prebuilt_lint.rs index 161dd1006..6577bbee8 100644 --- a/crates/batten/tests/it/prebuilt_lint.rs +++ b/crates/batten/tests/it/prebuilt_lint.rs @@ -12,9 +12,9 @@ //! The bats suite stood the WHOLE shipped config up in a fixture — symlinked //! manifest and sources, a copied `batten.toml`, a copied `AGENTS.md` for //! `[budget.instructions]`, a `.serena/project.yml` for `[[embedded]]`, a -//! provisioned ripsecrets stub for `no-secrets`, a resolvable `origin/main` for the +//! provisioned ripsecrets stub for `source carry unsafe`, a resolvable `origin/main` for the //! `ratchet` rows, and a task namespace synthesised from every `mise run ` in -//! the config so `command-task-defined` would not fire. Every one of those was a +//! the config so `task bind undefined` would not fire. Every one of those was a //! precondition of running the config, not of testing these two rows. //! //! These cases run the two rows and nothing else, so none of that is owed. That is @@ -36,10 +36,10 @@ use std::path::{Path, PathBuf}; use batten::rules::{self, Rule}; -/// `no-source-built-tool` as `batten.toml` declares it. +/// `pin add unsafe` as `batten.toml` declares it. fn no_source_built_tool() -> Rule { serde_json::from_value(serde_json::json!({ - "id": "no-source-built-tool", + "id": "pin add unsafe", "kind": "forbid", "glob": "mise.toml", "pattern": "\"cargo:", @@ -50,10 +50,10 @@ fn no_source_built_tool() -> Rule { .expect("the row batten.toml declares") } -/// `no-cargo-install-in-ci` as `batten.toml` declares it. +/// `cargo add loose` as `batten.toml` declares it. fn no_cargo_install_in_ci() -> Rule { serde_json::from_value(serde_json::json!({ - "id": "no-cargo-install-in-ci", + "id": "cargo add loose", "kind": "forbid", "glob": ".github/workflows/*.yml", "pattern": "cargo install", @@ -138,14 +138,14 @@ fn this_repository_is_clean_today() { } // --------------------------------------------------------------------------- -// `no-source-built-tool`: the mistake, and the shape that is not it. +// `pin add unsafe`: the mistake, and the shape that is not it. // --------------------------------------------------------------------------- // carried: "a cargo: backend in mise.toml is a violation, named and located" crates/batten/tests/it/prebuilt_lint.rs #[test] fn a_cargo_backend_in_the_manifest_is_a_violation_named_and_located() { let root = tools_with("cargo-backend", "\"cargo:cargo-hack\" = \"0.6\""); - assert_eq!(findings(&root), vec!["mise.toml:3 no-source-built-tool"]); + assert_eq!(findings(&root), vec!["mise.toml:3 pin add unsafe"]); } // carried: "a prebuilt backend is not a violation — the rule bans compiling, not installing" crates/batten/tests/it/prebuilt_lint.rs @@ -162,7 +162,7 @@ fn a_prebuilt_backend_is_not_a_violation() { } // --------------------------------------------------------------------------- -// `no-cargo-install-in-ci`: the same pair, spelled by hand in a workflow step. +// `cargo add loose`: the same pair, spelled by hand in a workflow step. // --------------------------------------------------------------------------- // carried: "cargo install in a workflow is a violation" crates/batten/tests/it/prebuilt_lint.rs @@ -172,7 +172,7 @@ fn cargo_install_in_a_workflow_is_a_violation() { workflow_with(&root, "cargo install cargo-hack"); assert_eq!( findings(&root), - vec![".github/workflows/t.yml:8 no-cargo-install-in-ci"] + vec![".github/workflows/t.yml:8 cargo add loose"] ); } diff --git a/crates/batten/tests/it/preset_segments.rs b/crates/batten/tests/it/preset_segments.rs index b928feeb8..c7f2eb3f6 100644 --- a/crates/batten/tests/it/preset_segments.rs +++ b/crates/batten/tests/it/preset_segments.rs @@ -17,7 +17,7 @@ //! one the engine produces constantly and the tests never did. //! //! **Judged against the committed `batten.toml`**, which enables -//! `trunk-based-preset` like any other consumer would (CLOUD-836). A fixture-only +//! `trunk push forced` like any other consumer would (CLOUD-836). A fixture-only //! suite would stay green after someone disabled the row, which is exactly the //! drift a corpus over the real config exists to catch. //! @@ -160,7 +160,7 @@ fn force_with_lease_survives_segmentation() { // the bypass rather than toward the safer flag. // // ASSERTED AS "THE PRESET DOES NOT FIRE" rather than as a clean exit, because - // this consumer now declares `leased-push` over the BARE spelling and the two + // this consumer now declares `branch write unsafe` over the BARE spelling and the two // statements are different. The preset's distinction is what this case // is named for and it is unchanged; whether THIS repository additionally // refuses the leased spelling is a consumer decision the preset has no view on. diff --git a/crates/batten/tests/it/privileged_lane.rs b/crates/batten/tests/it/privileged_lane.rs index 59c054cfe..a041acaf3 100644 --- a/crates/batten/tests/it/privileged_lane.rs +++ b/crates/batten/tests/it/privileged_lane.rs @@ -6,7 +6,7 @@ //! subject was `policy/privileged-lane.rego`, which CLOUD-1050 rewrote: the //! module's refusal stopped being prose and became a declared class, so the //! suite's fixture — a `batten.toml` with no `[[verdict]]` row — stopped -//! loading. `shell-retirement` refuses editing a bats suite in place, so the +//! loading. `shell retire partial` refuses editing a bats suite in place, so the //! open door is the migration. Every case below carries a `// carried:` arm. //! //! # What it keeps @@ -55,7 +55,7 @@ fn fixture(name: &str, workflow: &str, body: &str) -> PathBuf { concat!( "version = 1\n\n", "[[rule]]\n", - "id = \"privileged-lane-tests-origin\"\n", + "id = \"lane guard other\"\n", "kind = \"policy\"\n", "scope = \"tree\"\n", "sources = [\".github/workflows/*.yml\"]\n", @@ -124,7 +124,7 @@ fn denied(root: &Path) { String::from_utf8_lossy(&output.stderr) ); assert!( - text.contains("privileged-lane-tests-origin"), + text.contains("lane guard other"), "the finding names the rule: {text}" ); } diff --git a/crates/batten/tests/it/prose_only.rs b/crates/batten/tests/it/prose_only.rs index d942064db..625b8bcb3 100644 --- a/crates/batten/tests/it/prose_only.rs +++ b/crates/batten/tests/it/prose_only.rs @@ -23,7 +23,7 @@ //! The shell classified diff LINES; the engine compares REMAINDERS. Two cases //! here discriminate that directly and neither could have been written against //! the shell: a block of code moved within a file (identical remainders, so -//! prose-only holds even though every line of it appears as `+` and `-`), and a +//! diff ship early holds even though every line of it appears as `+` and `-`), and a //! comment reflowed across a boundary. // THE FILE-GRANULARITY RETIREMENT ARM (CLOUD-1059). Its grammar is disjoint from @@ -48,7 +48,7 @@ use batten::rules::{self, Rule}; /// the same column census a consumer's config does. fn row() -> Rule { serde_json::from_value(serde_json::json!({ - "id": "prose-only", + "id": "diff ship early", "kind": "policy", "scope": "tree", "base": "origin/main", @@ -129,8 +129,8 @@ fn refused(root: &Path) { let findings = findings(root); assert_eq!( findings, - vec!["prose-only".to_owned()], - "the branch should be priced as prose-only" + vec!["diff ship early".to_owned()], + "the branch should be priced as diff ship early" ); } @@ -431,8 +431,8 @@ fn a_comment_only_change_to_a_declaration_language_is_refused() { ); assert_eq!( findings(&root), - vec!["prose-only".to_owned()], - "a comment-only change to {path} must be priced as prose-only" + vec!["diff ship early".to_owned()], + "a comment-only change to {path} must be priced as diff ship early" ); } } diff --git a/crates/batten/tests/it/prospective_facts.rs b/crates/batten/tests/it/prospective_facts.rs index 895a61e57..1f80809f8 100644 --- a/crates/batten/tests/it/prospective_facts.rs +++ b/crates/batten/tests/it/prospective_facts.rs @@ -294,7 +294,7 @@ fn a_tool_carrying_no_content_could_not_look_rather_than_landing_nothing() { #[test] fn a_planted_secret_in_the_content_reaches_no_output_and_nothing_on_disk() { // Assembled rather than written, for the reason `contract_drift.rs` gives: - // a credential-shaped literal in a tracked file is what `no-secrets` exists + // a credential-shaped literal in a tracked file is what `source carry unsafe` exists // to catch, and it is right to. let planted = format!("{}_{}", "ghp", "thisIsTheSortOfThingAWriteMustNeverEcho"); let dir = scratch("prospective-pointer-only"); diff --git a/crates/batten/tests/it/ratchet.rs b/crates/batten/tests/it/ratchet.rs index 1a5878d8b..b9b3fbcd5 100644 --- a/crates/batten/tests/it/ratchet.rs +++ b/crates/batten/tests/it/ratchet.rs @@ -29,14 +29,14 @@ const BASE_SRC: &str = /// A config carrying one ratchet row over `src/**/*.rs`. fn ratchet_config(pattern: &str, direction: &str, severity: &str) -> String { format!( - "version = 1\n\n[[rule]]\nid = \"tests-not-deleted\"\nkind = \"ratchet\"\nglob = \"src/**/*.rs\"\npattern = \"{pattern}\"\ndirection = \"{direction}\"\nbase = \"main\"\nseverity = \"{severity}\"\n" + "version = 1\n\n[[rule]]\nid = \"test count dropped\"\nkind = \"ratchet\"\nglob = \"src/**/*.rs\"\npattern = \"{pattern}\"\ndirection = \"{direction}\"\nbase = \"main\"\nseverity = \"{severity}\"\n" ) } /// A repo whose base commit carries [`BASE_SRC`], with `config` committed. /// /// `base = "main"` rather than a remote-tracking ref: the fixtures carry no -/// origin literal (`no-origin-literal-in-fixtures`), and a local branch proves +/// origin literal (`forge name other`), and a local branch proves /// the same plumbing. fn ratchet_repo(name: &str, config: &str) -> PathBuf { let dir = Fixture::new(name) @@ -74,7 +74,7 @@ fn deleting_a_test_is_a_violation_naming_the_two_counts() { ); let text = stdout(&output); assert!( - text.contains("tests-not-deleted"), + text.contains("test count dropped"), "the finding names the rule: {text:?}" ); assert!( @@ -202,7 +202,7 @@ fn a_warn_row_reports_without_failing_until_promoted() { "a warn row reports without failing" ); assert!( - stdout(&output).contains("tests-not-deleted"), + stdout(&output).contains("test count dropped"), "and it does report: {}", stdout(&output) ); @@ -222,7 +222,7 @@ fn an_unresolvable_base_is_a_usage_error_naming_the_rev() { // deleted" having looked at nothing. let dir = ratchet_repo( "ratchet-bad-base", - "version = 1\n\n[[rule]]\nid = \"tests-not-deleted\"\nkind = \"ratchet\"\nglob = \"src/**/*.rs\"\npattern = \"#[test]\"\ndirection = \"non_decreasing\"\nbase = \"no-such-rev\"\nseverity = \"deny\"\n", + "version = 1\n\n[[rule]]\nid = \"test count dropped\"\nkind = \"ratchet\"\nglob = \"src/**/*.rs\"\npattern = \"#[test]\"\ndirection = \"non_decreasing\"\nbase = \"no-such-rev\"\nseverity = \"deny\"\n", ); let output = check(&dir); assert_eq!( @@ -368,7 +368,7 @@ fn a_waiver_suppresses_a_ratchet_and_a_lapsed_one_does_not() { // and tells nobody. let with_expiry = |expires: &str| { format!( - "{}\n[[waiver]]\nrule = \"tests-not-deleted\"\nreason = \"tracked in CLOUD-1; the suite is being consolidated\"\nexpires = \"{expires}\"\n", + "{}\n[[waiver]]\nrule = \"test count dropped\"\nreason = \"tracked in CLOUD-1; the suite is being consolidated\"\nexpires = \"{expires}\"\n", ratchet_config("#[test]", "non_decreasing", "deny") ) }; @@ -563,7 +563,7 @@ fn retirement_repo(name: &str, retires_with: Option<&str>) -> PathBuf { /// /// The base state is the FIRST commit rather than a second one plus a moved /// branch: git refuses to force a branch that is checked out, which is the same -/// defect `no-branch-f-main` gates on the bats corpus. +/// defect `branch edit unsafe` gates on the bats corpus. fn retirement_repo_declaring(name: &str, retires_with: Option<&str>, alpha: &str) -> PathBuf { let dir = Fixture::new(name) .config(&retirement_config(retires_with)) @@ -972,7 +972,7 @@ fn a_complete_mapping_is_the_second_admission_for_a_decrease() { // Arm (e) used to read: a complete mapping is NOT a second way to buy a // decrease, the subject still has to die. That composition is unsatisfiable // for the case CLOUD-1059 creates. A Bats suite whose subject is a LIVE - // `.rego` module cannot be edited in place — `shell-retirement` refuses + // `.rego` module cannot be edited in place — `shell retire partial` refuses // exactly that — and its subject is not dying, because the module is what // the migration keeps. So the only two doors were both shut, and a rule with // no open door is not a ratchet, it is a wall. @@ -2011,7 +2011,7 @@ fn removing_an_inline_body_never_violates_either_row() { // OWN existence — which script to prefer, what to fetch, what to verify about the // fetched bytes — and have no successor because they should have no subject. -// THE FILE-LEVEL ARM, which is the same ledger one granularity up. `shell-retirement` +// THE FILE-LEVEL ARM, which is the same ledger one granularity up. `shell retire partial` // reads these same markers keyed on the retired PATH rather than on a quoted case, so // the suite owes a row here as well as the eight case rows below — 908 conserves the // cases, 1059 conserves the file, and a withdrawal has to be spellable at both or the diff --git a/crates/batten/tests/it/raw_tracker_read.rs b/crates/batten/tests/it/raw_tracker_read.rs index d730aa8bc..71dcde4d0 100644 --- a/crates/batten/tests/it/raw_tracker_read.rs +++ b/crates/batten/tests/it/raw_tracker_read.rs @@ -125,14 +125,14 @@ fn every_spelling_of_the_raw_read_is_refused() { /// it that way. /// /// `save_issue` is deliberately NOT asserted here. It is refused by -/// `an-update-owes-a-recent-read` whenever no fresh receipt exists, so a case +/// `issue read stale` whenever no fresh receipt exists, so a case /// asserting either verdict for it would be pinning a different row's behaviour /// under this row's name. #[test] fn the_row_reaches_no_verb_it_does_not_name() { let repo = repo("raw-read-controls"); for tool in [ - // The search `filing-needs-a-search` requires must stay open. + // The search `issue list unread` requires must stay open. "mcp__Linear__list_issues", // Already projected on every measured call, and a `shape` row cannot // express "without `fields`". @@ -177,7 +177,7 @@ fn the_refusal_carries_no_byte_of_the_call() { ); assert_eq!(output.status.code(), Some(2), "the row must refuse"); assert!( - rendered.contains("no-raw-issue-read"), + rendered.contains("issue read loose"), "the refusal must name the row so a reader can find its remedy: {rendered}" ); assert!( diff --git a/crates/batten/tests/it/ready.rs b/crates/batten/tests/it/ready.rs index 6f5948e94..4d59db6d8 100644 --- a/crates/batten/tests/it/ready.rs +++ b/crates/batten/tests/it/ready.rs @@ -53,7 +53,7 @@ //! The claim gate's half of the same port DID land — its only caller named it by //! task name, which is why `mise.toml` could answer for it unchanged. //! -//! # RETIREMENT LEDGER, PER PATH — what `shell-retirement` reads +//! # RETIREMENT LEDGER, PER PATH — what `shell retire partial` reads //! // carried: mise-tasks/ready-lint.sh crates/batten/src/ready.rs kind:verb crates/batten/tests/it/ready.rs // carried: tests/ready-lint.bats crates/batten/src/ready.rs kind:verb crates/batten/tests/it/ready.rs @@ -376,7 +376,7 @@ fn with_pressure_test(name: &str, runner_exits: Option) -> PathBuf { "batten.toml", &format!( "version = 1\n\n[ready]\npressure_test_required_from = \"2026-06-01T00:00:00.000Z\"\n\n\ - [[rule]]\nid = \"review-dispatched\"\nkind = \"policy\"\nscope = \"tree\"\n\ + [[rule]]\nid = \"prompt run never\"\nkind = \"policy\"\nscope = \"tree\"\n\ module = \"policy/review-dispatched.rego\"\nseverity = \"deny\"\n\n\ [[rule.review]]\nid = \"ready-pressure-test-body\"\nprompt = \"ready-pressure-test\"\n\ runner = '{}'\nversion = \"0\"\nsubject = \"tracker-body\"\n\n{}", @@ -681,7 +681,7 @@ fn a_gate_that_names_no_task_is_refused() { // WHETHER THE TASK EXISTS IS NOT ASKED HERE, and that is rule 1 rather than // an omission — resolving it means the core naming the consumer's task // manifest, which `document_facts.rs` refuses and which caught the first - // draft of this doing it. `batten.toml`'s `command-task-defined` already + // draft of this doing it. `batten.toml`'s `task bind undefined` already // decides that question over the consumer's own declaration, so asking it // twice would be a second authority with only the newer one deciding. let dir = with_tasks("ready-claims-gate-task"); @@ -839,7 +839,7 @@ fn the_object_wins_and_the_prose_goes_unread() { #[test] fn a_prose_only_block_still_passes_and_is_named_as_a_dialect() { - // EVERY ISSUE READY TODAY IS STILL READY. Refusing a prose-only block would + // EVERY ISSUE READY TODAY IS STILL READY. Refusing a diff ship early block would // refuse ~40 refined rows for being written before the mechanism existed, // which is the recognise-to-report bargain this gate already runs twice. The // dialect is a FACT rather than a verdict, so a caller can find the rows diff --git a/crates/batten/tests/it/rebase.rs b/crates/batten/tests/it/rebase.rs index 5d5f3077d..2adcb0630 100644 --- a/crates/batten/tests/it/rebase.rs +++ b/crates/batten/tests/it/rebase.rs @@ -30,7 +30,7 @@ //! //! # The declared mutation, and why the row is in THIS file //! -//! `obligations-bound` reads the declared file's own lines for a row beginning +//! `test name undefined` reads the declared file's own lines for a row beginning //! `#MUTANT |`, and its `line_sources` covers `crates/batten/tests/**` and //! not `crates/batten/src/**` — so the row lives here even though the expression //! it applies belongs to `gitwrite::next_offer`. A block comment because the @@ -251,7 +251,7 @@ fn a_conflicting_replay_refuses() { /// /// Measured on #848: `3f308039` and `main`'s `a7935a7b` share patch identity /// `f185159e…`, and the lap stopped on it every time. Resolving it needed a hand -/// rebase, which `rebase-not-hand-stepped` denies — so this engine gap presented +/// rebase, which `patch run loose` denies — so this engine gap presented /// as a policy deadlock and cost a human override to get past. /// /// # The fixture is shaped by what makes the bug visible diff --git a/crates/batten/tests/it/receipt_verified.rs b/crates/batten/tests/it/receipt_verified.rs index 72f68d8da..fa0a71608 100644 --- a/crates/batten/tests/it/receipt_verified.rs +++ b/crates/batten/tests/it/receipt_verified.rs @@ -1,7 +1,7 @@ //! `batten receipt verified` over the compiled binary — the composed //! receipt read that retired `mise-tasks/verified.sh` (CLOUD-1148). //! -//! # RETIREMENT LEDGER, PER PATH — what `shell-retirement` reads +//! # RETIREMENT LEDGER, PER PATH — what `shell retire partial` reads //! //! The predecessor was a gate over three reads: a `verify` receipt for this //! exact HEAD, a `linear-check` receipt, and the `origin/main` that receipt was @@ -17,7 +17,7 @@ //! **Every title below is the base file's, byte for byte.** The first draft of //! this block invented them from the brief instead of reading //! `git show origin/main:tests/verified.bats`, so all ten arms matched nothing -//! and `bats-tests-not-deleted` reported ten unmapped cases — which is the +//! and `bats count dropped` reported ten unmapped cases — which is the //! ratchet doing exactly what it exists to do. //! //! CARRIED — the predicate moved intact onto the composed verb. diff --git a/crates/batten/tests/it/reclaim_report_once.rs b/crates/batten/tests/it/reclaim_report_once.rs index 56747fe4e..cee019604 100644 --- a/crates/batten/tests/it/reclaim_report_once.rs +++ b/crates/batten/tests/it/reclaim_report_once.rs @@ -24,7 +24,7 @@ //! # Why it drives the task body rather than a fabricated decision //! //! The suppression lives in `[tasks."session:census"]`'s body, because -//! `mise-tasks/reclaim-census.sh` is governed by `shell-retirement` and is not +//! `mise-tasks/reclaim-census.sh` is governed by `shell retire partial` and is not //! this row's to edit. A test that re-implemented the decision in Rust would be //! the `with input as` shape `rules/policy-modules.md` names one layer //! down: it would pass over a body that never runs, reads the wrong store, or diff --git a/crates/batten/tests/it/refusal_ceiling.rs b/crates/batten/tests/it/refusal_ceiling.rs index 4067ff3ff..a1f4794d6 100644 --- a/crates/batten/tests/it/refusal_ceiling.rs +++ b/crates/batten/tests/it/refusal_ceiling.rs @@ -22,7 +22,7 @@ //! the ceiling is reported; the allow half — anti-vacuity, and the load-bearing //! one (CLOUD-418) — is that every refusal this repository can actually emit //! passes. A ceiling that refuses correct output is a gate somebody switches -//! off, and the converted `no-tool-substitution` refusal is the specific line +//! off, and the converted `tool select other` refusal is the specific line //! the row names. //! //! **BOTH ARMS ARE MEASURED HERE NOW, EACH AGAINST ITS OWN CEILING** @@ -192,7 +192,7 @@ fn no_refusal_lost_its_pointer() { /// /// Modules copied by ENUMERATION rather than by name, for `board_receipts`' /// stated reason: naming a consumer's policy filenames inside `crates/**` is -/// non-negotiable rule 1, and `no-consumer-repo-name` computes that. +/// non-negotiable rule 1, and `source name other` computes that. fn fixture(name: &str) -> PathBuf { let staged = Fixture::new(name).config(include_str!("../../../../batten.toml")); let modules = staged.path().join("policy"); @@ -254,7 +254,7 @@ fn a_first_sighting_carries_the_gloss_and_its_route_by_kind() { "the pointer stays inline: {line}" ); assert!( - line.contains("no-tool-substitution"), + line.contains("tool select other"), "the rule id is the hop to the row's own remedy: {line}" ); assert!( @@ -292,7 +292,7 @@ fn a_repeat_drops_the_definition_and_keeps_the_pointers() { "the repeat is a byte prefix of the first sighting: {repeat:?} vs {first:?}" ); assert!( - repeat.contains("no-tool-substitution"), + repeat.contains("tool select other"), "the rule id stays on the repeat arm — for 66 rows it is the only \ discriminator (CLOUD-1637's second amendment): {repeat}" ); diff --git a/crates/batten/tests/it/release_provision_parity.rs b/crates/batten/tests/it/release_provision_parity.rs index a5ef4c7bd..b3feb9f65 100644 --- a/crates/batten/tests/it/release_provision_parity.rs +++ b/crates/batten/tests/it/release_provision_parity.rs @@ -34,7 +34,7 @@ //! linux-x86_64, macos-aarch64, macos-x86_64`. Upstream publishes three binaries //! and has since v0.1.2 — `aarch64-apple-darwin`, `x86_64-apple-darwin`, //! `x86_64-unknown-linux-gnu` — so `linux-aarch64` cannot be pinned, and -//! `no-source-built-tool` forbids compiling one. Building the mapping surfaced a +//! `pin add unsafe` forbids compiling one. Building the mapping surfaced a //! second instance nobody had found: `x86_64-pc-windows-gnu` has been in exactly //! the same state for its whole life, with no runner to reveal it. @@ -54,7 +54,7 @@ use batten::rules::{self, Rule}; /// the same column census a consumer's config does. fn row() -> Rule { serde_json::from_value(serde_json::json!({ - "id": "release-provision-parity", + "id": "release check partial", "kind": "policy", "scope": "tree", "sources": [".github/workflows/release-artifacts.yml", "batten.toml"], diff --git a/crates/batten/tests/it/remedy_authorship.rs b/crates/batten/tests/it/remedy_authorship.rs index 958839742..100ca3f5c 100644 --- a/crates/batten/tests/it/remedy_authorship.rs +++ b/crates/batten/tests/it/remedy_authorship.rs @@ -39,7 +39,7 @@ use batten::rules::{self, Rule}; /// would refuse cannot be smuggled in by hand. fn row(line_sources: &[&str], documents: &[&str]) -> Rule { serde_json::from_value(serde_json::json!({ - "id": "remedy-authorship", + "id": "remedy own other", "kind": "policy", "scope": "tree", "line_sources": line_sources, @@ -155,7 +155,7 @@ fn the_same_block_fully_prefixed_is_clean_and_was_evaluated() { scan.findings ); assert!( - !scan.not_evaluated.contains_key("remedy-authorship"), + !scan.not_evaluated.contains_key("remedy own other"), "and it looked — a skip here would make the case above pass for the \ wrong reason" ); @@ -300,7 +300,7 @@ fn a_declared_line_source_matching_nothing_is_not_a_pass() { "a rule that could not look reports no finding" ); assert!( - scan.not_evaluated.contains_key("remedy-authorship"), + scan.not_evaluated.contains_key("remedy own other"), "but it must be recorded NOT EVALUATED — silence here IS the vacuous pass" ); } diff --git a/crates/batten/tests/it/repaired_arms.rs b/crates/batten/tests/it/repaired_arms.rs index 2143c3eb4..7becf16b2 100644 --- a/crates/batten/tests/it/repaired_arms.rs +++ b/crates/batten/tests/it/repaired_arms.rs @@ -9,7 +9,7 @@ //! //! The issue named `batten mcp call Linear get_issue` as its first candidate. //! That was retired before it shipped: no network call can be guaranteed inside -//! `perf-assert`'s 100 ms hook ceiling, and every other rule at this boundary +//! `path measure wrong`'s 100 ms hook ceiling, and every other rule at this boundary //! adjudicates CACHED state — receipts, claims, captures — never a live read. //! `policy/module-layering.rego` now forbids `repair` the network modules so the //! bound is a gate rather than a sentence, and these fixtures repair by creating diff --git a/crates/batten/tests/it/repetition.rs b/crates/batten/tests/it/repetition.rs index 1ad3590aa..7d3d20432 100644 --- a/crates/batten/tests/it/repetition.rs +++ b/crates/batten/tests/it/repetition.rs @@ -51,7 +51,7 @@ fn config(module: &str, extra: &str) -> String { r#"version = 1 [[rule]] -id = "repetition-without-progress" +id = "turn run loose" kind = "policy" scope = "mediated_call" module = "{module}" diff --git a/crates/batten/tests/it/retirement_doctrine.rs b/crates/batten/tests/it/retirement_doctrine.rs index 22aa52c43..cb891e4ae 100644 --- a/crates/batten/tests/it/retirement_doctrine.rs +++ b/crates/batten/tests/it/retirement_doctrine.rs @@ -30,7 +30,7 @@ //! //! It does **not** catch an agent who does not read the file, and it holds nothing //! about whether a given change should have been reshaped as a retirement. That -//! axis is `shell-retirement`'s, it is decided over a real diff with a real exit +//! axis is `shell retire partial`'s, it is decided over a real diff with a real exit //! code, and a §7 here claiming otherwise would be this row's own defect one level //! up. //! diff --git a/crates/batten/tests/it/review_answered.rs b/crates/batten/tests/it/review_answered.rs index da6c0a3cc..bc4cea5c0 100644 --- a/crates/batten/tests/it/review_answered.rs +++ b/crates/batten/tests/it/review_answered.rs @@ -44,7 +44,7 @@ //! reports a result under. It is `[[mint]]`'s control through the same //! `selects_tool_name`, so there is no second matcher to drift. //! -//! # RETIREMENT LEDGER, PER PATH — what `shell-retirement` reads +//! # RETIREMENT LEDGER, PER PATH — what `shell retire partial` reads //! // carried: tests/review-answered.bats policy/review-answered.rego crates/batten/tests/it/review_answered.rs //! @@ -67,10 +67,10 @@ //! four moved AGAIN under CLOUD-690, because what produces the count changed: //! each is noted below with what the number is now and why. //! -// changed: "review-answered.bats::THE MEASURED SHAPE: a head carrying unresolved threads is refused, naming the count" crates/batten/tests/it/review_answered.rs the count is identical and where it is read from is not: `4 blocking` was a substring of a free string, and it is now the `Subject::Count` the engine renders beside the token (CLOUD-1050) -// changed: "review-answered.bats::VACUITY: zero threads and no review reads as unreviewed, not as all-addressed" crates/batten/tests/it/review_answered.rs the count is 0 now and the rule is `review-absent`: the condition was one element of a `--jq` projection and is a second fact with its own inverted comparison since CLOUD-690, so the assertion moved from prose to a different predicate's subject rather than only to a subject -// changed: "review-answered.bats::VACUITY: a page the command could not read refuses rather than passing" crates/batten/tests/it/review_answered.rs same number, different producer: the projection emitted an extra element and the `blocking` column adds one, so the discriminating pair with the all-answered case is now two identical thread sets under different page flags -// changed: "review-answered.bats::THE BYPASS: a compound command is still a ready" crates/batten/tests/it/review_answered.rs same cause, same number; what the case proves — that the receipt row's selection and this module's narrowing agree about one command — is unchanged +// changed: "review judge missing.bats::THE MEASURED SHAPE: a head carrying unresolved threads is refused, naming the count" crates/batten/tests/it/review_answered.rs the count is identical and where it is read from is not: `4 blocking` was a substring of a free string, and it is now the `Subject::Count` the engine renders beside the token (CLOUD-1050) +// changed: "review judge missing.bats::VACUITY: zero threads and no review reads as unreviewed, not as all-addressed" crates/batten/tests/it/review_answered.rs the count is 0 now and the rule is `review-absent`: the condition was one element of a `--jq` projection and is a second fact with its own inverted comparison since CLOUD-690, so the assertion moved from prose to a different predicate's subject rather than only to a subject +// changed: "review judge missing.bats::VACUITY: a page the command could not read refuses rather than passing" crates/batten/tests/it/review_answered.rs same number, different producer: the projection emitted an extra element and the `blocking` column adds one, so the discriminating pair with the all-answered case is now two identical thread sets under different page flags +// changed: "review judge missing.bats::THE BYPASS: a compound command is still a ready" crates/batten/tests/it/review_answered.rs same cause, same number; what the case proves — that the receipt row's selection and this module's narrowing agree about one command — is unchanged //! //! # Two cases the retired suite could not have //! @@ -488,14 +488,8 @@ fn a_ready_with_no_record_at_all_is_refused_and_the_remedy_names_the_read() { // line. The point this case makes survives the move — what a reader reaches // is still a ROUTE that can mint the record rather than a shell command no // selector would accept. - assert!( - decision.contains("ready-needs-the-threads-answered"), - "{decision}" - ); - let explained = run( - &dir, - &["policy", "explain", "ready-needs-the-threads-answered"], - ); + assert!(decision.contains("review answer partial"), "{decision}"); + let explained = run(&dir, &["policy", "explain", "review answer partial"]); assert_eq!(explained.status.code(), Some(0), "the row resolves"); let routes = String::from_utf8_lossy(&explained.stdout); assert!(routes.contains(&declared.selector), "{routes}"); @@ -679,18 +673,12 @@ fn a_sibling_method_answering_the_same_shape_is_not_a_review() { // module's zero-count one: the receipt row reports the check as unrecorded, so // what the sibling call answered was never a fact about reviews at all. The // module cannot even be reached, which is why this asserts the receipt row. - assert!( - decision.contains("ready-needs-a-review-to-exist"), - "{decision}" - ); + assert!(decision.contains("review list unread"), "{decision}"); // The remedy naming the RIGHT method is one hop away (CLOUD-1286), and it is // the half worth reaching for here: this whole case is about a sibling // method being counted as a review, so a remedy pointing at the wrong one // would be the same defect in the fix. - let explained = run( - &dir, - &["policy", "explain", "ready-needs-a-review-to-exist"], - ); + let explained = run(&dir, &["policy", "explain", "review list unread"]); assert_eq!(explained.status.code(), Some(0), "the row resolves"); assert!( String::from_utf8_lossy(&explained.stdout).contains("get_reviews"), @@ -842,7 +830,7 @@ fn reading_the_review_is_never_refused_so_the_remedy_is_reachable() { let payload = serde_json::json!({ "hook_event_name": "PreToolUse", "tool_name": declared.raw_tool(), - // NEUTRAL owner and repo, which `no-origin-literal-in-fixtures` is right + // NEUTRAL owner and repo, which `forge name other` is right // to insist on: what this case asserts is that a `mediated_call` row // judges an MCP invocation at all, and nothing about it depends on WHICH // repository the arguments name. @@ -906,7 +894,7 @@ fn an_undeclared_class_refuses_with_the_token_and_says_the_registry_is_silent() /// satisfies the check it names. const ROWS: &str = r#" [[rule]] -id = "ready-needs-the-threads-answered" +id = "review answer partial" kind = "receipt" scope = "mediated_call" severity = "deny" @@ -916,7 +904,7 @@ key = "head" reason = "read the threads with the pull_request_read tool, method get_review_comments" [[rule]] -id = "ready-needs-a-review-to-exist" +id = "review list unread" kind = "receipt" scope = "mediated_call" severity = "deny" @@ -926,7 +914,7 @@ key = "head" reason = "read the reviews with the pull_request_read tool, method get_reviews" [[rule]] -id = "review-answered" +id = "review judge missing" kind = "policy" scope = "mediated_call" module = "policy/review-answered.rego" diff --git a/crates/batten/tests/it/review_dispatched.rs b/crates/batten/tests/it/review_dispatched.rs index 371d0f50c..b259b1254 100644 --- a/crates/batten/tests/it/review_dispatched.rs +++ b/crates/batten/tests/it/review_dispatched.rs @@ -1,4 +1,4 @@ -//! `review-dispatched`, over the engine that builds its input (CLOUD-472). +//! `prompt run never`, over the engine that builds its input (CLOUD-472). //! //! # The seam, and why the module's own suite cannot reach it //! @@ -49,7 +49,7 @@ use std::path::{Path, PathBuf}; use batten::rules::{self, Rule, RuleKind, RuleScope}; -const RULE: &str = "review-dispatched"; +const RULE: &str = "prompt run never"; const REVIEW: &str = "ready-pressure-test"; const SUBJECT: &str = "subject.md"; diff --git a/crates/batten/tests/it/review_receipt_delta.rs b/crates/batten/tests/it/review_receipt_delta.rs index 2358c97f6..58073a594 100644 --- a/crates/batten/tests/it/review_receipt_delta.rs +++ b/crates/batten/tests/it/review_receipt_delta.rs @@ -85,7 +85,7 @@ fn fixture(name: &str, key: &str) -> PathBuf { }; // THROUGH `common::Fixture`, never a hand-rolled `git init` chain: the builder // copies a template rather than forking `git init` (CLOUD-1419 measured 1,819 - // init processes over one run from exactly that habit), and `fixture-forks` + // init processes over one run from exactly that habit), and `test fix duplicate` // refuses a new copy of it. `base_commit` also pins `refs/remotes/origin/main`, // which is the ref these rows name — no remote is needed, because what the // identity reads is a REF and a local one resolves identically. @@ -96,7 +96,7 @@ fn fixture(name: &str, key: &str) -> PathBuf { scope = \"mediated_call\"\nseverity = \"deny\"\npattern = \"git push\"\n\ checks = [\"{RECEIPT}\"]\nkey = \"{key}\"\n{base}\ reason = \"dispatch the code-review skill\"\n\n\ - [[rule]]\nid = \"ready-needs-review\"\nkind = \"receipt\"\n\ + [[rule]]\nid = \"review ask missing\"\nkind = \"receipt\"\n\ scope = \"mediated_call\"\nseverity = \"deny\"\npattern = \"gh pr ready\"\n\ checks = [\"{RECEIPT}\"]\nkey = \"{key}\"\n{base}\ reason = \"dispatch the code-review skill\"\n" diff --git a/crates/batten/tests/it/rule_cost_census.rs b/crates/batten/tests/it/rule_cost_census.rs index 0e7dfa159..3e83262ee 100644 --- a/crates/batten/tests/it/rule_cost_census.rs +++ b/crates/batten/tests/it/rule_cost_census.rs @@ -5,7 +5,7 @@ //! emitted two lines. No rule kind reported its own duration and every //! `command`-rule child has `Stdio::null()` on both streams, so the largest item //! in this repository's CI was unattributable *by construction*. Two sessions in -//! a row attributed it confidently and wrongly — once to `no-secrets` (which +//! a row attributed it confidently and wrongly — once to `source carry unsafe` (which //! measures 3%) and once to `forbid`/`ratchet` read amplification (which //! measures ~150ms) — before an instrument existed to ask. The census is that //! instrument and this is its gate: without a case under it, it is a log rather diff --git a/crates/batten/tests/it/rules_drift.rs b/crates/batten/tests/it/rules_drift.rs index ecbeb6f6d..faab5e314 100644 --- a/crates/batten/tests/it/rules_drift.rs +++ b/crates/batten/tests/it/rules_drift.rs @@ -72,7 +72,7 @@ // changed: "unreadable wiring is refused rather than reporting every event unwired" policy/rules-drift.rego conditioned on a wiring claim existing: an unreadable `.claude/settings.json` is `drift read unread` when some sentence claims a wiring, and silent when none does // changed: "unreadable schemas are refused rather than reporting every key unemittable" policy/rules-drift.rego same conditioning, plus a READ-BUT-EMPTY arm the predecessor did not need: this build of regorus has no `walk`, so the recursive descent became one fixed path, and a schema whose shape moved parses fine and yields nothing — invisible to `input.tree.missing`, so `schema_vacuous` covers it // changed: "unreadable policy source is refused rather than reporting every name unqueried" policy/rules-drift.rego same conditioning, on a named fixed rule existing -// changed: "the gate is wired into the hk gate, so a drift reddens a commit" policy/rules-drift.rego the assertion moves from the suite to the wiring itself: hk's `rules-drift` step now runs `mise run rules-drift`, which is an inline `batten check --rule 'rule watch other'`, so the step name and the rule id are one object rather than two that a grep held together +// changed: "the gate is wired into the hk gate, so a drift reddens a commit" policy/rules-drift.rego the assertion moves from the suite to the wiring itself: hk's `rule watch other` step now runs `mise run rules-drift`, which is an inline `batten check --rule 'rule watch other'`, so the step name and the rule id are one object rather than two that a grep held together // // ONE PREDICATE NARROWED, and it is recorded here rather than absorbed into the // carried arm above it. The predecessor took `head -n1` of grep order when a @@ -92,7 +92,7 @@ use std::path::Path; use common::{git_in, run, scratch, stdout, write}; /// The rule's own id, which is also what `--rule` selects. -const RULE: &str = "rules-drift"; +const RULE: &str = "rule watch other"; /// Materialize a repository carrying the committed module and this row. /// @@ -608,7 +608,7 @@ fn an_unparseable_authority_no_prose_claims_against_is_still_silent() { #[test] fn an_authority_no_prose_claims_against_is_silent() { // THE SCOPE MIRROR, and it is the difference from the predecessor worth - // measuring. `rules-drift.sh` exited 1 at startup when it could not read an + // measuring. `rule watch other.sh` exited 1 at startup when it could not read an // authority. A `[[rule]]` has NO CALL SITE — it runs wherever `batten check` // runs, including every fixture repository that inherits this config — so an // unconditional guard would make the row speak everywhere. That is the @@ -742,7 +742,7 @@ fn an_arm_named_without_a_count_is_untouched() { /// The sentence `rules/policy-modules.md` closes its key lists with, and /// the anchor `schema-key-undocumented` keys on. -const CLAIM: &str = "`rules-drift` holds the lists above to those two files.\n"; +const CLAIM: &str = "`rule watch other` holds the lists above to those two files.\n"; #[test] fn a_schema_key_the_claiming_file_omits_is_reported() { @@ -873,7 +873,7 @@ fn the_two_anchors_this_gate_keys_on_are_still_one_line_in_the_committed_files() // of the file asks: does a name in the tree agree with the mechanism that // judges it. These ask it of the two names a refusal line carries. // -// They use their own runner rather than `judge`: that one names rules-drift's +// They use their own runner rather than `judge`: that one names rule watch other's // own rule and reads stdout, and what is under test here is whether the config // LOADS at all, which is a usage error on stderr. diff --git a/crates/batten/tests/it/run_shape.rs b/crates/batten/tests/it/run_shape.rs index 811c7ec1d..16207a4e5 100644 --- a/crates/batten/tests/it/run_shape.rs +++ b/crates/batten/tests/it/run_shape.rs @@ -8,9 +8,9 @@ //! only a test has nothing under test. //! //! The guard carried FOUR families and all four had already landed, which is what -//! made it deletable whole rather than piecemeal — `shell-retirement`'s one +//! made it deletable whole rather than piecemeal — `shell retire partial`'s one //! admitted disposition, and the reason no line of it needed editing to qualify. -//! Three of the four are this module's; the fourth is `task-substitution`'s, so it +//! Three of the four are this module's; the fourth is `task run loose`'s, so it //! names that surface and its own tier. // carried: mise-tasks/run-shape-guard.sh policy/run-shape.rego crates/batten/tests/it/run_shape.rs @@ -24,7 +24,7 @@ // file and drops its cases is the silent coverage loss that row measured. // // Routed by FAMILY rather than by file: three of the guard's four families are -// this module's, and `cargo-substitutes-for-a-task` is `task-substitution`'s, so +// this module's, and `cargo-substitutes-for-a-task` is `task run loose`'s, so // those cases name that surface and its own tier instead. A row naming this file // for all 42 would claim coverage that is not here. // carried: "THE MEASURED SHAPE: a sleep in the middle of a compound is denied" policy/run-shape.rego crates/batten/tests/it/run_shape.rs @@ -57,7 +57,7 @@ // carried: "THE MEASURED SHAPE: a weaker clippy through the sanctioned escape is refused" policy/task-substitution.rego crates/batten/tests/it/task_receipt.rs // carried: "the task itself is allowed — this rule is about substitution, not about cargo" policy/task-substitution.rego crates/batten/tests/it/task_receipt.rs // carried: "a subcommand no task wraps is a genuine one-off and is untouched" policy/task-substitution.rego crates/batten/tests/it/task_receipt.rs -// carried: "a BARE cargo is no-bare-cargo's, so the two never report one command" policy/task-substitution.rego crates/batten/tests/it/task_receipt.rs +// carried: "a BARE cargo is cargo run loose's, so the two never report one command" policy/task-substitution.rego crates/batten/tests/it/task_receipt.rs // carried: "an EQUAL argv is not weaker, so spelling a task's own line out is allowed" policy/task-substitution.rego crates/batten/tests/it/task_receipt.rs // carried: "a narrower argv IS weaker, and the task it is weaker than is named" policy/task-substitution.rego crates/batten/tests/it/task_receipt.rs // carried: "a DIFFERENT program argv is a different command, not a weaker one" policy/task-substitution.rego crates/batten/tests/it/task_receipt.rs @@ -81,7 +81,7 @@ //! CLOUD-1059. The suite's own subject was `policy/run-shape.rego`, which //! CLOUD-1050 rewrote: the module's refusal stopped being prose and became a //! declared class. Its cases asserted the prose, so they went red — and -//! `shell-retirement` refuses editing a bats suite in place, which is the whole +//! `shell retire partial` refuses editing a bats suite in place, which is the whole //! point of that gate. Both doors shut on an edit; the open one is the //! migration, and this is it. Every case below carries a `// carried:` arm. //! @@ -154,7 +154,7 @@ fn fixture(name: &str) -> PathBuf { concat!( "version = 1\n\n", "[[rule]]\n", - "id = \"commit-message-obtainable\"\n", + "id = \"commit read missing\"\n", "kind = \"policy\"\n", "scope = \"mediated_call\"\n", "module = \"policy/run-shape.rego\"\n", @@ -400,7 +400,7 @@ fn a_redirect_bound_to_the_commits_own_element_is_a_message_source() { #[test] fn a_heredoc_body_is_not_shell() { - // CLOUD-723, the same parser change read in reverse. `verdict-not-discarded` + // CLOUD-723, the same parser change read in reverse. `verdict guard missing` // and every `pipeline` row decide over these segments, so a body carrying a // `;` used to split the list and turn a paragraph into its own command — // measured twice in one session, both times on the command that was writing diff --git a/crates/batten/tests/it/run_shape_guard_door.rs b/crates/batten/tests/it/run_shape_guard_door.rs index 6967fec4b..722a4907e 100644 --- a/crates/batten/tests/it/run_shape_guard_door.rs +++ b/crates/batten/tests/it/run_shape_guard_door.rs @@ -14,12 +14,12 @@ //! noticed. This file is the row that would have. //! //! **THE FIXTURE REPOSITORY CARRIES ONE HANDLER ROW AND NO `[[rule]]` AT ALL.** -//! That isolation is the whole design: `verdict-not-discarded` and the other +//! That isolation is the whole design: `verdict guard missing` and the other //! mediated rows in the real `batten.toml` refuse commands in this same family, //! so driving the real config would let an engine row's verdict stand in for the //! handler's — the substitution that hid the defect the first time. //! -//! **Rust rather than a `.bats` suite** (CLOUD-843). `shell-retirement` refuses +//! **Rust rather than a `.bats` suite** (CLOUD-843). `shell retire partial` refuses //! a new `tests/*.bats`, and it is right to: the campaign's corpus has to shrink //! rather than stay level while the census reports movement. Writing the //! door tier here costs nothing it would have had in bash — the fixture is the diff --git a/crates/batten/tests/it/runner_verdict.rs b/crates/batten/tests/it/runner_verdict.rs index caeb29c3f..c0d2c1f70 100644 --- a/crates/batten/tests/it/runner_verdict.rs +++ b/crates/batten/tests/it/runner_verdict.rs @@ -19,7 +19,7 @@ //! predicate one task over. //! //! **WHY IT IS NOT IN THAT BATS SUITE**, which is where it belongs on subject. The -//! `shell-retirement` row (`severity = "deny"`) refuses an EDITED `tests/**/*.bats` +//! `shell retire partial` row (`severity = "deny"`) refuses an EDITED `tests/**/*.bats` //! as `shell edit refused` and an ADDED one as `shell add refused`, and the one //! admitted edit is a line whose removal names a path the same change deletes. So //! the bats corpus is closed to an addition like this one. That is CLOUD-1088 — diff --git a/crates/batten/tests/it/sbom_inventory.rs b/crates/batten/tests/it/sbom_inventory.rs index 238904ffe..8b58249e0 100644 --- a/crates/batten/tests/it/sbom_inventory.rs +++ b/crates/batten/tests/it/sbom_inventory.rs @@ -15,7 +15,7 @@ //! are decided by the module from `line_sources`, so the producer cannot get them //! wrong on its behalf. Cases below drive both routes. //! -//! # RETIREMENT LEDGER, PER PATH — what `shell-retirement` reads +//! # RETIREMENT LEDGER, PER PATH — what `shell retire partial` reads //! //! `sbom-check.sh` re-ran `sbom.sh` twice and adjudicated the documents in shell. //! The scan stays outside — §9's prior art, and §5 makes `check` `read` — so @@ -125,7 +125,7 @@ fn config() -> String { r#"version = 1 [[rule]] -id = "sbom-inventory" +id = "manifest list wrong" kind = "policy" scope = "tree" module = "sbom-inventory.rego" diff --git a/crates/batten/tests/it/scanner_taxonomy.rs b/crates/batten/tests/it/scanner_taxonomy.rs index 8955d9677..03fe8f899 100644 --- a/crates/batten/tests/it/scanner_taxonomy.rs +++ b/crates/batten/tests/it/scanner_taxonomy.rs @@ -24,7 +24,7 @@ //! //! **The substitution axis is a different matter, and saying otherwise was a //! defect of its own** (CLOUD-998). This file used to state flatly that it was -//! "not a gate over tool choice"; `no-tool-substitution` is exactly that, over +//! "not a gate over tool choice"; `tool select other` is exactly that, over //! the command line, and its refusal redirects to the rules file. So the claim //! here is narrowed to the axis it holds for, and one assertion now pins the //! gate's name into the prose. This is otherwise the same shape as @@ -56,7 +56,7 @@ const INDEX: &str = "AGENTS.md"; /// /// ROW ONE NAMES A CAPABILITY, NOT A PRODUCT, and that is the correction /// CLOUD-998 landed. It used to pin the literal `` `grep` `` — the utility -/// `no-tool-substitution` refuses over a tracked path — so the gate's own +/// `tool select other` refuses over a tracked path — so the gate's own /// redirect sent a reader back to the refused call, and this assertion held the /// wrong answer in place. Naming a first-class tool instead would have been the /// same defect one layer over: which instruments a session carries varies, so @@ -107,7 +107,7 @@ const DISPOSITION: &str = "CLOUD-310"; /// gate existed over instrument choice, which was true when written and false /// once this row landed — and a reader who believes no gate exists has no reason /// to expect the refusal (CLOUD-998). -const SUBSTITUTION_GATE: &str = "no-tool-substitution"; +const SUBSTITUTION_GATE: &str = "tool select other"; /// The bare product names row one must not answer with. /// diff --git a/crates/batten/tests/it/secret_redaction.rs b/crates/batten/tests/it/secret_redaction.rs index 3441611c9..fb26ace17 100644 --- a/crates/batten/tests/it/secret_redaction.rs +++ b/crates/batten/tests/it/secret_redaction.rs @@ -40,7 +40,7 @@ use batten::secret::{REDACTED, Secret}; /// DELIBERATELY NOT SHAPED LIKE A REAL CREDENTIAL. /// /// It was `ghp_`-prefixed first, because a fixture standing in for a leaked PAT -/// reads better if it looks like one. `no-secrets` refused the tree for it and +/// reads better if it looks like one. `source carry unsafe` refused the tree for it and /// was right to — a scanner that skipped a well-formed token because it sat in a /// test file would have a hole in exactly the place this suite is about. The /// shape buys nothing here: every assertion is over a rendering, and redaction diff --git a/crates/batten/tests/it/secrets_kind.rs b/crates/batten/tests/it/secrets_kind.rs index 094abd52e..3fdcd0bb0 100644 --- a/crates/batten/tests/it/secrets_kind.rs +++ b/crates/batten/tests/it/secrets_kind.rs @@ -8,7 +8,7 @@ //! //! # The planted token is assembled at runtime, and that is load-bearing //! -//! Consumer #1's own `no-secrets` rule globs the whole tree, so a literal +//! Consumer #1's own `source carry unsafe` rule globs the whole tree, so a literal //! credential written into this file would be a standing violation of the rule //! this suite exists to prove — the repository would fail its own gate, forever, //! on its own test fixture. The token is therefore built from fragments at @@ -122,7 +122,7 @@ impl Env { sha256 = \"{sha}\"\n\ binary = \"ripsecrets\"\n\n\ [[rule]]\n\ - id = \"no-secrets\"\n\ + id = \"source carry unsafe\"\n\ kind = \"secrets\"\n\ glob = \"**/*.conf\"\n\ severity = \"deny\"\n\ @@ -169,7 +169,7 @@ fn collect(dir: &Path, out: &mut Vec) { /// The fragments the synthetic credentials are assembled from. /// /// Split so the contiguous token exists in no committed byte sequence — see the -/// module docs: a literal here would violate consumer #1's own `no-secrets` rule +/// module docs: a literal here would violate consumer #1's own `source carry unsafe` rule /// over this very file. const TOKEN_PARTS: [&str; 5] = ["AKIA", "7QF2", "NX8M", "3JD5", "W0PC"]; const OTHER_PARTS: [&str; 5] = ["AKIA", "B4T6", "LZ9R", "K1YV", "H2SD"]; @@ -263,7 +263,7 @@ fn a_planted_secret_is_a_pointer_and_never_its_bytes() { ); assert_eq!( String::from_utf8_lossy(&out.stdout), - "app.conf:1 no-secrets\n", + "app.conf:1 source carry unsafe\n", "stdout is the pointer line and nothing else" ); nowhere(&env, &out, &secret, "text output"); @@ -283,7 +283,7 @@ fn the_json_document_carries_no_span_either() { let finding = &document["findings"][0]; assert_eq!(finding["path"], "app.conf"); assert_eq!(finding["line"], 1); - assert_eq!(finding["rule"], "no-secrets"); + assert_eq!(finding["rule"], "source carry unsafe"); nowhere(&env, &out, &secret, "-J output"); } @@ -310,7 +310,7 @@ fn the_emitted_identity_is_secret_class_and_differs_from_the_unkeyed_digest() { // And the fingerprint is not the unkeyed digest of the same span. let unkeyed = batten::identity::code_fingerprint( - "no-secrets", + "source carry unsafe", "app.conf", &secret, batten::identity::SpanNormalization::Verbatim, @@ -367,7 +367,7 @@ fn the_same_input_twice_is_byte_identical_and_ordered() { assert_eq!(first.stdout, second.stdout, "text output is byte-stable"); assert_eq!( String::from_utf8_lossy(&first.stdout), - "a.conf:1 no-secrets\nb.conf:1 no-secrets\n", + "a.conf:1 source carry unsafe\nb.conf:1 source carry unsafe\n", "ordered by path, not by the order the scanner happened to emit" ); @@ -591,7 +591,7 @@ fn an_erroring_gate_exits_three_while_the_other_gates_still_evaluate() { ); let stderr = String::from_utf8_lossy(&out.stderr); assert!( - stderr.contains("errored no-secrets"), + stderr.contains("errored source carry unsafe"), "the erroring gate appears in output, by id: {stderr}" ); assert!( @@ -629,7 +629,7 @@ fn an_erroring_gate_does_not_suppress_another_gates_findings() { ); let stderr = String::from_utf8_lossy(&out.stderr); assert!( - stderr.contains("errored no-secrets"), + stderr.contains("errored source carry unsafe"), "precedence governs the exit code, never what appears in output: {stderr}" ); } @@ -658,7 +658,7 @@ fn a_contained_failure_still_names_what_went_wrong() { "the reason reaches the operator: {stderr}" ); assert!( - stderr.contains("errored no-secrets"), + stderr.contains("errored source carry unsafe"), "beside the id and the class: {stderr}" ); nowhere(&env, &out, &secret, "contained failure"); @@ -686,7 +686,7 @@ fn the_data_channel_reports_the_contained_failure_as_a_class_token_alone() { let document: serde_json::Value = serde_json::from_slice(&out.stdout).expect("-J stdout is JSON"); - assert_eq!(document["errored"][0]["rule"], "no-secrets"); + assert_eq!(document["errored"][0]["rule"], "source carry unsafe"); assert_eq!(document["errored"][0]["class"], "internal"); assert_eq!( document["errored"][0].as_object().map(serde_json::Map::len), diff --git a/crates/batten/tests/it/semver_gate.rs b/crates/batten/tests/it/semver_gate.rs index ad6b8e020..b8d6dff27 100644 --- a/crates/batten/tests/it/semver_gate.rs +++ b/crates/batten/tests/it/semver_gate.rs @@ -30,7 +30,7 @@ //! lock route's own end-to-end evidence is recorded in the commit that added it: //! 223 checks graded, 217 pass, 5 fail, route `lock`, exit 0. //! -//! # RETIREMENT LEDGER, PER PATH — what `shell-retirement` reads +//! # RETIREMENT LEDGER, PER PATH — what `shell retire partial` reads //! // carried: mise-tasks/semver.sh crates/batten/src/semver.rs kind:verb crates/batten/tests/it/semver_gate.rs // carried: tests/semver.bats crates/batten/src/semver.rs kind:verb crates/batten/tests/it/semver_gate.rs @@ -51,7 +51,7 @@ //! CHANGED — behaviour that diverges deliberately, each with its reason. //! // changed: "semver.bats::a missing cargo-semver-checks is exit 2, never a silent pass" crates/batten/src/semver.rs kind:verb the shell probed `command -v` and refused before running; the adapter has no separate probe because a spawn that cannot start IS the could-not-look it would have reported, and one channel cannot disagree with itself -// changed: "semver.bats::the toolchain defaults to the one on PATH, not to a floating channel" crates/batten/src/semver.rs kind:verb unchanged in effect and moved in place: `semver::toolchain` reads `rustc --version`, which is the same READ of the one authority the shell made; it lives in the adapter rather than beside its caller because `spawn-adapters` places spawns by module and `lib.rs` is not a placed one +// changed: "semver.bats::the toolchain defaults to the one on PATH, not to a floating channel" crates/batten/src/semver.rs kind:verb unchanged in effect and moved in place: `semver::toolchain` reads `rustc --version`, which is the same READ of the one authority the shell made; it lives in the adapter rather than beside its caller because `adapter place missing` places spawns by module and `lib.rs` is not a placed one // changed: "semver.bats::SEMVER_TOOLCHAIN still overrides, so the suite can drive another claim" crates/batten/src/semver.rs kind:verb the seam survives as the same environment variable, read at the same point; what changed is that a suite driving it no longer needs a stub on PATH to observe the effect // changed: "semver.bats::no rustc at all is exit 2, never a fall back to a floating channel" crates/batten/src/lib.rs kind:mechanism same predicate, same exit code, and the refusal now names the checkout rather than the channel because there is no channel left to fall back to //! diff --git a/crates/batten/tests/it/session_provisioning.rs b/crates/batten/tests/it/session_provisioning.rs index d98618c36..d161ca57d 100644 --- a/crates/batten/tests/it/session_provisioning.rs +++ b/crates/batten/tests/it/session_provisioning.rs @@ -41,7 +41,7 @@ //! the engine's behaviour is proved against stubs, and the declaration is proved //! against the file that ships. //! -//! # RETIREMENT LEDGER, PER PATH — what `shell-retirement` reads +//! # RETIREMENT LEDGER, PER PATH — what `shell retire partial` reads //! //! `.claude/hooks/session-start.sh` is not in `governed_when_deleted` (that set //! is `mise-tasks/` paths and `.bats` suites), so it owes no arm. The suite does. @@ -95,7 +95,7 @@ //! is lost is the end-to-end pairing of the step with its effect in one case. //! // changed: "the hook runs green on this checkout" crates/batten/tests/it/session_provisioning.rs the exit-0-and-silent half survives as `a_step_that_passes_says_nothing`, against stubs; the END-TO-END half is withdrawn, because dispatching the real rows provisions a container inside `test:cargo` — 141s measured cold — which is the cost CLOUD-1268 exists to stop moving between lanes. What covers it instead is the session itself: a failed step reports on the advisory channel at the moment it fails -// changed: "running the hook leaves the tracked lockfile untouched" crates/batten/tests/it/session_provisioning.rs narrowed from the EFFECT to the DECLARATION: `the_install_step_is_declared_lockfile_free` asserts `session:install` carries MISE_LOCKFILE=false, where the retired case ran the hook and diffed `git status -- mise.lock`. `[settings] lockfile = false` in mise.toml is the standing authority and `lock-complete` the standing gate; what is lost is the observation that this particular path honours it +// changed: "running the hook leaves the tracked lockfile untouched" crates/batten/tests/it/session_provisioning.rs narrowed from the EFFECT to the DECLARATION: `the_install_step_is_declared_lockfile_free` asserts `session:install` carries MISE_LOCKFILE=false, where the retired case ran the hook and diffed `git status -- mise.lock`. `[settings] lockfile = false` in mise.toml is the standing authority and `lock cover partial` the standing gate; what is lost is the observation that this particular path honours it // changed: "the session-start hook calls it — the whole point is WHEN it runs" crates/batten/tests/it/session_provisioning.rs from `tests/container-preflight.bats`, whose own subject survives. The case grepped the retired script for `container-preflight`; the property — that a preflight nothing runs at startup is worthless — is now the `session-container-preflight` row, and its POSITION is asserted too, which the grep could not say // changed: "the hook passes --degraded when provisioning failed" batten.toml the capability is gone rather than moved, and this is the one real loss in this retirement. `--degraded` told the preflight not to trust toolchain-dependent probes when an earlier step had failed, and it worked because the script carried a `fail` variable across its steps. Handlers share no state — each is its own process with its own outcome — so nothing can compute the flag. The consequence is bounded: a container whose install failed now gets the full probe set, so it may report a second symptom of one cause, and both refusals arrive in the same reply. Recovering it needs a fact the door does not carry; filed rather than papered over // changed: "the fixer is wired: session-start runs it, so a clone is compliant before it commits" crates/batten/tests/it/session_provisioning.rs from `tests/commit-attribution.bats`, whose own subject (hk.pkl, mise.toml) survives. The case grepped the retired script for its `step attribution-identity` line; the property — that the identity fixer runs before a clone commits — is now the `session-attribution-identity` row, asserted by `the_committed_provisioning_declares_every_step_in_order`. It is CHANGED rather than CARRIED because the retired case pinned the invocation's exact spelling inside a program and this pins a row's presence and position in a list @@ -619,7 +619,7 @@ fn the_reachable_set_is_not_empty() { /// binary byte-identical to the release already on PATH. /// /// So the property is asserted rather than explained. `install.sh` cannot -/// compile — `install-does-one-thing` in `batten.toml` bans `cargo` from it +/// compile — `program add other` in `batten.toml` bans `cargo` from it /// outright — and this is that same ban one layer up, over the tasks a session /// start actually dispatches. /// @@ -741,7 +741,7 @@ fn the_install_step_is_declared_lockfile_free() { assert!( body.contains("MISE_LOCKFILE=false"), "provisioning installs purely, so it cannot append a platform key `mise lock` \ - cannot produce and `lock-complete` rejects" + cannot produce and `lock cover partial` rejects" ); assert!( body.contains("mise install"), diff --git a/crates/batten/tests/it/shell_retirement.rs b/crates/batten/tests/it/shell_retirement.rs index 357b846df..a6f0aa90d 100644 --- a/crates/batten/tests/it/shell_retirement.rs +++ b/crates/batten/tests/it/shell_retirement.rs @@ -35,7 +35,7 @@ use batten::rules::{self, Rule}; /// refuse cannot be smuggled in by hand. pub(crate) fn row() -> Rule { serde_json::from_value(serde_json::json!({ - "id": "shell-retirement", + "id": "shell retire partial", "kind": "policy", "scope": "tree", "base": "origin/main", diff --git a/crates/batten/tests/it/shell_retirement_cost.rs b/crates/batten/tests/it/shell_retirement_cost.rs index 2421c1ffa..e80fd15de 100644 --- a/crates/batten/tests/it/shell_retirement_cost.rs +++ b/crates/batten/tests/it/shell_retirement_cost.rs @@ -178,7 +178,7 @@ fn arm(name: &str, count: usize) -> Duration { let costs = rules::rule_costs(); let cost = costs .iter() - .find(|cost| cost.rule == "shell-retirement") + .find(|cost| cost.rule == "shell retire partial") .expect("the census carries the row that just ran"); best = best.min(cost.elapsed); } diff --git a/crates/batten/tests/it/shell_write_advisory.rs b/crates/batten/tests/it/shell_write_advisory.rs index 6bcfa96e2..12a675f3b 100644 --- a/crates/batten/tests/it/shell_write_advisory.rs +++ b/crates/batten/tests/it/shell_write_advisory.rs @@ -18,7 +18,7 @@ //! # The drift gate //! //! [`the_two_authorities_agree_on_what_is_governed`] is the mechanism the module -//! header promises. §1 asks that `shell-retirement` and this advisory never +//! header promises. §1 asks that `shell retire partial` and this advisory never //! disagree about the governed set, and the clean way to guarantee it — calling //! the owning module's predicate — does not compile in this engine. So the //! predicate is restated, and restatement without a gate is how two authorities @@ -63,7 +63,7 @@ fn bash_payload(command: &str) -> String { /// /// **THE PREMISE THESE CASES USED TO INHERIT** (CLOUD-1434). They ran at the real /// root, and none of them established that nothing ELSE refuses the call first — -/// yet `claim-needs-receipt` denies any write inside the repository when the +/// yet `claim read unread` denies any write inside the repository when the /// branch carries no claim receipt, and a deny pre-empts the advisory. So the /// positives passed only while the SESSION RUNNING THE SUITE happened to hold a /// receipt, and the negatives passed *vacuously* under a deny: no advisory @@ -96,7 +96,7 @@ fn bench(name: &str) -> PathBuf { Fixture::new(name) .config( "version = 1\n\n\ - [[rule]]\nid = \"shell-write-advisory\"\nkind = \"policy\"\n\ + [[rule]]\nid = \"shell edit early\"\nkind = \"policy\"\n\ scope = \"mediated_call\"\nmodule = \"policy/shell-write-advisory.rego\"\n\ severity = \"warn\"\n", ) @@ -127,7 +127,7 @@ fn signals(dir: &Path, payload: &str) -> bool { /// /// The exit code is asserted alongside, because a `warn` that moved the status /// would be the deny this row refuses — and a deny at write time refuses the one -/// disposition `shell-retirement` admits. +/// disposition `shell retire partial` admits. #[test] fn a_write_to_a_governed_shell_path_signals_without_refusing() { let dir = bench("swa-a_write_to_a_governed_shell_path_s"); @@ -216,7 +216,7 @@ fn a_call_carrying_no_write_target_is_silent() { /// THE DRIFT GATE. The two authorities agree about what is governed. /// -/// The advisory restates `shell-retirement`'s path predicate because calling it +/// The advisory restates `shell retire partial`'s path predicate because calling it /// does not compile — a FUNCTION rule in another package is not reachable even /// though the bundle shares one engine. Restating creates two authorities that /// can disagree, and the disagreement would be invisible: each module keeps @@ -251,12 +251,12 @@ fn the_two_authorities_agree_on_what_is_governed() { ] { assert!( owner.contains(clause), - "shell-retirement no longer carries `{clause}` — the advisory mirrors a \ + "shell retire partial no longer carries `{clause}` — the advisory mirrors a \ predicate that moved, so update both or make the call compile" ); assert!( mirror.contains(clause), - "shell-write-advisory no longer carries `{clause}` — it has drifted from \ + "shell edit early no longer carries `{clause}` — it has drifted from \ the gate it advertises" ); } @@ -281,7 +281,7 @@ fn the_two_authorities_agree_on_what_is_governed() { /// # Why a FIXTURE rather than this repository /// /// The first version of this case drove the real tree and relied on -/// `claim-needs-receipt` to supply the deny. That made the premise depend on +/// `claim read unread` to supply the deny. That made the premise depend on /// whether the SESSION RUNNING THE SUITE happened to hold a claim receipt: with /// one, nothing refuses, only the advisory is emitted, and the case fails for a /// reason that has nothing to do with the defect. Measured — it did exactly that, @@ -298,7 +298,7 @@ fn an_advised_and_denied_call_emits_only_the_refusal() { let bench = Fixture::new("swa-advise-and-deny") .config( "version = 1\nprotected = [\"mise-tasks/**\"]\n\n\ - [[rule]]\nid = \"shell-write-advisory\"\nkind = \"policy\"\n\ + [[rule]]\nid = \"shell edit early\"\nkind = \"policy\"\n\ scope = \"mediated_call\"\nmodule = \"policy/shell-write-advisory.rego\"\n\ severity = \"warn\"\n", ) diff --git a/crates/batten/tests/it/sleep_ban.rs b/crates/batten/tests/it/sleep_ban.rs index d15bb51de..6d9f82e12 100644 --- a/crates/batten/tests/it/sleep_ban.rs +++ b/crates/batten/tests/it/sleep_ban.rs @@ -219,7 +219,7 @@ fn every_delay_carries_an_expect_naming_a_bound_that_resolves() { // that certifies a delay which should not exist is estimating, and // non-negotiable rule 3 forbids that. // - // What DOES stop a twelfth waiver is `delay-waivers-not-growing` in + // What DOES stop a twelfth waiver is `waiver add refused` in // `batten.toml` — a ratchet over the COUNT, which is a real object with a // real exit code and no judgement in it. This test keeps its narrower and // honest job: the annotations that exist point at something that resolves. diff --git a/crates/batten/tests/it/snapshots/it__snapshots__json_output_is_frozen.snap b/crates/batten/tests/it/snapshots/it__snapshots__json_output_is_frozen.snap index 8e8b0bd7d..b82e88dd2 100644 --- a/crates/batten/tests/it/snapshots/it__snapshots__json_output_is_frozen.snap +++ b/crates/batten/tests/it/snapshots/it__snapshots__json_output_is_frozen.snap @@ -6,24 +6,24 @@ expression: stdout_of(&output) "fail_on_warning": false, "findings": [ { - "rule": "no todo", + "rule": "no-todo", "path": "a.rs", "line": 1, "severity": "deny", "report": "fail", "identity": { - "fingerprint": "d9f86609ab986e2fd8b795a15c0d7b6c69d8bac7455046ba3b0889d9a8990169", + "fingerprint": "6010ecbf984d1bab0ef56ac4959473e3e209146d402d86aa13532c016932fba2", "version": "code:2026-08-06" } }, { - "rule": "no todo", + "rule": "no-todo", "path": "b.rs", "line": 1, "severity": "deny", "report": "fail", "identity": { - "fingerprint": "062895ed8666aa9fc5e3b743176424f375694c4fec6f1930135112f51c42d3dc", + "fingerprint": "11a067cdf556f9397aaf4746713629abff4994983289de7bd613d7930d5922b2", "version": "code:2026-08-06" } } diff --git a/crates/batten/tests/it/snapshots/it__snapshots__pointer_output_is_frozen.snap b/crates/batten/tests/it/snapshots/it__snapshots__pointer_output_is_frozen.snap index cd97fd353..2276ee2bf 100644 --- a/crates/batten/tests/it/snapshots/it__snapshots__pointer_output_is_frozen.snap +++ b/crates/batten/tests/it/snapshots/it__snapshots__pointer_output_is_frozen.snap @@ -2,5 +2,5 @@ source: crates/batten/tests/it/snapshots.rs expression: stdout_of(&output) --- -a.rs:1 no todo -b.rs:1 no todo +a.rs:1 no-todo +b.rs:1 no-todo diff --git a/crates/batten/tests/it/spawn_ceilings.rs b/crates/batten/tests/it/spawn_ceilings.rs index 8389e309b..de03b3675 100644 --- a/crates/batten/tests/it/spawn_ceilings.rs +++ b/crates/batten/tests/it/spawn_ceilings.rs @@ -38,7 +38,7 @@ //! //! ─── CLOUD-909's REPLAY, row 6 ─────────────────────────────────────────────── //! -// replay-call: tests/fanout-guard.bats 5a1c1dc mise-tasks/fanout-guard.sh a-spawn-names-few-artifacts deny=2 allow=0 +// replay-call: tests/fanout-guard.bats 5a1c1dc mise-tasks/fanout-guard.sh spawn count wrong deny=2 allow=0 // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] @@ -140,7 +140,7 @@ fn a_manifest_over_the_cap_is_refused() { ); let text = stderr(&refusal); assert!( - text.contains("a-spawn-names-few-artifacts"), + text.contains("spawn count wrong"), "the row that refused, so a reader can find it in the config: {text}" ); // The count AND the ceiling, which is what a reader acts on — one without the @@ -249,7 +249,7 @@ fn an_oversize_prompt_is_refused() { ); let text = stderr(&refusal); assert!( - text.contains("a-spawn-prompt-stays-in-budget"), + text.contains("prompt measure wrong"), "and the row that refused is the budget row, not the manifest one: {text}" ); @@ -273,7 +273,7 @@ fn only_a_spawn_is_judged() { let over = "read a.txt b.txt c.txt d.txt then act"; // ASSERTED BY THE ROWS THAT MUST STAY SILENT, not by the exit code. A tool // this row ignores may still be refused by a NEIGHBOUR — measured here: - // `mcp__Linear__save_issue` carries no `id`, so `filing-needs-a-search` + // `mcp__Linear__save_issue` carries no `id`, so `issue list unread` // refuses it and the exit 2 belongs to row 1. Reading that as row 6 judging a // non-spawn is the misattribution `replay.sh` calls `denied-by-another-row`, // one level in. @@ -290,10 +290,7 @@ fn only_a_spawn_is_judged() { &payload(tool, over), ); let text = stderr(&output); - for row in [ - "a-spawn-names-few-artifacts", - "a-spawn-prompt-stays-in-budget", - ] { + for row in ["spawn count wrong", "prompt measure wrong"] { assert!( !text.contains(row), "this call commits no fresh context window, so {row} owes it nothing: \ diff --git a/crates/batten/tests/it/spawn_census.rs b/crates/batten/tests/it/spawn_census.rs index edaa7da9c..cc0c1a207 100644 --- a/crates/batten/tests/it/spawn_census.rs +++ b/crates/batten/tests/it/spawn_census.rs @@ -71,7 +71,7 @@ fn the_lint_is_denied_in_the_manifest_itself() { // `clippy.toml` alone. Every other lint in this table is `warn`, promoted to // an error only by `mise run lint:clippy`'s `-D warnings`. CLOUD-822 measured // the consequence: `mise exec -- cargo clippy -p batten --all-targets` — the - // escape `no-bare-cargo`'s own refusal text recommends — omits that flag and + // escape `cargo run loose`'s own refusal text recommends — omits that flag and // missed 10 `expect_used` errors. A spawn gate at `warn` would report clean // over an unannotated spawn under a sanctioned command, and the agent would // then quote the clean run as verification. diff --git a/crates/batten/tests/it/spawn_widening.rs b/crates/batten/tests/it/spawn_widening.rs index f7886d22e..b81fb32ea 100644 --- a/crates/batten/tests/it/spawn_widening.rs +++ b/crates/batten/tests/it/spawn_widening.rs @@ -1,4 +1,4 @@ -//! `spawn-widening` over the compiled engine (CLOUD-1338). +//! `spawn add other` over the compiled engine (CLOUD-1338). //! //! # Why this tier, and what the module's own suite structurally cannot prove //! @@ -48,7 +48,7 @@ use batten::rules::{self, Rule}; /// or 3 above, and both reported clean. fn row() -> Rule { serde_json::from_value(serde_json::json!({ - "id": "spawn-widening", + "id": "spawn add other", "kind": "policy", "scope": "tree", "base": "origin/main", @@ -165,14 +165,14 @@ fn an_added_spawn_escape_is_refused() { ); assert_eq!( verdicts(&root), - vec![String::from("spawn-widening")], + vec![String::from("spawn add other")], "an escape this change added is the whole subject of the rule" ); } /// **THE CASE THAT ACTUALLY FIRED IN THE FIELD, half two.** /// -/// `spawn-adapters` refuses a spawn in an unplaced module; adding the module to +/// `adapter place missing` refuses a spawn in an unplaced module; adding the module to /// its set answers that refusal in one edit, and nothing read the edit. Two /// placements landed that way on the branch this rule was written for. #[test] @@ -193,7 +193,7 @@ fn an_added_spawn_placement_is_refused() { ); assert_eq!( verdicts(&root), - vec![String::from("spawn-widening")], + vec![String::from("spawn add other")], "the table is deny-by-omission, so widening it is the escape" ); } @@ -295,7 +295,7 @@ fn another_lints_allow_is_still_an_escape() { ); assert_eq!( verdicts(&root), - vec![String::from("spawn-widening")], + vec![String::from("spawn add other")], "`too_many_arguments` is a claim about the code, not about how a test reports failure" ); } diff --git a/crates/batten/tests/it/staged_facts.rs b/crates/batten/tests/it/staged_facts.rs index c7c4c5612..7e28e868a 100644 --- a/crates/batten/tests/it/staged_facts.rs +++ b/crates/batten/tests/it/staged_facts.rs @@ -8,7 +8,7 @@ //! about the checkout."* So the fixture stages one value, leaves a DIFFERENT //! value in the working tree, and asserts the module sees the staged one. //! -//! That matters beyond tidiness. `lock-complete` is the pure "committed bytes +//! That matters beyond tidiness. `lock cover partial` is the pure "committed bytes //! only" gate — it judges THE COMMIT, not the developer's working copy — so a //! successor reading the worktree would answer a different question and pass //! over a staged-but-unsaved edit. A silent wrong answer, not a missing feature. @@ -264,7 +264,7 @@ fn an_unstaged_path_is_could_not_look_never_an_empty_node() { /// The same probe, over a path whose extension no [`Format`] owns. /// /// `mise.lock` is TOML by content and `.lock` by name, and `Format::for_path` -/// decides on the NAME — so this config is the whole of `lock-complete`'s +/// decides on the NAME — so this config is the whole of `lock cover partial`'s /// blocker expressed as a fixture. const LOCK: &str = r#"version = 1 @@ -492,7 +492,7 @@ fn a_declared_format_does_not_override_an_extension_that_names_one() { /// A lockfile locking every platform this repository installs on, complete. /// /// The base the two cases below vary, and it has to be complete rather than -/// minimal: `lock-complete` decides eight predicates over one file, so a fixture +/// minimal: `lock cover partial` decides eight predicates over one file, so a fixture /// carrying a single platform would be refused for a reason neither case is /// about and the anti-vacuity mirror could never be green. const COMPLETE_LOCK: &str = r#"[[tools."aqua:example/tool"]] @@ -518,7 +518,7 @@ const PARTIAL_ENTRY: &str = r#" checksum = "sha256:abc" "#; -/// Materialize a repository carrying the committed `lock-complete` module. +/// Materialize a repository carrying the committed `lock cover partial` module. fn lock_fixture(name: &str, lock: &str) -> PathBuf { let module = std::fs::read_to_string( Path::new(env!("CARGO_MANIFEST_DIR")) @@ -565,7 +565,7 @@ fn the_committed_lock_rule_refuses_a_partial_entry_over_the_binary() { registered and deciding nothing again\n{answer}{cause}" ); // THE PREDICATE ID, NOT THE ROW ID. A module's finding carries the `rule` id - // the `violation` object declares — `lock-complete` is what `--rule` selects + // the `violation` object declares — `lock cover partial` is what `--rule` selects // and `lock-platform-uninstallable` is what decided — so asserting the row // name here would pass over any module that raised anything at all. assert!( diff --git a/crates/batten/tests/it/stop_posture.rs b/crates/batten/tests/it/stop_posture.rs index c3aeb65b9..e4dab6695 100644 --- a/crates/batten/tests/it/stop_posture.rs +++ b/crates/batten/tests/it/stop_posture.rs @@ -1,4 +1,4 @@ -//! `stop-posture` over the compiled binary (CLOUD-1051). +//! `prose report duplicate` over the compiled binary (CLOUD-1051). //! //! # The defect this file exists because of, stated first //! @@ -14,7 +14,7 @@ //! `batten adjudicate --harness claude-code` against a real payload and reads what a //! host would read. //! -//! # RETIREMENT LEDGER, PER PATH — what `shell-retirement` reads +//! # RETIREMENT LEDGER, PER PATH — what `shell retire partial` reads //! //! The program's successor is the engine's own Stop routine rather than a //! module, because four of `stop-guard`'s five rules spawn or read the tree and @@ -156,7 +156,7 @@ kind = "issue" target = "put it in the row that already owns it, or file one" [[rule]] -id = "stop-posture" +id = "prose report duplicate" kind = "policy" scope = "mediated_call" module = "policy/stop-posture.rego" @@ -424,7 +424,7 @@ fn a_hedged_final_message_reaches_the_host_advisory_channel() { "the nudge travels on the advisory channel: {stdout}" ); assert!( - stdout.contains("stop-posture"), + stdout.contains("prose report duplicate"), "and it names the predicate: {stdout}" ); } @@ -455,7 +455,7 @@ fn the_infinitive_hedge_reaches_the_host_advisory_channel() { "an advisory never changes the exit code: {stdout}" ); assert!( - stdout.contains("additionalContext") && stdout.contains("stop-posture"), + stdout.contains("additionalContext") && stdout.contains("prose report duplicate"), "the infinitive opener must reach the same channel as the pronoun one: {stdout}" ); } @@ -474,7 +474,7 @@ fn an_infinitive_that_is_not_a_flagging_verb_stays_silent() { &stop_payload("One thing to check is whether the exit code is 2.", false), )); assert!( - !stdout.contains("stop-posture"), + !stdout.contains("prose report duplicate"), "a plan to check is not an act of flagging: {stdout}" ); } @@ -642,7 +642,7 @@ fn a_stranded_finding_is_pointed_at_and_the_turn_still_ends() { ); } -/// PRECEDENCE IS MEASURED, NOT ASSERTED. `stop-posture` leads at 3/3 against +/// PRECEDENCE IS MEASURED, NOT ASSERTED. `prose report duplicate` leads at 3/3 against /// `finding-sink`'s 1/1, and two nudges on one turn is how a channel stops being /// read — so when both would fire, exactly one does and it is the first. // UNIX-ONLY, per CLOUD-113: this case spawns a `#!/bin/sh` stub, and the @@ -659,7 +659,7 @@ fn the_measured_rule_keeps_precedence_when_both_would_fire() { &stop_with_transcript(&dir, "One thing I would flag is the exit code."), )); assert!( - stdout.contains("stop-posture"), + stdout.contains("prose report duplicate"), "the measured rule speaks: {stdout}" ); assert!( diff --git a/crates/batten/tests/it/submodule.rs b/crates/batten/tests/it/submodule.rs index 9321ca257..f7544c6a5 100644 --- a/crates/batten/tests/it/submodule.rs +++ b/crates/batten/tests/it/submodule.rs @@ -46,7 +46,7 @@ fn bats(name: &str) -> String { /// A ratchet row over every `.bats` file at any depth — the glob that spans the /// submodule, which is the whole point. -const SPANNING_CONFIG: &str = "version = 1\n\n[[rule]]\nid = \"bats-tests-not-deleted\"\nkind = \"ratchet\"\nglob = \"tests/**/*.bats\"\npattern = \"@test \\\"\"\ndirection = \"non_decreasing\"\nbase = \"main\"\nseverity = \"deny\"\n"; +const SPANNING_CONFIG: &str = "version = 1\n\n[[rule]]\nid = \"bats count dropped\"\nkind = \"ratchet\"\nglob = \"tests/**/*.bats\"\npattern = \"@test \\\"\"\ndirection = \"non_decreasing\"\nbase = \"main\"\nseverity = \"deny\"\n"; /// A superproject with `config`, two of its own bats suites, and a real /// submodule at [`SUBMODULE`] carrying three more. @@ -207,7 +207,7 @@ fn deleting_a_matched_file_outside_the_submodule_still_fires() { "both counts are the superproject's alone: {text:?}" ); assert!( - text.contains("bats-tests-not-deleted"), + text.contains("bats count dropped"), "the finding names the rule: {text:?}" ); } diff --git a/crates/batten/tests/it/suite_subjects.rs b/crates/batten/tests/it/suite_subjects.rs index 44baa30d7..c388eb2a1 100644 --- a/crates/batten/tests/it/suite_subjects.rs +++ b/crates/batten/tests/it/suite_subjects.rs @@ -33,7 +33,7 @@ use batten::rules::{self, Rule}; /// the same column census a consumer's config does. fn row() -> Rule { serde_json::from_value(serde_json::json!({ - "id": "suite-subject-retirable", + "id": "suite retire unclear", "kind": "policy", "scope": "tree", "line_sources": ["tests/*.bats", "tests/**/*.bats"], diff --git a/crates/batten/tests/it/surface.rs b/crates/batten/tests/it/surface.rs index 72feb9664..91488164a 100644 --- a/crates/batten/tests/it/surface.rs +++ b/crates/batten/tests/it/surface.rs @@ -9,7 +9,7 @@ //! Kept out of `tests/cli.rs` deliberately — that file is the exit-code and //! output-contract suite, and other work appends to it. //! -//! # RETIREMENT LEDGER, PER PATH — what `shell-retirement` reads +//! # RETIREMENT LEDGER, PER PATH — what `shell retire partial` reads //! //! CLOUD-1145. `derived-check` was 289.8s — 23.8% of the bats corpus — spent //! re-answering a question this file already answers over the compiled binary. diff --git a/crates/batten/tests/it/target_prune.rs b/crates/batten/tests/it/target_prune.rs index f0904077d..1770a0bf0 100644 --- a/crates/batten/tests/it/target_prune.rs +++ b/crates/batten/tests/it/target_prune.rs @@ -25,7 +25,7 @@ //! # The retirement ledger //! //! `mise-tasks/target-prune.sh` and `tests/target-prune.bats` are retired here -//! under CLOUD-1059. The rows below are what `shell-retirement`'s arm C reads. +//! under CLOUD-1059. The rows below are what `shell retire partial`'s arm C reads. //! //! WHY IT WAS MIGRATED AT ALL, and this one is the campaign working on its author //! a third time — after `semver` and `perf-pair`, and less creditably than diff --git a/crates/batten/tests/it/task_prose.rs b/crates/batten/tests/it/task_prose.rs index fe5631289..04b812ee6 100644 --- a/crates/batten/tests/it/task_prose.rs +++ b/crates/batten/tests/it/task_prose.rs @@ -15,7 +15,7 @@ //! WHAT THIS ASSERTS, AND WHAT IT CANNOT. It asserts **agreement**: the task //! body in `mise.toml` is the authority, and the prose has to name the command //! that body actually runs. So a change to either side without the other is a -//! finding, in both directions — the drift `rules-drift` does not reach here, +//! finding, in both directions — the drift `rule watch other` does not reach here, //! because its restated-default scan is over declared defaults rather than over //! a task's own body. //! @@ -93,8 +93,8 @@ fn the_rules_file_names_the_command_fmt_runs() { /// the regression unguarded in both directions; asserting the opposite keeps one /// case on the sentence and moves which way it points. What now stops the /// regression on the CONFIG side — where it actually lives — is -/// `hk-fix-selection`, whose `task state wrong` reads this same clause -/// and `fix-selection-complete`, which holds hk's own selection to the gate's +/// `gate select wrong`, whose `task state wrong` reads this same clause +/// and `gate fix missing`, which holds hk's own selection to the gate's /// fixer-bearing steps in both directions. Prose alone was never the mechanism; /// it is the half a reader sees. #[test] @@ -103,7 +103,7 @@ fn fmt_is_described_as_the_formatters_only_subset_it_now_is() { assert!( prose.contains("formatters-only subset"), "{RULES} stopped calling `fmt` the formatters-only subset; it IS one since \ - CLOUD-681, and `hk-fix-selection` reads this clause to keep the config and \ + CLOUD-681, and `gate select wrong` reads this clause to keep the config and \ the prose from drifting apart" ); } diff --git a/crates/batten/tests/it/task_receipt.rs b/crates/batten/tests/it/task_receipt.rs index 86faa3156..6ed0b34cc 100644 --- a/crates/batten/tests/it/task_receipt.rs +++ b/crates/batten/tests/it/task_receipt.rs @@ -354,15 +354,15 @@ fn a_call_with_no_receipt_is_could_not_look() { /// never rendered. /// /// Measured on this repository's own policy: `cargo test -p batten` selects -/// `no-bare-cargo`, whose reason names both sanctioned routes, and -/// `task-substitution`, whose subject is whichever declared task leads with +/// `cargo run loose`, whose reason names both sanctioned routes, and +/// `task run loose`, whose subject is whichever declared task leads with /// `cargo` — 13 do. The reader was told to run `attribution-identity`, a task /// that has nothing to do with running tests: a remedy that does not do the job, /// which is the class CLOUD-1050 made unrepresentable in a verdict's own prose /// and the gate ordering put back. /// /// **CI could never have seen it.** A module reading `input.facts.tasks` is -/// could-not-look until a session-start receipt exists, so `task-substitution` +/// could-not-look until a session-start receipt exists, so `task run loose` /// is live in an agent session and inert on a runner — which is exactly why this /// case mints the receipt itself rather than asserting over the committed tree. /// A case over the committed tree passes on a runner whichever way the gates are diff --git a/crates/batten/tests/it/task_registry.rs b/crates/batten/tests/it/task_registry.rs index 035ba407b..ade8138eb 100644 --- a/crates/batten/tests/it/task_registry.rs +++ b/crates/batten/tests/it/task_registry.rs @@ -4,7 +4,7 @@ //! **BOTH HALVES, because the registry is one mechanism read from both ends.** //! An earlier revision of this file landed the reader alone and recorded the //! writer as blocked; that was wrong, and the retraction is on CLOUD-1283. The -//! claim was that `shell-retirement` admits a repointing at the BINDING +//! claim was that `shell retire partial` admits a repointing at the BINDING //! (`reg="$(dirname "$0")/task-registry.sh"`) and none at the SPEND //! (`"$reg" read "$pid" phase_since`). It admits both — the successor declared //! on the arm below is `batten task`, so the span the module derives over a @@ -898,7 +898,7 @@ fn outside_a_repository_a_write_is_could_not_look_rather_than_a_silent_success() // **THE CALL SITES ARE REPOINTED, NOT REWRITTEN.** `mise-tasks/land-lock.sh` // bound the program to `reg` and spent it three times; the successor declared on // the arms below is `batten task`, so each spend's derived span is exactly -// `"$reg"` and `shell-retirement`'s `repoints_at_the_declared_invocation` admits +// `"$reg"` and `shell retire partial`'s `repoints_at_the_declared_invocation` admits // the substitution. `mise.toml`'s `task-registry` task is the same repointing at // the other end — one line, translating the engine's `2`/`3` back to the shell's // `1`/`2` so a caller written against the retiring program's codes still reads diff --git a/crates/batten/tests/it/test_targets.rs b/crates/batten/tests/it/test_targets.rs index feea31abe..bb0d0a7da 100644 --- a/crates/batten/tests/it/test_targets.rs +++ b/crates/batten/tests/it/test_targets.rs @@ -82,7 +82,7 @@ fn install_module(root: &Path) { /// pass here. fn row() -> Rule { serde_json::from_value(serde_json::json!({ - "id": "test-targets", + "id": "test place wrong", "kind": "policy", "scope": "tree", "base": "origin/main", diff --git a/crates/batten/tests/it/todo_promotion.rs b/crates/batten/tests/it/todo_promotion.rs index 29c28c7ce..4dd138e19 100644 --- a/crates/batten/tests/it/todo_promotion.rs +++ b/crates/batten/tests/it/todo_promotion.rs @@ -10,7 +10,7 @@ //! What closed it is not a new predicate. The Ready-block grammar has been //! `crates/batten/src/ready.rs` since CLOUD-1121; `[[mint]] issue-read` has minted //! a receipt from every `get_issue` RESULT since CLOUD-1024; and -//! `an-update-owes-a-recent-read` has forced such a read within 300 seconds of any +//! `issue read stale` has forced such a read within 300 seconds of any //! write since CLOUD-312. This row is those three facts joined by one column: //! the mint's body grew a sixth field carrying the compiled authority's verdict, //! and `requires_field` lets the promotion row read it. @@ -41,7 +41,7 @@ use crate::common; use std::path::{Path, PathBuf}; -use common::{Fixture, run_with_stdin, stderr}; +use common::{Fixture, run, run_with_stdin, stderr, stdout}; /// This repository's own rows, as committed — never a fixture rewriting them. /// @@ -216,7 +216,7 @@ fn a_second_read_of_the_same_unready_row_changes_nothing() { /// The refusal names the row that refused and the state to reach. /// /// The ROW is asserted, not just the code: three rows select this tool now, so an -/// exit 2 alone would be satisfied by `an-update-owes-a-recent-read` firing on the +/// exit 2 alone would be satisfied by `issue read stale` firing on the /// same call — the misattribution `replay.sh` calls `denied-by-another-row`, and /// here it would hide the whole of this change behind a gate that already existed. #[test] @@ -231,12 +231,24 @@ fn the_refusal_names_this_row_and_carries_no_body() { ); let text = stderr(&refusal); assert!( - text.contains("a-todo-promotion-owes-a-ready-verdict"), + text.contains("plan grade unread"), "the refusing row must be nameable, or a reader cannot find it in the config: {text}" ); + // AND THE STATE TO REACH, THROUGH THE HOP THE GRAMMAR CREATES (CLOUD-1638). + // + // This used to read `text.contains("ready")`, which passed because the row + // was called `a-todo-promotion-owes-a-ready-verdict` — the assertion was + // satisfied by prose inside the id. An id drawn from a fixed vocabulary + // cannot carry an arbitrary state name, and that is the point rather than a + // regression: the line is a pointer and the remedy lives one dereference + // away. So the property is asserted where it now lives, which also proves + // the hop works rather than assuming it. + let remedy = run(&repo, &["policy", "rule", "plan grade unread"]); + let remedy_text = format!("{}{}", stdout(&remedy), stderr(&remedy)); assert!( - text.contains("ready"), - "and the state the row has to reach, which is what the reader acts on: {text}" + remedy_text.to_lowercase().contains("ready"), + "and the state the row has to reach, which the reader gets from \ + `policy rule`: {remedy_text}" ); // Rule 4, and it is load-bearing here rather than editorial: the verdict was // computed over an issue body, and a refusal echoing what it read would put diff --git a/crates/batten/tests/it/tool_verdict_facts.rs b/crates/batten/tests/it/tool_verdict_facts.rs index 03c6f99b3..a412c37df 100644 --- a/crates/batten/tests/it/tool_verdict_facts.rs +++ b/crates/batten/tests/it/tool_verdict_facts.rs @@ -20,7 +20,7 @@ //! `evaluator-io-check` and the spawn census are the gates on that and this suite //! does not duplicate them. //! -//! # RETIREMENT LEDGER, PER PATH — what `shell-retirement` reads +//! # RETIREMENT LEDGER, PER PATH — what `shell retire partial` reads //! //! CLOUD-1199's disposition, applied: `pkl-check` RAN `pkl` and then adjudicated //! its exit status in shell. The run stays outside either way — §9's prior art — @@ -76,7 +76,7 @@ //! //! CARRIED — the verdict pair, which is the gate's whole content. A clean config //! records `status clean` and denies nothing; a rejected or unparseable one -//! records `status error` and `validator-verdict-clean` refuses. The two +//! records `status error` and `tool judge dirty` refuses. The two //! rejection cases collapse into one successor because the producer cannot tell //! them apart and never could: both are "the validator exited non-zero", and the //! REASON stays on the terminal rather than entering the record (rule 4). @@ -88,7 +88,7 @@ //! CHANGED — the could-not-look arm, whose exit code moves and whose meaning does //! not. -// changed: "an unreadable config is exit 2, never a pass" crates/batten/tests/tool_verdict_facts.rs the shell gate exited 2 itself when it could not read the config. The producer exits 1 instead — a usage error, since the caller named a row whose declared input is unreadable — and the ADJUDICATION side is unchanged in substance: no record is written, so the id is absent from the map and `validator-verdict-clean` refuses nothing rather than reporting clean. `a_subject_that_cannot_be_read_is_refused_rather_than_keyed` is the successor, and it asserts the stronger half the shell case could not: that no key is composed at all, so a later reader cannot find a verdict over bytes nobody read +// changed: "an unreadable config is exit 2, never a pass" crates/batten/tests/tool_verdict_facts.rs the shell gate exited 2 itself when it could not read the config. The producer exits 1 instead — a usage error, since the caller named a row whose declared input is unreadable — and the ADJUDICATION side is unchanged in substance: no record is written, so the id is absent from the map and `tool judge dirty` refuses nothing rather than reporting clean. `a_subject_that_cannot_be_read_is_refused_rather_than_keyed` is the successor, and it asserts the stronger half the shell case could not: that no key is composed at all, so a later reader cannot find a verdict over bytes nobody read // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] @@ -542,7 +542,7 @@ const SHIPPED: &str = include_str!("../../../../policy/validator-verdict-clean.r const SHIPPED_CONFIG: &str = r#"version = 1 [[rule]] -id = "validator-verdict-clean" +id = "tool judge dirty" kind = "policy" scope = "tree" module = "validator-verdict-clean.rego" @@ -594,7 +594,7 @@ fn the_shipped_module_refuses_a_recorded_error() { let outcome = check(&dir); let (answer, cause) = (stdout(&outcome), stderr(&outcome)); assert!( - answer.contains("validator-verdict-clean"), + answer.contains("tool judge dirty"), "a recorded error must reach the shipped predicate and refuse\n{answer}{cause}" ); } @@ -616,7 +616,7 @@ fn the_shipped_module_passes_a_recorded_clean() { let outcome = check(&dir); let (answer, cause) = (stdout(&outcome), stderr(&outcome)); assert!( - !answer.contains("validator-verdict-clean"), + !answer.contains("tool judge dirty"), "the reserved clean status is not a finding\n{answer}{cause}" ); } diff --git a/crates/batten/tests/it/trunk_watch.rs b/crates/batten/tests/it/trunk_watch.rs index 26ca74f48..fecd7a083 100644 --- a/crates/batten/tests/it/trunk_watch.rs +++ b/crates/batten/tests/it/trunk_watch.rs @@ -7,7 +7,7 @@ //! sha, polling the ref endpoint conditionally so a quiet trunk cost no rate //! limit. `tests/main-watch.bats` pinned eight properties of that loop. Both are //! retired here, and this file is where those eight are answered — the ledger -//! arms below name it, and `shell-retirement`'s `test port missing` arm is what +//! arms below name it, and `shell retire partial`'s `test port missing` arm is what //! refuses a retirement that names no compiled tier at all. //! //! # What it reaches, and what it deliberately does not diff --git a/crates/batten/tests/it/verdict_registry.rs b/crates/batten/tests/it/verdict_registry.rs index 50aab8510..44b00b5ea 100644 --- a/crates/batten/tests/it/verdict_registry.rs +++ b/crates/batten/tests/it/verdict_registry.rs @@ -435,7 +435,7 @@ fn route_findings(name: &str, authority: &str, manifest: &str) -> Vec { common::git_in(&root, &["add", "-A"]); common::git_in(&root, &["commit", "-q", "-m", "fixture"]); let routes_row: Rule = serde_json::from_value(serde_json::json!({ - "id": "verdict-routes-resolve", + "id": "route resolve missing", "kind": "policy", "scope": "tree", "sources": ["batten.toml", "mise.toml"], @@ -489,7 +489,7 @@ fn a_command_route_naming_an_undefined_task_is_refused_over_the_engine() { &authority_with("command", "mise run absent"), MANIFEST ), - vec!["verdict-routes-resolve".to_owned()] + vec!["route resolve missing".to_owned()] ); } diff --git a/crates/batten/tests/it/waivers.rs b/crates/batten/tests/it/waivers.rs index 243adaac8..2c4161c53 100644 --- a/crates/batten/tests/it/waivers.rs +++ b/crates/batten/tests/it/waivers.rs @@ -118,7 +118,7 @@ fn without_a_waiver_the_rule_denies() { assert!(stdout.contains("lib.rs:2 no-todo"), "got: {stdout}"); } -// subsumed: "an exempted entry passes only through a waiver carrying a reason" crates/batten/tests/it/waivers.rs that case was about the waiver SURFACE rather than about `no-source-built-tool` — a live waiver clears the verdict and leaves a pointer-only audit line on stderr — and this drives the compiled binary over a `forbid` row to assert exactly that (CLOUD-1137) +// subsumed: "an exempted entry passes only through a waiver carrying a reason" crates/batten/tests/it/waivers.rs that case was about the waiver SURFACE rather than about `pin add unsafe` — a live waiver clears the verdict and leaves a pointer-only audit line on stderr — and this drives the compiled binary over a `forbid` row to assert exactly that (CLOUD-1137) #[test] fn a_live_waiver_clears_the_verdict_and_audits_on_stderr() { let (repo, home) = repo("waiver-live", &format!("{RULE}{}", waiver(LIVE))); diff --git a/crates/batten/tests/it/wiring_reclaim.rs b/crates/batten/tests/it/wiring_reclaim.rs index 7022ee663..69a4a2a37 100644 --- a/crates/batten/tests/it/wiring_reclaim.rs +++ b/crates/batten/tests/it/wiring_reclaim.rs @@ -32,7 +32,7 @@ //! up. `common::at_home` sets both spellings, so the isolation is one call //! rather than a variable per platform remembered per spawn. //! -//! **Rust rather than a `.bats` suite** (CLOUD-843): `shell-retirement` refuses +//! **Rust rather than a `.bats` suite** (CLOUD-843): `shell retire partial` refuses //! a new one, correctly — the campaign's corpus has to shrink rather than stay //! level while the census reports movement. Nothing here needed bash. diff --git a/crates/batten/tests/it/worktree_registration.rs b/crates/batten/tests/it/worktree_registration.rs index 94a0b74ab..f8b86de8a 100644 --- a/crates/batten/tests/it/worktree_registration.rs +++ b/crates/batten/tests/it/worktree_registration.rs @@ -37,7 +37,7 @@ use std::path::{Path, PathBuf}; use common::{Fixture, git_in, run, stdout}; /// The rule id under test, and the string a case reads its verdict off. -const RULE: &str = "worktree-registration-live"; +const RULE: &str = "registry read missing"; /// The module's real bytes, from this checkout. /// @@ -62,7 +62,7 @@ fn config() -> String { "version = 1\n\ \n\ [[rule]]\n\ - id = \"worktree-registration-live\"\n\ + id = \"registry read missing\"\n\ kind = \"policy\"\n\ scope = \"tree\"\n\ git = [\"worktrees\"]\n\ diff --git a/crates/batten/tests/it/zero_config.rs b/crates/batten/tests/it/zero_config.rs index da1c633b3..6e5fa65fd 100644 --- a/crates/batten/tests/it/zero_config.rs +++ b/crates/batten/tests/it/zero_config.rs @@ -55,7 +55,7 @@ const CONFLICTED: &str = "fn main() {}\n<<<<<<< HEAD\nours\n=======\ntheirs\n>>> /// into, because in that case there is no config file to match. Comparing a /// `regex` spelling isolates the variable this case is actually about: which /// layer supplied the policy, not what the policy says. -const SAME_AS_DEFAULTS: &str = "version = 1\n\n[[rule]]\nid = \"no-conflict-markers\"\n\ +const SAME_AS_DEFAULTS: &str = "version = 1\n\n[[rule]]\nid = \"source carry broken\"\n\ kind = \"forbid\"\nglob = \"**/*\"\nregex = \"^<{7} \"\nseverity = \"deny\"\n\ scope = \"tree\"\n"; @@ -96,7 +96,7 @@ fn a_seeded_violation_of_a_default_rule_is_a_violation() { assert_eq!(output.status.code(), Some(2), "stderr: {}", stderr(&output)); assert_eq!( stdout(&output), - "src/lib.rs:2 no-conflict-markers\n", + "src/lib.rs:2 source carry broken\n", "the finding is a pointer — `path:line rule-id`, never the matched line" ); assert!(stderr(&output).contains(config::DEFAULTS_NOTE)); diff --git a/crates/batten/tests/policy_modules.rs b/crates/batten/tests/policy_modules.rs index 0cb1fc69f..1bd9e678d 100644 --- a/crates/batten/tests/policy_modules.rs +++ b/crates/batten/tests/policy_modules.rs @@ -21,7 +21,7 @@ // THE ONE TARGET THAT STAYS SEPARATE (CLOUD-1210). `evaluator-io-check` // probes this file with `cargo test --test policy_modules`, and that task is -// a governed `mise-tasks/*.sh`: `shell-retirement` gives it exactly two +// a governed `mise-tasks/*.sh`: `shell retire partial` gives it exactly two // shapes — retire it whole, or leave it alone — so repointing the probe at // the group is not an edit this change may make. Keeping the target is the // cheaper half of that trade: one extra link against a gate that stays live. diff --git a/fuzz/corpus/config_parse/batten.example.toml b/fuzz/corpus/config_parse/batten.example.toml index a72b128d5..709afc61e 100644 --- a/fuzz/corpus/config_parse/batten.example.toml +++ b/fuzz/corpus/config_parse/batten.example.toml @@ -124,7 +124,7 @@ redirect = "append, or write through the surface that owns the file" # default). Scope is never severity: a severity value in the scope # key (or the reverse) is refused with exit 1, not reinterpreted. [[rule]] -id = "no-conflict-markers" +id = "source carry broken" kind = "forbid" glob = "**/*.rs" pattern = "<<<<<<< " @@ -244,7 +244,7 @@ reason = "set the tool's own severity to deny; do not let a warning ride an exit # A git-ignored batten.local.toml may NOT waive a rule declared here — a waiver # lowers the bar, so the durable tier is the committed authority alone (§8). [[waiver]] -rule = "no-conflict-markers" +rule = "source carry broken" reason = "the vendored tree is being replaced in CLOUD-123; gating it churns the diff" expires = "2026-12-31" path = "vendor/**" diff --git a/rules/README.md b/rules/README.md index 6df511b03..5f1e30a4a 100644 --- a/rules/README.md +++ b/rules/README.md @@ -52,13 +52,13 @@ task header or source comment, and the prose is a pointer; **(b) vendor-specific **(c) neutral doctrine with no mechanism** — rule 2's _half a change_, which owes a gate or a filed gap. -| file | class | the authority it points at, or the gap | -| ------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `rust.md` | (a) | `clippy.toml` (lint and spawn census), `Cargo.toml` workspace lints, `crates/batten/src/exit.rs` for the exit table. `crates/batten/tests/it/ambient_authority.rs` holds the client closure; `mem:core` holds the module map. | -| `toolchain.md` | (a) | `policy/shell-retirement.rego` owns the two shapes and every admission arm; `mise.toml` task bodies own the lifecycle; `mise-tasks/step-receipt.sh` owns the receipt spec. The prose restates none of the arm counts any more — CLOUD-1150 is what a restated count cost. | -| `commits.md` | (a) | `batten.toml`'s `[attribution]` table and `crates/batten/src/commit.rs`; `release-plz.toml` for the bump arrows. | -| `scanning.md` | (a) + a stated gap | `batten.toml`'s `no-tool-substitution` gates the substitution axis. **Instrument suitability is (c) and is declared unowned in the file itself** — there is no honest exit code over "was this the right class of instrument", and non-negotiable rule 3 refuses a gate over a judgement. `crates/batten/tests/it/scanner_taxonomy.rs` gates the prose's shape, not its advice. | -| `policy-modules.md` | (a) | `policy/rules-drift.rego` holds its `input.*` key lists to the generated schemas in both directions; `crates/batten/src/policy.rs` owns the load-time refusals. Its own §"What this file does not gate" states the residue. | +| file | class | the authority it points at, or the gap | +| ------------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `rust.md` | (a) | `clippy.toml` (lint and spawn census), `Cargo.toml` workspace lints, `crates/batten/src/exit.rs` for the exit table. `crates/batten/tests/it/ambient_authority.rs` holds the client closure; `mem:core` holds the module map. | +| `toolchain.md` | (a) | `policy/shell-retirement.rego` owns the two shapes and every admission arm; `mise.toml` task bodies own the lifecycle; `mise-tasks/step-receipt.sh` owns the receipt spec. The prose restates none of the arm counts any more — CLOUD-1150 is what a restated count cost. | +| `commits.md` | (a) | `batten.toml`'s `[attribution]` table and `crates/batten/src/commit.rs`; `release-plz.toml` for the bump arrows. | +| `scanning.md` | (a) + a stated gap | `batten.toml`'s `tool select other` gates the substitution axis. **Instrument suitability is (c) and is declared unowned in the file itself** — there is no honest exit code over "was this the right class of instrument", and non-negotiable rule 3 refuses a gate over a judgement. `crates/batten/tests/it/scanner_taxonomy.rs` gates the prose's shape, not its advice. | +| `policy-modules.md` | (a) | `policy/rules-drift.rego` holds its `input.*` key lists to the generated schemas in both directions; `crates/batten/src/policy.rs` owns the load-time refusals. Its own §"What this file does not gate" states the residue. | **No rule is class (b).** That is the finding rather than an omission: nothing in these five files described a Claude Code affordance. The only vendor-specific diff --git a/rules/commits.md b/rules/commits.md index b9a1b5d46..623db52fd 100644 --- a/rules/commits.md +++ b/rules/commits.md @@ -128,7 +128,7 @@ reason: unknown_key` — the key is unpublished, not absent. That is CLOUD-591's **So: refuse it, and do not re-derive this.** `mise run attribution-identity` writes the accountable identity repo-locally, and local beats global, which is why every commit here is attributed correctly and the gate has never failed. -`no-denied-identity-prescribed` is the standing half — a `forbid` row refusing +`remedy carry refused` is the standing half — a `forbid` row refusing any tracked Markdown that prescribes the denied identity, so the hook's remedy cannot be copied into this tree and become a second authority. A repo-level stop hook answering in the same channel was considered and **rejected on noise**: by diff --git a/rules/scanning.md b/rules/scanning.md index 42cd82b93..2c3a16ccb 100644 --- a/rules/scanning.md +++ b/rules/scanning.md @@ -223,7 +223,7 @@ symptom. `mem:serena-setup` carries the four gates and how to tell them apart; the rule here is the disposition: **report the prompt as a config finding and keep using the instrument, never quietly downgrade the instrument.** -`no-tool-substitution` in `batten.toml` is the authority on which utility over +`tool select other` in `batten.toml` is the authority on which utility over which path is refused, and on what it deliberately does not catch. Read it there; a second copy of that corpus here is the drift this file exists to avoid. @@ -239,7 +239,7 @@ would have to decide over is a judgement, and non-negotiable rule 3 says a gate resolves to a command and an exit code, never a model verdict. **Substitution is gated.** Reaching for a shell text utility where the structured -surface answers the question is decided by `no-tool-substitution`, a `pipeline` +surface answers the question is decided by `tool select other`, a `pipeline` row over the command line — a real object, a real exit code. It is a deny, and its refusal points back here to choose between the classes above. So silence from that gate is not evidence you picked the right class; it only means you did not From 5f42b3dd05627c6d46358bb83906dadb5f7ceb5a Mon Sep 17 00:00:00 2001 From: Alec Wenzowski Date: Wed, 9 Sep 2026 14:43:34 +0000 Subject: [PATCH 06/23] fix(policy): keep `load` under its line budget, and migrate the rego test ids MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two things the slow tier caught that the commit hook skips. `policy-test` failed on four module tests asserting `v.rule == ""` — `ancestry-decides-nothing`, `module-layering` and `spawn-adapters` (twice). A module's own tests name the row that raises the finding, so they are a migration surface like any other. `lint:clippy` had `load` at 115/100 once the per-rule class map landed in it. Two extractions, both at seams the function already had: `record_raised` is the record both arms of the loop owe — the union in `emitted` answers "is every declared class raised" and `per_rule` answers how many classes ONE row raises, which a merged set cannot give back — and `collidable_tokens` is the set a rule id could clash with, excluding retired classes because a tombstone is not a name in use. Worth recording: three earlier attempts at this reported an unchanged 115. The edits had not landed — a mutating script was chained with the long `lint:clippy` run in one backgrounded call, so the script's AssertionError went to a log nobody read while clippy ran on unmodified source. Verify the edit, then run the gate; never in one breath. Refs: CLOUD-1638 --- crates/batten/src/policy.rs | 64 ++++++++++++++++++---------- policy/ancestry-decides-nothing.rego | 2 +- policy/module-layering.rego | 2 +- policy/spawn-adapters.rego | 4 +- 4 files changed, 45 insertions(+), 27 deletions(-) diff --git a/crates/batten/src/policy.rs b/crates/batten/src/policy.rs index 942679c03..4dd9df846 100644 --- a/crates/batten/src/policy.rs +++ b/crates/batten/src/policy.rs @@ -856,12 +856,7 @@ pub fn load( check_tree_paths_are_emittable(rule, &bundle, source_key)?; check_no_inline_regex(rule, &bundle, &declared_patterns, source_key)?; check_verdicts_are_declared(rule, &bundle, ®istry, source_key)?; - let raised = emitted_verdicts(&bundle); - per_rule - .entry(rule.id.clone()) - .or_default() - .extend(raised.iter().cloned()); - emitted.extend(raised); + record_raised(rule, &bundle, &mut emitted, &mut per_rule); } claim_ids(&mut ids, &declared, source_key)?; bundles.push(bundle.with_severity(rule)); @@ -890,12 +885,7 @@ pub fn load( check_tree_paths_are_emittable(rule, &bundle, where_it_came_from)?; check_no_inline_regex(rule, &bundle, &declared_patterns, where_it_came_from)?; check_verdicts_are_declared(rule, &bundle, ®istry, where_it_came_from)?; - let raised = emitted_verdicts(&bundle); - per_rule - .entry(rule.id.clone()) - .or_default() - .extend(raised.iter().cloned()); - emitted.extend(raised); + record_raised(rule, &bundle, &mut emitted, &mut per_rule); } claim_ids(&mut ids, &declared, where_it_came_from)?; @@ -905,17 +895,7 @@ pub fn load( if checks == ModuleChecks::Run { check_registry_is_exhausted(verdicts, &emitted)?; - let tokens: BTreeSet = verdicts - .iter() - .filter(|entry| !entry.retired()) - .map(|entry| entry.id.clone()) - .chain( - crate::verdict::native_tokens() - .iter() - .map(|t| (*t).to_owned()), - ) - .collect(); - check_collapse(rules, &per_rule, &tokens)?; + check_collapse(rules, &per_rule, &collidable_tokens(verdicts))?; } Ok(bundles) } @@ -1630,6 +1610,44 @@ fn check_tree_paths_are_emittable(rule: &Rule, bundle: &Bundle, source: &str) -> /// # Errors /// /// A [`UsageError`] (exit `1`) naming the unraised tokens. +/// Record what one bundle raised, in the two shapes the checks below need. +/// +/// `emitted` answers "is every declared class raised"; `per_rule` answers how +/// many classes ONE row raises, which the collapse predicate needs and a set +/// already merged cannot give back (CLOUD-1638). Both arms of [`load`]'s loop +/// owe this, and a second copy is where the two drift. +fn record_raised( + rule: &Rule, + bundle: &Bundle, + emitted: &mut BTreeSet, + per_rule: &mut BTreeMap>, +) { + let classes = emitted_verdicts(bundle); + per_rule + .entry(rule.id.clone()) + .or_default() + .extend(classes.iter().cloned()); + emitted.extend(classes); +} + +/// Every class token a rule id could collide with: declared, live, and native. +/// +/// A RETIRED class is excluded deliberately — its token is a tombstone rather +/// than a name in use, and refusing a row for spelling one would refuse a name +/// nothing answers to. +fn collidable_tokens(verdicts: &[crate::verdict::DeclaredVerdict]) -> BTreeSet { + verdicts + .iter() + .filter(|entry| !entry.retired()) + .map(|entry| entry.id.clone()) + .chain( + crate::verdict::native_tokens() + .iter() + .map(|token| (*token).to_owned()), + ) + .collect() +} + /// One name where a rule and a class name one thing (CLOUD-1638). /// /// # The predicate is a property of the PAIR, in both directions diff --git a/policy/ancestry-decides-nothing.rego b/policy/ancestry-decides-nothing.rego index ffd1af2e6..6f71197b8 100644 --- a/policy/ancestry-decides-nothing.rego +++ b/policy/ancestry-decides-nothing.rego @@ -84,7 +84,7 @@ test_the_verb_in_command_position_is_refused if { "arguments": ["merge-base"], "line": 88, }]}}} - v.rule == "ancestry-decides-nothing" + v.rule == "patch judge wrong" } test_every_spelling_is_refused if { diff --git a/policy/module-layering.rego b/policy/module-layering.rego index f1dc6e204..fde73655c 100644 --- a/policy/module-layering.rego +++ b/policy/module-layering.rego @@ -760,7 +760,7 @@ test_the_documented_cycle_claim_is_refused if { "crates/batten/src/rules.rs", [internal("hook", 52)], ) - v.rule == "module-layering" + v.rule == "layer place wrong" } # The row's own acceptance clause, spelled as a case. diff --git a/policy/spawn-adapters.rego b/policy/spawn-adapters.rego index f65e5fe71..a09a37e74 100644 --- a/policy/spawn-adapters.rego +++ b/policy/spawn-adapters.rego @@ -246,7 +246,7 @@ at(path, line) := {"path": path, "line": line, "lint": "clippy::disallowed_types # The case the rule exists for: a spawn in a module nobody placed. test_a_spawn_in_an_unplaced_module_is_refused if { some v in violation with input as census([at("crates/batten/src/git.rs", 12)]) - v.rule == "spawn-adapters" + v.rule == "adapter place missing" } # And the placement is the point. `exec` is the sanctioned boundary; a spawn @@ -300,7 +300,7 @@ test_an_empty_census_is_a_real_clean if { # tree with no unplaced spawns. test_an_absent_census_refuses_rather_than_passing if { some v in violation with input as {"tree": {"symbols": null}} - v.rule == "spawn-adapters" + v.rule == "adapter place missing" } # The same answer when the key is missing altogether, which is what a row that From 655b17bad19f2bd9f97130f420de672c3bebe255 Mon Sep 17 00:00:00 2001 From: Alec Wenzowski Date: Wed, 9 Sep 2026 15:31:58 +0000 Subject: [PATCH 07/23] feat(policy)!: put the module finding ids in the grammar too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The second half of the grammar arm. A module's `"rule":` is a FINDING name, not the `[[rule]]` row id — `run-shape.rego` declares five under one row — and it is what the emitted line and `policy rule` carry, so migrating the row ids alone left half the names a reader meets as free-text kebab. 136 finding ids across 74 modules. SEVENTY-FIVE NAME THEMSELVES: a finding that solely raises one class takes that class's token, which is the collapse rule one level down. The other 25 are chosen, and nine of them had to be — the `sbom-*` findings share three classes between them, so no collapse is available and each needs its own name. THE GATE, or the rename is half a change. `check_finding_ids` runs at load, over the module SOURCE. Three readers were wrong before this one: - `Bundle::declared` is read back from the engine and is not a clean list — `module-layering`'s `forbidden["rules"] contains "hook"` surfaces `hook` there, so holding that set to the grammar refused a name no reader meets; - matching `rules contains` anywhere hit `named_rules contains` in `rules-drift.rego` and took `path` out of its body; - `"rule":` is also a fixture KEY — `{"rule": [{"id": "r", …}]}` is the consumer's table as test input — so the value must be a string. Checked at load and never on the hot path: this is a property of the declaration, so `hook` passes `words: None` and says so. `Resolved` now carries `[vocabulary]`, registered AUTHORITY-ONLY for `verdict`'s reason one level sharper: these are the words every class token and rule id is spelled from, so a local row would not add a name, it would change which names are sayable. Without the field the gate could not fire on the surface that actually loads modules — `check` and `enforce` resolve before they load, so a gate reading only `Config` would be one no real run reaches. The hand-rolled construction sites collapse into `Vocabulary::from`, which is also what got `run_recorded` back under its line budget. Two exemptions, both inherited rather than invented: a consumer declaring no vocabulary is not held to it, and an id that IS a class token is governed by the class registry — a vendored preset may spell a word no consumer declares, which is how `trunk push forced` refused itself twice before this landed. Schemas regenerated for the new key; `config-lint` and all 36 `config_schema` cases green. Refs: CLOUD-1638 Admits: cf13e9608d91a0ca386d6ae8d41c650d00ff3e5a5649388e369eefc1e80ca57c Admits-rule: protected-mutation Admits-verdict: path write refused Admits-subject: batten.toml Admits-anchor: call:a3d4e344b62c505d428afb671759e55223c182a6 Admits-epoch: c654be7099174b6df01bc0f1fe97e9fba5bd5f3d38eb013303a85a03778e40b8 Admits-author: alec@wenzowski.com Admits-prev: 904fd90a4c98168655f1f1a127d4ad1d1c07bfa94f24b49946d487ceadbfb69c Admits-answer-lost: The grammar arm covers 136 row ids and stops short of the 136 finding ids the same section puts in scope, so the gate would ship looking complete while covering half the names a reader actually meets on a refusal line. Admits-answer-precondition: No surface can express this change. `config read first` reads batten.toml and `patch run first` restores it; neither writes. The finding-id half of CLOUD-1638's grammar arm renames the `[[waiver]] rule` entries that name a module finding, and `taplo` reformatted the table it sits in — both are writes to the authority that only a write can make. The diff is reviewed in the PR closing CLOUD-1638. Admits-answer-rejected-route: I rejected `patch run first`: restoring batten.toml reverts the migration rather than completing it, and leaves the waiver rows naming ids no module declares any more. `config read first` does not apply — reading is what produced the census that found these rows, and it changes nothing. --- batten.toml | 8 +- crates/batten/src/hook.rs | 3 + crates/batten/src/lib.rs | 17 +-- crates/batten/src/policy.rs | 131 +++++++++++++++++- .../ci-hygiene/spend-is-authorised.rego | 16 +-- .../ci-hygiene/wiring-can-be-reached.rego | 34 ++--- .../commit-hygiene/no-empty-commit.rego | 12 +- .../already-landed-work-is-not-relanded.rego | 6 +- .../graded-head-is-not-regraded.rego | 6 +- .../landing-loop/lap-waits-on-one-answer.rego | 6 +- .../lease-authorises-the-branch.rego | 6 +- .../rebase-conflict-stops-the-lap.rego | 6 +- .../mise/action-version-matches-the-pin.rego | 14 +- .../presets/mise/task-over-executable.rego | 6 +- .../pinned-program-probed-bare.rego | 4 +- .../pinned-program-via-the-pin.rego | 6 +- .../shebang-names-its-language.rego | 6 +- .../shell-hygiene/sibling-resolves.rego | 6 +- .../presets/trunk-based/no-force-push.rego | 14 +- crates/batten/src/resolve.rs | 16 +++ crates/batten/tests/it/admission_narrowing.rs | 2 + crates/batten/tests/it/bats_invocation.rs | 1 + crates/batten/tests/it/cfg_gated_test.rs | 1 + crates/batten/tests/it/ci_cache_declared.rs | 1 + crates/batten/tests/it/ci_hygiene.rs | 1 + crates/batten/tests/it/ci_parity.rs | 2 + crates/batten/tests/it/ci_suite_lane.rs | 1 + crates/batten/tests/it/claim_order.rs | 1 + crates/batten/tests/it/document_read_count.rs | 1 + crates/batten/tests/it/filed_here.rs | 1 + crates/batten/tests/it/fixture_forks.rs | 1 + crates/batten/tests/it/hk_fix_selection.rs | 1 + crates/batten/tests/it/identity_churn.rs | 1 + crates/batten/tests/it/landing_roster.rs | 1 + crates/batten/tests/it/mise_preset.rs | 1 + .../batten/tests/it/mutation_declared_case.rs | 1 + crates/batten/tests/it/nextest_slow.rs | 1 + crates/batten/tests/it/obligations_bound.rs | 1 + crates/batten/tests/it/plan_complete.rs | 1 + crates/batten/tests/it/policy_test_suite.rs | 1 + crates/batten/tests/it/policy_tree.rs | 1 + crates/batten/tests/it/prebuilt_lint.rs | 1 + crates/batten/tests/it/prose_only.rs | 2 + .../tests/it/release_provision_parity.rs | 1 + crates/batten/tests/it/remedy_authorship.rs | 1 + crates/batten/tests/it/review_dispatched.rs | 3 + crates/batten/tests/it/rule_cost_census.rs | 1 + crates/batten/tests/it/shell_retirement.rs | 1 + crates/batten/tests/it/spawn_widening.rs | 1 + crates/batten/tests/it/suite_subjects.rs | 1 + crates/batten/tests/it/test_targets.rs | 1 + crates/batten/tests/it/verdict_registry.rs | 2 + crates/batten/tests/policy_modules.rs | 2 + policy/agentic-experiment-record.rego | 14 +- policy/cfg-gated-test.rego | 6 +- policy/ci-cache-declared.rego | 44 +++--- policy/ci-parity.rego | 62 ++++----- policy/filed-here.rego | 12 +- policy/fixture-forks.rego | 10 +- policy/hk-plan-required.rego | 20 +-- policy/landing-roster-guarded.rego | 4 +- policy/lock-complete.rego | 66 ++++----- policy/nextest-slow.rego | 12 +- policy/obligations-bound.rego | 6 +- policy/perf-assert.rego | 16 +-- policy/plan-complete.rego | 8 +- policy/release-provision-parity.rego | 10 +- policy/remedy-authorship.rego | 12 +- policy/repetition-without-progress.rego | 4 +- policy/review-answered.rego | 16 +-- policy/rules-drift.rego | 72 +++++----- policy/run-shape.rego | 40 +++--- policy/sbom-inventory.rego | 68 ++++----- policy/shell-retirement.rego | 30 ++-- policy/shell-write-advisory.rego | 4 +- policy/test-targets.rego | 6 +- 76 files changed, 545 insertions(+), 360 deletions(-) diff --git a/batten.toml b/batten.toml index a32757d81..ff926316c 100644 --- a/batten.toml +++ b/batten.toml @@ -7722,13 +7722,13 @@ expires = "2026-10-31" # finding anchor by the module id too — and nothing gates the mismatch, which is # the third surface keyed off a different id from the one `--rule` selects. [[waiver]] -rule = "platform-gated-test-added" +rule = "test cover unseen" path = "crates/batten/tests/it/mutate.rs" reason = "the sweep's own watchdog needs a suite that never returns, and every part of that fixture is already unix-only: the case reaches `TOY_GATE` (:107), `CAUGHT` (:134) and `lend_bats` (:177), each carrying `#[cfg(unix)]` because bats is a bash program with no extension that Windows can neither symlink nor execute. So the `cfg!` arm this rule prefers would not type-check — the three items do not exist on that target — and the attribute is required rather than chosen to silence a leg, which is the one bar this route is held to. The case it covers is the anti-vacuity for CLOUD-1726: `BATS_TEST_TIMEOUT` was removed because the runner does not reap its `sleep N` watchdog on a FAILING case and a caught mutation is a failing case, so without a bound of our own a hanging mutant would hold a sweep forever. `base = \"origin/main\"` makes this floor itself on landing, so the row is expected to lapse unused." expires = "2026-10-31" [[waiver]] -rule = "platform-gated-test-added" +rule = "test cover unseen" path = "crates/batten/src/provision.rs" reason = "the relink case asserts over `std::os::unix::fs::MetadataExt::ino`, a symbol absent on the Windows target, so the `cfg!` arm this rule prefers would not type-check and the attribute is required rather than chosen to silence a leg. `base = \"origin/main\"` makes this floor itself on landing, so the row is expected to lapse unused." expires = "2026-10-31" @@ -7804,7 +7804,7 @@ expires = "2026-10-31" # that inversion; the fix is in `lint.rs` in this commit, because a wrongly # refusing gate is a defect to repair rather than a ticket to file. [[waiver]] -rule = "filed-over-own-diff" +rule = "issue file same" reason = "CLOUD-1547 is implemented and closed by this PR, so `closes` is the exemption that applies; it cannot fire because `pr-closes` is minted from a `gh pr view` call and there is no `gh` on this host (CLOUD-1126, residual on CLOUD-1481). The class's own `path admit first` override route does not consume — eight admissions were spent and the findings did not move, because the anchor resolver falls back to a `call:` anchor on zero stored-finding matches and nothing queries one for a tree finding (CLOUD-1551). Remove this with CLOUD-1551." expires = "2026-10-11" @@ -7839,7 +7839,7 @@ expires = "2026-10-11" # # Remove this with CLOUD-1551, alongside its sibling above. [[waiver]] -rule = "filed-and-left-open" +rule = "issue file held" reason = "CLOUD-1551 defeats this rule's override route the same way it defeats `filed-over-own-diff` above, and measurably worse: an admission binds `call:`, so the replay every landing lap performs when trunk moves orphans the anchor and the spent admission stops suppressing. Measured on two consecutive laps of one tree — lap 1 replayed nothing and reported 0, lap 2 replayed and reported 1, with `06b322be` spent throughout. Re-minting cannot escape it, since the commit carrying the `Admits:` block becomes the new HEAD; and `Waiver::path` cannot narrow this rule, whose subject is an artifact id rather than a path. The deferral's articulation is hash-bound in `13c0dd5e` regardless. Remove this with CLOUD-1551." expires = "2026-10-11" diff --git a/crates/batten/src/hook.rs b/crates/batten/src/hook.rs index adf64dd04..285d47115 100644 --- a/crates/batten/src/hook.rs +++ b/crates/batten/src/hook.rs @@ -3285,6 +3285,8 @@ impl Policy { crate::policy::Vocabulary { patterns: &resolved.patterns, verdicts: &resolved.verdicts, + // The hot path does not re-check an authoring property (CLOUD-1638). + words: None, recorders: &resolved.recorders, }, crate::policy::ModuleChecks::SkipOnHotPath, @@ -12038,6 +12040,7 @@ mod tests { crate::policy::Vocabulary { patterns: &[], verdicts: &fixture_verdicts, + words: None, recorders: &[], }, crate::policy::ModuleChecks::Run, diff --git a/crates/batten/src/lib.rs b/crates/batten/src/lib.rs index 5a01abecb..3ca2493f1 100644 --- a/crates/batten/src/lib.rs +++ b/crates/batten/src/lib.rs @@ -800,6 +800,7 @@ fn run_baseline( policy::Vocabulary { patterns: &config.patterns, verdicts: &config.verdicts, + words: (!config.vocabulary.is_empty()).then_some(&config.vocabulary), recorders: &config.recorders, }, &root, @@ -1312,11 +1313,7 @@ fn run_state_record( rules::run_recorded( &config.rules, &config.provisions, - policy::Vocabulary { - patterns: &config.patterns, - verdicts: &config.verdicts, - recorders: &config.recorders, - }, + policy::Vocabulary::from(&config), Path::new("."), checks, surface, @@ -5890,11 +5887,7 @@ fn admission_anchor( let Ok(bundles) = policy::load( root, &policy_rows, - policy::Vocabulary { - patterns: &config.patterns, - verdicts: &config.verdicts, - recorders: &config.recorders, - }, + policy::Vocabulary::from(config), // The same entitlement the run below is given. A mint answers about // one rule over one subject, so registry equality's exhausted half — // a property of the whole authority — is not this verb's to assert. @@ -5928,6 +5921,7 @@ fn admission_anchor( policy::Vocabulary { patterns: &config.patterns, verdicts: &config.verdicts, + words: (!config.vocabulary.is_empty()).then_some(&config.vocabulary), recorders: &config.recorders, }, root, @@ -6153,6 +6147,7 @@ fn run_policy_test(json: bool, overrides: &Overrides, out: &mut dyn Write) -> Re policy::Vocabulary { patterns: &config.patterns, verdicts: &config.verdicts, + words: (!config.vocabulary.is_empty()).then_some(&config.vocabulary), recorders: &config.recorders, }, policy::ModuleChecks::Run, @@ -15121,6 +15116,7 @@ fn filed_here_pointers( let vocabulary = policy::Vocabulary { patterns: &config.patterns, verdicts: &config.verdicts, + words: (!config.vocabulary.is_empty()).then_some(&config.vocabulary), recorders: &config.recorders, }; // `run_static_over` WITH AN INSTANT, because the four-argument wrapper hands @@ -18108,6 +18104,7 @@ fn run_rules( let vocabulary = policy::Vocabulary { patterns: &config.patterns, verdicts: &config.verdicts, + words: (!config.vocabulary.is_empty()).then_some(&config.vocabulary), recorders: &config.recorders, }; let (selected, checks) = select_rules(&config.rules, only)?; diff --git a/crates/batten/src/policy.rs b/crates/batten/src/policy.rs index 4dd9df846..ebde97992 100644 --- a/crates/batten/src/policy.rs +++ b/crates/batten/src/policy.rs @@ -593,6 +593,12 @@ pub struct Vocabulary<'a> { pub patterns: &'a [crate::pattern::NamedPattern], /// The `[[verdict]]` table (CLOUD-1050). pub verdicts: &'a [crate::verdict::DeclaredVerdict], + /// The `[vocabulary]` word lists (CLOUD-1638). + /// + /// `None` where the consumer declared none, which is the same exemption the + /// class grammar takes: a tree with no lists cannot satisfy membership, so + /// holding its finding ids to them would be a demand with no fix available. + pub words: Option<&'a crate::verdict::Vocabulary>, /// The `[[recorder]]` table (CLOUD-1051). /// /// Here for the reason stated above rather than as a third thing bolted on: @@ -613,6 +619,7 @@ impl Vocabulary<'_> { pub const EMPTY: Vocabulary<'static> = Vocabulary { patterns: &[], verdicts: &[], + words: None, recorders: &[], }; } @@ -622,11 +629,28 @@ impl<'a> From<&'a crate::config::Config> for Vocabulary<'a> { Vocabulary { patterns: &config.patterns, verdicts: &config.verdicts, + words: (!config.vocabulary.is_empty()).then_some(&config.vocabulary), recorders: &config.recorders, } } } +/// The same four tables off a RESOLVED config (CLOUD-1638). +/// +/// `check` and `enforce` resolve before they load, so a converter that only +/// took `Config` left every real run hand-rolling the struct — and the finding +/// grammar reached none of them, because the hand-rolled sites had no `words`. +impl<'a> From<&'a crate::resolve::Resolved> for Vocabulary<'a> { + fn from(resolved: &'a crate::resolve::Resolved) -> Self { + Vocabulary { + patterns: &resolved.patterns, + verdicts: &resolved.verdicts, + words: (!resolved.vocabulary.is_empty()).then_some(&resolved.vocabulary), + recorders: &resolved.recorders, + } + } +} + /// Whether a `load` re-derives the AST-borne config checks. /// /// **A placement decision, and it was measured rather than reasoned.** The two @@ -690,6 +714,7 @@ pub fn load( let Vocabulary { patterns, verdicts, + words, recorders: _, } = vocabulary; // The table is validated at PARSE, beside `verbs` and `redirects` and for @@ -711,6 +736,7 @@ pub fn load( // classes THIS row raises — and a set that has already been merged cannot // answer it. let mut per_rule: BTreeMap> = BTreeMap::new(); + let tokens = collidable_tokens(verdicts); let mut bundles = Vec::new(); // Keyed on the scope's WORD rather than the enum, so this set does not oblige // `RuleScope` to carry `Ord` for one local lookup — the derive would be a @@ -869,6 +895,9 @@ pub fn load( (None, None) => Vec::new(), }; let sources = read_sources(root, &paths, reference, &rule.id, checks)?; + if checks != ModuleChecks::SkipOnHotPath { + check_finding_ids(&sources, words, &tokens)?; + } // EVERYTHING PAST THE READ IS PURE, and the split is what lets the // composition property be tested without a filesystem: `compile` builds @@ -895,7 +924,7 @@ pub fn load( if checks == ModuleChecks::Run { check_registry_is_exhausted(verdicts, &emitted)?; - check_collapse(rules, &per_rule, &collidable_tokens(verdicts))?; + check_collapse(rules, &per_rule, &tokens)?; } Ok(bundles) } @@ -1636,10 +1665,16 @@ fn record_raised( /// than a name in use, and refusing a row for spelling one would refuse a name /// nothing answers to. fn collidable_tokens(verdicts: &[crate::verdict::DeclaredVerdict]) -> BTreeSet { + // ALL THREE SOURCES. The consumer's rows, this binary's native sites, and + // the VENDORED PRESETS — the third is the one that bites: `trunk push + // forced` is a preset's class, and a set built from the first two refuses + // a name the collapse arm requires. + let vendored = crate::preset::verdict_rows(); verdicts .iter() .filter(|entry| !entry.retired()) .map(|entry| entry.id.clone()) + .chain(vendored.into_iter().map(|entry| entry.id)) .chain( crate::verdict::native_tokens() .iter() @@ -1648,6 +1683,100 @@ fn collidable_tokens(verdicts: &[crate::verdict::DeclaredVerdict]) -> BTreeSet, + tokens: &BTreeSet, +) -> Result<()> { + let Some(grammar) = vocabulary else { + return Ok(()); + }; + for (path, text) in sources { + for id in finding_ids(text) { + if tokens.contains(&id) { + continue; + } + crate::verdict::check_rule_id(&id, grammar) + .map_err(|error| UsageError::raise(format!("`{path}` declares `{id}`: {error}")))?; + } + } + Ok(()) +} + +/// The finding ids a module's SOURCE declares, in declaration order. +/// +/// Both spellings the engine honours: `rules contains ""`, which publishes +/// the id, and a violation's `"rule": ""`, which names it. Read as literals +/// because every one of them is written out — a computed id would be invisible +/// here, and is also invisible to a reader of the module, which is the same +/// objection. +fn finding_ids(text: &str) -> BTreeSet { + /// The first double-quoted run after `from`, if the line has one. + fn quoted(line: &str, from: usize) -> Option<&str> { + let rest = line.get(from..)?; + let open = rest.find('"')?; + let after = rest.get(open + 1..)?; + let close = after.find('"')?; + after.get(..close) + } + + let mut ids = BTreeSet::new(); + // PER LINE, because both spellings are single-line literals and a reader + // scanning across them runs from one declaration into the next — measured, + // on the first draft of this function, which spliced `violation contains {` + // into an id and reported it as an undeclared subject. + for line in text.lines() { + // AT THE START OF THE LINE. `rules contains` is a substring of + // `named_rules contains`, and `rules-drift.rego` has one — a reader + // matching anywhere took `path` out of its `{"path": path, …}` body. + if let Some(rest) = line.strip_prefix("rules contains ") + && let Some(id) = quoted(rest, 0) + { + ids.insert(id.to_owned()); + } + // `"rule":` must be followed by a STRING. A module's own test fixtures + // carry `"rule": [{"id": "r", …}]` — the consumer's `[[rule]]` TABLE as + // input — and a reader taking the next quoted run out of that returns + // `id`, which is what the first draft reported as a one-word name. + if let Some(at) = line.find("\"rule\"") + && let Some(colon) = line.get(at..).and_then(|rest| rest.find(':')) + && line + .get(at + colon + 1..) + .is_some_and(|rest| rest.trim_start().starts_with('"')) + && let Some(id) = quoted(line, at + colon) + { + ids.insert(id.to_owned()); + } + } + ids +} + /// One name where a rule and a class name one thing (CLOUD-1638). /// /// # The predicate is a property of the PAIR, in both directions diff --git a/crates/batten/src/policy/presets/ci-hygiene/spend-is-authorised.rego b/crates/batten/src/policy/presets/ci-hygiene/spend-is-authorised.rego index 65bf5df4e..5343960ec 100644 --- a/crates/batten/src/policy/presets/ci-hygiene/spend-is-authorised.rego +++ b/crates/batten/src/policy/presets/ci-hygiene/spend-is-authorised.rego @@ -57,13 +57,13 @@ package batten.ci_hygiene import rego.v1 -rules contains "no-job-runs-on-a-draft" +rules contains "job run early" -rules contains "pull-request-workflow-supersedes-itself" +rules contains "workflow run twice" -rules contains "workflow-declares-a-concurrency-group" +rules contains "workflow declare missing" -rules contains "draft-gated-workflow-subscribes-to-ready" +rules contains "review watch missing" # --- what counts as a workflow this rule may judge ---------------------------- # @@ -111,7 +111,7 @@ job_is_draft_gated(path, name) if { } violation contains { - "rule": "no-job-runs-on-a-draft", + "rule": "job run early", "verdict": "job run early", "subjects": [{"path": path}, {"artifact": name}], } if { @@ -130,7 +130,7 @@ violation contains { supersedes_itself(path) if workflow[path].concurrency["cancel-in-progress"] == true violation contains { - "rule": "pull-request-workflow-supersedes-itself", + "rule": "workflow run twice", "verdict": "workflow run twice", "subjects": [{"path": path}], } if { @@ -171,7 +171,7 @@ races_itself(path) if { } violation contains { - "rule": "workflow-declares-a-concurrency-group", + "rule": "workflow declare missing", "verdict": "workflow declare missing", "subjects": [{"path": path}], } if { @@ -196,7 +196,7 @@ subscribes_to_ready(path) if { } violation contains { - "rule": "draft-gated-workflow-subscribes-to-ready", + "rule": "review watch missing", "verdict": "review watch missing", "subjects": [{"path": path}], } if { diff --git a/crates/batten/src/policy/presets/ci-hygiene/wiring-can-be-reached.rego b/crates/batten/src/policy/presets/ci-hygiene/wiring-can-be-reached.rego index a38a079b1..b85a613bc 100644 --- a/crates/batten/src/policy/presets/ci-hygiene/wiring-can-be-reached.rego +++ b/crates/batten/src/policy/presets/ci-hygiene/wiring-can-be-reached.rego @@ -24,21 +24,21 @@ package batten.ci_hygiene import rego.v1 -rules contains "workflow-run-filters-at-the-trigger" +rules contains "workflow run loose" -rules contains "comment-trigger-is-anchored" +rules contains "event bind loose" -rules contains "comment-merge-reads-draft-state" +rules contains "merge run early" -rules contains "declared-trigger-reaches-a-job" +rules contains "event reach dead" -rules contains "schedules-do-not-collide" +rules contains "job start same" -rules contains "fan-in-asserts-its-whole-needs" +rules contains "job require unseen" -rules contains "cache-warm-compile-is-guarded" +rules contains "job guard missing" -rules contains "interpolation-is-not-swallowed" +rules contains "input render dropped" # --- a `workflow_run` trigger filters where filtering is free ----------------- # @@ -70,7 +70,7 @@ scopes_head_branch(path) if contains(job_conditions[path], "workflow_run.head_br trigger_filters_branches(path) if _ := triggers(path).workflow_run.branches violation contains { - "rule": "workflow-run-filters-at-the-trigger", + "rule": "workflow run loose", "verdict": "workflow run loose", "subjects": [{"path": path}], } if { @@ -92,7 +92,7 @@ violation contains { # happened to be read that way. violation contains { - "rule": "comment-trigger-is-anchored", + "rule": "event bind loose", "verdict": "event bind loose", "subjects": [{"path": path}], } if { @@ -122,7 +122,7 @@ reads_draft_state(path) if { } violation contains { - "rule": "comment-merge-reads-draft-state", + "rule": "merge run early", "verdict": "merge run early", "subjects": [{"path": path}], } if { @@ -156,7 +156,7 @@ admits(path, trigger) if contains(job_conditions[path], sprintf("github.event_na admits(path, "workflow_run") if contains(job_conditions[path], "github.event.workflow_run") violation contains { - "rule": "declared-trigger-reaches-a-job", + "rule": "event reach dead", "verdict": "event reach dead", "subjects": [{"path": path}, {"artifact": trigger}], } if { @@ -186,7 +186,7 @@ colliding contains expr if { } violation contains { - "rule": "schedules-do-not-collide", + "rule": "job start same", "verdict": "job start same", "subjects": [{"path": path}, {"artifact": expr}], } if { @@ -218,7 +218,7 @@ names_the_dependency(path, name, dep) if contains(job_body(path, name), sprintf( names_the_dependency(path, name, dep) if contains(job_body(path, name), sprintf("needs['%s']", [dep])) violation contains { - "rule": "fan-in-asserts-its-whole-needs", + "rule": "job require unseen", "verdict": "job require unseen", "subjects": [{"path": path}, {"artifact": dep}], } if { @@ -293,7 +293,7 @@ job_caches(path, name) if { } violation contains { - "rule": "cache-warm-compile-is-guarded", + "rule": "job guard missing", "verdict": "cache build loose", "subjects": [{"path": path}, {"artifact": name}], } if { @@ -331,7 +331,7 @@ step_id_exists(path, id) if { } violation contains { - "rule": "cache-warm-compile-is-guarded", + "rule": "job guard missing", "verdict": "cache name unknown", "subjects": [{"path": path}, {"artifact": id}], } if { @@ -385,7 +385,7 @@ swallowed(line) if { } violation contains { - "rule": "interpolation-is-not-swallowed", + "rule": "input render dropped", "verdict": "input render dropped", "subjects": [{"path": path, "line": number}], } if { diff --git a/crates/batten/src/policy/presets/commit-hygiene/no-empty-commit.rego b/crates/batten/src/policy/presets/commit-hygiene/no-empty-commit.rego index 0a07f4f43..780085b18 100644 --- a/crates/batten/src/policy/presets/commit-hygiene/no-empty-commit.rego +++ b/crates/batten/src/policy/presets/commit-hygiene/no-empty-commit.rego @@ -13,10 +13,10 @@ package batten.commit_hygiene import rego.v1 -rules contains "no-empty-commit" +rules contains "commit ship empty" violation contains { - "rule": "no-empty-commit", + "rule": "commit ship empty", "verdict": "commit ship empty", } if { # ON THE PROGRAM (CLOUD-1382), and this module has now carried its sibling @@ -54,7 +54,7 @@ violation contains { # bypasses sit under a green suite. test_no_empty_commit if { some v in violation with input as {"call": {"programs": [{"program": "git", "name": "git", "arguments": ["commit", "--allow-empty", "-m", "x"], "mediated": false}]}} - v.rule == "no-empty-commit" + v.rule == "commit ship empty" } test_an_empty_commit_later_in_a_list_is_caught if { @@ -62,20 +62,20 @@ test_an_empty_commit_later_in_a_list_is_caught if { {"program": "cd", "name": "cd", "arguments": ["/tmp"], "mediated": false}, {"program": "git", "name": "git", "arguments": ["commit", "--allow-empty", "-m", "x"], "mediated": false}, ]}} - v.rule == "no-empty-commit" + v.rule == "commit ship empty" } # The grammar case (CLOUD-1382): the caller wrote `time git commit # --allow-empty`, and the walk steps past `time`, so the entry names git. test_a_grammar_token_does_not_hide_the_program if { some v in violation with input as {"call": {"programs": [{"program": "git", "name": "git", "arguments": ["commit", "--allow-empty"], "mediated": false}]}} - v.rule == "no-empty-commit" + v.rule == "commit ship empty" } # Reached through a path, still git — what `name` buys over `program`. test_git_reached_through_a_path_is_still_git if { some v in violation with input as {"call": {"programs": [{"program": "/usr/bin/git", "name": "git", "arguments": ["commit", "--allow-empty"], "mediated": false}]}} - v.rule == "no-empty-commit" + v.rule == "commit ship empty" } test_an_ordinary_commit_is_left_alone if { diff --git a/crates/batten/src/policy/presets/landing-loop/already-landed-work-is-not-relanded.rego b/crates/batten/src/policy/presets/landing-loop/already-landed-work-is-not-relanded.rego index ea925cfc7..5b0b5f467 100644 --- a/crates/batten/src/policy/presets/landing-loop/already-landed-work-is-not-relanded.rego +++ b/crates/batten/src/policy/presets/landing-loop/already-landed-work-is-not-relanded.rego @@ -62,7 +62,7 @@ package batten.landing_loop import rego.v1 -rules contains "already-landed-work-is-not-relanded" +rules contains "patch ship twice" # Every declared target that already carries this branch's work. # @@ -92,7 +92,7 @@ relanded contains target if { # own row declared, and naming it is what makes the refusal diagnosable when # several targets are declared. Never a commit, never the unlanded list. violation contains { - "rule": "already-landed-work-is-not-relanded", + "rule": "patch ship twice", "verdict": "patch ship twice", "subjects": [{"artifact": target}], } if { @@ -125,7 +125,7 @@ test_a_squash_landed_branch_is_refused_though_unlanded_is_not_empty if { "landed": true, "unlanded": ["1111111", "2222222"], }) - v.rule == "already-landed-work-is-not-relanded" + v.rule == "patch ship twice" } # NOTHING TO LAND IS NOT A DUPLICATE, and this case is the one that keeps the diff --git a/crates/batten/src/policy/presets/landing-loop/graded-head-is-not-regraded.rego b/crates/batten/src/policy/presets/landing-loop/graded-head-is-not-regraded.rego index d9f687e7c..4dad1a573 100644 --- a/crates/batten/src/policy/presets/landing-loop/graded-head-is-not-regraded.rego +++ b/crates/batten/src/policy/presets/landing-loop/graded-head-is-not-regraded.rego @@ -37,7 +37,7 @@ package batten.landing_loop import rego.v1 -rules contains "graded-head-is-not-regraded" +rules contains "head grade twice" # The compiled tier is the one that runs this the way a consumer gets it, with # the empty vocabulary — the doc above says so, and CLOUD-1267 makes it the @@ -72,7 +72,7 @@ graded contains sha if { # already holds, and naming it is what makes a stopped lap diagnosable rather # than mysterious. Never a check body, never a fetched payload. violation contains { - "rule": "graded-head-is-not-regraded", + "rule": "head grade twice", "verdict": "head grade twice", "subjects": [{"artifact": sha}], } if { @@ -98,7 +98,7 @@ test_a_judged_commit_is_refused if { # practice is about re-grading, never about which way the grade went. test_a_red_commit_is_refused_too if { some v in violation with input as recorded({"final": "failure"}) - v.rule == "graded-head-is-not-regraded" + v.rule == "head grade twice" } # THE ANTI-VACUITY MIRROR. Without it the two cases above are satisfied by a diff --git a/crates/batten/src/policy/presets/landing-loop/lap-waits-on-one-answer.rego b/crates/batten/src/policy/presets/landing-loop/lap-waits-on-one-answer.rego index c51e8e3c8..10269ecde 100644 --- a/crates/batten/src/policy/presets/landing-loop/lap-waits-on-one-answer.rego +++ b/crates/batten/src/policy/presets/landing-loop/lap-waits-on-one-answer.rego @@ -40,7 +40,7 @@ package batten.landing_loop import rego.v1 -rules contains "lap-waits-on-one-answer" +rules contains "wait read both" # TWO MUTATIONS ON ONE CONJUNCT, IN OPPOSITE DIRECTIONS, which is the pair rather # than a duplicate. `loser-read` makes the predicate never fire, so a lap that @@ -160,7 +160,7 @@ wait_shas := {answer.sha | # The arms are a closed vocabulary the consumer's own recorder writes, so naming # them carries no content. violation contains { - "rule": "lap-waits-on-one-answer", + "rule": "wait read both", "verdict": "wait read both", "subjects": [{"count": count(wait_answered)}, {"artifact": sha}], } if { @@ -179,7 +179,7 @@ violation contains { # inventing a `-` pointer. The two arms are mutually exclusive, so a lap that # read both answers yields exactly one finding either way. violation contains { - "rule": "lap-waits-on-one-answer", + "rule": "wait read both", "verdict": "wait read both", "subjects": [{"count": count(wait_answered)}], } if { diff --git a/crates/batten/src/policy/presets/landing-loop/lease-authorises-the-branch.rego b/crates/batten/src/policy/presets/landing-loop/lease-authorises-the-branch.rego index 3a53779b8..82c24fe75 100644 --- a/crates/batten/src/policy/presets/landing-loop/lease-authorises-the-branch.rego +++ b/crates/batten/src/policy/presets/landing-loop/lease-authorises-the-branch.rego @@ -47,7 +47,7 @@ package batten.landing_loop import rego.v1 -rules contains "lease-authorises-the-branch" +rules contains "lease grant other" # The lease answers this branch wrote, IN WRITE ORDER. # @@ -139,7 +139,7 @@ refused if { # exists to avoid. So the pointer names the subject of the refusal rather than its # cause, and a reader wanting the holder asks the producer. violation contains { - "rule": "lease-authorises-the-branch", + "rule": "lease grant other", "verdict": "lease grant other", "subjects": [{"artifact": latest.branch}], } if { @@ -204,7 +204,7 @@ test_a_reserved_successor_may_spend if { # clause above from being satisfied by the mere presence of any reservation. test_a_reservation_for_another_branch_does_not_admit_this_one if { some v in violation with input as lease_line("lease held-elsewhere theirs mine") - v.rule == "lease-authorises-the-branch" + v.rule == "lease grant other" } # A CLONE WITH NO BRANCH CANNOT BE COMPARED, so it allows. A detached HEAD is a diff --git a/crates/batten/src/policy/presets/landing-loop/rebase-conflict-stops-the-lap.rego b/crates/batten/src/policy/presets/landing-loop/rebase-conflict-stops-the-lap.rego index 4f66c32dd..02684c621 100644 --- a/crates/batten/src/policy/presets/landing-loop/rebase-conflict-stops-the-lap.rego +++ b/crates/batten/src/policy/presets/landing-loop/rebase-conflict-stops-the-lap.rego @@ -47,7 +47,7 @@ package batten.landing_loop import rego.v1 -rules contains "rebase-conflict-stops-the-lap" +rules contains "replay halt conflict" # The compiled tier is the one that runs this the way a consumer gets it, with # the empty vocabulary, and CLOUD-1267 makes it the DECLARED suite rather than a @@ -125,7 +125,7 @@ replay_conflicted if { # never a hunk, never a conflict marker, never a byte of either side's content — # which is the whole of what a conflict actually consists of. violation contains { - "rule": "rebase-conflict-stops-the-lap", + "rule": "replay halt conflict", "verdict": "replay halt conflict", "subjects": [{"path": last_replay.path}, {"artifact": last_replay.commit}], } if { @@ -141,7 +141,7 @@ violation contains { # not exist. The two arms are mutually exclusive on the same column, so a # conflicted lap yields exactly one finding either way. violation contains { - "rule": "rebase-conflict-stops-the-lap", + "rule": "replay halt conflict", "verdict": "replay halt conflict", "subjects": [{"artifact": last_replay.commit}], } if { diff --git a/crates/batten/src/policy/presets/mise/action-version-matches-the-pin.rego b/crates/batten/src/policy/presets/mise/action-version-matches-the-pin.rego index 55641b0cb..ad72d4690 100644 --- a/crates/batten/src/policy/presets/mise/action-version-matches-the-pin.rego +++ b/crates/batten/src/policy/presets/mise/action-version-matches-the-pin.rego @@ -51,7 +51,7 @@ package batten.mise_action_version import rego.v1 -rules contains "action-version-matches-the-pin" +rules contains "job pin wrong" # --- the two documents, found by shape ---------------------------------------- @@ -157,7 +157,7 @@ disagrees(path, name) if { } violation contains { - "rule": "action-version-matches-the-pin", + "rule": "job pin wrong", "verdict": "job pin other", "subjects": [{"path": path, "line": number}], } if { @@ -170,7 +170,7 @@ violation contains { } violation contains { - "rule": "action-version-matches-the-pin", + "rule": "job pin wrong", "verdict": "job pin other", "subjects": [{"path": path}], } if { @@ -200,7 +200,7 @@ unpinned_reader(path, name) if { } violation contains { - "rule": "action-version-matches-the-pin", + "rule": "job pin wrong", "verdict": "job pin missing", "subjects": [{"path": path, "line": number}], } if { @@ -213,7 +213,7 @@ violation contains { } violation contains { - "rule": "action-version-matches-the-pin", + "rule": "job pin wrong", "verdict": "job pin missing", "subjects": [{"path": path}], } if { @@ -232,7 +232,7 @@ violation contains { # green over a file it never read. violation contains { - "rule": "action-version-matches-the-pin", + "rule": "job pin wrong", "verdict": "workflow parse unread", "subjects": [{"path": path}], } if { @@ -241,7 +241,7 @@ violation contains { } violation contains { - "rule": "action-version-matches-the-pin", + "rule": "job pin wrong", "verdict": "workflow parse unread", "subjects": [{"path": path}], } if { diff --git a/crates/batten/src/policy/presets/mise/task-over-executable.rego b/crates/batten/src/policy/presets/mise/task-over-executable.rego index a281c71f2..6f319468c 100644 --- a/crates/batten/src/policy/presets/mise/task-over-executable.rego +++ b/crates/batten/src/policy/presets/mise/task-over-executable.rego @@ -38,7 +38,7 @@ package batten.mise import rego.v1 -rules contains "task-over-executable" +rules contains "task reach loose" # The tasks this project's receipt defines, as name -> argv. # @@ -97,7 +97,7 @@ defined[name] := argv if { # uses `program` for this reason. No task here is path-spelled, so nothing in # this repository exercised it. violation contains { - "rule": "task-over-executable", + "rule": "task reach loose", "verdict": "task reach loose", "subjects": [{"artifact": name}], } if { @@ -125,7 +125,7 @@ test_a_tasks_own_program_reached_directly_is_refused if { "call": {"programs": [{"name": "a-program", "program": "a-program", "arguments": ["--flag"], "mediated": false}]}, } - finding.rule == "task-over-executable" + finding.rule == "task reach loose" } # The refusal names the TASK, never the program: that is the affordance a guard diff --git a/crates/batten/src/policy/presets/pinned-toolchain/pinned-program-probed-bare.rego b/crates/batten/src/policy/presets/pinned-toolchain/pinned-program-probed-bare.rego index 710066a58..b440944ed 100644 --- a/crates/batten/src/policy/presets/pinned-toolchain/pinned-program-probed-bare.rego +++ b/crates/batten/src/policy/presets/pinned-toolchain/pinned-program-probed-bare.rego @@ -51,7 +51,7 @@ package batten.pinned_toolchain_probe import rego.v1 -rules contains "pinned-program-probed-bare" +rules contains "pin probe bare" # The programs this project's pin provides. # @@ -121,7 +121,7 @@ probed contains name if { # The mediation reading is the BOUNDARY's, taken off `programs` rather than # re-derived here, for the same reason the sibling rule takes it there. violation contains { - "rule": "pinned-program-probed-bare", + "rule": "pin probe bare", "verdict": "pin probe bare", "subjects": [{"artifact": name}], } if { diff --git a/crates/batten/src/policy/presets/pinned-toolchain/pinned-program-via-the-pin.rego b/crates/batten/src/policy/presets/pinned-toolchain/pinned-program-via-the-pin.rego index 09bf19870..274bf74ec 100644 --- a/crates/batten/src/policy/presets/pinned-toolchain/pinned-program-via-the-pin.rego +++ b/crates/batten/src/policy/presets/pinned-toolchain/pinned-program-via-the-pin.rego @@ -25,7 +25,7 @@ package batten.pinned_toolchain import rego.v1 -rules contains "pinned-program-via-the-pin" +rules contains "pin reach loose" # The programs this project's pin provides. # @@ -48,7 +48,7 @@ provided contains name if { # argv the engine already parses, and the class of defect that authority split # exists to prevent. violation contains { - "rule": "pinned-program-via-the-pin", + "rule": "pin reach loose", "verdict": "pin reach loose", "subjects": [{"artifact": entry.name}], } if { @@ -68,7 +68,7 @@ test_a_pinned_program_reached_around_the_pin_is_refused if { "call": {"programs": [{"program": "./tests/bats/bin/bats", "name": "bats", "mediated": false}]}, "facts": {"pinned-programs": ["bats", "jq"]}, } - v.rule == "pinned-program-via-the-pin" + v.rule == "pin reach loose" } test_the_same_program_through_the_pin_is_left_alone if { diff --git a/crates/batten/src/policy/presets/shell-hygiene/shebang-names-its-language.rego b/crates/batten/src/policy/presets/shell-hygiene/shebang-names-its-language.rego index f74748145..6f77f2220 100644 --- a/crates/batten/src/policy/presets/shell-hygiene/shebang-names-its-language.rego +++ b/crates/batten/src/policy/presets/shell-hygiene/shebang-names-its-language.rego @@ -23,7 +23,7 @@ package batten.shell_hygiene import rego.v1 -rules contains "shebang-names-its-language" +rules contains "program name unnamed" # The interpreters worth naming. `env`-mediated and absolute spellings both # reduce to the same question, so the match is on the interpreter word rather @@ -46,7 +46,7 @@ names_shell(path) if endswith(path, ".sh") names_shell(path) if endswith(path, ".bash") violation contains { - "rule": "shebang-names-its-language", + "rule": "program name unnamed", "verdict": "program name unnamed", "subjects": [{"path": path}], } if { @@ -64,7 +64,7 @@ test_an_extensionless_shell_program_is_named if { # so a tree-wide rename that "helpfully" appended `.sh` here would turn the # deny test into a clean one and the suite would still be green. some v in violation with input as {"tree": {"lines": {"tools/deploy": ["#!/usr/bin/env bash", "set -euo pipefail"]}}} - v.rule == "shebang-names-its-language" + v.rule == "program name unnamed" } test_an_absolute_interpreter_counts_too if { diff --git a/crates/batten/src/policy/presets/shell-hygiene/sibling-resolves.rego b/crates/batten/src/policy/presets/shell-hygiene/sibling-resolves.rego index d22dddb62..59bae03ae 100644 --- a/crates/batten/src/policy/presets/shell-hygiene/sibling-resolves.rego +++ b/crates/batten/src/policy/presets/shell-hygiene/sibling-resolves.rego @@ -23,7 +23,7 @@ package batten.shell_hygiene import rego.v1 -rules contains "sibling-resolves" +rules contains "program resolve missing" # A line that computes THIS SCRIPT'S OWN directory. Both markers are required: # `dirname` alone catches `dirname "$file"`, which is somebody else's directory, @@ -135,7 +135,7 @@ constructed(path) := {resolved | tracked_set contains entry if some entry in input.tree.tracked violation contains { - "rule": "sibling-resolves", + "rule": "program resolve missing", # TWO SUBJECTS, IN THIS ORDER. The file carrying the reference comes first # because that is where the fix goes; the path it computed comes second # because that is what the reader has to reconcile. Reversing them would send @@ -160,7 +160,7 @@ test_a_sibling_that_lost_its_extension_is_a_finding if { "lines": {"mise-tasks/stop-guard.sh": [`field="$(dirname -- "${BASH_SOURCE[0]}")/payload-field"`]}, "tracked": ["mise-tasks/stop-guard.sh", "mise-tasks/payload-field.sh"], }} - v.rule == "sibling-resolves" + v.rule == "program resolve missing" } # THE LOAD-BEARING ALLOW: the same line, once the reference is repaired. Without diff --git a/crates/batten/src/policy/presets/trunk-based/no-force-push.rego b/crates/batten/src/policy/presets/trunk-based/no-force-push.rego index b439079d2..df8dcdfb7 100644 --- a/crates/batten/src/policy/presets/trunk-based/no-force-push.rego +++ b/crates/batten/src/policy/presets/trunk-based/no-force-push.rego @@ -15,10 +15,10 @@ package batten.trunk_based import rego.v1 -rules contains "no-force-push" +rules contains "trunk push forced" violation contains { - "rule": "no-force-push", + "rule": "trunk push forced", "verdict": "trunk push forced", } if { # ON THE PROGRAM, NOT ON THE FIRST WORD (CLOUD-1382). This is the anchor's @@ -88,7 +88,7 @@ test_no_force_push if { "command": "git push --force origin main", "programs": [{"program": "git", "name": "git", "arguments": ["push", "--force", "origin", "main"], "mediated": false}], }} - v.rule == "no-force-push" + v.rule == "trunk push forced" } test_short_force_flag_is_caught_too if { @@ -96,7 +96,7 @@ test_short_force_flag_is_caught_too if { "command": "git push -f origin main", "programs": [{"program": "git", "name": "git", "arguments": ["push", "-f", "origin", "main"], "mediated": false}], }} - v.rule == "no-force-push" + v.rule == "trunk push forced" } # THE CASE CLOUD-857 WAS FILED ON: the force push is the SECOND element of a @@ -113,7 +113,7 @@ test_a_force_push_later_in_a_list_is_caught if { "command": "cd /tmp && git push --force origin main", "programs": [{"program": "cd", "name": "cd", "arguments": ["/tmp"], "mediated": false}, {"program": "git", "name": "git", "arguments": ["push", "--force", "origin", "main"], "mediated": false}], }} - v.rule == "no-force-push" + v.rule == "trunk push forced" } # THE CASE CLOUD-1382 WAS FILED ON. `time` is grammar the boundary steps past, so @@ -124,7 +124,7 @@ test_a_grammar_token_does_not_hide_the_program if { "command": "time git push --force origin main", "programs": [{"program": "git", "name": "git", "arguments": ["push", "--force", "origin", "main"], "mediated": false}], }} - v.rule == "no-force-push" + v.rule == "trunk push forced" } # A PROGRAM REACHED THROUGH A PATH IS THE SAME PROGRAM, which is what `name` @@ -134,7 +134,7 @@ test_git_reached_through_a_path_is_still_git if { "command": "/usr/bin/git push --force origin main", "programs": [{"program": "/usr/bin/git", "name": "git", "arguments": ["push", "--force", "origin", "main"], "mediated": false}], }} - v.rule == "no-force-push" + v.rule == "trunk push forced" } test_force_with_lease_is_left_alone if { diff --git a/crates/batten/src/resolve.rs b/crates/batten/src/resolve.rs index 05f5a4c85..b1da69d93 100644 --- a/crates/batten/src/resolve.rs +++ b/crates/batten/src/resolve.rs @@ -546,6 +546,14 @@ pub struct Resolved { /// redefining a refusal is not one. #[serde(rename = "verdict")] pub verdicts: Vec, + /// The `[vocabulary]` word lists (CLOUD-1638), carried for + /// [`Resolved::verdicts`]' reason and layered the same way. + /// + /// Without it the finding-id grammar could not fire on the surface that + /// actually loads modules — `check` and `enforce` resolve before they load, + /// so a gate reading only `Config` would be one no real run reaches. + #[serde(default)] + pub vocabulary: crate::verdict::Vocabulary, /// The per-path-class redirect table (CLOUD-280), authority rows plus any a /// local file **added**. Local rows append after committed ones, and the /// lookup takes the first match, so an uncommitted file can add a class the @@ -1685,6 +1693,7 @@ fn assemble( verbs: repo.verbs.clone(), patterns: repo.patterns.clone(), verdicts: repo.verdicts.clone(), + vocabulary: repo.vocabulary.clone(), redirects: tables.redirects, facts: tables.facts, // Straight from the authority, never through `tables`: see the field's @@ -1787,6 +1796,13 @@ fn attribution( // supply the WORDS a committed gate refuses in — the token stays the same // and what it means changes, which is a weakening dressed as an addition. ("verdict", authority_set(!repo.verdicts.is_empty())), + // AUTHORITY-ONLY for `verdict`'s own reason, one level sharper + // (CLOUD-1638): these are the WORDS every class token and every rule id + // is spelled from, so a local row would not add a name — it would + // change which names are sayable, and therefore what a committed gate + // can be renamed to. That is the weakening-dressed-as-an-addition the + // row above refuses. + ("vocabulary", authority_set(!repo.vocabulary.is_empty())), ("marker", authority_set(!repo.markers.is_empty())), ( "exec_pattern", diff --git a/crates/batten/tests/it/admission_narrowing.rs b/crates/batten/tests/it/admission_narrowing.rs index 1c0af85f0..8c5b3388f 100644 --- a/crates/batten/tests/it/admission_narrowing.rs +++ b/crates/batten/tests/it/admission_narrowing.rs @@ -103,6 +103,7 @@ fn load_fixture(root: &Path, rows: &[Rule]) -> Vec { // consumer supplies. patterns: &[], verdicts: &verdicts, + words: None, recorders: &[], }, policy::ModuleChecks::RunOverSelection, @@ -203,6 +204,7 @@ fn the_committed_bundles_publish_no_engine_side_rule_name() { policy::Vocabulary { patterns: &config.patterns, verdicts: &config.verdicts, + words: None, recorders: &config.recorders, }, policy::ModuleChecks::RunOverSelection, diff --git a/crates/batten/tests/it/bats_invocation.rs b/crates/batten/tests/it/bats_invocation.rs index b57650131..d4a7ec37f 100644 --- a/crates/batten/tests/it/bats_invocation.rs +++ b/crates/batten/tests/it/bats_invocation.rs @@ -132,6 +132,7 @@ fn findings_declared_by(root: &Path, vocabulary_root: &Path) -> Vec<(String, Opt batten::policy::Vocabulary { patterns: &[], verdicts: &verdicts, + words: None, recorders: &[], }, root, diff --git a/crates/batten/tests/it/cfg_gated_test.rs b/crates/batten/tests/it/cfg_gated_test.rs index d77f7a14c..a3b96903a 100644 --- a/crates/batten/tests/it/cfg_gated_test.rs +++ b/crates/batten/tests/it/cfg_gated_test.rs @@ -119,6 +119,7 @@ fn scan(root: &Path) -> rules::Scan { batten::policy::Vocabulary { patterns: &[], verdicts: &verdicts, + words: None, recorders: &[], }, root, diff --git a/crates/batten/tests/it/ci_cache_declared.rs b/crates/batten/tests/it/ci_cache_declared.rs index 08a87cb5b..9d6fd70d4 100644 --- a/crates/batten/tests/it/ci_cache_declared.rs +++ b/crates/batten/tests/it/ci_cache_declared.rs @@ -117,6 +117,7 @@ fn findings_declared_by(root: &Path, vocabulary_root: &Path) -> Vec<(String, Opt batten::policy::Vocabulary { patterns: &patterns, verdicts: &verdicts, + words: None, recorders: &[], }, root, diff --git a/crates/batten/tests/it/ci_hygiene.rs b/crates/batten/tests/it/ci_hygiene.rs index b42d7eac8..ac0483e7f 100644 --- a/crates/batten/tests/it/ci_hygiene.rs +++ b/crates/batten/tests/it/ci_hygiene.rs @@ -94,6 +94,7 @@ fn findings(root: &Path) -> Vec<(String, Option)> { batten::policy::Vocabulary { patterns: &[], verdicts: &[], + words: None, recorders: &[], }, root, diff --git a/crates/batten/tests/it/ci_parity.rs b/crates/batten/tests/it/ci_parity.rs index 6b1fb911b..370b17ecc 100644 --- a/crates/batten/tests/it/ci_parity.rs +++ b/crates/batten/tests/it/ci_parity.rs @@ -240,6 +240,7 @@ fn findings_declared_by(root: &Path, vocabulary_root: &Path) -> Vec<(String, Opt batten::policy::Vocabulary { patterns: &patterns, verdicts: &verdicts, + words: None, recorders: &[], }, root, @@ -272,6 +273,7 @@ fn verdicts_raised(root: &Path) -> Vec { batten::policy::Vocabulary { patterns: &patterns, verdicts: &verdicts, + words: None, recorders: &[], }, root, diff --git a/crates/batten/tests/it/ci_suite_lane.rs b/crates/batten/tests/it/ci_suite_lane.rs index 8f836ad51..99b368676 100644 --- a/crates/batten/tests/it/ci_suite_lane.rs +++ b/crates/batten/tests/it/ci_suite_lane.rs @@ -101,6 +101,7 @@ fn findings_declared_by(root: &Path, vocabulary_root: &Path) -> Vec<(String, Opt batten::policy::Vocabulary { patterns: &[], verdicts: &verdicts, + words: None, recorders: &[], }, root, diff --git a/crates/batten/tests/it/claim_order.rs b/crates/batten/tests/it/claim_order.rs index f968d6b4d..531d8d0b3 100644 --- a/crates/batten/tests/it/claim_order.rs +++ b/crates/batten/tests/it/claim_order.rs @@ -119,6 +119,7 @@ fn findings_declared_by(root: &Path, vocabulary_root: &Path) -> Vec { batten::policy::Vocabulary { patterns: &[], verdicts: &verdicts, + words: None, recorders: &[], }, root, diff --git a/crates/batten/tests/it/document_read_count.rs b/crates/batten/tests/it/document_read_count.rs index 669d1bbe3..13d738d0d 100644 --- a/crates/batten/tests/it/document_read_count.rs +++ b/crates/batten/tests/it/document_read_count.rs @@ -57,6 +57,7 @@ fn fixtures(root: &Path) -> batten::policy::Vocabulary<'static> { batten::policy::Vocabulary { patterns: &[], verdicts: table, + words: None, recorders: &[], } } diff --git a/crates/batten/tests/it/filed_here.rs b/crates/batten/tests/it/filed_here.rs index dcb9e6938..5a2cadcc5 100644 --- a/crates/batten/tests/it/filed_here.rs +++ b/crates/batten/tests/it/filed_here.rs @@ -248,6 +248,7 @@ fn scan(root: &Path) -> rules::Scan { batten::policy::Vocabulary { patterns: &patterns, verdicts: &verdicts, + words: None, recorders: &declared, }, root, diff --git a/crates/batten/tests/it/fixture_forks.rs b/crates/batten/tests/it/fixture_forks.rs index 2cf807d70..9d289b991 100644 --- a/crates/batten/tests/it/fixture_forks.rs +++ b/crates/batten/tests/it/fixture_forks.rs @@ -120,6 +120,7 @@ fn scan(root: &Path) -> rules::Scan { batten::policy::Vocabulary { patterns: &patterns, verdicts: &verdicts, + words: None, recorders: &[], }, root, diff --git a/crates/batten/tests/it/hk_fix_selection.rs b/crates/batten/tests/it/hk_fix_selection.rs index 86e0ebf03..2370590a1 100644 --- a/crates/batten/tests/it/hk_fix_selection.rs +++ b/crates/batten/tests/it/hk_fix_selection.rs @@ -112,6 +112,7 @@ fn findings_declared_by(root: &Path, vocabulary_root: &Path) -> Vec<(String, Opt batten::policy::Vocabulary { patterns: &[], verdicts: &verdicts, + words: None, recorders: &[], }, root, diff --git a/crates/batten/tests/it/identity_churn.rs b/crates/batten/tests/it/identity_churn.rs index 164cdc3a4..f4d37b0bf 100644 --- a/crates/batten/tests/it/identity_churn.rs +++ b/crates/batten/tests/it/identity_churn.rs @@ -83,6 +83,7 @@ impl Scan { batten::policy::Vocabulary { patterns: &[], verdicts: &verdicts, + words: None, recorders: &[], }, root, diff --git a/crates/batten/tests/it/landing_roster.rs b/crates/batten/tests/it/landing_roster.rs index a588431d7..c6aebe068 100644 --- a/crates/batten/tests/it/landing_roster.rs +++ b/crates/batten/tests/it/landing_roster.rs @@ -113,6 +113,7 @@ fn scan(root: &Path) -> rules::Scan { batten::policy::Vocabulary { patterns: &[], verdicts: &verdicts, + words: None, recorders: &[], }, root, diff --git a/crates/batten/tests/it/mise_preset.rs b/crates/batten/tests/it/mise_preset.rs index 7811dc71c..e7881915e 100644 --- a/crates/batten/tests/it/mise_preset.rs +++ b/crates/batten/tests/it/mise_preset.rs @@ -85,6 +85,7 @@ fn findings(root: &Path) -> Vec<(String, Option)> { batten::policy::Vocabulary { patterns: &[], verdicts: &[], + words: None, recorders: &[], }, root, diff --git a/crates/batten/tests/it/mutation_declared_case.rs b/crates/batten/tests/it/mutation_declared_case.rs index f5eca501f..61eab29f2 100644 --- a/crates/batten/tests/it/mutation_declared_case.rs +++ b/crates/batten/tests/it/mutation_declared_case.rs @@ -87,6 +87,7 @@ fn verdicts(root: &Path) -> Vec { batten::policy::Vocabulary { patterns: &[], verdicts: &verdicts, + words: None, recorders: &[], }, root, diff --git a/crates/batten/tests/it/nextest_slow.rs b/crates/batten/tests/it/nextest_slow.rs index c8c5e14e8..211df1e19 100644 --- a/crates/batten/tests/it/nextest_slow.rs +++ b/crates/batten/tests/it/nextest_slow.rs @@ -101,6 +101,7 @@ fn scan(root: &Path) -> rules::Scan { batten::policy::Vocabulary { patterns: &[], verdicts: &verdicts, + words: None, recorders: &[], }, root, diff --git a/crates/batten/tests/it/obligations_bound.rs b/crates/batten/tests/it/obligations_bound.rs index 7a95d7ac8..ebfccb7be 100644 --- a/crates/batten/tests/it/obligations_bound.rs +++ b/crates/batten/tests/it/obligations_bound.rs @@ -157,6 +157,7 @@ fn verdicts(root: &Path) -> Vec { batten::policy::Vocabulary { patterns: &[], verdicts: &verdicts, + words: None, recorders: &declared, }, root, diff --git a/crates/batten/tests/it/plan_complete.rs b/crates/batten/tests/it/plan_complete.rs index 46f569405..b3e7b26eb 100644 --- a/crates/batten/tests/it/plan_complete.rs +++ b/crates/batten/tests/it/plan_complete.rs @@ -130,6 +130,7 @@ fn scan(root: &Path) -> rules::Scan { batten::policy::Vocabulary { patterns: &[], verdicts: &verdicts, + words: None, recorders: &[], }, root, diff --git a/crates/batten/tests/it/policy_test_suite.rs b/crates/batten/tests/it/policy_test_suite.rs index 0afb983d1..63927a7a7 100644 --- a/crates/batten/tests/it/policy_test_suite.rs +++ b/crates/batten/tests/it/policy_test_suite.rs @@ -64,6 +64,7 @@ fn fixtures(root: &Path) -> batten::policy::Vocabulary<'static> { batten::policy::Vocabulary { patterns: &[], verdicts: table, + words: None, recorders: &[], } } diff --git a/crates/batten/tests/it/policy_tree.rs b/crates/batten/tests/it/policy_tree.rs index eb37ca082..b4284f361 100644 --- a/crates/batten/tests/it/policy_tree.rs +++ b/crates/batten/tests/it/policy_tree.rs @@ -47,6 +47,7 @@ fn fixtures(root: &Path) -> batten::policy::Vocabulary<'static> { batten::policy::Vocabulary { patterns: &[], verdicts: table, + words: None, recorders: &[], } } diff --git a/crates/batten/tests/it/prebuilt_lint.rs b/crates/batten/tests/it/prebuilt_lint.rs index 6577bbee8..ab1e28143 100644 --- a/crates/batten/tests/it/prebuilt_lint.rs +++ b/crates/batten/tests/it/prebuilt_lint.rs @@ -76,6 +76,7 @@ fn findings(root: &Path) -> Vec { batten::policy::Vocabulary { patterns: &[], verdicts: &[], + words: None, recorders: &[], }, root, diff --git a/crates/batten/tests/it/prose_only.rs b/crates/batten/tests/it/prose_only.rs index 625b8bcb3..4312f7c23 100644 --- a/crates/batten/tests/it/prose_only.rs +++ b/crates/batten/tests/it/prose_only.rs @@ -114,6 +114,7 @@ fn findings(root: &Path) -> Vec { batten::policy::Vocabulary { patterns: &[], verdicts: &verdicts, + words: None, recorders: &[], }, root, @@ -482,6 +483,7 @@ fn the_finding_carries_a_count_and_never_a_path() { batten::policy::Vocabulary { patterns: &[], verdicts: &verdicts, + words: None, recorders: &[], }, &root, diff --git a/crates/batten/tests/it/release_provision_parity.rs b/crates/batten/tests/it/release_provision_parity.rs index b3feb9f65..e3c4e109c 100644 --- a/crates/batten/tests/it/release_provision_parity.rs +++ b/crates/batten/tests/it/release_provision_parity.rs @@ -126,6 +126,7 @@ fn findings_declared_by(root: &Path, vocabulary_root: &Path) -> Vec { batten::policy::Vocabulary { patterns: &[], verdicts: &verdicts, + words: None, recorders: &[], }, root, diff --git a/crates/batten/tests/it/remedy_authorship.rs b/crates/batten/tests/it/remedy_authorship.rs index 100ca3f5c..1e7d1f803 100644 --- a/crates/batten/tests/it/remedy_authorship.rs +++ b/crates/batten/tests/it/remedy_authorship.rs @@ -93,6 +93,7 @@ fn scan(root: &Path, rule: Rule) -> rules::Scan { batten::policy::Vocabulary { patterns: &[], verdicts: &verdicts, + words: None, recorders: &[], }, root, diff --git a/crates/batten/tests/it/review_dispatched.rs b/crates/batten/tests/it/review_dispatched.rs index b259b1254..4d64840e6 100644 --- a/crates/batten/tests/it/review_dispatched.rs +++ b/crates/batten/tests/it/review_dispatched.rs @@ -162,6 +162,7 @@ fn verdicts_for(root: &Path, declared: bool) -> Vec { batten::policy::Vocabulary { patterns: &[], verdicts: &verdicts, + words: None, recorders: &[], }, root, @@ -221,6 +222,7 @@ fn the_mediated_surface_resolves_no_effect_fact_and_withholds_the_rule() { let vocabulary = batten::policy::Vocabulary { patterns: &[], verdicts: &verdicts, + words: None, recorders: &[], }; let rows = [row(&root, true)]; @@ -485,6 +487,7 @@ fn verdicts_with(root: &Path, extra: &serde_json::Value) -> Vec { batten::policy::Vocabulary { patterns: &[], verdicts: &verdicts, + words: None, recorders: &[], }, root, diff --git a/crates/batten/tests/it/rule_cost_census.rs b/crates/batten/tests/it/rule_cost_census.rs index 3e83262ee..ade915e78 100644 --- a/crates/batten/tests/it/rule_cost_census.rs +++ b/crates/batten/tests/it/rule_cost_census.rs @@ -39,6 +39,7 @@ fn vocabulary() -> batten::policy::Vocabulary<'static> { batten::policy::Vocabulary { patterns: &[], verdicts: &[], + words: None, recorders: &[], } } diff --git a/crates/batten/tests/it/shell_retirement.rs b/crates/batten/tests/it/shell_retirement.rs index a6f0aa90d..cb017b0c9 100644 --- a/crates/batten/tests/it/shell_retirement.rs +++ b/crates/batten/tests/it/shell_retirement.rs @@ -129,6 +129,7 @@ pub(crate) fn scan(root: &Path) -> rules::Scan { batten::policy::Vocabulary { patterns: &patterns, verdicts: &verdicts, + words: None, recorders: &[], }, root, diff --git a/crates/batten/tests/it/spawn_widening.rs b/crates/batten/tests/it/spawn_widening.rs index b81fb32ea..8cf4d1712 100644 --- a/crates/batten/tests/it/spawn_widening.rs +++ b/crates/batten/tests/it/spawn_widening.rs @@ -127,6 +127,7 @@ fn verdicts(root: &Path) -> Vec { batten::policy::Vocabulary { patterns: &patterns, verdicts: &declared, + words: None, recorders: &[], }, root, diff --git a/crates/batten/tests/it/suite_subjects.rs b/crates/batten/tests/it/suite_subjects.rs index c388eb2a1..004f3b826 100644 --- a/crates/batten/tests/it/suite_subjects.rs +++ b/crates/batten/tests/it/suite_subjects.rs @@ -83,6 +83,7 @@ fn findings_declared_by(root: &Path, vocabulary_root: &Path) -> Vec<(String, Opt batten::policy::Vocabulary { patterns: &[], verdicts: &verdicts, + words: None, recorders: &[], }, root, diff --git a/crates/batten/tests/it/test_targets.rs b/crates/batten/tests/it/test_targets.rs index bb0d0a7da..feacbe876 100644 --- a/crates/batten/tests/it/test_targets.rs +++ b/crates/batten/tests/it/test_targets.rs @@ -101,6 +101,7 @@ fn scan(root: &Path) -> rules::Scan { batten::policy::Vocabulary { patterns: &[], verdicts: &verdicts, + words: None, recorders: &[], }, root, diff --git a/crates/batten/tests/it/verdict_registry.rs b/crates/batten/tests/it/verdict_registry.rs index 44b00b5ea..d300f9534 100644 --- a/crates/batten/tests/it/verdict_registry.rs +++ b/crates/batten/tests/it/verdict_registry.rs @@ -66,6 +66,7 @@ fn load( Vocabulary { patterns: &[], verdicts, + words: None, recorders: &[], }, policy::ModuleChecks::Run, @@ -453,6 +454,7 @@ fn route_findings(name: &str, authority: &str, manifest: &str) -> Vec { Vocabulary { patterns: &[], verdicts: &verdicts, + words: None, recorders: &[], }, &root, diff --git a/crates/batten/tests/policy_modules.rs b/crates/batten/tests/policy_modules.rs index 1bd9e678d..eb2ddd9ba 100644 --- a/crates/batten/tests/policy_modules.rs +++ b/crates/batten/tests/policy_modules.rs @@ -97,6 +97,7 @@ fn fixtures(root: &Path) -> policy::Vocabulary<'static> { policy::Vocabulary { patterns: &[], verdicts: table, + words: None, recorders: &[], } } @@ -109,6 +110,7 @@ fn fixtures_with( policy::Vocabulary { patterns, verdicts: fixtures(root).verdicts, + words: None, recorders: &[], } } diff --git a/policy/agentic-experiment-record.rego b/policy/agentic-experiment-record.rego index 89150c295..d01547a4c 100644 --- a/policy/agentic-experiment-record.rego +++ b/policy/agentic-experiment-record.rego @@ -54,11 +54,11 @@ package batten.agentic_experiment_record import rego.v1 -rules contains "agentic-record-incomplete" +rules contains "test declare partial" -rules contains "agentic-finding-unsupported" +rules contains "test state early" -rules contains "agentic-record-unreadable" +rules contains "input read absent" trials_path := "bench/agentic/trials.toml" @@ -106,7 +106,7 @@ method := input.tree.documents[method_path].method # --- completeness ------------------------------------------------------------ violation contains { - "rule": "agentic-record-incomplete", + "rule": "test declare partial", "verdict": "test declare partial", "subjects": [{"path": trials_path}, {"count": count(incomplete)}], } if { @@ -140,7 +140,7 @@ incomplete contains index if { # window nobody declared makes every row's outcome unattributable, so the record # SET is what is partial rather than any one row. violation contains { - "rule": "agentic-record-incomplete", + "rule": "test declare partial", "verdict": "test declare partial", "subjects": [{"path": method_path}, {"count": count(method_gaps)}], } if { @@ -168,7 +168,7 @@ method_gaps contains key if { # --- a finding without a result ---------------------------------------------- violation contains { - "rule": "agentic-finding-unsupported", + "rule": "test state early", "verdict": "test state early", "subjects": [{"path": trials_path}, {"count": count(unsupported)}], } if { @@ -210,7 +210,7 @@ unsupported contains index if { # on the decision surface. Both records are named, because either one absent # leaves the joint predicate unable to decide. violation contains { - "rule": "agentic-record-unreadable", + "rule": "input read absent", "verdict": "input read absent", "subjects": [{"path": path}], } if { diff --git a/policy/cfg-gated-test.rego b/policy/cfg-gated-test.rego index 4ce89d0a1..774168ee7 100644 --- a/policy/cfg-gated-test.rego +++ b/policy/cfg-gated-test.rego @@ -60,7 +60,7 @@ package batten import rego.v1 -rules contains "platform-gated-test-added" +rules contains "test cover unseen" # The branch's own diff, BOUND THROUGH AN OBJECT GUARD because `null` is not # `undefined`: the engine emits `null` where the base would not resolve, and @@ -75,7 +75,7 @@ delta := d if { # refuses nothing is byte-identical to a tree that added nothing on the decision # surface, so the read failure is REPORTED rather than passed. violation contains { - "rule": "platform-gated-test-added", + "rule": "test cover unseen", "verdict": "diff read absent", "subjects": [{"path": "batten.toml"}], } if { @@ -233,7 +233,7 @@ grew contains [path, after] if { } violation contains { - "rule": "platform-gated-test-added", + "rule": "test cover unseen", "verdict": "test cover partial", "subjects": [{"path": path}, {"count": after}], } if { diff --git a/policy/ci-cache-declared.rego b/policy/ci-cache-declared.rego index 014e0a7bc..c086b81bf 100644 --- a/policy/ci-cache-declared.rego +++ b/policy/ci-cache-declared.rego @@ -59,13 +59,13 @@ package batten.ci_cache_declared import rego.v1 -rules contains "cache-key-carries-a-content-hash" +rules contains "step key dead" -rules contains "cargo-reach-declares-a-cache" +rules contains "cargo carry missing" -rules contains "warmed-family-is-read-only" +rules contains "job write unsafe" -rules contains "read-family-has-a-warm-writer" +rules contains "job read empty" # --- what is being judged, and whether there is anything to judge ------------- @@ -216,7 +216,7 @@ hash_keyed(path) if { } violation contains { - "rule": "cache-key-carries-a-content-hash", + "rule": "step key dead", "verdict": "step key dead", "subjects": [{"path": path, "line": number}], } if { @@ -229,7 +229,7 @@ violation contains { } violation contains { - "rule": "cache-key-carries-a-content-hash", + "rule": "step key dead", "verdict": "step key dead", "subjects": [{"path": path}], } if { @@ -366,7 +366,7 @@ reach contains [path, name, task] if { } violation contains { - "rule": "cargo-reach-declares-a-cache", + "rule": "cargo carry missing", "verdict": "job declare missing", "subjects": [{"path": path, "line": number}, {"artifact": task}], } if { @@ -383,7 +383,7 @@ violation contains { } violation contains { - "rule": "cargo-reach-declares-a-cache", + "rule": "cargo carry missing", "verdict": "job declare missing", "subjects": [{"path": path}, {"artifact": task}], } if { @@ -397,7 +397,7 @@ violation contains { } violation contains { - "rule": "cargo-reach-declares-a-cache", + "rule": "cargo carry missing", "verdict": "task resolve missing", "subjects": [{"path": path}, {"artifact": task}], } if { @@ -464,7 +464,7 @@ contested(path, name) if { } violation contains { - "rule": "warmed-family-is-read-only", + "rule": "job write unsafe", "verdict": "job write unsafe", "subjects": [{"path": path, "line": number}], } if { @@ -480,7 +480,7 @@ violation contains { } violation contains { - "rule": "warmed-family-is-read-only", + "rule": "job write unsafe", "verdict": "job write unsafe", "subjects": [{"path": path}], } if { @@ -536,7 +536,7 @@ orphaned(path, name) if { } violation contains { - "rule": "read-family-has-a-warm-writer", + "rule": "job read empty", "verdict": "job read empty", "subjects": [{"path": path, "line": number}], } if { @@ -552,7 +552,7 @@ violation contains { } violation contains { - "rule": "read-family-has-a-warm-writer", + "rule": "job read empty", "verdict": "job read empty", "subjects": [{"path": path}], } if { @@ -573,7 +573,7 @@ violation contains { # abstains rather than saying so. violation contains { - "rule": "cargo-reach-declares-a-cache", + "rule": "cargo carry missing", "verdict": "workflow read unread", "subjects": [{"path": path}], } if { @@ -582,7 +582,7 @@ violation contains { } violation contains { - "rule": "cargo-reach-declares-a-cache", + "rule": "cargo carry missing", "verdict": "task resolve missing", "subjects": [{"path": path}], } if { @@ -604,18 +604,18 @@ test_a_readable_key_with_a_cache_is_clean if { test_a_shared_key_carrying_a_content_hash_is_refused if { some finding in violation with input as tree(warm_writer, pr_reader("ci-${{ hashFiles('Cargo.toml') }}", false)) - finding.rule == "cache-key-carries-a-content-hash" + finding.rule == "step key dead" } test_a_cargo_job_with_no_cache_step_is_refused if { some finding in violation with input as tree(warm_writer, uncached_reader) - finding.rule == "cargo-reach-declares-a-cache" + finding.rule == "cargo carry missing" finding.verdict == "job declare missing" } test_a_pull_request_writer_of_a_warmed_family_is_refused if { some finding in violation with input as tree(warm_writer, pr_reader("ci-", true)) - finding.rule == "warmed-family-is-read-only" + finding.rule == "job write unsafe" } # ANTI-VACUITY, AND IT IS WHAT DISCRIMINATES THE THIRD PREDICATE FROM A BLANKET @@ -646,7 +646,7 @@ test_the_same_key_on_the_same_architecture_is_still_refused if { warm_writer_on("ubuntu-24.04-arm"), pr_reader_on("ci-", true, "ubuntu-24.04-arm"), ) - finding.rule == "warmed-family-is-read-only" + finding.rule == "job write unsafe" } # RULE 4, AND THE FIXTURE IS THE ORPHANING THAT MOTIVATED IT: a read-only @@ -658,7 +658,7 @@ test_a_read_only_consumer_of_an_unwarmed_family_is_refused if { warm_writer_on("ubuntu-24.04-arm"), pr_reader_on("ci-", false, "ubuntu-latest"), ) - finding.rule == "read-family-has-a-warm-writer" + finding.rule == "job read empty" } # The other direction on the KEY rather than the architecture: a read-only @@ -666,7 +666,7 @@ test_a_read_only_consumer_of_an_unwarmed_family_is_refused if { # only ever noticing the architecture split. test_a_read_only_consumer_of_a_family_nothing_writes_is_refused if { some finding in violation with input as tree(no_writer, pr_reader("ci-", false)) - finding.rule == "read-family-has-a-warm-writer" + finding.rule == "job read empty" } # ANTI-VACUITY FOR RULE 4, and it is the bound the predicate's header argues for: @@ -709,7 +709,7 @@ test_a_job_reaching_no_cargo_needs_no_cache if { test_a_cargo_reach_through_depends_is_seen if { some finding in violation with input as tree(warm_writer, indirect_reader) - finding.rule == "cargo-reach-declares-a-cache" + finding.rule == "cargo carry missing" } test_an_unparsed_workflow_is_could_not_look if { diff --git a/policy/ci-parity.rego b/policy/ci-parity.rego index 2064c20e4..693e75bdc 100644 --- a/policy/ci-parity.rego +++ b/policy/ci-parity.rego @@ -52,25 +52,25 @@ package batten.ci_parity import rego.v1 -rules contains "ci-task-parity" +rules contains "job run other" -rules contains "required-roster-matches-jobs" +rules contains "check list other" -rules contains "release-pr-opens-as-a-draft" +rules contains "release open early" -rules contains "one-bot-serves-every-ecosystem" +rules contains "bound cover partial" -rules contains "fan-in-is-wired" +rules contains "job wire missing" -rules contains "lease-authorises-before-spending" +rules contains "lease ask missing" -rules contains "check-status-decided-in-one-place" +rules contains "check grade twice" -rules contains "every-bot-branch-has-a-watcher" +rules contains "branch watch missing" -rules contains "foreign-cargo-is-the-declared-spelling" +rules contains "cargo spelling wrong" -rules contains "cache-path-is-rebase-stable" +rules contains "path reach dead" # --- the manifest, and the guard ---------------------------------------------- @@ -184,7 +184,7 @@ ci_task_used contains [path, task] if { } violation contains { - "rule": "ci-task-parity", + "rule": "job run other", "verdict": "task run missing", "subjects": [{"path": path}, {"artifact": task}], } if { @@ -343,7 +343,7 @@ job_in_roster(name) if name in roster_names roster_name_has_a_job(name) if name in job_display_names violation contains { - "rule": "required-roster-matches-jobs", + "rule": "check list other", "verdict": "check name unknown", "subjects": [{"path": "mise.toml"}, {"artifact": name}], } if { @@ -353,7 +353,7 @@ violation contains { } violation contains { - "rule": "required-roster-matches-jobs", + "rule": "check list other", "verdict": "job list missing", "subjects": [{"path": "mise.toml"}, {"artifact": name}], } if { @@ -373,7 +373,7 @@ violation contains { release_config := input.tree.documents["release-plz.toml"] violation contains { - "rule": "release-pr-opens-as-a-draft", + "rule": "release open early", "verdict": "release open early", "subjects": [{"path": "release-plz.toml"}], } if { @@ -405,7 +405,7 @@ renovate := input.tree.documents["renovate.json5"] dependabot_absent if not ".github/dependabot.yml" in input.tree.tracked violation contains { - "rule": "one-bot-serves-every-ecosystem", + "rule": "bound cover partial", "verdict": "config carry duplicate", "subjects": [{"path": ".github/dependabot.yml"}], } if { @@ -429,7 +429,7 @@ renovate_key_ok("minimumReleaseAge") if count(renovate.minimumReleaseAge) > 0 renovate_key_ok("vulnerabilityAlerts") if is_object(renovate.vulnerabilityAlerts) violation contains { - "rule": "one-bot-serves-every-ecosystem", + "rule": "bound cover partial", "verdict": "bound declare missing", "subjects": [{"path": "renovate.json5"}, {"artifact": key}], } if { @@ -453,7 +453,7 @@ commit_type_is_scoped if { } violation contains { - "rule": "one-bot-serves-every-ecosystem", + "rule": "bound cover partial", "verdict": "commit name unnamed", "subjects": [{"path": "renovate.json5"}], } if { @@ -470,7 +470,7 @@ violation contains { maintained_ecosystems := ["cargo", "github-actions", "mise"] violation contains { - "rule": "one-bot-serves-every-ecosystem", + "rule": "bound cover partial", "verdict": "manifest cover missing", "subjects": [{"path": "renovate.json5"}, {"artifact": eco}], } if { @@ -497,7 +497,7 @@ fanin_check := manifest_env.CI_FANIN_CHECK fanin_workflow := manifest_env.CI_FANIN_WORKFLOW violation contains { - "rule": "fan-in-is-wired", + "rule": "job wire missing", "verdict": "job require missing", "subjects": [{"path": "mise.toml"}, {"artifact": fanin_check}], } if { @@ -506,7 +506,7 @@ violation contains { } violation contains { - "rule": "fan-in-is-wired", + "rule": "job wire missing", "verdict": "workflow declare empty", "subjects": [{"path": fanin_workflow}, {"artifact": fanin_check}], } if { @@ -570,7 +570,7 @@ abandon_reads_declaration if { } violation contains { - "rule": "fan-in-is-wired", + "rule": "job wire missing", "verdict": "job declare duplicate", "subjects": [{"path": "crates/batten/src/lib.rs"}], } if { @@ -593,7 +593,7 @@ lander_calls_abandon if { } violation contains { - "rule": "fan-in-is-wired", + "rule": "job wire missing", "verdict": "job reach dead", "subjects": [{"path": "crates/batten/src/lib.rs"}], } if { @@ -624,7 +624,7 @@ starts_with_the_lease(path, name) if { } violation contains { - "rule": "lease-authorises-before-spending", + "rule": "lease ask missing", "verdict": "lease guard absent", "subjects": [{"path": path}, {"artifact": name}], } if { @@ -693,7 +693,7 @@ tolerated_at(path, i) if { } violation contains { - "rule": "lease-authorises-before-spending", + "rule": "lease ask missing", "verdict": "lease guard unsafe", "subjects": [{"path": path}], } if { @@ -725,7 +725,7 @@ decides_through_checks_green(path) if { } violation contains { - "rule": "check-status-decided-in-one-place", + "rule": "check grade twice", "verdict": "check grade twice", "subjects": [{"path": path}], } if { @@ -768,7 +768,7 @@ watched(prefix) if { } violation contains { - "rule": "every-bot-branch-has-a-watcher", + "rule": "branch watch missing", "verdict": "branch watch missing", "subjects": [{"path": config}, {"artifact": bot_prefix(config)}], } if { @@ -822,7 +822,7 @@ cache_uses(uses) if startswith(uses, "actions/cache/") path_varies_between_runs(step) if contains(object.get(step, ["with", "path"], ""), "${{") violation contains { - "rule": "cache-path-is-rebase-stable", + "rule": "path reach dead", "verdict": "path reach dead", "subjects": [{"path": path}, {"artifact": name}], } if { @@ -841,7 +841,7 @@ violation contains { # boundary tried and failed. Spelling those the same way is how a gate reports # green over a file it never read. violation contains { - "rule": "ci-task-parity", + "rule": "job run other", "verdict": "workflow read unread", "subjects": [{"path": path}], } if { @@ -910,7 +910,7 @@ foreign_cargo contains [path, number, cmd] if { } violation contains { - "rule": "foreign-cargo-is-the-declared-spelling", + "rule": "cargo spelling wrong", "verdict": "cargo spelling other", "subjects": [{"path": path, "line": number}, {"artifact": cmd}], } if { @@ -926,7 +926,7 @@ violation contains { # is not answering this question, and refusing it would fire on every fixture # that carries a copy of this config and none of its subjects. violation contains { - "rule": "foreign-cargo-is-the-declared-spelling", + "rule": "cargo spelling wrong", "verdict": "cargo reach absent", "subjects": [{"count": 0}], } if { @@ -937,7 +937,7 @@ violation contains { } violation contains { - "rule": "foreign-cargo-is-the-declared-spelling", + "rule": "cargo spelling wrong", "verdict": "task read unread", "subjects": [{"artifact": "test:cargo"}], } if { diff --git a/policy/filed-here.rego b/policy/filed-here.rego index b2e8ee9b2..61a449d3a 100644 --- a/policy/filed-here.rego +++ b/policy/filed-here.rego @@ -75,11 +75,11 @@ package batten.filed_here import rego.v1 -rules contains "filed-unrefined" +rules contains "issue file unclear" -rules contains "filed-over-own-diff" +rules contains "issue file same" -rules contains "filed-and-left-open" +rules contains "issue file held" # The record, or nothing. ABSENT IS NOT EMPTY: a branch whose recorder never ran # has no key here at all, Rego reads that as *does not hold*, and every rule below @@ -189,7 +189,7 @@ body_read if { # # `ready` passes and so does `-`; only the tracker's own `unready` refuses. violation contains { - "rule": "filed-unrefined", + "rule": "issue file unclear", "verdict": "issue file unclear", "subjects": [{"artifact": id}], } if { @@ -262,7 +262,7 @@ cites_only(id) if { # ONE FINDING PER PATH, as the shell emitted, so a reviewer sees which file rather # than a count they have to go and reconstruct. violation contains { - "rule": "filed-over-own-diff", + "rule": "issue file same", "verdict": "issue file same", "subjects": [{"path": path}, {"artifact": id}], } if { @@ -330,7 +330,7 @@ closes_unreadable if { # so the partition cannot be evaluated and the row stays judged as it was # before this arm existed. violation contains { - "rule": "filed-and-left-open", + "rule": "issue file held", "verdict": "issue file held", "subjects": [{"artifact": id}], } if { diff --git a/policy/fixture-forks.rego b/policy/fixture-forks.rego index 834fe6f54..50a45afaa 100644 --- a/policy/fixture-forks.rego +++ b/policy/fixture-forks.rego @@ -73,7 +73,7 @@ package batten.fixture_forks import rego.v1 -rules contains "fixture-fork-added" +rules contains "test add duplicate" # The branch's own diff. NULL when the base rev does not resolve. # @@ -108,7 +108,7 @@ delta := d if { # CI checkout with the base unfetched, the fork with no `origin/main` — and for # an absent key alike. violation contains { - "rule": "fixture-fork-added", + "rule": "test add duplicate", "verdict": "diff read absent", "subjects": [{"path": "batten.toml"}], } if { @@ -171,7 +171,7 @@ base_lines(path) := lines if { # AN ADDED FIXTURE THAT FORKS. Every matching line is new by construction — the # file is absent from base — so each one is a finding with its own pointer. violation contains { - "rule": "fixture-fork-added", + "rule": "test add duplicate", "verdict": "spawn add refused", "subjects": [{"path": path, "line": index + 1}], } if { @@ -192,7 +192,7 @@ violation contains { # rule is about the file's total, so naming every line would report a count as a # list. violation contains { - "rule": "fixture-fork-added", + "rule": "test add duplicate", "verdict": "spawn add refused", "subjects": [{"path": path, "line": first_fork(path)}], } if { @@ -213,7 +213,7 @@ first_fork(path) := min([found | # belongs in `missing` rather than being silently absent, and a module that # iterates only the delta reports green over a file it never read. violation contains { - "rule": "fixture-fork-added", + "rule": "test add duplicate", "verdict": "source read unread", "subjects": [{"path": path}], } if { diff --git a/policy/hk-plan-required.rego b/policy/hk-plan-required.rego index 9d3cf0314..2fa01a760 100644 --- a/policy/hk-plan-required.rego +++ b/policy/hk-plan-required.rego @@ -55,11 +55,11 @@ package batten.hk_plan_required import rego.v1 -rules contains "plan-required-step" +rules contains "step run missing" -rules contains "plan-prohibited-profile" +rules contains "plan run refused" -rules contains "plan-unacquired" +rules contains "plan read missing" # Every declared row, as the engine emitted it: an id maps to the plan it took, # or to `null` where it could not take one. `input.tree.plan` is itself `null` @@ -88,7 +88,7 @@ acquired[id] := plan if { # otherwise identical on the decision surface, which is the whole reason this # fires rather than passing. violation contains { - "rule": "plan-unacquired", + "rule": "plan read missing", "verdict": "plan read missing", "subjects": [{"artifact": id}], } if { @@ -102,7 +102,7 @@ violation contains { # carries which: an absent step has no reason kind, and an excluded one carries # the runner's own kind token. violation contains { - "rule": "plan-required-step", + "rule": "step run missing", "verdict": "step run missing", "subjects": [{"artifact": id}, {"count": count(missing_in(id))}], } if { @@ -132,7 +132,7 @@ included(plan, name) if { # step missing is a change to the gate, where a prohibited profile is a change to # how the gate was invoked. violation contains { - "rule": "plan-prohibited-profile", + "rule": "plan run refused", "verdict": "plan run refused", "subjects": [{"artifact": id}], } if { @@ -162,7 +162,7 @@ test_a_required_step_the_plan_excludes_is_refused if { "steps": [{"name": "one", "status": "skipped", "reasonKind": "profile_exclude"}], }}}} - finding.rule == "plan-required-step" + finding.rule == "step run missing" } test_a_required_step_the_plan_never_names_is_refused if { @@ -174,13 +174,13 @@ test_a_required_step_the_plan_never_names_is_refused if { "steps": [{"name": "one", "status": "included", "reasonKind": "filter_match"}], }}}} - finding.rule == "plan-required-step" + finding.rule == "step run missing" } test_a_declared_plan_that_could_not_be_acquired_is_refused if { some finding in violation with input as {"tree": {"plan": {"gate": null}}} - finding.rule == "plan-unacquired" + finding.rule == "plan read missing" } test_a_prohibited_profile_is_refused if { @@ -192,7 +192,7 @@ test_a_prohibited_profile_is_refused if { "steps": [{"name": "one", "status": "included", "reasonKind": "filter_match"}], }}}} - finding.rule == "plan-prohibited-profile" + finding.rule == "plan run refused" } test_a_repository_declaring_no_plan_owes_none if { diff --git a/policy/landing-roster-guarded.rego b/policy/landing-roster-guarded.rego index b3797a6d0..bb09f703d 100644 --- a/policy/landing-roster-guarded.rego +++ b/policy/landing-roster-guarded.rego @@ -84,7 +84,7 @@ package batten.landing_roster import rego.v1 -rules contains "landing-roster-unguarded" +rules contains "check read never" # The one workflow that moves `main`. A consumer path in a consumer module, which # is where non-negotiable rule 1 puts it: `crates/batten` may not name it and @@ -126,7 +126,7 @@ guarded if { # `main`. Refusing on a whole-tree acquisition failure too is the safe direction # — a landing workflow that cannot be read is not one that has been checked. violation contains { - "rule": "landing-roster-unguarded", + "rule": "check read never", "verdict": "check read never", "subjects": [{"path": landing_workflow}], } if { diff --git a/policy/lock-complete.rego b/policy/lock-complete.rego index dd7513b84..dab3478d0 100644 --- a/policy/lock-complete.rego +++ b/policy/lock-complete.rego @@ -61,21 +61,21 @@ package batten.lockcomplete import rego.v1 -rules contains "lock-platform-residue" +rules contains "lock write other" -rules contains "lock-platform-uninstallable" +rules contains "lock reach unsafe" -rules contains "lock-tool-unlocked" +rules contains "tool pin partial" -rules contains "lock-tool-missing" +rules contains "tool pin absent" -rules contains "lock-pin-stale" +rules contains "pin read stale" -rules contains "lockfile-writes-enabled" +rules contains "lock write unsafe" -rules contains "workflow-installs-unlocked" +rules contains "workflow run unsafe" -rules contains "lock-unreadable" +rules contains "lock read unread" # --- the two committed authorities -------------------------------------------- # @@ -203,7 +203,7 @@ pointer(path, needles) := {"path": path} if not line_of(path, needles) # checksum and no url and which `lock-check` reported "complete and current" over # on every run. violation contains { - "rule": "lock-platform-residue", + "rule": "lock write other", "verdict": "lock write other", "subjects": [ pointer("mise.lock", [sprintf("\"platforms.%s\"", [platform]), name]), @@ -224,7 +224,7 @@ violation contains { # decision upstream made. That is the defect of the gate being replaced, one level # down. violation contains { - "rule": "lock-platform-uninstallable", + "rule": "lock reach unsafe", "verdict": "lock reach missing", "subjects": [ pointer("mise.lock", [sprintf("\"platforms.%s\"", [platform]), name]), @@ -241,7 +241,7 @@ violation contains { # one real platform: a tool that locks nothing is predicate 3's finding, and # reporting it three times here as well would bury the one line a reader acts on. violation contains { - "rule": "lock-platform-uninstallable", + "rule": "lock reach unsafe", "verdict": "lock reach missing", "subjects": [ pointer("mise.lock", [sprintf("[[tools.%s]]", [name])]), @@ -268,7 +268,7 @@ violation contains { # wherever it appears — including on a platform this repository does not install # on, where predicate 2 deliberately says nothing. violation contains { - "rule": "lock-platform-uninstallable", + "rule": "lock reach unsafe", "verdict": "lock write partial", "subjects": [ pointer("mise.lock", [sprintf("\"platforms.%s\"", [platform]), name]), @@ -291,7 +291,7 @@ violation contains { # exempt backends CANNOT lock a url; a fetch-an-asset backend can, so for one of # those "locks nothing" means unlocked rather than exempt. violation contains { - "rule": "lock-tool-unlocked", + "rule": "tool pin partial", "verdict": "tool pin missing", "subjects": [ pointer("mise.lock", [sprintf("[[tools.%s]]", [name])]), @@ -308,7 +308,7 @@ violation contains { # different remedy: what it installs cannot be determined rather than can be and # is unverified. violation contains { - "rule": "lock-tool-unlocked", + "rule": "tool pin partial", "verdict": "tool declare missing", "subjects": [ pointer("mise.lock", [sprintf("[[tools.%s]]", [name])]), @@ -358,7 +358,7 @@ declared_tools[name] := value if { } violation contains { - "rule": "lock-tool-missing", + "rule": "tool pin absent", "verdict": "tool pin absent", "subjects": [pointer("mise.toml", [name]), {"artifact": name}], } if { @@ -401,7 +401,7 @@ satisfies(locked, pin) if startswith(locked, sprintf("%s.", [pin])) plain_version(pin) if regex.match(data.batten.patterns["plain-dotted-version"], pin) violation contains { - "rule": "lock-pin-stale", + "rule": "pin read stale", "verdict": "pin read stale", "subjects": [pointer("mise.toml", [name]), {"artifact": name}], } if { @@ -426,7 +426,7 @@ writes_enabled if manifest.settings.lockfile == true writes_enabled if manifest.settings.lockfile == 1 violation contains { - "rule": "lockfile-writes-enabled", + "rule": "lock write unsafe", "verdict": "lock write unsafe", "subjects": [pointer("mise.toml", ["lockfile"])], } if { @@ -468,7 +468,7 @@ sets_lockfile(path) if { } violation contains { - "rule": "workflow-installs-unlocked", + "rule": "workflow run unsafe", "verdict": "workflow run unsafe", "subjects": [pointer(path, ["mise-action"])], } if { @@ -492,7 +492,7 @@ declares_tools if { } violation contains { - "rule": "lock-unreadable", + "rule": "lock read unread", "verdict": "lock read unread", "subjects": [{"path": "mise.lock"}], } if { @@ -540,7 +540,7 @@ test_a_platform_key_mise_does_not_emit_is_a_finding if { fixture_manifest, ) with data.batten.patterns as fixture_patterns - v.rule == "lock-platform-residue" + v.rule == "lock write other" } # A LITERAL RATHER THAN `object.union`, and the difference is the reason this case @@ -561,7 +561,7 @@ test_a_required_platform_with_no_url_is_a_finding if { fixture_manifest, ) with data.batten.patterns as fixture_patterns - v.rule == "lock-platform-uninstallable" + v.rule == "lock reach unsafe" } # THE NEAR-MISS. A url-less stub on a NON-required platform is mise recording that @@ -624,7 +624,7 @@ test_a_required_platform_missing_entirely_is_a_finding if { fixture_manifest, ) with data.batten.patterns as fixture_patterns - v.rule == "lock-platform-uninstallable" + v.rule == "lock reach unsafe" } test_a_backend_that_cannot_lock_is_exempt_from_locking_nothing if { @@ -646,7 +646,7 @@ test_an_asset_backend_that_locks_nothing_is_a_finding if { {"settings": {"lockfile": false}, "tools": {}}, ) with data.batten.patterns as fixture_patterns - v.rule == "lock-tool-unlocked" + v.rule == "tool pin partial" } test_a_tool_declaring_no_backend_is_a_finding if { @@ -667,7 +667,7 @@ test_a_declared_tool_with_no_lock_entry_is_a_finding if { }}, ) with data.batten.patterns as fixture_patterns - v.rule == "lock-tool-missing" + v.rule == "tool pin absent" } # THE ALLOWLIST IS FAIL-CLOSED, so a bare name other than `rust` must lock: @@ -678,7 +678,7 @@ test_a_bare_name_other_than_rust_must_lock if { {"settings": {"lockfile": false}, "tools": {"t": "1.0.0", "node": "24"}}, ) with data.batten.patterns as fixture_patterns - v.rule == "lock-tool-missing" + v.rule == "tool pin absent" } test_a_pin_its_entry_does_not_name_is_a_finding if { @@ -687,11 +687,11 @@ test_a_pin_its_entry_does_not_name_is_a_finding if { {"settings": {"lockfile": false}, "tools": {"t": "2.0.0"}}, ) with data.batten.patterns as fixture_patterns - v.rule == "lock-pin-stale" + v.rule == "pin read stale" } test_a_partial_pin_the_lock_extends_is_not if { - count({v | some v in violation; v.rule == "lock-pin-stale"}) == 0 with input as fixture_input( + count({v | some v in violation; v.rule == "pin read stale"}) == 0 with input as fixture_input( fixture_lock, {"settings": {"lockfile": false}, "tools": {"t": "1.0"}}, ) @@ -713,7 +713,7 @@ test_the_extension_must_be_at_a_component_boundary if { {"settings": {"lockfile": false}, "tools": {"t": "1.9"}}, ) with data.batten.patterns as fixture_patterns - v.rule == "lock-pin-stale" + v.rule == "pin read stale" } # The spelling the measured defect was written in, and the one a bare-string @@ -727,11 +727,11 @@ test_an_inline_table_pin_is_read if { }}}, ) with data.batten.patterns as fixture_patterns - v.rule == "lock-pin-stale" + v.rule == "pin read stale" } test_a_pin_that_is_not_a_dotted_version_is_skipped if { - count({v | some v in violation; v.rule == "lock-pin-stale"}) == 0 with input as fixture_input( + count({v | some v in violation; v.rule == "pin read stale"}) == 0 with input as fixture_input( fixture_lock, {"settings": {"lockfile": false}, "tools": {"t": "latest"}}, ) @@ -744,7 +744,7 @@ test_re_enabled_lockfile_writes_are_a_finding if { {"settings": {"lockfile": true}, "tools": {"t": "1.0.0"}}, ) with data.batten.patterns as fixture_patterns - v.rule == "lockfile-writes-enabled" + v.rule == "lock write unsafe" } # A `lockfile` key outside `[settings]` is not the setting. The predecessor needed @@ -765,7 +765,7 @@ test_a_workflow_installing_without_the_lockfile_env_is_a_finding if { {"tree": {"lines": {".github/workflows/w.yml": [" - uses: jdx/mise-action@abc"]}}}, ) with data.batten.patterns as fixture_patterns - v.rule == "workflow-installs-unlocked" + v.rule == "workflow run unsafe" } test_the_same_workflow_setting_it_is_not if { @@ -798,7 +798,7 @@ test_an_unreadable_lockfile_a_manifest_depends_on_is_a_finding if { "missing": {"mise.lock": "absent"}, }} with data.batten.patterns as fixture_patterns - v.rule == "lock-unreadable" + v.rule == "lock read unread" } test_an_unreadable_lockfile_no_manifest_depends_on_is_silent if { diff --git a/policy/nextest-slow.rego b/policy/nextest-slow.rego index 02236afb8..6870a906e 100644 --- a/policy/nextest-slow.rego +++ b/policy/nextest-slow.rego @@ -90,11 +90,11 @@ package batten.nextest_slow import rego.v1 -rules contains "nextest-slow-unbounded" +rules contains "suite bind missing" -rules contains "nextest-slow-raised" +rules contains "bound edit refused" -rules contains "nextest-slow-override-unfiled" +rules contains "waiver file missing" # The runner's committed configuration. A consumer path in a consumer module, # which is where non-negotiable rule 1 puts it. @@ -198,7 +198,7 @@ kill_seconds contains kill if { # is in force that can be vouched for. Refusing on an unreadable unit is the # fail-closed direction: the alternative is a silently unreachable comparison. violation contains { - "rule": "nextest-slow-unbounded", + "rule": "suite bind missing", "verdict": "suite bind missing", "subjects": [{"path": config}], } if { @@ -208,7 +208,7 @@ violation contains { # THE RATCHET. A kill threshold above the committed ceiling is refused; below it # is free, so making the suite faster never has to negotiate with this gate. violation contains { - "rule": "nextest-slow-raised", + "rule": "bound edit refused", "verdict": "bound edit refused", "subjects": [{"path": config}], } if { @@ -221,7 +221,7 @@ violation contains { # behind it is just the ban switched off for whichever test was inconvenient, and # it is the thing that rots. violation contains { - "rule": "nextest-slow-override-unfiled", + "rule": "waiver file missing", "verdict": "waiver file missing", "subjects": [{"path": config}], } if { diff --git a/policy/obligations-bound.rego b/policy/obligations-bound.rego index 95e5ecfad..51b25f34a 100644 --- a/policy/obligations-bound.rego +++ b/policy/obligations-bound.rego @@ -77,7 +77,7 @@ package batten.obligations_bound import rego.v1 -rules contains "obligation-unbound" +rules contains "test name undefined" # The board record, or nothing. ABSENT IS NOT EMPTY: a branch whose recorder # never ran has no key here, Rego reads that as *does not hold*, and this module @@ -226,7 +226,7 @@ declares_slug(entry) if { # than a count they have to reconstruct. The path leads, because that is what a # reader opens; the row's id follows it, carried rather than as the pointer. violation contains { - "rule": "obligation-unbound", + "rule": "test name undefined", "verdict": "test name undefined", "subjects": [{"path": obligation_row.file}, {"artifact": obligation_row.id}], } if { @@ -242,7 +242,7 @@ violation contains { # and never given a mutation that could kill it. Collapsing them would hand the # author one message for two problems. violation contains { - "rule": "obligation-unbound", + "rule": "test name undefined", "verdict": "test name undefined", "subjects": [{"path": obligation_row.file}, {"artifact": obligation_row.id}], } if { diff --git a/policy/perf-assert.rego b/policy/perf-assert.rego index 04c9ba0cf..95bdfb8d8 100644 --- a/policy/perf-assert.rego +++ b/policy/perf-assert.rego @@ -74,13 +74,13 @@ package batten.perf_assert import rego.v1 -rules contains "perf-over-budget" +rules contains "path measure late" -rules contains "perf-record-incomplete" +rules contains "path measure partial" -rules contains "perf-budget-unpublished" +rules contains "prose state wrong" -rules contains "perf-budget-unreadable" +rules contains "source read missing" # The budgets, in milliseconds, written once here as data — the placement the # predecessor's `BUDGETS` table had, one level over. @@ -132,7 +132,7 @@ judged := measurements if { # A budgeted path whose measured p95 is over its ceiling. violation contains { - "rule": "perf-over-budget", + "rule": "path measure late", "verdict": "path measure late", "subjects": [{"count": count(over_budget)}], } if { @@ -152,7 +152,7 @@ over_budget contains id if { # absence within a PRESENT record is a finding. The guard is that `judged` itself # must resolve: with no record at all there is nothing to be incomplete about. violation contains { - "rule": "perf-record-incomplete", + "rule": "path measure partial", "verdict": "path measure partial", "subjects": [{"count": count(unmeasured)}], } if { @@ -208,7 +208,7 @@ published[id] := budget if { # A budgeted path README publishes a different number for. violation contains { - "rule": "perf-budget-unpublished", + "rule": "prose state wrong", "verdict": "prose state wrong", "subjects": [{"count": count(disagreeing)}], } if { @@ -237,7 +237,7 @@ disagreeing contains id if { # causes apart deliberately, so this reports that it could not look rather than # deciding. violation contains { - "rule": "perf-budget-unreadable", + "rule": "source read missing", "verdict": "source read missing", "subjects": [{"path": "README.md"}], } if { diff --git a/policy/plan-complete.rego b/policy/plan-complete.rego index 209c96fe8..a9b06e538 100644 --- a/policy/plan-complete.rego +++ b/policy/plan-complete.rego @@ -51,9 +51,9 @@ package batten.plan_complete import rego.v1 -rules contains "plan-unfinished" +rules contains "plan declare held" -rules contains "plan-unrecorded" +rules contains "plan declare absent" # The store, or nothing. ABSENT IS NOT EMPTY, and the two reach different arms # below on purpose: an empty file is "I recorded a plan and it holds no entries", @@ -99,7 +99,7 @@ changed contains path if { # `plan-unfinished`: an entry the branch declared and left in flight. violation contains { - "rule": "plan-unfinished", + "rule": "plan declare held", "verdict": "plan declare held", "subjects": [{"artifact": entry_row.id}], } if { @@ -137,7 +137,7 @@ claimed if { # keeps the remedy honest for a genuinely trivial change: one call saying so, # rather than a fabricated entry. violation contains { - "rule": "plan-unrecorded", + "rule": "plan declare absent", "verdict": "plan declare absent", "subjects": [{"count": count(changed)}], } if { diff --git a/policy/release-provision-parity.rego b/policy/release-provision-parity.rego index 0c3e0b221..a45a8c072 100644 --- a/policy/release-provision-parity.rego +++ b/policy/release-provision-parity.rego @@ -29,7 +29,7 @@ package batten.release_provision_parity import rego.v1 -rules contains "release-target-has-a-provisioned-scanner" +rules contains "release cover missing" # A rust triple is not a platform key. `provision.rs`'s `platform_key()` builds # `-` with no libc flavour, so `-gnu` and `-musl` collapse to one key — @@ -85,7 +85,7 @@ pinned[name] := keys if { # --- a published target every provision row can serve ------------------------- violation contains { - "rule": "release-target-has-a-provisioned-scanner", + "rule": "release cover missing", "verdict": "release cover partial", "subjects": [{"artifact": target}, {"artifact": name}], } if { @@ -105,7 +105,7 @@ violation contains { # look" and "your gate saying so" is this clause. violation contains { - "rule": "release-target-has-a-provisioned-scanner", + "rule": "release cover missing", "verdict": "workflow read unread", "subjects": [{"path": path}], } if { @@ -119,7 +119,7 @@ violation contains { # dead-gate shape the whole file guards against. A new release target therefore # reddens here until the map names it, which is the trigger the gate exists for. violation contains { - "rule": "release-target-has-a-provisioned-scanner", + "rule": "release cover missing", "verdict": "release cover partial", "subjects": [{"artifact": target}], } if { @@ -156,7 +156,7 @@ test_a_covered_target_is_clean if { test_an_uncovered_undeclared_target_is_refused if { found := violation with input as tree(["x86_64-apple-darwin"], covered) some f in found - f.rule == "release-target-has-a-provisioned-scanner" + f.rule == "release cover missing" f.verdict == "release cover partial" } diff --git a/policy/remedy-authorship.rego b/policy/remedy-authorship.rego index ff5e3546b..069444769 100644 --- a/policy/remedy-authorship.rego +++ b/policy/remedy-authorship.rego @@ -84,16 +84,16 @@ package batten.remedy_authorship import rego.v1 -rules contains "remedy-reaches-the-reader" +rules contains "remedy select dropped" -rules contains "remedy-has-one-author" +rules contains "remedy own duplicate" # --------------------------------------------------------------------------- # A: every line of a stderr block carries the error prefix. # --------------------------------------------------------------------------- violation contains { - "rule": "remedy-reaches-the-reader", + "rule": "remedy select dropped", "verdict": "remedy select dropped", "subjects": [{"path": path, "line": i + 1}], } if { @@ -190,7 +190,7 @@ emits_a_literal(line) if { # --------------------------------------------------------------------------- violation contains { - "rule": "remedy-has-one-author", + "rule": "remedy own duplicate", "verdict": "remedy own duplicate", "subjects": [{"artifact": name}, {"artifact": var}], } if { @@ -300,7 +300,7 @@ test_an_unprefixed_stderr_line_is_refused if { "\techo \"here is the fix\"", "} >&2", ]}}} - v.rule == "remedy-reaches-the-reader" + v.rule == "remedy select dropped" } test_a_fully_prefixed_block_passes if { @@ -324,7 +324,7 @@ test_a_block_not_redirected_to_stderr_is_not_judged if { test_a_caller_naming_a_bypass_it_does_not_read_is_refused if { some v in violation with input as {"tree": {"documents": {"mise.toml": {"tasks": {"verify": {"run": "echo \"set BATTEN_PROSE_ONLY_OVERRIDE=1 to record the exception\""}}}}}} - v.rule == "remedy-has-one-author" + v.rule == "remedy own duplicate" } # THE DISCRIMINATING CASE for B: the gate that OWNS a hatch must be able to name diff --git a/policy/repetition-without-progress.rego b/policy/repetition-without-progress.rego index 702b74a88..11eb28b38 100644 --- a/policy/repetition-without-progress.rego +++ b/policy/repetition-without-progress.rego @@ -62,7 +62,7 @@ package batten.repetition_without_progress import rego.v1 -rules contains "agent-turn-run" +rules contains "turn run loose" # OpenHands' monologue threshold. Adopted rather than derived: CLOUD-1352 makes a # replay over this repository's own history the precondition for promoting any @@ -76,7 +76,7 @@ threshold := 3 # here, because an extraction this host cannot answer is absent from the map and # an absent key is undefined, which does not hold. violation contains { - "rule": "agent-turn-run", + "rule": "turn run loose", "verdict": "turn run loose", "subjects": [{"count": input.facts.extracted["agent-turn-run"]}], } if { diff --git a/policy/review-answered.rego b/policy/review-answered.rego index 270089529..9ba4a952b 100644 --- a/policy/review-answered.rego +++ b/policy/review-answered.rego @@ -74,9 +74,9 @@ package batten.review_answered import rego.v1 -rules contains "review-unanswered" +rules contains "review answer missing" -rules contains "review-absent" +rules contains "review read absent" # NO BATS SUITE, and that is CLOUD-1059's doing rather than a gap. The suite that # drove this module end to end asserted the refusal's PROSE, which CLOUD-1050 @@ -91,7 +91,7 @@ rules contains "review-absent" # count — so killing `readying` left that case green. Measured: it survived. #MUTANT ready-unread|s@^\treadying$@\tfalse@|the_measured_shape_a_head_carrying_unresolved_threads_is_refused_naming_the_count violation contains { - "rule": "review-unanswered", + "rule": "review answer missing", "verdict": "review answer missing", "subjects": [{"count": record.rows}], } if { @@ -110,7 +110,7 @@ violation contains { # information — kept because the ABI's shape is uniform and a refusal with an # empty subject list reads as a refusal nobody could locate. violation contains { - "rule": "review-absent", + "rule": "review read absent", "verdict": "review read absent", "subjects": [{"count": record.rows}], } if { @@ -187,7 +187,7 @@ test_a_head_with_open_threads_is_refused if { "review-happened": {"rows": 1}, }}, } - v.rule == "review-unanswered" + v.rule == "review answer missing" } test_a_head_with_every_thread_answered_is_left_alone if { @@ -212,7 +212,7 @@ test_zero_threads_and_no_review_reads_as_unreviewed if { "review-happened": {"rows": 0}, }}, } - v.rule == "review-absent" + v.rule == "review read absent" } # THE DISCRIMINATING HALF, and without it the case above would pass over a @@ -279,7 +279,7 @@ test_a_truncated_page_still_refuses_because_it_is_counted if { "review-happened": {"rows": 1}, }}, } - v.rule == "review-unanswered" + v.rule == "review answer missing" } # A COMPOUND COMMAND IS STILL A READY, and this is the case that was missing when @@ -295,7 +295,7 @@ test_a_compound_command_is_still_a_ready if { "review-happened": {"rows": 1}, }}, } - v.rule == "review-unanswered" + v.rule == "review answer missing" } # NO PROSE CASE LIVES HERE, deliberately, and its absence is the honest reading. diff --git a/policy/rules-drift.rego b/policy/rules-drift.rego index 631fc3df2..15d82afaa 100644 --- a/policy/rules-drift.rego +++ b/policy/rules-drift.rego @@ -24,19 +24,19 @@ package batten.rulesdrift import rego.v1 -rules contains "restated-default-drifts" +rules contains "default state other" -rules contains "named-event-unwired" +rules contains "event wire missing" -rules contains "named-input-key-unemittable" +rules contains "input key dead" -rules contains "named-fixed-rule-unqueried" +rules contains "rule ask missing" -rules contains "restated-arm-count-drifts" +rules contains "rule count other" -rules contains "schema-key-undocumented" +rules contains "input name missing" -rules contains "drift-authority-unreadable" +rules contains "drift read unread" # --- the prose surfaces ------------------------------------------------------- # @@ -97,7 +97,7 @@ observed(name) if { } violation contains { - "rule": "restated-default-drifts", + "rule": "default state other", "verdict": "default state other", "subjects": [{"path": claim.path, "line": claim.line}], } if { @@ -201,7 +201,7 @@ event_pointer(claim, name) := line if { } violation contains { - "rule": "named-event-unwired", + "rule": "event wire missing", "verdict": "event wire missing", "subjects": [{"path": claim.path, "line": event_pointer(claim, name)}], } if { @@ -297,7 +297,7 @@ named_keys contains {"path": path, "line": index + 1, "surface": surface, "key": } violation contains { - "rule": "named-input-key-unemittable", + "rule": "input key dead", "verdict": "input key dead", "subjects": [{"path": named.path, "line": named.line}], } if { @@ -332,7 +332,7 @@ named_rules contains {"path": path, "line": index + 1, "name": name} if { } violation contains { - "rule": "named-fixed-rule-unqueried", + "rule": "rule ask missing", "verdict": "rule ask missing", "subjects": [{"path": named.path, "line": named.line}], } if { @@ -396,7 +396,7 @@ arm_count(name) := total if { } violation contains { - "rule": "restated-arm-count-drifts", + "rule": "rule count other", "verdict": "rule count other", "subjects": [{"path": claim.path, "line": claim.line}, {"count": arm_count(claim.name)}], } if { @@ -437,7 +437,7 @@ names_key(path, surface, key) if { } violation contains { - "rule": "schema-key-undocumented", + "rule": "input name missing", "verdict": "input name missing", "subjects": [{"path": claimant.path, "line": claimant.line}, {"artifact": sprintf("input.%s.%s", [surface, key])}], } if { @@ -480,7 +480,7 @@ authority_needed contains "crates/batten/src/policy.rs" if { # form of `sources` is what keeps the rule alive across an absent file, and the # key simply not being in `documents` is then the honest signal. violation contains { - "rule": "drift-authority-unreadable", + "rule": "drift read unread", "verdict": "drift read unread", "subjects": [{"path": path}], } if { @@ -494,7 +494,7 @@ violation contains { # named in prose would silently stop being judged. That is the vacuity the fixed # path above buys, paid for here rather than left implicit. violation contains { - "rule": "drift-authority-unreadable", + "rule": "drift read unread", "verdict": "drift read unread", "subjects": [{"path": schema_path[named.surface]}], } if { @@ -518,11 +518,11 @@ test_a_restated_default_that_disagrees_is_a_finding if { "missing": {}, }} with data.batten.patterns as fixture_patterns - v.rule == "restated-default-drifts" + v.rule == "default state other" } test_a_restated_default_that_agrees_is_not if { - count({v | some v in violation; v.rule == "restated-default-drifts"}) == 0 with input as {"tree": { + count({v | some v in violation; v.rule == "default state other"}) == 0 with input as {"tree": { "lines": { "a.md": ["the cap is `MAX_LAPS` (2) laps"], "t.sh": ["laps=\"${MAX_LAPS:-2}\""], @@ -537,7 +537,7 @@ test_a_restated_default_that_agrees_is_not if { # knob named with no value asserted must be untouched, because demanding the # value be restated is the discipline this gate would otherwise invert. test_a_knob_named_without_a_value_is_untouched if { - count({v | some v in violation; v.rule == "restated-default-drifts"}) == 0 with input as {"tree": { + count({v | some v in violation; v.rule == "default state other"}) == 0 with input as {"tree": { "lines": { "a.md": ["the cap is `MAX_LAPS`, read it there"], "t.sh": ["laps=\"${MAX_LAPS:-2}\""], @@ -549,7 +549,7 @@ test_a_knob_named_without_a_value_is_untouched if { } test_a_variable_no_program_defaults_is_untouched if { - count({v | some v in violation; v.rule == "restated-default-drifts"}) == 0 with input as {"tree": { + count({v | some v in violation; v.rule == "default state other"}) == 0 with input as {"tree": { "lines": {"a.md": ["the cap is `MAX_LAPS` (8) laps"], "t.sh": ["true"]}, "documents": {}, "missing": {}, @@ -564,11 +564,11 @@ test_an_unwired_event_a_sentence_claims_is_a_finding if { "missing": {}, }} with data.batten.patterns as fixture_patterns - v.rule == "named-event-unwired" + v.rule == "event wire missing" } test_a_wired_event_is_not if { - count({v | some v in violation; v.rule == "named-event-unwired"}) == 0 with input as {"tree": { + count({v | some v in violation; v.rule == "event wire missing"}) == 0 with input as {"tree": { "lines": {"a.md": ["the guard runs on `PreToolUse` today"]}, "documents": {".claude/settings.json": {"hooks": {"PreToolUse": []}}}, "missing": {}, @@ -581,7 +581,7 @@ test_a_wired_event_is_not if { # same breath, and a paragraph-wide check would forbid the repo from writing its # own gaps down beside the wiring they qualify. test_a_gap_recorded_beside_a_wiring_is_untouched if { - count({v | some v in violation; v.rule == "named-event-unwired"}) == 0 with input as {"tree": { + count({v | some v in violation; v.rule == "event wire missing"}) == 0 with input as {"tree": { "lines": {"a.md": [ "the guard runs on `PreToolUse`. The `PostToolBatch` entry stays", "absent, and CLOUD-461 is why", @@ -599,11 +599,11 @@ test_an_unemittable_tree_key_is_a_finding if { "missing": {}, }} with data.batten.patterns as fixture_patterns - v.rule == "named-input-key-unemittable" + v.rule == "input key dead" } test_an_emittable_tree_key_is_not if { - count({v | some v in violation; v.rule == "named-input-key-unemittable"}) == 0 with input as {"tree": { + count({v | some v in violation; v.rule == "input key dead"}) == 0 with input as {"tree": { "lines": {"a.md": ["a module iterates `input.tree.documents` here"]}, "documents": {"schema/policy-input.schema.json": {"properties": {"tree": {"properties": {"documents": {}}}}}}, "missing": {}, @@ -621,11 +621,11 @@ test_an_unqueried_fixed_rule_is_a_finding if { "missing": {}, }} with data.batten.patterns as fixture_patterns - v.rule == "named-fixed-rule-unqueried" + v.rule == "rule ask missing" } test_a_queried_fixed_rule_is_not if { - count({v | some v in violation; v.rule == "named-fixed-rule-unqueried"}) == 0 with input as {"tree": { + count({v | some v in violation; v.rule == "rule ask missing"}) == 0 with input as {"tree": { "lines": { "a.md": ["publish `data.batten.deny` to contribute"], "crates/batten/src/policy.rs": ["const DENY_RULE: &str = \"deny\";"], @@ -643,13 +643,13 @@ test_an_unreadable_authority_a_claim_depends_on_is_a_finding if { "missing": {}, }} with data.batten.patterns as fixture_patterns - v.rule == "drift-authority-unreadable" + v.rule == "drift read unread" } # THE SCOPE MIRROR. An authority nothing claims against is silent, which is what # keeps this row from speaking in every fixture repository inheriting the config. test_an_unreadable_authority_no_claim_depends_on_is_silent if { - count({v | some v in violation; v.rule == "drift-authority-unreadable"}) == 0 with input as {"tree": { + count({v | some v in violation; v.rule == "drift read unread"}) == 0 with input as {"tree": { "lines": {"a.md": ["ordinary prose naming nothing"]}, "documents": {}, "missing": {}, @@ -673,14 +673,14 @@ test_a_restated_arm_count_that_disagrees_is_a_finding if { "missing": {}, }} with data.batten.patterns as fixture_patterns - v.rule == "restated-arm-count-drifts" + v.rule == "rule count other" } # AND THE INDENTED HEAD IS NOT ONE. The fixture above carries a tab-indented # occurrence among five lines, so a count of four is the agreeing case — which is # the anchor doing its job rather than a coincidence of the numbers. test_a_restated_arm_count_that_agrees_is_not if { - count({v | some v in violation; v.rule == "restated-arm-count-drifts"}) == 0 with input as {"tree": { + count({v | some v in violation; v.rule == "rule count other"}) == 0 with input as {"tree": { "lines": { "a.md": ["the admission is `admitted_addition` (4 arms) today"], "policy/x.rego": [ @@ -701,7 +701,7 @@ test_a_restated_arm_count_that_agrees_is_not if { # asserting how many arms it has must be untouched, or the gate demands that every # mention of a mechanism enumerate it. test_an_arm_named_without_a_count_is_untouched if { - count({v | some v in violation; v.rule == "restated-arm-count-drifts"}) == 0 with input as {"tree": { + count({v | some v in violation; v.rule == "rule count other"}) == 0 with input as {"tree": { "lines": { "a.md": ["`admitted_addition` is the authority; read it there"], "policy/x.rego": ["admitted_addition(_, a) if b(a)"], @@ -713,7 +713,7 @@ test_an_arm_named_without_a_count_is_untouched if { } test_a_count_over_a_rule_no_module_defines_is_untouched if { - count({v | some v in violation; v.rule == "restated-arm-count-drifts"}) == 0 with input as {"tree": { + count({v | some v in violation; v.rule == "rule count other"}) == 0 with input as {"tree": { "lines": { "a.md": ["the admission is `invented_rule` (3 arms) today"], "policy/x.rego": ["admitted_addition(_, a) if b(a)"], @@ -734,11 +734,11 @@ test_a_schema_key_the_claiming_file_does_not_name_is_a_finding if { "missing": {}, }} with data.batten.patterns as fixture_patterns - v.rule == "schema-key-undocumented" + v.rule == "input name missing" } test_a_schema_key_the_claiming_file_names_is_not if { - count({v | some v in violation; v.rule == "schema-key-undocumented"}) == 0 with input as {"tree": { + count({v | some v in violation; v.rule == "input name missing"}) == 0 with input as {"tree": { "lines": {"a.md": [ "a module iterates `input.tree.documents` and `input.tree.symbols` here", "rules-drift holds the lists above to those two files", @@ -752,7 +752,7 @@ test_a_schema_key_the_claiming_file_names_is_not if { # THE SCOPE MIRROR, and it is what keeps this inside the anti-restatement bound: a # file that makes no claim to enumerate the key set is an ordinary consumer. test_a_file_making_no_authority_claim_is_untouched if { - count({v | some v in violation; v.rule == "schema-key-undocumented"}) == 0 with input as {"tree": { + count({v | some v in violation; v.rule == "input name missing"}) == 0 with input as {"tree": { "lines": {"a.md": ["a module iterates `input.tree.documents` here"]}, "documents": {"schema/policy-input.schema.json": {"properties": {"tree": {"properties": {"documents": {}, "symbols": {}}}}}}, "missing": {}, @@ -764,7 +764,7 @@ test_a_file_making_no_authority_claim_is_untouched if { # arm every hyphenated key would be reported as undocumented, which is the eight # false findings that would have made this predicate unusable on its first run. test_a_subscripted_key_counts_as_named if { - count({v | some v in violation; v.rule == "schema-key-undocumented"}) == 0 with input as {"tree": { + count({v | some v in violation; v.rule == "input name missing"}) == 0 with input as {"tree": { "lines": {"a.md": [ "a module reads `input.tree[\"base-delta\"]` here", "rules-drift holds the lists above to those two files", diff --git a/policy/run-shape.rego b/policy/run-shape.rego index e7e77db0e..9a32fda1a 100644 --- a/policy/run-shape.rego +++ b/policy/run-shape.rego @@ -57,15 +57,15 @@ package batten.run_shape import rego.v1 -rules contains "commit-names-no-message-source" +rules contains "commit write missing" -rules contains "unsatisfiable-commit" +rules contains "commit bind missing" -rules contains "foreground-sleep" +rules contains "sleep run blocked" -rules contains "background-timer" +rules contains "timer run refused" -rules contains "polls-a-local-process" +rules contains "task watch duplicate" rules contains "foreground-mise" @@ -96,7 +96,7 @@ rules contains "background-redirect" #MUTANT bracket-is-an-exit|s@^\tcondition_program(segment) in {"pgrep", "pkill", "ps", "jobs"}$@\tcondition_program(segment) in {"pgrep", "pkill", "ps", "jobs"}; not contains(segment.raw, "[")@|a_bracketed_pattern_is_refused_just_the_same violation contains { - "rule": "commit-names-no-message-source", + "rule": "commit write missing", "verdict": "commit write missing", } if { # THE CHEAP TERM FIRST, and it is load-bearing rather than tidy. Everything @@ -112,7 +112,7 @@ violation contains { } violation contains { - "rule": "unsatisfiable-commit", + "rule": "commit bind missing", "verdict": "commit bind missing", } if { some segment in input.call.segments @@ -128,7 +128,7 @@ violation contains { } violation contains { - "rule": "foreground-sleep", + "rule": "sleep run blocked", "verdict": "sleep run blocked", } if { sleeps @@ -142,7 +142,7 @@ violation contains { } violation contains { - "rule": "background-timer", + "rule": "timer run refused", "verdict": "timer run refused", } if { sleeps @@ -177,7 +177,7 @@ violation contains { # instead of eleven broken ones, every one still redundant. The waste is the # wait, not the typo, so a bracketed pattern is refused here too. violation contains { - "rule": "polls-a-local-process", + "rule": "task watch duplicate", "verdict": "task watch duplicate", "subjects": [{"count": count(process_probes)}], } if { @@ -645,7 +645,7 @@ names_a_message_source(stage) if { test_a_commit_with_no_message_source_is_refused if { some v in violation with input as {"call": {"command": "git commit"}} - v.rule == "commit-names-no-message-source" + v.rule == "commit write missing" } test_a_commit_that_names_one_is_left_alone if { @@ -654,7 +654,7 @@ test_a_commit_that_names_one_is_left_alone if { test_a_later_element_is_judged_too if { some v in violation with input as {"call": {"command": "cd /tmp && git commit"}} - v.rule == "commit-names-no-message-source" + v.rule == "commit write missing" } test_another_tool_is_not_judged if { @@ -672,7 +672,7 @@ test_a_short_cluster_names_a_message_source if { # nothing about the change. test_a_non_cluster_carrying_m_is_not_a_message_source if { some v in violation with input as {"call": {"command": "git commit -x=mfoo"}} - v.rule == "commit-names-no-message-source" + v.rule == "commit write missing" } # --------------------------------------------------------------------------- @@ -718,7 +718,7 @@ test_a_commit_whose_heredoc_binds_to_a_later_element_is_refused if { seg(["mise", "run", "land", "<<'EOF'"], null, true), ], }} - v.rule == "unsatisfiable-commit" + v.rule == "commit bind missing" } # THE DISCRIMINATING ALLOW, and it is the same two words in the same order — @@ -755,7 +755,7 @@ test_the_long_flag_spelling_is_judged_too if { "run-in-background": null, "segments": [seg(["git", "commit", "--file=-"], null, false)], }} - v.rule == "unsatisfiable-commit" + v.rule == "commit bind missing" } test_a_foreground_sleep_is_refused if { @@ -764,7 +764,7 @@ test_a_foreground_sleep_is_refused if { "run-in-background": null, "segments": [seg(["sleep", "90"], null, false)], }} - v.rule == "foreground-sleep" + v.rule == "sleep run blocked" } test_a_sleep_in_a_later_segment_is_refused_too if { @@ -777,7 +777,7 @@ test_a_sleep_in_a_later_segment_is_refused_too if { seg(["git", "log"], null, false), ], }} - v.rule == "foreground-sleep" + v.rule == "sleep run blocked" } test_a_backgrounded_bare_sleep_is_a_timer if { @@ -789,7 +789,7 @@ test_a_backgrounded_bare_sleep_is_a_timer if { seg(["tail", "-6", "land.log"], null, false), ], }} - v.rule == "background-timer" + v.rule == "timer run refused" } # THE ALLOW THAT MATTERS. This is the form both refusals recommend, and denying @@ -890,7 +890,7 @@ test_a_foreground_wait_on_a_condition_is_refused if { inner(["sleep", "1"], "until", "body", null), ], }} - v.rule == "foreground-sleep" + v.rule == "sleep run blocked" } # A `for` LOOP IS A TIMER: it counts iterations rather than testing a condition, @@ -901,7 +901,7 @@ test_a_backgrounded_counting_loop_is_a_timer if { "run-in-background": true, "segments": [inner(["sleep", "10"], "for", "body", null)], }} - v.rule == "background-timer" + v.rule == "timer run refused" } # The exemption's other reachable shape: a bare sleep and a loop keyword in one diff --git a/policy/sbom-inventory.rego b/policy/sbom-inventory.rego index 1a82ec6f8..d961ebed1 100644 --- a/policy/sbom-inventory.rego +++ b/policy/sbom-inventory.rego @@ -49,25 +49,25 @@ package batten.sbom_inventory import rego.v1 -rules contains "sbom-empty" +rules contains "manifest list empty" -rules contains "sbom-unrecorded" +rules contains "manifest file missing" -rules contains "sbom-package-drift" +rules contains "manifest count other" -rules contains "sbom-unstable" +rules contains "manifest mint twice" -rules contains "sbom-components-inflated" +rules contains "manifest count ahead" -rules contains "sbom-supplier-unset" +rules contains "manifest own missing" -rules contains "sbom-copyright-unenriched" +rules contains "manifest own unnamed" -rules contains "sbom-license-unenriched" +rules contains "manifest grant missing" -rules contains "sbom-action-unenriched" +rules contains "adapter own missing" -rules contains "sbom-action-unmapped" +rules contains "pin table missing" # The lockfile the cargo count is stated against, and the table every SHA-pinned # action must appear in. Both are committed text this row declares as @@ -97,7 +97,7 @@ count_of(key) := value if { # missed would otherwise pass every equality below trivially: two empty documents # agree, and an empty count matches an empty count. violation contains { - "rule": "sbom-empty", + "rule": "manifest list empty", "verdict": "tool read broken", "subjects": [{"count": count_of(format)}], } if { @@ -109,7 +109,7 @@ violation contains { # input. Told apart from ABSENT by `is_object` plus the count — an id nothing # recorded never binds `scan` at all. violation contains { - "rule": "sbom-unrecorded", + "rule": "manifest file missing", "verdict": "tool read broken", "subjects": [{"artifact": "sbom"}], } if { @@ -122,7 +122,7 @@ violation contains { # SPDX, a fresh serial number and timestamp in CycloneDX. This is what makes the # published document a function of the source rather than of when it was cut. violation contains { - "rule": "sbom-unstable", + "rule": "manifest mint twice", "verdict": "tool read broken", "subjects": [{"artifact": format}], } if { @@ -134,7 +134,7 @@ violation contains { # inventory: the subject is what the component counts are measured against, so # without it every one of them is taken over the wrong set. violation contains { - "rule": "sbom-unrecorded", + "rule": "manifest file missing", "verdict": "tool read broken", "subjects": [{"artifact": "describes"}], } if { @@ -167,7 +167,7 @@ declared := count([line | # Each format is counted separately because they render purls differently, so a # regression in one renderer is invisible to a gate that only ever reads the other. violation contains { - "rule": "sbom-package-drift", + "rule": "manifest count other", "verdict": "manifest count wrong", "subjects": [{"path": lockfile}, {"count": count_of(format)}], } if { @@ -192,7 +192,7 @@ violation contains { # property of the DOCUMENT rather than of the normaliser — a cataloger that starts # emitting a new inflated shape is caught without anyone having predicted which. violation contains { - "rule": "sbom-components-inflated", + "rule": "manifest count ahead", "verdict": "manifest count wrong", "subjects": [{"count": count_of("entries")}], } if { @@ -200,7 +200,7 @@ violation contains { } violation contains { - "rule": "sbom-components-inflated", + "rule": "manifest count ahead", "verdict": "manifest count wrong", "subjects": [{"count": count_of(shape)}], } if { @@ -221,7 +221,7 @@ violation contains { # entry is a personal name and often an email address, so the finding carries # counts and never a value. violation contains { - "rule": "sbom-supplier-unset", + "rule": "manifest own missing", "verdict": "manifest state missing", "subjects": [{"count": count_of(field)}], } if { @@ -239,7 +239,7 @@ violation contains { # This field needs pointer-only more than any other: a copyright statement is a # personal name, so echoing the value would publish names into every CI log. violation contains { - "rule": "sbom-copyright-unenriched", + "rule": "manifest own unnamed", "verdict": "manifest state missing", "subjects": [{"count": count_of("copyright-unset")}], } if { @@ -254,7 +254,7 @@ violation contains { # field rather than a missing one — worse than an honest NOASSERTION, in a field # whose whole purpose is to be parsed. violation contains { - "rule": "sbom-license-unenriched", + "rule": "manifest grant missing", "verdict": "manifest state missing", "subjects": [{"count": count_of(field)}], } if { @@ -264,7 +264,7 @@ violation contains { # Every `pkg:github` component carries both a license and a copyright. violation contains { - "rule": "sbom-action-unenriched", + "rule": "adapter own missing", "verdict": "manifest state missing", "subjects": [{"count": count_of("action-unset")}], } if { @@ -323,7 +323,7 @@ mapped contains line if { # Matched on repo AND sha together, because a table row whose sha is stale is # exactly the drift. violation contains { - "rule": "sbom-action-unmapped", + "rule": "pin table missing", "verdict": "pin table missing", "subjects": [{"path": actions_table}, {"count": count(unmapped)}], } if { @@ -344,7 +344,7 @@ unmapped contains line if { # belongs in `input.tree.missing`, and a module that iterates only what it could # read reports green over a file it never opened. violation contains { - "rule": "sbom-unrecorded", + "rule": "manifest file missing", "verdict": "tool read broken", "subjects": [{"path": path}], } if { @@ -425,17 +425,17 @@ test_an_empty_catalog_is_not_also_reported_as_drift if { test_two_scans_that_disagree_are_refused if { some v in violation with input as tree(object.union(clean, {"spdx-stable": "no"})) - v.rule == "sbom-unstable" + v.rule == "manifest mint twice" } test_an_inflated_component_set_is_refused if { some v in violation with input as tree(object.union(clean, {"distinct": "1"})) - v.rule == "sbom-components-inflated" + v.rule == "manifest count ahead" } test_a_pathlike_component_is_refused if { some v in violation with input as tree(object.union(clean, {"pathlike": "1"})) - v.rule == "sbom-components-inflated" + v.rule == "manifest count ahead" } test_a_document_describing_nothing_is_could_not_look if { @@ -445,28 +445,28 @@ test_a_document_describing_nothing_is_could_not_look if { test_an_unset_supplier_is_refused if { some v in violation with input as tree(object.union(clean, {"nosupplier": "4"})) - v.rule == "sbom-supplier-unset" + v.rule == "manifest own missing" } # THE AGREEMENT HALF, which a supplier count alone cannot see. test_an_originator_disagreeing_with_the_manifest_is_refused if { some v in violation with input as tree(object.union(clean, {"originator-disagrees": "1"})) - v.rule == "sbom-supplier-unset" + v.rule == "manifest own missing" } test_an_unset_copyright_is_refused if { some v in violation with input as tree(object.union(clean, {"copyright-unset": "7"})) - v.rule == "sbom-copyright-unenriched" + v.rule == "manifest own unnamed" } test_a_slash_form_license_is_refused if { some v in violation with input as tree(object.union(clean, {"license-slashed": "1"})) - v.rule == "sbom-license-unenriched" + v.rule == "manifest grant missing" } test_an_unenriched_action_is_refused if { some v in violation with input as tree(object.union(clean, {"action-unset": "2"})) - v.rule == "sbom-action-unenriched" + v.rule == "adapter own missing" } # --- the pinned actions, over committed text rather than a record -------------- @@ -494,7 +494,7 @@ test_a_pin_the_table_declares_is_clean if { # line, and the gate fires rather than degrading the document silently. test_a_pin_with_no_table_row_is_refused if { some v in violation with input as workflows([pin], []) - v.rule == "sbom-action-unmapped" + v.rule == "pin table missing" } # A STALE SHA IS THE DRIFT, so a row naming the same repository at a different @@ -504,7 +504,7 @@ test_a_row_naming_a_different_sha_does_not_map_the_pin if { [pin], ["actions/checkout@0000000000000000000000000000000000000000\tMIT\tGitHub"], ) - v.rule == "sbom-action-unmapped" + v.rule == "pin table missing" } # ANTI-VACUITY: a workflow line that is not a SHA-pinned `uses:` is not a pin, so @@ -552,7 +552,7 @@ test_an_unrecorded_scan_is_not_refused if { # count above pass over an absent key. test_a_recorded_but_empty_scan_is_refused if { some v in violation with input as recorded({}) - v.rule == "sbom-unrecorded" + v.rule == "manifest file missing" } # COULD-NOT-LOOK, and without the `is_object` guard this case would fault rather diff --git a/policy/shell-retirement.rego b/policy/shell-retirement.rego index 0c3167650..b770bd7d2 100644 --- a/policy/shell-retirement.rego +++ b/policy/shell-retirement.rego @@ -127,7 +127,7 @@ package batten.shell_retirement import rego.v1 -rules contains "shell-rule-retired" +rules contains "shell retire other" # --------------------------------------------------------------------------- # The changed-file set, and the could-not-look channel. @@ -225,7 +225,7 @@ governed_when_deleted(path) if is_bats(path) # --------------------------------------------------------------------------- violation contains { - "rule": "shell-rule-retired", + "rule": "shell retire other", "verdict": "shell add refused", "subjects": [{"path": path}], } if { @@ -275,7 +275,7 @@ declares_it_stays_bash(path) if { # --------------------------------------------------------------------------- violation contains { - "rule": "shell-rule-retired", + "rule": "shell retire other", "verdict": "shell edit refused", "subjects": [{"path": path}], } if { @@ -1071,7 +1071,7 @@ truncates_a_retired_reference(line, removed) if { # --------------------------------------------------------------------------- violation contains { - "rule": "shell-rule-retired", + "rule": "shell retire other", "verdict": "shell retire missing", "subjects": [{"path": path}], } if { @@ -1081,7 +1081,7 @@ violation contains { } violation contains { - "rule": "shell-rule-retired", + "rule": "shell retire other", "verdict": "shell retire unclear", "subjects": [{"path": path}], } if { @@ -1105,7 +1105,7 @@ violation contains { # nothing would be the defect rather than the deliverable. So the arm that could # be waived is, and the arm that carries the coverage is not. violation contains { - "rule": "shell-rule-retired", + "rule": "shell retire other", "verdict": "shell port missing", "subjects": [{"path": path}], } if { @@ -1118,7 +1118,7 @@ violation contains { } violation contains { - "rule": "shell-rule-retired", + "rule": "shell retire other", "verdict": "test port missing", "subjects": [{"path": path}], } if { @@ -1144,7 +1144,7 @@ violation contains { # consumer module or a preset is already unambiguous, so demanding a field of it # would be a refusal that teaches nothing. violation contains { - "rule": "shell-rule-retired", + "rule": "shell retire other", "verdict": "shell port unnamed", "subjects": [{"path": path}], } if { @@ -1165,7 +1165,7 @@ violation contains { # blanket permission to delete governed files, which is the thing the module exists # to refuse. violation contains { - "rule": "shell-rule-retired", + "rule": "shell retire other", "verdict": "shell retire never", "subjects": [{"path": path}], } if { @@ -1196,7 +1196,7 @@ violation contains { # so naming a successor can never trip this, and naming a shell program or a bats # suite that this delta does not retire always does. violation contains { - "rule": "shell-rule-retired", + "rule": "shell retire other", "verdict": "program retire never", "subjects": [{"path": path}, {"path": subject}], } if { @@ -1261,7 +1261,7 @@ named_and_alive(path) := subjects if { # imitates. That is the shape a fifth arm could have been, and this is what stops # it being that. violation contains { - "rule": "shell-rule-retired", + "rule": "shell retire other", "verdict": "suite port unnamed", "subjects": [{"path": path}], } if { @@ -1279,7 +1279,7 @@ violation contains { # Admitting it under both markers would let the ledger record one event in two # vocabularies, which is the drift every seam in this module is written against. violation contains { - "rule": "shell-rule-retired", + "rule": "shell retire other", "verdict": "suite port dead", "subjects": [{"path": path}, {"path": subject}], } if { @@ -1309,7 +1309,7 @@ violation contains { # program alive and untested. Each owes its own row, and this refuses until one # arrives. violation contains { - "rule": "shell-rule-retired", + "rule": "shell retire other", "verdict": "suite port held", "subjects": [{"path": path}, {"path": subject}], } if { @@ -1324,7 +1324,7 @@ violation contains { # It names no successor, so the reason is the only thing a reader can check the # claim against. An arm with neither is a file deleted with a marker on it. violation contains { - "rule": "shell-rule-retired", + "rule": "shell retire other", "verdict": "shell retire empty", "subjects": [{"path": path}], } if { @@ -2434,7 +2434,7 @@ test_a_bats_binding_outside_the_suite_directory_is_not_a_retired_reference if { "crates/batten/tests/old_gate.rs": ["// carried: mise-tasks/old-gate.sh policy/old-gate.rego crates/batten/tests/old_gate.rs runs:mise+run+old-gate"], }, }} - v.rule == "shell-rule-retired" + v.rule == "shell retire other" } # ANTI-VACUITY: the invocation must be one the LEDGER declares. Without this the diff --git a/policy/shell-write-advisory.rego b/policy/shell-write-advisory.rego index f73ac46a8..cbc1ebaa6 100644 --- a/policy/shell-write-advisory.rego +++ b/policy/shell-write-advisory.rego @@ -69,7 +69,7 @@ # - input: schema["policy-call.schema"] package batten.shell_write_advisory -rules contains "shell-write-at-the-edit" +rules contains "shell edit early" # The governed set — RESTATED, and that is a defect carrying a mechanism rather # than a preference. @@ -119,7 +119,7 @@ is_bats(path) if { # conjunct already fails. That is why this advisory cannot impede the one # disposition the tree gate admits. violation contains { - "rule": "shell-write-at-the-edit", + "rule": "shell edit early", "verdict": "shell edit early", "subjects": [{"path": path}], } if { diff --git a/policy/test-targets.rego b/policy/test-targets.rego index 6d94feebd..871f58778 100644 --- a/policy/test-targets.rego +++ b/policy/test-targets.rego @@ -100,7 +100,7 @@ package batten import rego.v1 -rules contains "test-target-added" +rules contains "test place duplicate" # The branch's own diff, BOUND THROUGH AN OBJECT GUARD because `null` is not # `undefined` (review of #848). @@ -127,7 +127,7 @@ delta := d if { # undefined make `not` hold in Rego, so the bare spelling would be DEAD for # exactly the `null` this arm exists for. violation contains { - "rule": "test-target-added", + "rule": "test place duplicate", "verdict": "diff read absent", "subjects": [{"path": "batten.toml"}], } if { @@ -183,7 +183,7 @@ added_target contains path if { } violation contains { - "rule": "test-target-added", + "rule": "test place duplicate", "verdict": "test add refused", "subjects": [{"path": path}], } if { From 8a1ba5600153f0761b02e1b66ac262cee1f9ad2f Mon Sep 17 00:00:00 2001 From: Alec Wenzowski Date: Wed, 9 Sep 2026 16:08:28 +0000 Subject: [PATCH 08/23] fix(policy): carry the finding-id migration to every surface that names one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fallout pass, and a dereference the row asked for and the tree did not have. `policy rule` NOW RESOLVES A FINDING ID. §2 lists "a module's `"rule":`" and "the argument of `policy rule`" as the same grammar, but the verb resolved `[[rule]]` rows only — and a `policy` finding is emitted under the MODULE's id, `test add duplicate`, not the row `test fix duplicate` that binds it. So the name a reader actually sees on the line dereferenced to nothing, which is the opposite of this row's premise that an id is a live pointer. It falls back to the owning row; the row id is tried first, because where the two coincide the row is the more specific answer. 225 occurrences outside the modules — 172 in tests, 50 in `src`, and prose in `mise-tasks/sbom.sh` that names two findings as pointers. FOUR THINGS THAT LOOK LIKE A FINDING ID AND ARE NOT, each caught by a test rather than by care, and each the same shape as the `perf-assert` slip in the row-id pass: - `count = "agent-turn-run"` is a RECORDER VARIANT. Renaming it made the fixture's config unparseable — `unknown variant`, and the enum still lists the old name because the enum was right. - `[[fact]] id = "agent-turn-run"` and `input.facts.extracted["agent-turn-run"]` are the extracted fact's KEY, paired across the fixture and its module. The module had it right; the fixture lost the pairing. - `repo("landing-roster-unguarded")` and `repo("nextest-slow")` are fixture DIRECTORY names. Renamed, they become paths with spaces in them. - `policy/.rego` is a filename. And my own fixture was caught by the gate this row adds: `collapse-probe.rego` declared a two-word finding id. The gate was right, so the fixture changed rather than the gate — a fixture exempt from the rule it exercises proves nothing. Refs: CLOUD-1638 --- crates/batten/src/claim.rs | 2 +- crates/batten/src/commit.rs | 2 +- crates/batten/src/hook.rs | 2 +- crates/batten/src/land.rs | 16 +++--- crates/batten/src/lib.rs | 46 ++++++++++++---- crates/batten/src/lint.rs | 2 +- crates/batten/src/policy.rs | 4 +- crates/batten/src/record.rs | 4 +- crates/batten/src/recorder.rs | 2 +- crates/batten/src/surface.rs | 2 +- crates/batten/tests/it/admission.rs | 2 +- crates/batten/tests/it/admission_narrowing.rs | 6 +-- crates/batten/tests/it/agentic_record.rs | 18 +++---- crates/batten/tests/it/board_record.rs | 2 +- crates/batten/tests/it/cfg_gated_test.rs | 2 +- crates/batten/tests/it/ci_parity.rs | 2 +- crates/batten/tests/it/cli.rs | 8 +-- .../batten/tests/it/commit_arm_sequencing.rs | 2 +- crates/batten/tests/it/filed_here.rs | 14 ++--- crates/batten/tests/it/fixture_forks.rs | 2 +- crates/batten/tests/it/forced_push.rs | 2 +- crates/batten/tests/it/hk_contract.rs | 2 +- crates/batten/tests/it/land.rs | 8 +-- crates/batten/tests/it/landing_roster.rs | 4 +- crates/batten/tests/it/lease_record.rs | 2 +- crates/batten/tests/it/lock_complete.rs | 24 ++++----- crates/batten/tests/it/nextest_slow.rs | 4 +- crates/batten/tests/it/obligations_bound.rs | 4 +- crates/batten/tests/it/perf_assert.rs | 14 ++--- crates/batten/tests/it/pinned_programs.rs | 2 +- crates/batten/tests/it/plan_complete.rs | 14 ++--- crates/batten/tests/it/policy_presets.rs | 30 +++++------ crates/batten/tests/it/policy_test_suite.rs | 22 ++++---- crates/batten/tests/it/preset_segments.rs | 4 +- crates/batten/tests/it/record_closes.rs | 2 +- crates/batten/tests/it/remedy_authorship.rs | 4 +- crates/batten/tests/it/repetition.rs | 6 +-- crates/batten/tests/it/review_answered.rs | 4 +- crates/batten/tests/it/rules_drift.rs | 26 ++++----- crates/batten/tests/it/run_shape.rs | 10 ++-- crates/batten/tests/it/sbom_inventory.rs | 18 +++---- crates/batten/tests/it/shell_retirement.rs | 54 +++++++++---------- crates/batten/tests/it/sinks.rs | 2 +- crates/batten/tests/it/staged_facts.rs | 4 +- crates/batten/tests/it/startup.rs | 2 +- crates/batten/tests/it/startup_bootstrap.rs | 2 +- crates/batten/tests/it/test_targets.rs | 2 +- crates/batten/tests/policy_modules.rs | 8 +-- mise-tasks/sbom.sh | 4 +- 49 files changed, 225 insertions(+), 199 deletions(-) diff --git a/crates/batten/src/claim.rs b/crates/batten/src/claim.rs index 2692330e8..113e68ac2 100644 --- a/crates/batten/src/claim.rs +++ b/crates/batten/src/claim.rs @@ -713,7 +713,7 @@ fn receipt_on_the_same_base(receipt: &Path, base: Option<&str>) -> Option` still named that -/// PR's keys, and `filed-over-own-diff`'s exemption was evaluated against them. +/// PR's keys, and `issue file same`'s exemption was evaluated against them. /// /// **The CLAIM rather than the base, and the difference is what makes it usable.** /// A base moves on every rebase, and `land` rebases every lap — keying a record on diff --git a/crates/batten/src/commit.rs b/crates/batten/src/commit.rs index e40609ca1..bafa7c945 100644 --- a/crates/batten/src/commit.rs +++ b/crates/batten/src/commit.rs @@ -398,7 +398,7 @@ pub fn judge_arm_sequencing(sequences: &[ArmSequence]) -> Vec { // // Measured the hard way: the row was written HERE first, with only a prose // mention of the slug in the suite. `declares_slug` matches a line PREFIX, a -// mention inside a doc comment is not one, and `obligation-unbound` fired over a +// mention inside a doc comment is not one, and `test name undefined` fired over a // promise that was in fact kept — which is the gate being right about the // binding and me being wrong about where it reads. diff --git a/crates/batten/src/hook.rs b/crates/batten/src/hook.rs index 285d47115..c0ca09b5a 100644 --- a/crates/batten/src/hook.rs +++ b/crates/batten/src/hook.rs @@ -9332,7 +9332,7 @@ const SHELL_GRAMMAR: [&str; 9] = [ /// repository's committed config: /// /// * `(git push origin main --force)` — allowed, because `arguments` ended -/// `--force)` and `no-force-push` compares for equality. The same command with +/// `--force)` and `trunk push forced` compares for equality. The same command with /// the flag written earlier denied, so the bypass was a matter of word order. /// * `(rm batten.toml)` — allowed, because the operand was `batten.toml)` and no /// protected path matches it. That one predates this row and is the same diff --git a/crates/batten/src/land.rs b/crates/batten/src/land.rs index dbce97b2e..134472001 100644 --- a/crates/batten/src/land.rs +++ b/crates/batten/src/land.rs @@ -11,9 +11,9 @@ //! # It DECIDES nothing, and that separation is the whole design //! //! Two policy questions arise in a lap and neither is answered here. *May a lap -//! continue past a conflicted replay?* is `rebase-conflict-stops-the-lap`'s. +//! continue past a conflicted replay?* is `replay halt conflict`'s. //! *Which answer may a lap act on when its wait raced two questions?* is -//! `lap-waits-on-one-answer`'s. Both are `landing-loop` preset predicates over +//! `wait read both`'s. Both are `landing-loop` preset predicates over //! the records this module writes, which is CLOUD-1148's thesis read forwards: //! the mechanics move to the engine and the decisions become Rego. So nothing //! here branches on "should we stop" — it does the work, writes down what it @@ -80,7 +80,7 @@ impl Replay { /// The record line this outcome writes. /// /// Four columns, `rebase `, which is the layout - /// `rebase-conflict-stops-the-lap` reads and the reason it is stated in both + /// `replay halt conflict` reads and the reason it is stated in both /// places rather than derived: the module is vendored into every consumer's /// binary and this writer is one consumer of it, so neither can be the /// other's authority. `crates/batten/tests/it/land.rs` holds them together. @@ -105,7 +105,7 @@ impl Replay { // this requires `count(columns) == 4`, so `docs/my notes.md` made // FIVE and the line was dropped from `replays` entirely — // `last_replay` fell back to the previous lap's clean line and - // `rebase-conflict-stops-the-lap` reported clean over the lap's + // `replay halt conflict` reported clean over the lap's // one human stop. A dropped line and a clean tree are // byte-identical on the decision surface, which is the shape this // repository refuses everywhere. @@ -412,7 +412,7 @@ pub const LAP_RECORD: &str = "lap"; /// a run by hand. /// /// The arms are named rather than numbered because the record is what -/// `lap-waits-on-one-answer` reads, and a reviewer chasing a refusal needs to +/// `wait read both` reads, and a reviewer chasing a refusal needs to /// know WHICH question answered, not that some arm did. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Arm { @@ -456,7 +456,7 @@ impl Answered { /// The record line this answer writes. /// /// Four columns, `wait `, which is the layout - /// `lap-waits-on-one-answer` reads. Stated in both places rather than derived + /// `wait read both` reads. Stated in both places rather than derived /// for the reason its sibling gives: the module is vendored into every /// consumer's binary and this writer is one consumer of it, so neither can be /// the other's authority. @@ -694,7 +694,7 @@ pub fn wait( /// abandoned unread — and gets the pair back. The alternative, letting the caller /// assemble a `Vec`, is what makes recording only the winner writable, and a /// record with a winner and no loser is byte-identical to what a lap that read -/// BOTH sides produces. `lap-waits-on-one-answer` would then have nothing to +/// BOTH sides produces. `wait read both` would then have nothing to /// tell the two apart, which is the whole property. /// /// Both arms always appear, so the count of ANSWERING arms is what varies and @@ -2223,7 +2223,7 @@ pub struct Abandoned { /// The constructor is the whole mechanism: a caller reaching for the check name /// has to write [`FanIn::from_workflow_path`] over it, which is a lie a reader /// can see rather than an argument position that accepts anything. `ci-parity`'s -/// `fan-in-is-wired` binds on that constructor appearing on the same line as the +/// `job wire missing` binds on that constructor appearing on the same line as the /// declaration read, so the module and the compiler hold the same join — the /// module alone could not, because two independent line matches are satisfied by /// an unrelated read plus a wrong argument (found in review of #848). diff --git a/crates/batten/src/lib.rs b/crates/batten/src/lib.rs index 3ca2493f1..288f68c84 100644 --- a/crates/batten/src/lib.rs +++ b/crates/batten/src/lib.rs @@ -5537,19 +5537,45 @@ fn run_policy_rule( out: &mut dyn Write, ) -> Result { let config = resolve::resolve(Path::new("."), overrides)?; - let Some(rule) = config.rules.iter().find(|rule| rule.id == id) else { + // BOTH NAMES A LINE CAN CARRY (CLOUD-1638). A `policy` row's finding is + // emitted under the MODULE's `"rule":` — `test add duplicate`, not the row + // `test fix duplicate` that binds the module — so resolving only `[[rule]]` + // ids left the id a reader actually sees pointing at nothing. Falling back + // to the owning row answers the question they asked: what refused me, and + // what does its row say to do. The row id is tried FIRST, because where the + // two coincide the row is the more specific answer. + let owner = config + .rules + .iter() + .find(|rule| rule.id == id) + .or_else(|| owning_row(&config, id)); + let Some(rule) = owner else { // Named, and the id is the caller's own argument rather than anything // read out of the tree. A list of what IS declared would be every row on // stderr; the count plus the sibling verb is the pointer-shaped answer. return Err(error::UsageError::raise(format!( - "no `[[rule]]` row declares `{id}`; this config declares {} rule(s). A three-word \ - name is a CLASS rather than a row — resolve it with `batten policy explain`", + "no `[[rule]]` row and no module declares `{id}`; this config declares {} rule(s). \ + A three-word name may be a CLASS instead — resolve it with `batten policy explain`", config.rules.len(), ))); }; explain_rule(rule, &config.facts, json, out) } +/// The `[[rule]]` row whose module declares this finding id (CLOUD-1638). +/// +/// Read off the module SOURCE for [`policy::finding_ids`]' reasons — the +/// compiled set is not a clean list of finding ids, and this verb must answer +/// without standing up an engine. +fn owning_row<'a>(config: &'a resolve::Resolved, id: &str) -> Option<&'a rules::Rule> { + config.rules.iter().find(|rule| { + rule.module.as_deref().is_some_and(|module| { + std::fs::read_to_string(module) + .is_ok_and(|text| policy::finding_ids(&text).contains(id)) + }) + }) +} + /// Judge this session's hook output against its declared budget (CLOUD-417). /// /// # The measurement runs whether or not a ceiling is declared @@ -5826,7 +5852,7 @@ fn admission_anchor( // A POLICY PREDICATE IS NOT A ROW ID, and narrowing as though it were made // every policy admission a silent no-op (CLOUD-1087, CLOUD-1125). // - // `filed-here` publishes `filed-over-own-diff`; the refusal names the + // `filed-here` publishes `issue file same`; the refusal names the // PREDICATE, so that is what `--rule` carries here. Filtering on // `declared.id == rule` therefore selected NOTHING, the scan below produced // no finding, the match count was `0`, and the mint took the `head()` @@ -6671,7 +6697,7 @@ fn run_lease( /// /// A conflicted replay is `2`. That is the policy verdict everywhere /// (non-negotiable rule 5) and it is what a conflict is: the lap may not -/// continue, decided by `rebase-conflict-stops-the-lap` over the record this +/// continue, decided by `replay halt conflict` over the record this /// writes rather than by an arm here. A clone this cannot resolve a remote or a /// branch for is `3` — could-not-look, never a false `2`, because a lap that /// could not be attempted has not judged the branch. @@ -7563,7 +7589,7 @@ fn unwind_lap( // An unset one cancels NOTHING rather than guessing; // `land::abandon` holds that guard. // THE CONSTRUCTOR AND THE DECLARATION ARE ONE EXPRESSION, and - // that adjacency is what `ci-parity` binds on. `fan-in-is-wired` + // that adjacency is what `ci-parity` binds on. `job wire missing` // used to ask two independent questions of this file — does // something read the declaration, does something call // `land::abandon` — which an unrelated read plus a wrong argument @@ -9827,7 +9853,7 @@ fn verdict_for(repo: &str, sha: &str) -> Option { /// /// [`land::record_wait`] takes both in one call precisely so this cannot write /// only the winner: a record with one answer and no loser is what a lap that -/// read BOTH sides also produces, and `lap-waits-on-one-answer` would then have +/// read BOTH sides also produces, and `wait read both` would then have /// nothing to tell them apart. fn run_land_wait( root: &Path, @@ -14657,7 +14683,7 @@ fn stop_nudges(overrides: &Overrides, envelope: &hook::Envelope) -> Option Option { /// /// # The pointer is the PATH, and that is a stated difference /// -/// The retired shell emitted ` filed-over-own-diff ` and suppressed on +/// The retired shell emitted ` issue file same ` and suppressed on /// the id. A `Finding` carries its first path-bearing subject as its pointer and /// the row's id travels as an ordered subject the engine does not project onto /// the struct, so the nudge names the path and the suppression key is the path. @@ -15228,7 +15254,7 @@ fn filed_here_pointers( /// The row the two nudge modes read, and the predicate whose findings rule 3 uses. const FILED_HERE_ROW: &str = "filed-here"; -const FILED_OVER_OWN_DIFF: &str = "filed-over-own-diff"; +const FILED_OVER_OWN_DIFF: &str = "issue file same"; /// The record the checklist enumerates. const BOARD_RECORD: &str = "board-writes"; diff --git a/crates/batten/src/lint.rs b/crates/batten/src/lint.rs index 9bddf29d2..7742d24ea 100644 --- a/crates/batten/src/lint.rs +++ b/crates/batten/src/lint.rs @@ -303,7 +303,7 @@ fn waiver_smells( // // `waiver::apply` filters FINDINGS, and a policy finding carries the // predicate id rather than the row's. Measured on this repository: - // `rule = "filed-over-own-diff"` suppressed and was refused here, + // `rule = "issue file same"` suppressed and was refused here, // while `rule = "filed-here"` was clean here and suppressed nothing — // so no value satisfied both halves, and the one this smell blessed // was the dead one. That is precisely the "exemption someone is diff --git a/crates/batten/src/policy.rs b/crates/batten/src/policy.rs index ebde97992..e90f76bab 100644 --- a/crates/batten/src/policy.rs +++ b/crates/batten/src/policy.rs @@ -468,7 +468,7 @@ impl Bundle { /// **The narrowing a mint needs, and the reason it is a function rather than a /// filter written at its one call site.** [`crate::admission`]'s anchor has to /// re-run the rule a refusal named in order to recover its fingerprint, and -/// `--rule` carries a PREDICATE id — `filed-here` publishes `filed-over-own-diff` +/// `--rule` carries a PREDICATE id — `filed-here` publishes `issue file same` /// — so a row-id match selects nothing and the mint silently binds the head /// (CLOUD-1087, CLOUD-1125). Widening from there to every `policy` row fixed that /// and cost 2m22s per mint, because "which KIND of row" is not "which row": @@ -1736,7 +1736,7 @@ fn check_finding_ids( /// because every one of them is written out — a computed id would be invisible /// here, and is also invisible to a reader of the module, which is the same /// objection. -fn finding_ids(text: &str) -> BTreeSet { +pub(crate) fn finding_ids(text: &str) -> BTreeSet { /// The first double-quoted run after `from`, if the line has one. fn quoted(line: &str, from: usize) -> Option<&str> { let rest = line.get(from..)?; diff --git a/crates/batten/src/record.rs b/crates/batten/src/record.rs index eff7efc05..dd3fabb1c 100644 --- a/crates/batten/src/record.rs +++ b/crates/batten/src/record.rs @@ -253,7 +253,7 @@ pub fn run_closes(overrides: &Overrides) -> Result { // PARTITIONED BY THE CLAIM, exactly as the reader partitions (CLOUD-1300), // and `pr-closes` is the record that defect was MEASURED on: after #810 // merged and its branch was reset onto the new trunk, this file still named - // that PR's keys and `filed-over-own-diff`'s exemption was evaluated against + // that PR's keys and `issue file same`'s exemption was evaluated against // them. A writer that skipped the partition while the reader applied it // would be the same staleness with an extra step — the reader would look // under the partitioned name, find nothing, and refuse where it used to @@ -308,7 +308,7 @@ fn claim_of(git_dir: &Path, branch: &str) -> Option { /// `lap` joins them for the same reason and with one difference worth stating: /// it is the only one of the three that is a HISTORY rather than a current /// state. `land::replay` appends a line per lap, and -/// `rebase-conflict-stops-the-lap` reads the last one — so a conflict resolved by +/// `replay halt conflict` reads the last one — so a conflict resolved by /// a later lap stops refusing, which a store keeping only the newest line could /// not express. pub const VERB_WRITTEN: &[&str] = &["claim", "plan", crate::land::LAP_RECORD]; diff --git a/crates/batten/src/recorder.rs b/crates/batten/src/recorder.rs index c1e0f59d2..e8ff95a4f 100644 --- a/crates/batten/src/recorder.rs +++ b/crates/batten/src/recorder.rs @@ -1172,7 +1172,7 @@ pub fn blocked_path(git_dir: &Path, branch: &str, claim: Option<&str>) -> std::p /// **PARTITIONED BY THE BRANCH'S CLAIM, NOT BY THE BRANCH ALONE (CLOUD-1300).** A /// branch name outlives the branch it described, so keying on the name alone let /// the next attempt read the previous one's lines as its own — measured, where a -/// `pr-closes` record still named a merged PR's keys and `filed-over-own-diff`'s +/// `pr-closes` record still named a merged PR's keys and `issue file same`'s /// exemption was evaluated against them. That direction is the dangerous one: it /// exempts silently, and nothing downstream re-checks. /// diff --git a/crates/batten/src/surface.rs b/crates/batten/src/surface.rs index 5c3680528..3bea51ec2 100644 --- a/crates/batten/src/surface.rs +++ b/crates/batten/src/surface.rs @@ -4720,7 +4720,7 @@ pub const SURFACE: &[CommandDecl] = &[ flags: &[], }, // The same argument one layer over, and here the envelope route is not merely - // per-harness — it is unreliable in the ordinary case. `filed-over-own-diff` + // per-harness — it is unreliable in the ordinary case. `issue file same` // exempts a row the PR CLOSES, and reads that from a `pr-closes` record the // `pr-body-closes` recorder mints from an observed `gh pr view --jq .body` // envelope. `land` fetches exactly that body and pipes it to diff --git a/crates/batten/tests/it/admission.rs b/crates/batten/tests/it/admission.rs index 65f77dbec..02285f48e 100644 --- a/crates/batten/tests/it/admission.rs +++ b/crates/batten/tests/it/admission.rs @@ -1246,7 +1246,7 @@ fn admits_fixture_with_predicate(name: &str) -> PathBuf { /// a mismatched anchor produces. /// /// Measured before the fix, on this repository: two admissions for -/// `filed-over-own-diff` and `filed-and-left-open` — both predicates of the +/// `issue file same` and `issue file held` — both predicates of the /// `issue file other` row — were issued, spent, committed, and honoured by neither /// gate. `batten-check` reported both findings unchanged afterwards. /// diff --git a/crates/batten/tests/it/admission_narrowing.rs b/crates/batten/tests/it/admission_narrowing.rs index 8c5b3388f..78eedc677 100644 --- a/crates/batten/tests/it/admission_narrowing.rs +++ b/crates/batten/tests/it/admission_narrowing.rs @@ -5,7 +5,7 @@ //! //! `admission_anchor` re-runs the rule a refusal named so it can recover that //! finding's fingerprint and bind the admission to it. `--rule` carries a -//! PREDICATE id — `issue file other` publishes `filed-over-own-diff` — so filtering +//! PREDICATE id — `issue file other` publishes `issue file same` — so filtering //! `declared.id == rule` selected nothing, the scan produced no finding, and the //! mint silently took the `head()` fallback: an admission answered, spent, and //! queried by nothing (CLOUD-1087, CLOUD-1125). @@ -183,7 +183,7 @@ fn the_fixture_bundles_actually_publish_their_predicates() { /// again and the 2m22s comes back with nothing to announce it. That is what this /// case refuses, and no fixture can. /// -/// The committed `filed-over-own-diff` is asserted beside it in the same +/// The committed `issue file same` is asserted beside it in the same /// function, because a repository whose bundles failed to load would give an /// empty set for the first assertion and pass it for exactly the wrong reason. #[test] @@ -214,7 +214,7 @@ fn the_committed_bundles_publish_no_engine_side_rule_name() { // ANTI-VACUITY FIRST, so the refusal below cannot pass over an empty set. assert!( - policy::publishers_of(&bundles, "filed-over-own-diff") + policy::publishers_of(&bundles, "issue file same") .into_iter() .eq(["issue file other"]), "the committed tree still publishes a predicate under a differently-named \ diff --git a/crates/batten/tests/it/agentic_record.rs b/crates/batten/tests/it/agentic_record.rs index e72eee61d..17048c3c9 100644 --- a/crates/batten/tests/it/agentic_record.rs +++ b/crates/batten/tests/it/agentic_record.rs @@ -224,7 +224,7 @@ fn a_trial_missing_a_required_key_is_reported() { ); let answer = findings(&dir); assert!( - answer.contains("agentic-record-incomplete"), + answer.contains("test declare partial"), "a trial naming no fixture cannot be rerun, so the row is incomplete:\n{answer}" ); } @@ -238,7 +238,7 @@ fn a_single_armed_trial_is_reported() { ); let answer = findings(&dir); assert!( - answer.contains("agentic-record-incomplete"), + answer.contains("test declare partial"), "a trial with no baseline arm is an anecdote with a run count:\n{answer}" ); } @@ -257,7 +257,7 @@ fn a_disposition_asserting_a_finding_without_a_result_is_reported() { ); let answer = findings(&dir); assert!( - answer.contains("agentic-finding-unsupported"), + answer.contains("test state early"), "a disposition asserting a finding owes a `[trial.result]`:\n{answer}" ); } @@ -289,7 +289,7 @@ fn a_disposition_the_method_record_does_not_declare_is_reported() { let dir = repo("invented", Some(&body), Some(&complete_method())); let answer = findings(&dir); assert!( - answer.contains("agentic-finding-unsupported"), + answer.contains("test state early"), "a disposition the method record does not declare means nothing:\n{answer}" ); } @@ -308,7 +308,7 @@ fn a_method_record_naming_no_unmeasured_dimension_is_reported() { ); let answer = findings(&dir); assert!( - answer.contains("agentic-record-incomplete"), + answer.contains("test declare partial"), "the method record owes the dimensions it deliberately does not measure:\n{answer}" ); } @@ -321,7 +321,7 @@ fn a_tree_with_no_records_at_all_is_silent() { let dir = repo("absent", None, None); let answer = findings(&dir); assert!( - answer.contains("agentic-record-unreadable"), + answer.contains("input read absent"), "a declared record that could not be read is reported, never assumed clean:\n{answer}" ); } @@ -335,7 +335,7 @@ fn an_unreadable_method_record_is_reported() { let dir = repo("no-method", Some(&complete_trial()), None); let answer = findings(&dir); assert!( - answer.contains("agentic-record-unreadable"), + answer.contains("input read absent"), "the method record is half the joint predicate; its absence is not a clean tree:\n{answer}" ); } @@ -419,7 +419,7 @@ fn replayed_findings( verdicts: &[batten::verdict::DeclaredVerdict], ) -> String { let row: batten::rules::Rule = serde_json::from_value(serde_json::json!({ - "id": "agentic-experiment-record", + "id": "fact file missing", "kind": "policy", "scope": "tree", "documents": [TRIALS, METHOD], @@ -502,7 +502,7 @@ fn replay_block(block: usize) { // without a second commit — and if that were ever untrue this assertion // would go to zero rather than quietly passing. write(&dir, TRIALS, &mutated); - if replayed_findings(&dir, &verdicts).contains("agentic-record-incomplete") { + if replayed_findings(&dir, &verdicts).contains("test declare partial") { fired += 1; } } diff --git a/crates/batten/tests/it/board_record.rs b/crates/batten/tests/it/board_record.rs index 9e8ad542d..160eaf3e8 100644 --- a/crates/batten/tests/it/board_record.rs +++ b/crates/batten/tests/it/board_record.rs @@ -741,7 +741,7 @@ fn pointer_never_payload_no_byte_of_the_description_reaches_the_record() { /// /// Measured before the fix, on this repository's own branch: after PR #810 merged /// and the branch was reset, `pr-closes.` still named that PR's keys, and -/// `filed-over-own-diff`'s exemption was evaluated against them. A row the +/// `issue file same`'s exemption was evaluated against them. A row the /// PREVIOUS PR closed would have been exempted on a PR that does not close it — /// silently, with nothing downstream to re-check. #[test] diff --git a/crates/batten/tests/it/cfg_gated_test.rs b/crates/batten/tests/it/cfg_gated_test.rs index a3b96903a..176e270eb 100644 --- a/crates/batten/tests/it/cfg_gated_test.rs +++ b/crates/batten/tests/it/cfg_gated_test.rs @@ -41,7 +41,7 @@ use batten::rules::{self, Rule}; /// `test cover missing`. The two differ, and the difference is load-bearing: an /// admission resolves its anchor by the FINDING's rule, so minting against the /// config id silently produces a `call:` anchor that suppresses nothing. -const GATED_ADDED: &str = "platform-gated-test-added"; +const GATED_ADDED: &str = "test cover unseen"; /// A fixture repository whose base commit carries `before` at /// `crates/batten/src/subject.rs` and whose working tree carries `after`. diff --git a/crates/batten/tests/it/ci_parity.rs b/crates/batten/tests/it/ci_parity.rs index 370b17ecc..4445b48e9 100644 --- a/crates/batten/tests/it/ci_parity.rs +++ b/crates/batten/tests/it/ci_parity.rs @@ -31,7 +31,7 @@ //! is that half's tier. //! //! Whether the foreign-runner cargo invocation still matches the task's own is -//! this row's, as `foreign-cargo-is-the-declared-spelling`. It reads +//! this row's, as `cargo spelling wrong`. It reads //! `test:cargo`'s body out of the manifest rather than out of `mise tasks info`, //! which no policy module can spawn for — and the two are the same bytes only //! while that task carries no template. `task read unread` is the arm diff --git a/crates/batten/tests/it/cli.rs b/crates/batten/tests/it/cli.rs index 3a5bee9d2..68b73e110 100644 --- a/crates/batten/tests/it/cli.rs +++ b/crates/batten/tests/it/cli.rs @@ -3767,16 +3767,16 @@ fn the_committed_policy_gates_ready_on_receipts_rather_than_banning_it() { // CLOUD-690's two tool-sourced siblings, each a receipt row over // one check, and the two module predicates that read what those // records found. The module rows belong here for the same reason - // the receipt rows do: `review-unanswered` refuses until the - // threads are answered and `review-absent` until a review exists, + // the receipt rows do: `review answer missing` refuses until the + // threads are answered and `review read absent` until a review exists, // so both are preconditions on the WORK and neither is a ban on // the command. Which one fires first is a property of the // checkout — measured, a head with a record carrying unresolved // threads reaches the module rather than any receipt row. "review answer partial", "review list unread", - "review-unanswered", - "review-absent", + "review answer missing", + "review read absent", ] .iter() .any(|row| stderr.contains(row)), diff --git a/crates/batten/tests/it/commit_arm_sequencing.rs b/crates/batten/tests/it/commit_arm_sequencing.rs index d72479ba7..64d017079 100644 --- a/crates/batten/tests/it/commit_arm_sequencing.rs +++ b/crates/batten/tests/it/commit_arm_sequencing.rs @@ -55,7 +55,7 @@ //! //! It is a block comment because the match is on a line PREFIX and Rust has no //! line comment that starts with `#`. Written first as a prose mention in this -//! header, it did not bind and `obligation-unbound` fired — correctly. +//! header, it did not bind and `test name undefined` fired — correctly. //! //! **What the row does NOT yet buy is the sweep, and saying so is the point.** //! `mutate`'s `Gate::name` resolves sources from a task name, a module stem or a diff --git a/crates/batten/tests/it/filed_here.rs b/crates/batten/tests/it/filed_here.rs index 5a2cadcc5..345fe9e2a 100644 --- a/crates/batten/tests/it/filed_here.rs +++ b/crates/batten/tests/it/filed_here.rs @@ -274,9 +274,9 @@ fn pointers(root: &Path) -> Vec { .collect() } -const UNREFINED: &str = "filed-unrefined"; -const OVER_DIFF: &str = "filed-over-own-diff"; -const LEFT_OPEN: &str = "filed-and-left-open"; +const UNREFINED: &str = "issue file unclear"; +const OVER_DIFF: &str = "issue file same"; +const LEFT_OPEN: &str = "issue file held"; // --------------------------------------------------------------------------- // The pass side first: without it every refusal below is satisfied by a module @@ -326,7 +326,7 @@ fn a_branch_name_with_a_slash_finds_its_record() { } // --------------------------------------------------------------------------- -// `filed-unrefined`. +// `issue file unclear`. // --------------------------------------------------------------------------- #[test] @@ -406,7 +406,7 @@ fn the_refusal_carries_the_id_and_no_prose_from_the_row() { } // --------------------------------------------------------------------------- -// `filed-over-own-diff`. +// `issue file same`. // --------------------------------------------------------------------------- #[test] @@ -424,7 +424,7 @@ fn a_row_naming_a_file_this_branch_is_changing_stops_the_lap() { } /// A path outside the diff is not a punt against it — for the PROXIMITY refusal, -/// which is the only one this case was ever about. `filed-and-left-open` takes it +/// which is the only one this case was ever about. `issue file held` takes it /// instead, and asserting the exact verdict rather than "not empty" is what makes /// the partition falsifiable from this tier. #[test] @@ -694,7 +694,7 @@ fn a_six_field_record_with_no_sec1_column_is_judged_exactly_as_before() { } // --------------------------------------------------------------------------- -// `filed-and-left-open` (CLOUD-1311). The set refusal: a row this branch put on +// `issue file held` (CLOUD-1311). The set refusal: a row this branch put on // the board that it is not landing. // // Its whole reason for existing is the class the two arms above cannot see — a diff --git a/crates/batten/tests/it/fixture_forks.rs b/crates/batten/tests/it/fixture_forks.rs index 9d289b991..0b47842a9 100644 --- a/crates/batten/tests/it/fixture_forks.rs +++ b/crates/batten/tests/it/fixture_forks.rs @@ -35,7 +35,7 @@ use std::path::{Path, PathBuf}; use batten::rules::{self, Rule}; /// The predicate id the module declares. -const FORK_ADDED: &str = "fixture-fork-added"; +const FORK_ADDED: &str = "test add duplicate"; /// A line that forks `git init`, in the short spelling. const FORKING: &str = r#" git_in(&dir, &["init", "-q"]);"#; diff --git a/crates/batten/tests/it/forced_push.rs b/crates/batten/tests/it/forced_push.rs index 8b36960b6..9bc513b74 100644 --- a/crates/batten/tests/it/forced_push.rs +++ b/crates/batten/tests/it/forced_push.rs @@ -16,7 +16,7 @@ //! //! # Why this row is only one flag //! -//! `trunk-based/no-force-push` already denies `--force` and `-f`, per segment. +//! `trunk-based/trunk push forced` already denies `--force` and `-f`, per segment. //! It excludes `--force-with-lease` on a stated argument: it "refuses when the //! remote moved". That is true when the sibling's push arrived AFTER your last //! fetch — the remote-tracking ref is stale, the comparison differs, the push is diff --git a/crates/batten/tests/it/hk_contract.rs b/crates/batten/tests/it/hk_contract.rs index 179781cc5..c4795f8a3 100644 --- a/crates/batten/tests/it/hk_contract.rs +++ b/crates/batten/tests/it/hk_contract.rs @@ -265,7 +265,7 @@ fn the_refusal_is_a_declared_class_with_a_route() { fn a_drifted_contract_exits_two_and_names_the_class() { // `Fixture` rather than a `tempfile`: it is this suite's own scratch // convention and needs no dev-dependency the binary does not link. - let scratch = common::Fixture::new("gate table other"); + let scratch = common::Fixture::new("hk-contract-drift"); let root = scratch.path(); fs::copy(common::at_root("hk.pkl"), root.join("hk.pkl")).expect("the runner config copies"); fs::create_dir_all(root.join("contracts")).expect("the artifact directory"); diff --git a/crates/batten/tests/it/land.rs b/crates/batten/tests/it/land.rs index aad930787..508cab32b 100644 --- a/crates/batten/tests/it/land.rs +++ b/crates/batten/tests/it/land.rs @@ -3,7 +3,7 @@ //! //! # Why this tier exists and what the module's own suite cannot do //! -//! `rebase-conflict-stops-the-lap` carries a load-time tier that pins its +//! `replay halt conflict` carries a load-time tier that pins its //! predicate, and every case in it supplies the record with `with input as`. That //! fabricates the very shape the engine may be unable to produce — here, the //! COLUMN LAYOUT and the STORE the whole family turns on — so the module's suite @@ -109,7 +109,7 @@ fn a_conflicted_lap_is_refused_and_a_clean_one_is_not() { "a conflicted lap is the policy verdict: {err}{out}" ); assert!( - format!("{out}{err}").contains("rebase-conflict-stops-the-lap"), + format!("{out}{err}").contains("replay halt conflict"), "the finding names its own predicate, got {out}{err}" ); @@ -195,7 +195,7 @@ fn a_lap_that_read_both_answers_is_refused_and_one_answer_is_not() { let (code, out, err) = check(&repo); assert_eq!(code, 2, "a lap that read both answers: {err}{out}"); assert!( - format!("{out}{err}").contains("lap-waits-on-one-answer"), + format!("{out}{err}").contains("wait read both"), "the finding names its own predicate, got {out}{err}" ); } @@ -262,7 +262,7 @@ fn a_conflict_with_no_path_still_refuses() { "a conflict with no path to name is still a conflict: {err}{out}" ); assert!( - format!("{out}{err}").contains("rebase-conflict-stops-the-lap"), + format!("{out}{err}").contains("replay halt conflict"), "the finding names its own predicate, got {out}{err}" ); } diff --git a/crates/batten/tests/it/landing_roster.rs b/crates/batten/tests/it/landing_roster.rs index c6aebe068..f800ba0d3 100644 --- a/crates/batten/tests/it/landing_roster.rs +++ b/crates/batten/tests/it/landing_roster.rs @@ -31,7 +31,7 @@ use std::path::{Path, PathBuf}; use batten::rules::{self, Rule}; /// The predicate id the module declares. -const UNGUARDED: &str = "landing-roster-unguarded"; +const UNGUARDED: &str = "check read never"; /// The one path the module is anchored on. const LANDING: &str = ".github/workflows/fast-forward.yml"; @@ -195,7 +195,7 @@ jobs: /// the guard's presence rather than on the fixture being a fixture. #[test] fn a_fixture_landing_workflow_that_consults_the_roster_is_clean() { - let root = repo("check read never", Some(GUARDED)); + let root = repo("landing-roster-guarded", Some(GUARDED)); assert!(rules_fired(&root).is_empty()); } diff --git a/crates/batten/tests/it/lease_record.rs b/crates/batten/tests/it/lease_record.rs index 9cdd5aa32..e7959c773 100644 --- a/crates/batten/tests/it/lease_record.rs +++ b/crates/batten/tests/it/lease_record.rs @@ -2,7 +2,7 @@ //! //! # The tier this is, and what the preset's own suite structurally cannot prove //! -//! `lease-authorises-the-branch` is a vendored preset, and +//! `lease grant other` is a vendored preset, and //! `crates/batten/tests/it/policy_presets.rs` already proves the PREDICATE decides //! for a consumer with no vocabulary of its own. What neither that tier nor the //! module's `test_` rules can prove is that the ENGINE writes the line the diff --git a/crates/batten/tests/it/lock_complete.rs b/crates/batten/tests/it/lock_complete.rs index 7dfc730ad..9f8ab189e 100644 --- a/crates/batten/tests/it/lock_complete.rs +++ b/crates/batten/tests/it/lock_complete.rs @@ -199,7 +199,7 @@ fn a_platform_key_mise_does_not_emit_is_reported() { ); assert_eq!(code, Some(2), "install-time residue is a finding\n{said}"); assert!( - said.contains("lock-platform-residue"), + said.contains("lock write other"), "the finding names its rule\n{said}" ); } @@ -233,7 +233,7 @@ fn a_required_platform_missing_entirely_is_reported() { ); assert_eq!(code, Some(2), "an unlocked platform is a finding\n{said}"); assert!( - said.contains("lock-platform-uninstallable"), + said.contains("lock reach unsafe"), "the finding names its rule\n{said}" ); } @@ -247,7 +247,7 @@ fn an_asset_backend_that_locks_no_platform_is_reported() { let (code, said) = judge_repo("lock-complete-unlocked", lock, &manifest(""), &[]); assert_eq!(code, Some(2), "an unverified download is a finding\n{said}"); assert!( - said.contains("lock-tool-unlocked"), + said.contains("tool pin partial"), "the finding names its rule\n{said}" ); } @@ -285,7 +285,7 @@ fn a_declared_tool_with_no_lock_entry_is_reported() { "a declared tool with no entry is a finding\n{said}" ); assert!( - said.contains("lock-tool-missing"), + said.contains("tool pin absent"), "the finding names its rule\n{said}" ); } @@ -294,7 +294,7 @@ fn a_declared_tool_with_no_lock_entry_is_reported() { fn a_declared_tool_on_an_exempt_backend_with_no_lock_entry_is_reported() { // CLOUD-611. The case above uses an `aqua:` pin, so for its whole life the // presence question was only ever asked of backends the url question already - // judged — and `lock-tool-missing` inherited `locks_nothing` from that other + // judged — and `tool pin absent` inherited `locks_nothing` from that other // question, which excused exactly the backends nothing else covered. // // The two questions are different and only one needs the exemption. "Does @@ -326,7 +326,7 @@ fn a_declared_tool_on_an_exempt_backend_with_no_lock_entry_is_reported() { "an exempt backend with no entry is still a finding\n{said}" ); assert!( - said.contains("lock-tool-missing"), + said.contains("tool pin absent"), "the finding names its rule\n{said}" ); } @@ -344,7 +344,7 @@ fn a_pin_its_entry_does_not_name_is_reported() { ); assert_eq!(code, Some(2), "a stale pin is a finding\n{said}"); assert!( - said.contains("lock-pin-stale"), + said.contains("pin read stale"), "the finding names its rule\n{said}" ); } @@ -373,7 +373,7 @@ fn a_pin_the_lock_extends_only_across_a_boundary_is_reported() { "an extension that is not at a component boundary is stale\n{said}" ); assert!( - said.contains("lock-pin-stale"), + said.contains("pin read stale"), "the finding names its rule\n{said}" ); } @@ -405,7 +405,7 @@ fn re_enabled_lockfile_writes_are_reported() { ); assert_eq!(code, Some(2), "re-enabled writes are a finding\n{said}"); assert!( - said.contains("lockfile-writes-enabled"), + said.contains("lock write unsafe"), "the finding names its rule\n{said}" ); } @@ -427,7 +427,7 @@ fn a_workflow_installing_without_the_lockfile_env_is_reported() { ); assert_eq!(code, Some(2), "an unlocked install is a finding\n{said}"); assert!( - said.contains("workflow-installs-unlocked"), + said.contains("workflow run unsafe"), "the finding names its rule\n{said}" ); } @@ -481,7 +481,7 @@ fn the_index_answers_not_the_worktree_for_the_committed_rule() { let (staged, also) = judge(&dir); assert_eq!(staged, Some(2), "a staged residue key still fails\n{also}"); assert!( - also.contains("lock-platform-residue"), + also.contains("lock write other"), "the finding names its rule\n{also}" ); } @@ -498,7 +498,7 @@ fn an_unreadable_lockfile_a_manifest_depends_on_is_refused() { )); assert_eq!(code, Some(2), "an unreadable lockfile is a finding\n{said}"); assert!( - said.contains("lock-unreadable"), + said.contains("lock read unread"), "the finding names its rule\n{said}" ); } diff --git a/crates/batten/tests/it/nextest_slow.rs b/crates/batten/tests/it/nextest_slow.rs index 211df1e19..4a604d5b0 100644 --- a/crates/batten/tests/it/nextest_slow.rs +++ b/crates/batten/tests/it/nextest_slow.rs @@ -39,8 +39,8 @@ use std::path::{Path, PathBuf}; use batten::rules::{self, Rule}; /// The predicate ids the module declares. -const UNBOUNDED: &str = "nextest-slow-unbounded"; -const RAISED: &str = "nextest-slow-raised"; +const UNBOUNDED: &str = "suite bind missing"; +const RAISED: &str = "bound edit refused"; /// The one path the module is anchored on. const CONFIG: &str = ".config/nextest.toml"; diff --git a/crates/batten/tests/it/obligations_bound.rs b/crates/batten/tests/it/obligations_bound.rs index ebfccb7be..1874e3a34 100644 --- a/crates/batten/tests/it/obligations_bound.rs +++ b/crates/batten/tests/it/obligations_bound.rs @@ -22,7 +22,7 @@ //! recorder has never written. `recorder.rs` renders a counted column as //! `` and this one declares `counted-with = ":"`, //! so the real line is `1::`. The module parsed it with a comma, so -//! every obligation resolved to the file `"1"`, and `obligation-unbound` fired on +//! every obligation resolved to the file `"1"`, and `test name undefined` fired on //! EVERY row carrying one. Measured 2026-09-03, on CLOUD-1402's own obligation. //! //! Reaching the engine was not enough, because this suite still hand-wrote the @@ -169,7 +169,7 @@ fn verdicts(root: &Path) -> Vec { .collect() } -const UNBOUND: &str = "obligation-unbound"; +const UNBOUND: &str = "test name undefined"; /// The record line the recorder writes: eight fields, with the obligation set /// last. Built here rather than inlined so an off-by-one in the module's column diff --git a/crates/batten/tests/it/perf_assert.rs b/crates/batten/tests/it/perf_assert.rs index 6069d150c..aa716108b 100644 --- a/crates/batten/tests/it/perf_assert.rs +++ b/crates/batten/tests/it/perf_assert.rs @@ -284,7 +284,7 @@ fn an_over_budget_path_is_refused() { ); let answer = findings(&dir); assert!( - answer.contains("perf-over-budget"), + answer.contains("path measure late"), "a p95 over its ceiling is a finding:\n{answer}" ); } @@ -301,7 +301,7 @@ fn a_budgeted_path_absent_from_a_present_record_is_refused() { ); let answer = findings(&dir); assert!( - answer.contains("perf-record-incomplete"), + answer.contains("path measure partial"), "a budgeted path missing from a present record is a finding:\n{answer}" ); } @@ -332,7 +332,7 @@ fn a_record_does_not_survive_its_subject() { write(&dir, "subject.bin", "a rebuilt binary\n"); let answer = findings(&dir); assert!( - !answer.contains("perf-over-budget"), + !answer.contains("path measure late"), "a record taken over bytes that have since changed must not answer:\n{answer}" ); } @@ -349,7 +349,7 @@ fn a_readme_publishing_a_different_budget_is_refused() { record(&dir, &clean_record()); let answer = findings(&dir); assert!( - answer.contains("perf-budget-unpublished"), + answer.contains("prose state wrong"), "the published budget and the enforced one must agree:\n{answer}" ); } @@ -366,7 +366,7 @@ fn a_readme_with_no_row_for_a_budgeted_path_is_refused() { record(&dir, &clean_record()); let answer = findings(&dir); assert!( - answer.contains("perf-budget-unpublished"), + answer.contains("prose state wrong"), "a budgeted path README does not publish is a disagreement:\n{answer}" ); } @@ -381,7 +381,7 @@ fn an_unreadable_readme_is_reported() { record(&dir, &clean_record()); let answer = findings(&dir); assert!( - answer.contains("perf-budget-unreadable"), + answer.contains("source read missing"), "a declared source that could not be read is reported, never assumed clean:\n{answer}" ); } @@ -398,7 +398,7 @@ fn the_committed_readme_publishes_the_budgets_this_module_enforces() { let dir = repo("committed-readme", Some(&committed)); let answer = findings(&dir); assert!( - !answer.contains("perf-budget-unpublished"), + !answer.contains("prose state wrong"), "README's Performance table must publish the budget `policy/perf-assert.rego` \ enforces — move both together:\n{answer}" ); diff --git a/crates/batten/tests/it/pinned_programs.rs b/crates/batten/tests/it/pinned_programs.rs index 96aa1b69c..0036e8ad9 100644 --- a/crates/batten/tests/it/pinned_programs.rs +++ b/crates/batten/tests/it/pinned_programs.rs @@ -368,7 +368,7 @@ fn a_probe_for_an_unpinned_program_is_not_reported() { /// assumed. /// /// The boundary looks THROUGH `command`, so `command -v gh` resolves an effective -/// program of `gh` and `pinned-program-via-the-pin` already speaks for it. Firing +/// program of `gh` and `pin reach loose` already speaks for it. Firing /// here too would put one call under two classes with two different remedies. /// /// This case is why the rule covers `which`/`type` and not `command` at all: diff --git a/crates/batten/tests/it/plan_complete.rs b/crates/batten/tests/it/plan_complete.rs index b3e7b26eb..720d5af17 100644 --- a/crates/batten/tests/it/plan_complete.rs +++ b/crates/batten/tests/it/plan_complete.rs @@ -39,7 +39,7 @@ fn repo(name: &str, changed: &[&str], plan: Option<&[&str]>) -> PathBuf { /// The same fixture, with the claim receipt under the caller's control. /// -/// `claimed` is the population `plan-unrecorded` asks about — a branch that +/// `claimed` is the population `plan declare absent` asks about — a branch that /// pulled a row — so a case about an UNCLAIMED tree needs to build one, and that /// case is what keeps the committed config usable over a scratch repository. fn claimed_repo(name: &str, changed: &[&str], plan: Option<&[&str]>, claimed: bool) -> PathBuf { @@ -80,7 +80,7 @@ fn write_record(root: &Path, record: &str, lines: &[&str]) { // PARTITIONED EXACTLY AS THE READER PARTITIONS (CLOUD-1300), and the comment // above is what caught this: writing the unpartitioned name while // `recorder_records` resolved the claim pointed the two at different files, - // and the `plan-unrecorded` arm went red because the reader found nothing + // and the `plan declare absent` arm went red because the reader found nothing // where the writer had put something. // // The `claim` receipt itself is never partitioned, and cannot be: it is the @@ -154,8 +154,8 @@ fn pointers(root: &Path) -> Vec { .collect() } -const UNFINISHED: &str = "plan-unfinished"; -const UNRECORDED: &str = "plan-unrecorded"; +const UNFINISHED: &str = "plan declare held"; +const UNRECORDED: &str = "plan declare absent"; // --------------------------------------------------------------------------- // THE READ SEAM. Without these two the whole module is a `with input as` suite @@ -197,13 +197,13 @@ fn an_empty_store_and_an_absent_one_reach_different_arms() { } // --------------------------------------------------------------------------- -// `plan-unfinished`. +// `plan declare held`. // --------------------------------------------------------------------------- #[test] fn an_unfinished_entry_stops_the_lap() { let root = repo( - "plan-unfinished", + "plan declare held", &["src/a.rs"], Some(&["1 completed", "2 in_progress"]), ); @@ -259,7 +259,7 @@ fn the_refusal_carries_no_entry_prose() { } // --------------------------------------------------------------------------- -// `plan-unrecorded` — the anti-vacuity arm. +// `plan declare absent` — the anti-vacuity arm. // --------------------------------------------------------------------------- #[test] diff --git a/crates/batten/tests/it/policy_presets.rs b/crates/batten/tests/it/policy_presets.rs index d18b5c84f..41b0100ff 100644 --- a/crates/batten/tests/it/policy_presets.rs +++ b/crates/batten/tests/it/policy_presets.rs @@ -171,7 +171,7 @@ fn a_preset_predicate_denies_and_is_green_by_turns() { assert_eq!(violations.len(), 1, "the practice-level predicate fired"); assert_eq!( bundles[0].attribute(&violations[0]), - "no-force-push", + "trunk push forced", "a preset finding names ITS OWN predicate id — never `preset` as a \ category, and never the enabling row" ); @@ -212,7 +212,7 @@ fn the_commit_hygiene_preset_decides_both_ways() { else { panic!("the preset answered"); }; - assert_eq!(bundles[0].attribute(&violations[0]), "no-empty-commit"); + assert_eq!(bundles[0].attribute(&violations[0]), "commit ship empty"); assert_eq!( policy::deny(&bundles[0], &call("git commit -m x")), Look::Is(Vec::new()) @@ -276,7 +276,7 @@ fn the_mise_preset_names_the_task_and_fails_open_on_a_stale_receipt() { }; assert_eq!( bundles[0].attribute(&violations[0]), - "task-over-executable", + "task reach loose", "a direct call of a receipted task's program is refused" ); @@ -364,7 +364,7 @@ fn a_preset_id_colliding_with_an_in_repo_id_is_refused_at_load() { // An in-repo module claiming the preset's own id. fs::write( root.join("mine.rego"), - "package batten\nimport rego.v1\nrules contains \"no-force-push\"\n", + "package batten\nimport rego.v1\nrules contains \"trunk push forced\"\n", ) .expect("write module"); let mine: Rule = serde_json::from_value(serde_json::json!({ @@ -385,7 +385,7 @@ fn a_preset_id_colliding_with_an_in_repo_id_is_refused_at_load() { ) .expect_err("one id, two publishers across the boundary"); let text = format!("{err}"); - assert!(text.contains("no-force-push"), "names the id: {text}"); + assert!(text.contains("trunk push forced"), "names the id: {text}"); assert!( text.contains("mine.rego") && text.contains("trunk-based"), "and BOTH sides, one of which is vendored: {text}" @@ -622,7 +622,7 @@ fn decided(bundle: &policy::Bundle, document: &str) -> Vec { violations } -/// (CLOUD-1269) `graded-head-is-not-regraded` refuses a judged commit and is +/// (CLOUD-1269) `head grade twice` refuses a judged commit and is /// silent on one nothing has looked at. /// /// Both halves, because the first alone passes on a preset that refuses @@ -638,7 +638,7 @@ fn the_landing_loop_preset_refuses_a_regrade_and_is_green_by_turns() { assert_eq!(judged.len(), 1, "a commit the forge already judged"); assert_eq!( bundle.attribute(&judged[0]), - "graded-head-is-not-regraded", + "head grade twice", "a preset finding names ITS OWN predicate id — never `preset` as a \ category, and never the enabling row" ); @@ -808,7 +808,7 @@ fn a_tree_module_reading_a_call_fact_is_refused_at_load() { .expect("the same module on the surface that emits the fact"); } -/// (CLOUD-1280) `already-landed-work-is-not-relanded` refuses a target that +/// (CLOUD-1280) `patch ship twice` refuses a target that /// already carries this work, and is silent on one that does not. /// /// The deny and its anti-vacuity mirror, plus the two answers that are NOT @@ -827,7 +827,7 @@ fn the_landing_loop_preset_refuses_a_reland_and_is_green_by_turns() { assert_eq!(landed.len(), 1, "the target already carries this work"); assert_eq!( bundle.attribute(&landed[0]), - "already-landed-work-is-not-relanded", + "patch ship twice", "a preset finding names its own predicate id" ); @@ -893,7 +893,7 @@ fn the_landing_loop_preset_refuses_a_reland_and_is_green_by_turns() { ); } -/// (CLOUD-1280) `lease-authorises-the-branch` refuses a live lease held by +/// (CLOUD-1280) `lease grant other` refuses a live lease held by /// another branch, and ALLOWS every reading it cannot take. /// /// The fail-open asymmetry is the load-bearing half of this predicate, so the @@ -914,7 +914,7 @@ fn the_landing_loop_preset_refuses_a_lease_held_elsewhere_and_fails_open() { assert_eq!(held.len(), 1, "a live lease grading this clone out"); assert_eq!( bundle.attribute(&held[0]), - "lease-authorises-the-branch", + "lease grant other", "a preset finding names its own predicate id" ); @@ -988,7 +988,7 @@ fn the_landing_loop_preset_refuses_a_lease_held_elsewhere_and_fails_open() { ); } -/// (CLOUD-1335) `rebase-conflict-stops-the-lap` refuses a lap that conflicted and +/// (CLOUD-1335) `replay halt conflict` refuses a lap that conflicted and /// is silent on one that replayed. /// /// **This is the case both of the module's declared mutations must redden, and @@ -1011,7 +1011,7 @@ fn the_landing_loop_preset_stops_a_conflicted_lap_and_is_green_by_turns() { assert_eq!(stopped.len(), 1, "a lap whose replay conflicted"); assert_eq!( bundle.attribute(&stopped[0]), - "rebase-conflict-stops-the-lap", + "replay halt conflict", "a preset finding names its own predicate id" ); @@ -1061,7 +1061,7 @@ fn the_landing_loop_preset_stops_a_conflicted_lap_and_is_green_by_turns() { ); } -/// (CLOUD-1338) `lap-waits-on-one-answer` refuses a lap that read both sides of +/// (CLOUD-1338) `wait read both` refuses a lap that read both sides of /// its race and is silent on one that read a single answer. /// /// **The case both declared mutations must redden, from opposite sides.** @@ -1094,7 +1094,7 @@ fn the_landing_loop_preset_refuses_a_lap_that_read_both_answers() { ); assert_eq!( bundle.attribute(&both[0]), - "lap-waits-on-one-answer", + "wait read both", "a preset finding names its own predicate id" ); diff --git a/crates/batten/tests/it/policy_test_suite.rs b/crates/batten/tests/it/policy_test_suite.rs index 63927a7a7..f0c80f9aa 100644 --- a/crates/batten/tests/it/policy_test_suite.rs +++ b/crates/batten/tests/it/policy_test_suite.rs @@ -121,10 +121,10 @@ package batten.probe import rego.v1 -rules contains "no-force-push" +rules contains "trunk push forced" violation contains { - "rule": "no-force-push", + "rule": "trunk push forced", "verdict": "trunk push forced", } if { words := split(input.call.command, " ") @@ -133,7 +133,7 @@ violation contains { test_no_force_push if { some v in violation with input as {"call": {"command": "git push --force"}} - v.rule == "no-force-push" + v.rule == "trunk push forced" count(violation) == 0 with input as {"call": {"command": "git push --force-with-lease"}} } @@ -144,7 +144,7 @@ test_no_force_push if { # rules file mandates. test_a_force_push_in_a_list_is_caught_too if { some v in violation with input as {"call": {"command": "cd /tmp && git push --force"}} - v.rule == "no-force-push" + v.rule == "trunk push forced" } "#; @@ -156,10 +156,10 @@ package batten.probe import rego.v1 -rules contains "no-force-push" +rules contains "trunk push forced" violation contains { - "rule": "no-force-push", + "rule": "trunk push forced", "verdict": "trunk push forced", } if { contains(input.call.command, "--force") @@ -167,7 +167,7 @@ violation contains { test_no_force_push if { some v in violation with input as {"call": {"command": "git push --force"}} - v.rule == "no-force-push" + v.rule == "trunk push forced" count(violation) == 0 with input as {"call": {"command": "git push --force-with-lease"}} } "#; @@ -498,10 +498,10 @@ package batten.probe import rego.v1 -rules contains "no-force-push" +rules contains "trunk push forced" violation contains { - "rule": "no-force-push", + "rule": "trunk push forced", "verdict": "trunk push forced", } if { some path, _ in input.tree.documents @@ -510,7 +510,7 @@ violation contains { test_no_force_push if { some v in violation with input as {"tree": {"documents": {"a.forbidden": {}}}} - v.rule == "no-force-push" + v.rule == "trunk push forced" count(violation) == 0 with input as {"tree": {"documents": {"a.json": {}}}} } "#; @@ -663,5 +663,5 @@ fn a_registered_module_with_tests_still_loads_and_denies() { panic!("the module answered nothing"); }; assert_eq!(violations.len(), 1); - assert_eq!(violations[0].rule.as_deref(), Some("no-force-push")); + assert_eq!(violations[0].rule.as_deref(), Some("trunk push forced")); } diff --git a/crates/batten/tests/it/preset_segments.rs b/crates/batten/tests/it/preset_segments.rs index c7f2eb3f6..413cbd86c 100644 --- a/crates/batten/tests/it/preset_segments.rs +++ b/crates/batten/tests/it/preset_segments.rs @@ -64,7 +64,7 @@ fn assert_preset_denies(command: &str) { let (code, cause) = adjudicate(command); assert_eq!(code, Some(2), "must refuse: {command}"); assert!( - cause.contains("no-force-push"), + cause.contains("trunk push forced"), "the deny must be the preset's own, not a neighbouring row's: {command}\n{cause}" ); } @@ -82,7 +82,7 @@ fn assert_preset_denies(command: &str) { fn assert_preset_allows(command: &str) { let (_, cause) = adjudicate(command); assert!( - !cause.contains("no-force-push"), + !cause.contains("trunk push forced"), "the preset must not refuse: {command}\n{cause}" ); } diff --git a/crates/batten/tests/it/record_closes.rs b/crates/batten/tests/it/record_closes.rs index e1fb7fea0..13ff9930a 100644 --- a/crates/batten/tests/it/record_closes.rs +++ b/crates/batten/tests/it/record_closes.rs @@ -3,7 +3,7 @@ //! //! # The seam this tier owns //! -//! `policy/filed-here.rego`'s `filed-over-own-diff` exempts a row the pull +//! `policy/filed-here.rego`'s `issue file same` exempts a row the pull //! request CLOSES, and reads that from `input.tree.records["pr-closes"]`. Until //! this verb existed that record had exactly one producer: the `pr-body-closes` //! `[[recorder]]` row, minted from an observed `gh pr view --jq .body` tool diff --git a/crates/batten/tests/it/remedy_authorship.rs b/crates/batten/tests/it/remedy_authorship.rs index 1e7d1f803..4bc660a5f 100644 --- a/crates/batten/tests/it/remedy_authorship.rs +++ b/crates/batten/tests/it/remedy_authorship.rs @@ -129,7 +129,7 @@ fn an_unprefixed_remedy_line_in_a_stderr_block_is_a_finding() { scan.findings ); assert_eq!( - scan.findings[0].rule, "remedy-reaches-the-reader", + scan.findings[0].rule, "remedy select dropped", "THE PREDICATE's id, not the row's (CLOUD-832)" ); } @@ -235,7 +235,7 @@ fn a_caller_naming_a_bypass_it_does_not_implement_is_a_finding() { "the predicate fired: {:?}", scan.findings ); - assert_eq!(scan.findings[0].rule, "remedy-has-one-author"); + assert_eq!(scan.findings[0].rule, "remedy own duplicate"); } /// THE DISCRIMINATING CASE for B. The gate that OWNS a hatch must be able to diff --git a/crates/batten/tests/it/repetition.rs b/crates/batten/tests/it/repetition.rs index 7d3d20432..230185d08 100644 --- a/crates/batten/tests/it/repetition.rs +++ b/crates/batten/tests/it/repetition.rs @@ -6,7 +6,7 @@ //! they are green over a shape the engine may never build. Two things here can //! only be proved against the real boundary, and both are this row's whole point: //! -//! * that `agent-turn-run` is the RUN the engine computes, not the author's +//! * that `turn run loose` is the RUN the engine computes, not the author's //! arithmetic — a fabricated `{"agent-turn-run": 3}` asserts the latter; //! * that an extraction this host cannot answer is **absent** from the map rather //! than reported as `0`. A `with input as` case cannot distinguish those at all, @@ -51,7 +51,7 @@ fn config(module: &str, extra: &str) -> String { r#"version = 1 [[rule]] -id = "turn run loose" +id = "agent-turn-run" kind = "policy" scope = "mediated_call" module = "{module}" @@ -62,7 +62,7 @@ id = "agent-turn-run" count = "agent-turn-run" {extra} [[verdict]] -id = "turn run loose" +id = "agent-turn-run" gloss = "this session has taken several turns in a row without doing anything" class = "Several turns in a row with no tool call between them." diff --git a/crates/batten/tests/it/review_answered.rs b/crates/batten/tests/it/review_answered.rs index bc4cea5c0..7af9ae45f 100644 --- a/crates/batten/tests/it/review_answered.rs +++ b/crates/batten/tests/it/review_answered.rs @@ -68,7 +68,7 @@ //! each is noted below with what the number is now and why. //! // changed: "review judge missing.bats::THE MEASURED SHAPE: a head carrying unresolved threads is refused, naming the count" crates/batten/tests/it/review_answered.rs the count is identical and where it is read from is not: `4 blocking` was a substring of a free string, and it is now the `Subject::Count` the engine renders beside the token (CLOUD-1050) -// changed: "review judge missing.bats::VACUITY: zero threads and no review reads as unreviewed, not as all-addressed" crates/batten/tests/it/review_answered.rs the count is 0 now and the rule is `review-absent`: the condition was one element of a `--jq` projection and is a second fact with its own inverted comparison since CLOUD-690, so the assertion moved from prose to a different predicate's subject rather than only to a subject +// changed: "review judge missing.bats::VACUITY: zero threads and no review reads as unreviewed, not as all-addressed" crates/batten/tests/it/review_answered.rs the count is 0 now and the rule is `review read absent`: the condition was one element of a `--jq` projection and is a second fact with its own inverted comparison since CLOUD-690, so the assertion moved from prose to a different predicate's subject rather than only to a subject // changed: "review judge missing.bats::VACUITY: a page the command could not read refuses rather than passing" crates/batten/tests/it/review_answered.rs same number, different producer: the projection emitted an extra element and the `blocking` column adds one, so the discriminating pair with the all-answered case is now two identical thread sets under different page flags // changed: "review judge missing.bats::THE BYPASS: a compound command is still a ready" crates/batten/tests/it/review_answered.rs same cause, same number; what the case proves — that the receipt row's selection and this module's narrowing agree about one command — is unchanged //! @@ -505,7 +505,7 @@ fn the_measured_shape_a_head_carrying_unresolved_threads_is_refused_naming_the_c reviewed(&dir, &declared); let decision = ready(&dir); denied(&decision); - assert!(decision.contains("review-unanswered"), "{decision}"); + assert!(decision.contains("review answer missing"), "{decision}"); // THE COUNT, as the typed ABI renders it: the token and the `Subject::Count` // beside it. The retired case read `4 blocking` out of a free string; the // number is the same and it is now a decoded subject, and since CLOUD-1286 diff --git a/crates/batten/tests/it/rules_drift.rs b/crates/batten/tests/it/rules_drift.rs index faab5e314..c82232f53 100644 --- a/crates/batten/tests/it/rules_drift.rs +++ b/crates/batten/tests/it/rules_drift.rs @@ -342,7 +342,7 @@ fn a_restated_default_that_disagrees_is_reported_with_its_pointer() { "a disagreeing restatement is a finding\n{said}" ); assert!( - said.contains("restated-default-drifts"), + said.contains("default state other"), "the finding names its rule\n{said}" ); assert!( @@ -391,7 +391,7 @@ fn a_sentence_claiming_an_unwired_event_is_reported() { ); assert_eq!(code, Some(2), "an unwired claim is a finding\n{said}"); assert!( - said.contains("named-event-unwired"), + said.contains("event wire missing"), "the finding names its rule\n{said}" ); } @@ -427,7 +427,7 @@ fn a_named_input_key_the_schema_does_not_carry_is_reported() { ); assert_eq!(code, Some(2), "an unemittable key is a finding\n{said}"); assert!( - said.contains("named-input-key-unemittable"), + said.contains("input key dead"), "the finding names its rule\n{said}" ); } @@ -468,7 +468,7 @@ fn a_named_fixed_rule_the_evaluator_does_not_query_is_reported() { ); assert_eq!(code, Some(2), "an unqueried name is a finding\n{said}"); assert!( - said.contains("named-fixed-rule-unqueried"), + said.contains("rule ask missing"), "the finding names its rule\n{said}" ); } @@ -529,7 +529,7 @@ fn an_absent_authority_a_claim_depends_on_is_refused() { "an absent authority a claim depends on must not be silent\n{said}" ); assert!( - said.contains("drift-authority-unreadable"), + said.contains("drift read unread"), "and it must be its OWN class, distinguishable from a clean pass and \ from a wiring that is merely absent\n{said}" ); @@ -568,7 +568,7 @@ fn an_unparseable_authority_a_claim_depends_on_is_refused() { "an authority that will not parse is could-not-look, not a clean tree\n{said}" ); assert!( - said.contains("drift-authority-unreadable"), + said.contains("drift read unread"), "and it reaches the same class as the absent case — one channel, two \ causes, neither of them silence\n{said}" ); @@ -599,7 +599,7 @@ fn an_unparseable_authority_no_prose_claims_against_is_still_silent() { unreadable it is\n{said}" ); assert!( - !said.contains("drift-authority-unreadable"), + !said.contains("drift read unread"), "and the could-not-look class must not fire on a tree that asked no \ question of the file\n{said}" ); @@ -703,7 +703,7 @@ fn a_restated_arm_count_that_disagrees_with_the_module_is_reported() { ); assert_eq!(code, Some(2), "a wrong arm count is a finding\n{said}"); assert!( - said.contains("restated-arm-count-drifts"), + said.contains("rule count other"), "the finding names its rule\n{said}" ); } @@ -741,7 +741,7 @@ fn an_arm_named_without_a_count_is_untouched() { } /// The sentence `rules/policy-modules.md` closes its key lists with, and -/// the anchor `schema-key-undocumented` keys on. +/// the anchor `input name missing` keys on. const CLAIM: &str = "`rule watch other` holds the lists above to those two files.\n"; #[test] @@ -765,7 +765,7 @@ fn a_schema_key_the_claiming_file_omits_is_reported() { "an omitted emittable key is a finding\n{said}" ); assert!( - said.contains("schema-key-undocumented"), + said.contains("input name missing"), "the finding names its rule\n{said}" ); } @@ -862,7 +862,7 @@ fn the_two_anchors_this_gate_keys_on_are_still_one_line_in_the_committed_files() .lines() .any(|line| line.contains("holds the lists above to those two files")), "the schema authority claim must survive on one line or \ - `schema-key-undocumented` silently stops judging it" + `input name missing` silently stops judging it" ); } @@ -901,10 +901,10 @@ package batten.collapse_probe import rego.v1 -rules contains "collapse-probe" +rules contains "probe read absent" violation contains { - "rule": "collapse-probe", + "rule": "probe read absent", "verdict": "probe read absent", "subjects": [{"count": 1}], } if { diff --git a/crates/batten/tests/it/run_shape.rs b/crates/batten/tests/it/run_shape.rs index 16207a4e5..0bf96f5fd 100644 --- a/crates/batten/tests/it/run_shape.rs +++ b/crates/batten/tests/it/run_shape.rs @@ -431,7 +431,7 @@ fn a_foreground_sleep_is_refused() { // The harness kills a foreground call at ~2 minutes, so a poll meant to be // patient FAILS instead — measured at exit 143 and 144 over a hung commit, // after which the container was reclaimed with the work uncommitted. - let root = fixture("foreground-sleep"); + let root = fixture("sleep run blocked"); denied_background(&root, "sleep 90", false); // Judged per segment: the measured shape had the sleep in the middle. denied_background(&root, "cd /tmp; sleep 90; git log --oneline -1", false); @@ -447,7 +447,7 @@ fn a_backgrounded_bare_sleep_is_a_timer() { // in one session against 523 of 524 backgrounded tasks re-invoking their // caller on exit. Two of the 490 changed a decision. denied_background( - &fixture("background-timer"), + &fixture("timer run refused"), "sleep 590; tail -6 /tmp/land.log", true, ); @@ -499,7 +499,7 @@ fn a_loop_body_is_reached_and_the_exemption_decides_it() { fn a_backgrounded_bare_sleep_raises_the_timer_and_not_the_foreground_rule() { // THE DISCRIMINATING CASE for `run-in-background`, and it has to read the // verdict rather than the decision: both rules deny, so an exit-code - // assertion passes over a `foreground-sleep` that ignored the flag entirely. + // assertion passes over a `sleep run blocked` that ignored the flag entirely. let (deny, text) = hook_background( &fixture("timer-not-foreground"), "sleep 590; tail -6 /tmp/land.log", @@ -561,7 +561,7 @@ fn a_token_carrying_an_m_is_not_a_flag_cluster() { // carried: "a compound list is judged per element, not by its first word" crates/batten/tests/it/run_shape.rs #[test] fn a_compound_list_is_judged_per_element() { - // THE SHAPE A RAW-STRING MODULE MISSES. The vendored `no-force-push` preset + // THE SHAPE A RAW-STRING MODULE MISSES. The vendored `trunk push forced` preset // anchors on `words[0] == "git"` over the whole command, so `cd /tmp && git // push --force` reaches it as `cd` and is allowed — green tests, silent // gate. Every element is a command here. @@ -658,7 +658,7 @@ fn the_refusal_names_its_predicate_its_class_and_the_route_out() { let (deny, text) = hook(&root, "git commit"); assert!(deny, "the shape is still refused: {text}"); assert!( - text.contains("commit-names-no-message-source"), + text.contains("commit write missing"), "the predicate id: {text}" ); assert!( diff --git a/crates/batten/tests/it/sbom_inventory.rs b/crates/batten/tests/it/sbom_inventory.rs index 8b58249e0..8fd11895e 100644 --- a/crates/batten/tests/it/sbom_inventory.rs +++ b/crates/batten/tests/it/sbom_inventory.rs @@ -257,7 +257,7 @@ fn a_drifted_cargo_count_is_refused_over_the_real_lockfile() { Some(2), "a cargo count disagreeing with the lockfile is a policy verdict\n{answer}{cause}" ); - assert!(answer.contains("sbom-package-drift"), "{answer}{cause}"); + assert!(answer.contains("manifest count other"), "{answer}{cause}"); } #[test] @@ -299,7 +299,7 @@ fn an_empty_catalog_is_refused() { let outcome = check(&dir); let answer = stdout(&outcome); assert_eq!(outcome.status.code(), Some(2), "{answer}"); - assert!(answer.contains("sbom-empty"), "{answer}"); + assert!(answer.contains("manifest list empty"), "{answer}"); } #[test] @@ -318,7 +318,7 @@ fn an_unstable_scan_is_refused() { let outcome = check(&dir); let answer = stdout(&outcome); assert_eq!(outcome.status.code(), Some(2), "{answer}"); - assert!(answer.contains("sbom-unstable"), "{answer}"); + assert!(answer.contains("manifest mint twice"), "{answer}"); } #[test] @@ -335,7 +335,7 @@ fn an_inflated_component_set_is_refused() { let outcome = check(&dir); let answer = stdout(&outcome); assert_eq!(outcome.status.code(), Some(2), "{answer}"); - assert!(answer.contains("sbom-components-inflated"), "{answer}"); + assert!(answer.contains("manifest count ahead"), "{answer}"); } #[test] @@ -361,7 +361,7 @@ fn an_unmapped_action_pin_is_refused_from_committed_text_alone() { let outcome = check(&dir); let answer = stdout(&outcome); assert_eq!(outcome.status.code(), Some(2), "{answer}"); - assert!(answer.contains("sbom-action-unmapped"), "{answer}"); + assert!(answer.contains("pin table missing"), "{answer}"); } #[test] @@ -401,7 +401,7 @@ fn a_recorded_but_empty_scan_is_refused() { let outcome = check(&dir); let answer = stdout(&outcome); assert_eq!(outcome.status.code(), Some(2), "{answer}"); - assert!(answer.contains("sbom-unrecorded"), "{answer}"); + assert!(answer.contains("manifest file missing"), "{answer}"); } #[test] @@ -477,9 +477,9 @@ fn the_report_is_pointer_only() { let (answer, cause) = (stdout(&outcome), stderr(&outcome)); assert_eq!(outcome.status.code(), Some(2), "{answer}{cause}"); for id in [ - "sbom-supplier-unset", - "sbom-copyright-unenriched", - "sbom-license-unenriched", + "manifest own missing", + "manifest own unnamed", + "manifest grant missing", ] { assert!(answer.contains(id), "{id} is not reported\n{answer}{cause}"); } diff --git a/crates/batten/tests/it/shell_retirement.rs b/crates/batten/tests/it/shell_retirement.rs index cb017b0c9..a8b0f33be 100644 --- a/crates/batten/tests/it/shell_retirement.rs +++ b/crates/batten/tests/it/shell_retirement.rs @@ -589,7 +589,7 @@ fn an_edit_repointing_a_task_name_while_rewriting_the_line_is_refused() { ); assert_eq!( findings(&root), - vec![String::from("shell-rule-retired")], + vec![String::from("shell retire other")], "the span is derived from the diff, so a line that also changed elsewhere \ has no single repointed span and is refused" ); @@ -625,7 +625,7 @@ fn a_task_name_repointed_at_an_undeclared_invocation_is_refused() { ); assert_eq!( findings(&root), - vec![String::from("shell-rule-retired")], + vec![String::from("shell retire other")], "without a `runs:` arm there is no declared invocation, so nothing admits \ the repointing" ); @@ -659,7 +659,7 @@ fn an_edit_truncating_a_line_at_a_live_reference_is_refused() { ); assert_eq!( findings(&root), - vec![String::from("shell-rule-retired")], + vec![String::from("shell retire other")], "dropping a reference to a file that is still here is maintenance in place" ); } @@ -694,7 +694,7 @@ fn an_added_shell_rule_is_refused() { removed: &[], }, ); - assert_eq!(findings(&root), vec!["shell-rule-retired".to_owned()]); + assert_eq!(findings(&root), vec!["shell retire other".to_owned()]); } #[test] @@ -707,7 +707,7 @@ fn an_added_bats_suite_is_refused() { removed: &[], }, ); - assert_eq!(findings(&root), vec!["shell-rule-retired".to_owned()]); + assert_eq!(findings(&root), vec!["shell retire other".to_owned()]); } /// CLOUD-1088, and this is the tier that matters for it. @@ -758,7 +758,7 @@ fn the_stays_bash_declaration_does_not_admit_an_edit() { removed: &[], }, ); - assert_eq!(findings(&root), vec!["shell-rule-retired".to_owned()]); + assert_eq!(findings(&root), vec!["shell retire other".to_owned()]); } /// The load-bearing arm: an edit is invisible to every other sensor in the tree. @@ -775,7 +775,7 @@ fn a_shell_rule_edited_in_place_is_refused() { removed: &[], }, ); - assert_eq!(findings(&root), vec!["shell-rule-retired".to_owned()]); + assert_eq!(findings(&root), vec!["shell retire other".to_owned()]); } #[test] @@ -791,7 +791,7 @@ fn a_bats_suite_edited_in_place_is_refused() { removed: &[], }, ); - assert_eq!(findings(&root), vec!["shell-rule-retired".to_owned()]); + assert_eq!(findings(&root), vec!["shell retire other".to_owned()]); } #[test] @@ -804,7 +804,7 @@ fn a_deletion_with_no_mapping_is_refused() { removed: &["mise-tasks/old-gate.sh"], }, ); - assert_eq!(findings(&root), vec!["shell-rule-retired".to_owned()]); + assert_eq!(findings(&root), vec!["shell retire other".to_owned()]); } #[test] @@ -821,7 +821,7 @@ fn a_deletion_carrying_two_arms_is_refused() { removed: &["mise-tasks/old-gate.sh"], }, ); - assert_eq!(findings(&root), vec!["shell-rule-retired".to_owned()]); + assert_eq!(findings(&root), vec!["shell retire other".to_owned()]); } #[test] @@ -837,7 +837,7 @@ fn a_mapping_naming_no_policy_surface_is_refused() { removed: &["mise-tasks/old-gate.sh"], }, ); - assert_eq!(findings(&root), vec!["shell-rule-retired".to_owned()]); + assert_eq!(findings(&root), vec!["shell retire other".to_owned()]); } /// The successor KIND obligation, over the engine (CLOUD-1182). @@ -860,7 +860,7 @@ fn an_engine_source_arm_without_a_declared_kind_is_refused() { removed: &["mise-tasks/old-gate.sh"], }, ); - assert_eq!(findings(&root), vec!["shell-rule-retired".to_owned()]); + assert_eq!(findings(&root), vec!["shell retire other".to_owned()]); } /// The anti-vacuity mirror, and the case that proves this gate does not ban a @@ -949,7 +949,7 @@ fn a_mapping_naming_no_compiled_binary_test_is_refused() { removed: &["mise-tasks/old-gate.sh"], }, ); - assert_eq!(findings(&root), vec!["shell-rule-retired".to_owned()]); + assert_eq!(findings(&root), vec!["shell retire other".to_owned()]); } // --------------------------------------------------------------------------- @@ -987,7 +987,7 @@ fn a_carried_row_naming_a_live_subject_is_refused() { ); assert_eq!( findings(&root), - vec!["shell-rule-retired".to_owned()], + vec!["shell retire other".to_owned()], "every successor obligation is met and the subject is still standing" ); } @@ -1083,7 +1083,7 @@ fn a_withdrawal_over_a_live_subject_is_refused() { removed: &["tests/old-gate.bats"], }, ); - assert_eq!(findings(&root), vec!["shell-rule-retired".to_owned()]); + assert_eq!(findings(&root), vec!["shell retire other".to_owned()]); } #[test] @@ -1104,7 +1104,7 @@ fn a_withdrawal_naming_no_reason_is_refused() { removed: &["tests/old-gate.bats", ".claude/old-wrapper.sh"], }, ); - assert_eq!(findings(&root), vec!["shell-rule-retired".to_owned()]); + assert_eq!(findings(&root), vec!["shell retire other".to_owned()]); } #[test] @@ -1126,7 +1126,7 @@ fn the_successor_obligation_still_binds_the_other_three_arms() { removed: &["tests/old-gate.bats", ".claude/old-wrapper.sh"], }, ); - assert_eq!(findings(&root), vec!["shell-rule-retired".to_owned()]); + assert_eq!(findings(&root), vec!["shell retire other".to_owned()]); } // --------------------------------------------------------------------------- @@ -1409,7 +1409,7 @@ fn a_repointing_at_a_command_no_arm_declares_is_refused() { }, ); assert!( - findings(&root).contains(&"shell-rule-retired".to_owned()), + findings(&root).contains(&"shell retire other".to_owned()), "the target must come from the ledger, never from the editor: {:?}", findings(&root) ); @@ -1443,7 +1443,7 @@ fn a_repointing_that_also_changes_the_rest_of_the_line_is_refused() { }, ); assert!( - findings(&root).contains(&"shell-rule-retired".to_owned()), + findings(&root).contains(&"shell retire other".to_owned()), "dropping ` 2>/dev/null` alongside the repointing is a second edit: {:?}", findings(&root) ); @@ -1481,7 +1481,7 @@ fn replacing_a_span_that_is_not_a_reference_to_the_retired_path_is_refused() { }, ); assert!( - findings(&root).contains(&"shell-rule-retired".to_owned()), + findings(&root).contains(&"shell retire other".to_owned()), "a span in somebody else's tree is not a reference to the retired path: {:?}", findings(&root) ); @@ -1515,7 +1515,7 @@ fn a_malformed_invocation_field_declares_no_command() { }, ); assert!( - findings(&root).contains(&"shell-rule-retired".to_owned()), + findings(&root).contains(&"shell retire other".to_owned()), "an empty `runs:` tail is not a command: {:?}", findings(&root) ); @@ -1557,7 +1557,7 @@ fn a_retired_name_dropped_from_a_list_is_admitted() { }, ); assert!( - !findings(&root).contains(&"shell-rule-retired".to_owned()), + !findings(&root).contains(&"shell retire other".to_owned()), "dropping the name of a gate this delta retires is the honest edit, and \ both halves see it: the removed line mentions `old-gate` and the added \ line is that line minus exactly that name: {:?}", @@ -1599,7 +1599,7 @@ fn dropping_the_name_while_also_changing_the_line_is_refused() { }, ); assert!( - findings(&root).contains(&"shell-rule-retired".to_owned()), + findings(&root).contains(&"shell retire other".to_owned()), "the name is one this delta retires, so the arm is reached — and the added \ line is not the removed one minus that name, so it is still refused: {:?}", findings(&root) @@ -1636,7 +1636,7 @@ fn dropping_a_name_this_delta_does_not_retire_is_still_refused() { }, ); assert!( - findings(&root).contains(&"shell-rule-retired".to_owned()), + findings(&root).contains(&"shell retire other".to_owned()), "`other-gate` names nothing this delta deletes, so dropping it is an \ ordinary edit to a governed program and stays refused: {:?}", findings(&root) @@ -1767,7 +1767,7 @@ fn a_bats_case_testing_a_live_path_is_still_refused() { let root = suite_repo("bats-case-live", CASE_SUITE_LIVE_DROPPED); assert_eq!( findings(&root), - vec![String::from("shell-rule-retired")], + vec![String::from("shell retire other")], "the surviving case names nothing this delta retires, so dropping it is an \ ordinary edit to a governed suite" ); @@ -1784,7 +1784,7 @@ fn a_half_deleted_bats_case_is_still_refused() { let root = suite_repo("bats-case-half", CASE_SUITE_OPENER_ONLY); assert_eq!( findings(&root), - vec![String::from("shell-rule-retired")], + vec![String::from("shell retire other")], "a case whose naming line survives was edited, not retired" ); } @@ -1875,7 +1875,7 @@ fn a_bats_case_spending_a_surviving_binding_is_refused() { let root = bind_repo("bats-bind-kept", BIND_SUITE_BINDING_KEPT); assert_eq!( findings(&root), - vec![String::from("shell-rule-retired")], + vec![String::from("shell retire other")], "the binding is still there, so nothing about this case is going away" ); } diff --git a/crates/batten/tests/it/sinks.rs b/crates/batten/tests/it/sinks.rs index 363503f7a..6dc5b10d5 100644 --- a/crates/batten/tests/it/sinks.rs +++ b/crates/batten/tests/it/sinks.rs @@ -338,7 +338,7 @@ fn a_sink_on_a_mediated_call_kind_is_refused_at_load() { "version = 1\n\ \n\ [[rule]]\n\ - id = \"no-force-push\"\n\ + id = \"trunk push forced\"\n\ kind = \"shape\"\n\ pattern = \"git push --force\"\n\ severity = \"deny\"\n\ diff --git a/crates/batten/tests/it/staged_facts.rs b/crates/batten/tests/it/staged_facts.rs index 7e28e868a..5ed6be62e 100644 --- a/crates/batten/tests/it/staged_facts.rs +++ b/crates/batten/tests/it/staged_facts.rs @@ -566,10 +566,10 @@ fn the_committed_lock_rule_refuses_a_partial_entry_over_the_binary() { ); // THE PREDICATE ID, NOT THE ROW ID. A module's finding carries the `rule` id // the `violation` object declares — `lock cover partial` is what `--rule` selects - // and `lock-platform-uninstallable` is what decided — so asserting the row + // and `lock reach unsafe` is what decided — so asserting the row // name here would pass over any module that raised anything at all. assert!( - answer.contains("lock-platform-uninstallable"), + answer.contains("lock reach unsafe"), "and the finding names the predicate that decided it\n{answer}{cause}" ); } diff --git a/crates/batten/tests/it/startup.rs b/crates/batten/tests/it/startup.rs index 7b2970d27..d925df8d8 100644 --- a/crates/batten/tests/it/startup.rs +++ b/crates/batten/tests/it/startup.rs @@ -257,7 +257,7 @@ fn the_commit_gate_sub_verb_answers_only_its_own_question() { /// will not run it" is indistinguishable from healthy to a probe that only stats. /// /// **`cfg!` IN THE BODY RATHER THAN `#[cfg(unix)]` ON THE CASE**, which is -/// `platform-gated-test-added`'s own remedy and the reason it exists. Narrowing +/// `test cover unseen`'s own remedy and the reason it exists. Narrowing /// the case to unix would turn a red leg green while leaving the Windows /// contract unstated and one arm never compiled on the host that authors it. /// Both arms compile on every target here, and the Windows expectation is diff --git a/crates/batten/tests/it/startup_bootstrap.rs b/crates/batten/tests/it/startup_bootstrap.rs index d842283ea..7c3675d9b 100644 --- a/crates/batten/tests/it/startup_bootstrap.rs +++ b/crates/batten/tests/it/startup_bootstrap.rs @@ -190,7 +190,7 @@ fn linked_fixture(name: &str, dest: &str, body: &[u8]) -> (PathBuf, PathBuf) { ); write(&dir, "a.txt", "x\n"); // `init_repo`, never a `git init` fork: main's fixture-fork ratchet - // (`fixture-fork-added`) refuses the fork, and under `CARGO_TARGET_TMPDIR` + // (`test add duplicate`) refuses the fork, and under `CARGO_TARGET_TMPDIR` // this copies the published template at zero forks instead. init_repo(&dir); (dir, artifact) diff --git a/crates/batten/tests/it/test_targets.rs b/crates/batten/tests/it/test_targets.rs index feacbe876..6308416b3 100644 --- a/crates/batten/tests/it/test_targets.rs +++ b/crates/batten/tests/it/test_targets.rs @@ -35,7 +35,7 @@ use std::path::{Path, PathBuf}; use batten::rules::{self, Rule}; /// The predicate id the module declares. -const TARGET_ADDED: &str = "test-target-added"; +const TARGET_ADDED: &str = "test place duplicate"; /// A fixture repository whose base is one commit back and whose working tree /// ADDS `changed`, so the engine's own `base-delta` resolution is what produces diff --git a/crates/batten/tests/policy_modules.rs b/crates/batten/tests/policy_modules.rs index eb2ddd9ba..1adf19891 100644 --- a/crates/batten/tests/policy_modules.rs +++ b/crates/batten/tests/policy_modules.rs @@ -1001,9 +1001,9 @@ package batten.git import rego.v1 -rules contains "no-force-push" +rules contains "trunk push forced" -violation contains {"rule": "no-force-push", "verdict": "trunk push forced"} if { +violation contains {"rule": "trunk push forced", "verdict": "trunk push forced"} if { input.call.operation == "write" } "#; @@ -1018,7 +1018,7 @@ violation contains {"rule": "no-force-push", "verdict": "trunk push forced"} if .expect("a sub-package module loads"); assert!( - bundles[0].declared().contains("no-force-push"), + bundles[0].declared().contains("trunk push forced"), "the ids a sub-package publishes are reached too, not just its denials" ); @@ -1028,7 +1028,7 @@ violation contains {"rule": "no-force-push", "verdict": "trunk push forced"} if }; assert_eq!( violations, - vec![attributed("no-force-push", "trunk push forced")], + vec![attributed("trunk push forced", "trunk push forced")], "the package prefix is `batten` and the RULE NAMES are what is fixed; \ pinning the whole path leaves this module silently unreachable" ); diff --git a/mise-tasks/sbom.sh b/mise-tasks/sbom.sh index 8a0fde450..5d6476955 100755 --- a/mise-tasks/sbom.sh +++ b/mise-tasks/sbom.sh @@ -90,7 +90,7 @@ OUT_DIR="${SBOM_OUT_DIR:-sbom}" # harness, no more part of the shipped artifact than `tests/` is — so cataloging # it would make the inventory overstate what a consumer receives, which is the # false claim this document exists not to make. It would also break the -# `sbom-package-drift` invariant by construction: that clause compares the +# `manifest count other` invariant by construction: that clause compares the # cargo count against the ROOT `Cargo.lock`, and a second lockfile in scope adds # packages no root lockfile names (measured: 175 -> 281). readonly EXCLUDES=(--exclude ./tests/bats --exclude ./target --exclude ./fuzz) @@ -349,7 +349,7 @@ copyright_of() { # MATCHED ON THE REPO, NOT THE SHA, and that is forced rather than chosen: syft # keys these components by the `# vX` comment beside the pin, not by the pin # itself — `pkg:github/actions/checkout@v7` for a component whose `uses:` line -# resolves to `3d3c42e5…`. The sha in the table is what `sbom-action-unmapped` +# resolves to `3d3c42e5…`. The sha in the table is what `pin table missing` # compares against the workflows, so drift is still caught at the pin; using it # here would match nothing. ACTIONS_TABLE="${SBOM_ACTIONS_TABLE:-$(cd "$(dirname "$0")" && pwd)/sbom-actions.tsv}" From 19c83f561baa792aeb778ddbb6378910e856d91b Mon Sep 17 00:00:00 2001 From: Alec Wenzowski Date: Wed, 9 Sep 2026 16:50:02 +0000 Subject: [PATCH 09/23] fix(tests): stop forking the repo setup, and finish the bats migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two findings the slow tier raised, both mine. `test add duplicate` on `rules_drift.rs`: my collapse fixture built its own repo with a second `git init`, copying the three git calls already at the top of the file. `fixture-forks` counts those lines and refuses a new one, and it was right — the two helpers wanted the same thing. Extracted to `init_repo`, so the file now carries ONE init where it carried two. The commit matters as much as the init: `check` resolves its base against a HEAD, so an init-only repo answers could-not-look instead of judging the config under test, which is why the first version of that fixture was quietly weaker than it looked. The bats suites named 25 old ids across 11 files — the same migration surface as the Rust tests, reached by the same structural rewrite. `policy-test` 850/850, `test:bats` 0 failures, `lint:clippy` clean, and the cargo suite green at 5317. AND ONE REVERT. My prose fix in `mise-tasks/sbom.sh` — two comments naming finding ids by their old names — tripped `shell retire other`, the shell moratorium. The gate cannot tell a comment from code and refuses an edit to a governed `mise-tasks/` path, which is its job; the two comments now name ids that no longer exist, and that staleness is the cheaper cost. Recorded on CLOUD-1638 rather than worked around. Refs: CLOUD-1638 --- crates/batten/tests/it/rules_drift.rs | 19 +++++++++++++++---- policy/repetition-without-progress.rego | 4 ++-- policy/review-answered.rego | 2 +- tests/batten-glob-check.bats | 2 +- tests/closing-key-check.bats | 6 +++--- tests/hk-selection.bats | 8 ++++---- tests/landed-check.bats | 2 +- tests/ntia-check.bats | 10 +++++----- tests/release-tracking-check.bats | 6 +++--- tests/remedy-payload-source.bats | 6 +++--- tests/serena-mcp.bats | 2 +- tests/signing-posture.bats | 2 +- tests/spawn-census.bats | 4 ++-- tests/zizmor-split.bats | 2 +- 14 files changed, 43 insertions(+), 32 deletions(-) diff --git a/crates/batten/tests/it/rules_drift.rs b/crates/batten/tests/it/rules_drift.rs index c82232f53..7b83cdaa4 100644 --- a/crates/batten/tests/it/rules_drift.rs +++ b/crates/batten/tests/it/rules_drift.rs @@ -252,9 +252,7 @@ target = "policy/rules-drift.rego" for (path, body) in files { write(&dir, path, body); } - git_in(&dir, &["init", "--initial-branch=main"]); - git_in(&dir, &["add", "-A"]); - git_in(&dir, &["commit", "-m", "fixture"]); + init_repo(&dir); dir } @@ -959,10 +957,23 @@ severity = "deny" condition = "", ), ); - git_in(&dir, &["init", "-q"]); + init_repo(&dir); dir } +/// The three git calls a fixture repo needs, in ONE place (CLOUD-1638). +/// +/// `fixture-forks` counts the `git init` lines a suite carries and refuses a +/// new one, which is right: this file had two helpers building the same repo, +/// and the second was a copy of the first. The commit matters as much as the +/// init — `check` resolves its base against a HEAD, so an init-only repo +/// answers could-not-look instead of judging the config under test. +fn init_repo(dir: &Path) { + git_in(dir, &["init", "--initial-branch=main"]); + git_in(dir, &["add", "-A"]); + git_in(dir, &["commit", "-m", "fixture"]); +} + /// `check` over the fixture's own config, reading the channel a usage error uses. fn load(dir: &Path) -> (Option, String) { let out = run(dir, &["check"]); diff --git a/policy/repetition-without-progress.rego b/policy/repetition-without-progress.rego index 11eb28b38..ba42ba7b5 100644 --- a/policy/repetition-without-progress.rego +++ b/policy/repetition-without-progress.rego @@ -105,13 +105,13 @@ test_the_finding_carries_a_count_and_nothing_else if { # ONE BELOW THE THRESHOLD IS CLEAN. An off-by-one here moves the whole population # the rule fires on. test_two_turns_in_a_row_is_not_a_run if { - count(violation) == 0 with input as session({"agent-turn-run": 2}) + count(violation) == 0 with input as session({"turn run loose": 2}) } # THE ARM THAT MAKES ADJACENCY WORTH HAVING: a session that acts between turns # has a trailing run of one however long it runs. test_a_session_that_acts_between_turns_is_clean if { - count(violation) == 0 with input as session({"agent-turn-run": 1}) + count(violation) == 0 with input as session({"turn run loose": 1}) } # COULD NOT LOOK IS NOT INNOCENCE, and it is not guilt either. diff --git a/policy/review-answered.rego b/policy/review-answered.rego index 9ba4a952b..32cef10b0 100644 --- a/policy/review-answered.rego +++ b/policy/review-answered.rego @@ -240,7 +240,7 @@ test_a_head_failing_both_raises_both if { "review-happened": {"rows": 0}, }}, } - raised == {"review-unanswered", "review-absent"} + raised == {"review answer missing", "review read absent"} } # `land` re-drafts on a red run, and that is what closes the CI tap. Refusing it diff --git a/tests/batten-glob-check.bats b/tests/batten-glob-check.bats index 7afde66ea..92581226f 100644 --- a/tests/batten-glob-check.bats +++ b/tests/batten-glob-check.bats @@ -111,7 +111,7 @@ hooks_with() { @test "a config the gate parses nothing out of is exit 2, not a pass" { # The vacuous green a containment check produces most easily: parse zero # requirements and every list covers them. Distinguished from a violation, - # the same way a missing lockfile is in lock-complete. + # the same way a missing lockfile is in lock cover partial. printf '[epoch]\ntracked = ["batten.toml"]\n' >"$CONFIG" hooks_with "crates/**" diff --git a/tests/closing-key-check.bats b/tests/closing-key-check.bats index 25a4498cf..9f4fadf86 100644 --- a/tests/closing-key-check.bats +++ b/tests/closing-key-check.bats @@ -148,12 +148,12 @@ Refs: CLOUD-593, CLOUD-344, CLOUD-661, CLOUD-103' } @test "a body naming no key at all is the key rule's case, not this one" { - # One rule, one authority. The engine's `pr-names-an-issue` row judges this + # One rule, one authority. The engine's `review name unnamed` row judges this # at `gh pr create`, which is earlier and cheaper. It was `issue-guard` until # CLOUD-446 retired that program. run bash -c "printf 'A body with no key.\n' | $GATE" [ "$status" -eq 0 ] - [[ "$output" == *"pr-names-an-issue rule owns that case"* ]] + [[ "$output" == *"review name unnamed rule owns that case"* ]] } @test "one closed key is enough, even beside a named-but-unclosed one" { @@ -171,7 +171,7 @@ Refs: CLOUD-593, CLOUD-344, CLOUD-661, CLOUD-103' # boundary deleted. run bash -c "printf 'SUBCLOUD-17 is a different system.\n' | $GATE" [ "$status" -eq 0 ] - [[ "$output" == *"pr-names-an-issue rule owns that case"* ]] + [[ "$output" == *"review name unnamed rule owns that case"* ]] } @test "several named keys are each reported, in stable numeric order" { diff --git a/tests/hk-selection.bats b/tests/hk-selection.bats index 53e64dac9..cd12ad353 100644 --- a/tests/hk-selection.bats +++ b/tests/hk-selection.bats @@ -39,7 +39,7 @@ status_of() { @test "every path selects batten-check once a rule globs the whole tree" { # SUPERSEDES "a Markdown file that is not an input does not select # batten-check", which asserted `skipped` for README.md on the premise that - # it "carries no rule glob and is in no budget". CLOUD-59's `no-secrets` row + # it "carries no rule glob and is in no budget". CLOUD-59's `source carry unsafe` row # globs `**`, so that premise is simply false now: a credential can be in # README.md, and narrowing the rule would be choosing which files are # allowed to carry one. @@ -82,12 +82,12 @@ status_of() { } @test "every non-crates path a batten.toml rule globs selects batten-check" { - # mise.toml (no-source-built-tool), workflows (no-cargo-install-in-ci), - # tests/*.bats (bats-tests-not-deleted). These are the ones the issue's + # mise.toml (pin add unsafe), workflows (cargo add loose), + # tests/*.bats (bats count dropped). These are the ones the issue's # proposed glob would have dropped. [ "$(status_of batten-check mise.toml)" = "included" ] [ "$(status_of batten-check .github/workflows/ci.yml)" = "included" ] - [ "$(status_of batten-check tests/lock-complete.bats)" = "included" ] + [ "$(status_of batten-check tests/lock cover partial.bats)" = "included" ] } @test "the embedded budget path selects batten-check" { diff --git a/tests/landed-check.bats b/tests/landed-check.bats index 7798ba7a8..8d0fd9a93 100644 --- a/tests/landed-check.bats +++ b/tests/landed-check.bats @@ -29,7 +29,7 @@ setup() { # that is currently checked out, so the same line failed outright the moment # a developer's default was the trunk's own name. Naming the branch makes the # topology explicit instead of inheriting it, and `main` is then a fresh name - # needing no force at all. `no-branch-f-main` in batten.toml keeps the old + # needing no force at all. `branch edit unsafe` in batten.toml keeps the old # form out; the literal is not spelled here, because that row is a substring # rule over this directory and would fire on its own explanation. git init -q -b work "$REPO" diff --git a/tests/ntia-check.bats b/tests/ntia-check.bats index b2a5905e3..de1bbac42 100644 --- a/tests/ntia-check.bats +++ b/tests/ntia-check.bats @@ -207,7 +207,7 @@ EOF @test "a receipt that cannot be written is reported, never a nonconformance" { # THE FALSE VERDICT CI REPORTED (CLOUD-631). `batten receipt record` exits 1 # where the configured transcript is unreadable, which is a runner's ordinary - # state, and `set -e` made that the document's verdict — `sbom-ntia-conformance` + # state, and `set -e` made that the document's verdict — `manifest cover partial` # red over a document sbomcheck had just passed. The receipt is a cache written # after the answer, so its failure costs the next hook a scan and nothing else. : >"$BATS_TEST_TMPDIR/receipt.fails" @@ -422,23 +422,23 @@ EOF # ─── CLOUD-631: the promotion, asserted over the committed bytes ────────────── -@test "THE PROMOTION: the committed batten.toml declares deny on sbom-ntia-conformance" { +@test "THE PROMOTION: the committed batten.toml declares deny on manifest cover partial" { # Asserted over the bytes rather than inferred from behaviour, so the row cannot # be quietly relaxed later — `config-lint`s weakening class covers that shape, # and this pins the value the promotion set. local toml="$BATS_TEST_DIRNAME/../batten.toml" - run awk '/^id = "sbom-ntia-conformance"$/ { found = 1 } + run awk '/^id = "manifest cover partial"$/ { found = 1 } found && /^severity = / { print; exit }' "$toml" [ "$status" -eq 0 ] [ "$output" = 'severity = "deny"' ] } @test "the precondition row is STILL deny, and the two are not the same question" { - # `sbom-ntia-precondition` answers "could we look" and was always deny; the + # `manifest check unread` answers "could we look" and was always deny; the # promotion moves the verdict row only. A change that collapsed them would make # an unresolvable checker indistinguishable from a nonconformant document. local toml="$BATS_TEST_DIRNAME/../batten.toml" - run awk '/^id = "sbom-ntia-precondition"$/ { found = 1 } + run awk '/^id = "manifest check unread"$/ { found = 1 } found && /^severity = / { print; exit }' "$toml" [ "$status" -eq 0 ] [ "$output" = 'severity = "deny"' ] diff --git a/tests/release-tracking-check.bats b/tests/release-tracking-check.bats index 06b123714..918db64e0 100644 --- a/tests/release-tracking-check.bats +++ b/tests/release-tracking-check.bats @@ -1,6 +1,6 @@ #!/usr/bin/env bats -# subject: mise-tasks/release-tracking-check.sh .github/workflows/release-plz.yml .github/workflows/linear-release-backfill.yml -# release-tracking-check's decision table (CLOUD-618). +# subject: mise-tasks/release wire missing.sh .github/workflows/release-plz.yml .github/workflows/linear-release-backfill.yml +# release wire missing's decision table (CLOUD-618). # # Every case below is a shape that leaves a job GREEN while a shipped tag fails to # reach Linear, which is what makes them worth a gate at all — a dropped @@ -21,7 +21,7 @@ # nothing. setup() { - GATE="$BATS_TEST_DIRNAME/../mise-tasks/release-tracking-check.sh" + GATE="$BATS_TEST_DIRNAME/../mise-tasks/release wire missing.sh" WORKFLOW="$BATS_TEST_TMPDIR/release-plz.yml" BACKFILL="$BATS_TEST_TMPDIR/linear-release-backfill.yml" clean_workflow >"$WORKFLOW" diff --git a/tests/remedy-payload-source.bats b/tests/remedy-payload-source.bats index a81b003ed..d672d55db 100644 --- a/tests/remedy-payload-source.bats +++ b/tests/remedy-payload-source.bats @@ -31,9 +31,9 @@ setup() { # PREDICATE is unchanged and that is the point: this suite asks whether the # message names a source that works on any host, and where the message lives is # not what it is about. - READ_GUARD=$(awk '/^id = "an-update-owes-a-recent-read"/{f=1} f&&/^reason = """/{c=1} c{print} c&&/"""$/&&!/^reason/{exit}' batten.toml) - SEARCH_GUARD=$(awk '/^id = "filing-needs-a-search"/{f=1} f&&/^reason = """/{c=1} c{print} c&&/"""$/&&!/^reason/{exit}' batten.toml) - CLAIM_ROW=$(awk '/^id = "claim-needs-receipt"/{f=1} f&&/^reason = """/{c=1} c{print} c&&/"""$/&&!/^reason/{exit}' batten.toml) + READ_GUARD=$(awk '/^id = "issue read stale"/{f=1} f&&/^reason = """/{c=1} c{print} c&&/"""$/&&!/^reason/{exit}' batten.toml) + SEARCH_GUARD=$(awk '/^id = "issue list unread"/{f=1} f&&/^reason = """/{c=1} c{print} c&&/"""$/&&!/^reason/{exit}' batten.toml) + CLAIM_ROW=$(awk '/^id = "claim read unread"/{f=1} f&&/^reason = """/{c=1} c{print} c&&/"""$/&&!/^reason/{exit}' batten.toml) ABSENT=$(awk '/no readable transcript/,/^fi$/' mise-tasks/board-payloads.sh) } diff --git a/tests/serena-mcp.bats b/tests/serena-mcp.bats index 8b4da6fbc..fe7050a63 100644 --- a/tests/serena-mcp.bats +++ b/tests/serena-mcp.bats @@ -98,7 +98,7 @@ shim() { (cd "$REPO" && "$SHIM" "$@"); } @test "the launch args stay in .mcp.json, so the pin gate still reads them" { # The shim moved the COMMAND and deliberately not the args. If the pinned, - # scoped `mise exec` argv migrated into this script, `mise-pin-agreement` + # scoped `mise exec` argv migrated into this script, `pin declare wrong` # would report a clean pass over a file that no longer carries a pin. run jq -r '.mcpServers.serena.args | join(" ")' "$BATS_TEST_DIRNAME/../.mcp.json" [[ "$output" == "exec pipx:serena-agent@"*" -- serena start-mcp-server"* ]] diff --git a/tests/signing-posture.bats b/tests/signing-posture.bats index dbe97a213..058958936 100644 --- a/tests/signing-posture.bats +++ b/tests/signing-posture.bats @@ -17,7 +17,7 @@ setup() { REPO="$BATS_TEST_TMPDIR/repo" mkdir -p "$REPO" # `main` from the start, so no row has to force a branch into place later: - # `no-branch-f-main` forbids that shape in a suite, and rightly — a `branch -f` + # `branch edit unsafe` forbids that shape in a suite, and rightly — a `branch -f` # that escaped the fixture would move the real trunk. git -C "$REPO" init --quiet --initial-branch=main # Per fixture, never inherited: a CI runner carries no global identity. diff --git a/tests/spawn-census.bats b/tests/spawn-census.bats index c4e495cf3..408f15b01 100644 --- a/tests/spawn-census.bats +++ b/tests/spawn-census.bats @@ -37,7 +37,7 @@ setup() { # The PINNED toolchain, resolved once from the repository that pins it. The # toy crate lives outside this tree, so a bare `cargo` there would resolve to - # whatever is ambient — which is the exact defect `no-bare-cargo` refuses, and + # whatever is ambient — which is the exact defect `cargo run loose` refuses, and # the exact defect this suite is about. CARGO=$(cd "$BATS_TEST_DIRNAME/.." && mise which cargo) [ -x "$CARGO" ] || skip "no pinned cargo to drive clippy with" @@ -175,7 +175,7 @@ toy_clippy() { @test "at warn the gate reports clean, and at deny the same source is refused" { # CLOUD-822's measurement, reproduced as the argument for where the level - # lives. The escape `no-bare-cargo`'s own refusal text recommends omits + # lives. The escape `cargo run loose`'s own refusal text recommends omits # `-D warnings`; under it a lint left at `warn` reports clean over an # unannotated spawn, and the agent then quotes the clean run as verification. # diff --git a/tests/zizmor-split.bats b/tests/zizmor-split.bats index 0883408e9..cc8b88dba 100644 --- a/tests/zizmor-split.bats +++ b/tests/zizmor-split.bats @@ -8,7 +8,7 @@ # dropping `--offline` restores a green-looking gate whose verdict depends on # api.github.com, and nothing else in the tree would notice until a rate limit # stopped a landing again. So the split is pinned as text, the same way -# `hk-version` and `mise-pin-agreement` pin agreements no runtime check reaches. +# `hk-version` and `pin declare wrong` pin agreements no runtime check reaches. # # The measurement behind it: two laps of one `land` run over an unchanged tree — # `No findings to report`, then `403 Forbidden` on From 98e2c1704553ffef50ad9c8887016e254e817b25 Mon Sep 17 00:00:00 2001 From: Alec Wenzowski Date: Wed, 9 Sep 2026 18:40:11 +0000 Subject: [PATCH 10/23] feat(policy)!: admit a rename rewrite in a governed shell file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The gate change CLOUD-1638 needed, and the owner authorised. THE DEADLOCK. `tests/ntia-check.bats` and `tests/remedy-payload-source.bats` slice the authority with `awk '/^id = "sbom-ntia-conformance"/'`. Putting the rule ids in the three-word grammar breaks eight of their cases, and fixing them trips `shell edit refused` — a class with no override route and no `bypass_env`, whose one admitted edit covers REMOVALS only. So the campaign mandated an edit it was structurally unable to land, which is `only_drops_a_retired_reference`'s own words for why that arm exists. This is the same shape one axis over: there, a deleted path; here, a renamed id. THE NARROWING IS EXACT AND NOT A JUDGEMENT. Every removed line must pair with an added line that is the same bytes either side of ONE span; that span must become an id the authority declares; and what it replaced must not be one. A branch renaming no rule has no admitted rewrite, and an added line with no removed counterpart is refused, so this cannot become a licence to maintain a shell rule in place. NO SUBSTRING ENUMERATION, which is what makes it decidable. The new id is known — the authority declares it — so finding it in the added line fixes the prefix and suffix, and the token it replaced is whatever the removed line carries between the same two. Nothing is guessed, which is the arm the model verdict non-negotiable rule 3 would otherwise forbid. FOUR THINGS WERE WRONG BEFORE IT HELD, and three of them passed their own tests: - `replace` is not a builtin the shipped engine implements, so the body was UNDEFINED rather than false and both negative cases passed vacuously while the positive one failed. `drops_a_retired_name` above already does span surgery with `indexof`/`substring`; this now does the same. - The symmetry check ran the predicate backwards, looking for the OLD token in a line that by construction carries the new one. Asked as "has an origin". - It read `delta["base-lines"]["batten.toml"]`, which the tree surface populates only for the paths this module governs — `mise-tasks/**` and `tests/**`. Undefined on every real tree; green on its own fixture. - And `input.tree.documents` is populated only for a row that names its source, so `shell retire partial` now declares `sources = ["batten.toml"]`. The middle two are the same failure as the first: a gate that cannot fire while reporting that it did. Each was caught by running the real tree, never by the synthetic case. `//MUTANT rename-rewrite-unchecked` disables the not-a-live-id conjunct against `a_rewrite_naming_an_unrenamed_token_is_refused`. `policy-test` 853/853, `batten-check` clean. Refs: CLOUD-1638 Admits: c93e2c4fa50cae76bba7e9da076e4cc233628c20991647994998b6be8b31afd5 Admits-rule: protected-mutation Admits-verdict: path write refused Admits-subject: batten.toml Admits-anchor: call:c513f3350817d98f31b1610e3ce963709519a4c2 Admits-epoch: 3846c0511093d12c174aab4991ed6152b84e97ab9ceba6c92f7e1155783cb4a6 Admits-author: alec@wenzowski.com Admits-prev: cf13e9608d91a0ca386d6ae8d41c650d00ff3e5a5649388e369eefc1e80ca57c Admits-answer-lost: CLOUD-1638 cannot land. The migration renames four rule ids that two frozen bats suites slice by literal, fixing them trips `shell edit refused`, and the arm the owner authorised to admit that fix cannot see the config it must consult. Admits-answer-precondition: No surface can express this change. `config read first` reads batten.toml and `patch run first` restores it; neither writes. The `shell retire partial` row must declare `sources = ["batten.toml"]` or the rename-rewrite admission this branch adds is undefined on every real tree — `input.tree.documents` is populated only for a row that names its source. One line, reviewed in the PR closing CLOUD-1638. Admits-answer-rejected-route: I rejected `patch run first`: restoring batten.toml reverts the migration rather than completing it. `config read first` does not apply — reading is what established that `documents` is unpopulated without this declaration, and it changes nothing. --- batten.toml | 6 ++ mise-tasks/sbom.sh | 4 +- policy/shell-retirement.rego | 139 ++++++++++++++++++++++++++++++ tests/batten-glob-check.bats | 2 +- tests/closing-key-check.bats | 6 +- tests/hk-selection.bats | 8 +- tests/landed-check.bats | 2 +- tests/ntia-check.bats | 6 +- tests/release-tracking-check.bats | 6 +- tests/serena-mcp.bats | 2 +- tests/signing-posture.bats | 2 +- tests/spawn-census.bats | 4 +- tests/zizmor-split.bats | 2 +- 13 files changed, 167 insertions(+), 22 deletions(-) diff --git a/batten.toml b/batten.toml index ff926316c..11071192e 100644 --- a/batten.toml +++ b/batten.toml @@ -5853,6 +5853,12 @@ id = "shell retire partial" kind = "policy" scope = "tree" base = "origin/main" +# THE PARSED AUTHORITY, for the rename-rewrite admission (CLOUD-1638). That arm +# asks whether a token a governed file started naming is a rule id this config +# declares, and `input.tree.documents` is populated only for a row that names +# its source — without this the arm is undefined on every real tree while its +# own synthetic cases pass, which is exactly how it first shipped. +sources = ["batten.toml"] # THE SELECTOR IS `**` FOR THE FOURTH ARM (CLOUD-1080), and it is the same # correction `prose-only` records one row down. What this row GOVERNS is unchanged # — `governed_at_head` and `governed_when_deleted` still select `mise-tasks/**` and diff --git a/mise-tasks/sbom.sh b/mise-tasks/sbom.sh index 5d6476955..8a0fde450 100755 --- a/mise-tasks/sbom.sh +++ b/mise-tasks/sbom.sh @@ -90,7 +90,7 @@ OUT_DIR="${SBOM_OUT_DIR:-sbom}" # harness, no more part of the shipped artifact than `tests/` is — so cataloging # it would make the inventory overstate what a consumer receives, which is the # false claim this document exists not to make. It would also break the -# `manifest count other` invariant by construction: that clause compares the +# `sbom-package-drift` invariant by construction: that clause compares the # cargo count against the ROOT `Cargo.lock`, and a second lockfile in scope adds # packages no root lockfile names (measured: 175 -> 281). readonly EXCLUDES=(--exclude ./tests/bats --exclude ./target --exclude ./fuzz) @@ -349,7 +349,7 @@ copyright_of() { # MATCHED ON THE REPO, NOT THE SHA, and that is forced rather than chosen: syft # keys these components by the `# vX` comment beside the pin, not by the pin # itself — `pkg:github/actions/checkout@v7` for a component whose `uses:` line -# resolves to `3d3c42e5…`. The sha in the table is what `pin table missing` +# resolves to `3d3c42e5…`. The sha in the table is what `sbom-action-unmapped` # compares against the workflows, so drift is still caught at the pin; using it # here would match nothing. ACTIONS_TABLE="${SBOM_ACTIONS_TABLE:-$(cd "$(dirname "$0")" && pwd)/sbom-actions.tsv}" diff --git a/policy/shell-retirement.rego b/policy/shell-retirement.rego index b770bd7d2..936d59546 100644 --- a/policy/shell-retirement.rego +++ b/policy/shell-retirement.rego @@ -282,6 +282,7 @@ violation contains { some path in delta.edited governed_at_head(path) not only_drops_a_retired_reference(path) + not only_rewrites_a_renamed_id(path) } # THE ONE ADMITTED EDIT, and it is what makes this campaign able to clean up @@ -2490,3 +2491,141 @@ test_an_invocation_field_is_not_a_successor if { # and it would have passed over a module refusing for a different reason. v.verdict == "shell port missing" } + +# THE SECOND ADMITTED EDIT, and it exists for the sibling's reason one axis over +# (CLOUD-1638). +# +# `only_drops_a_retired_reference` admits a governed file dropping a reference to +# a path THIS delta deleted. The same deadlock arrives when the delta renames a +# `[[rule]] id` rather than deleting a file: `tests/ntia-check.bats` and +# `tests/remedy-payload-source.bats` slice the authority with +# `awk '/^id = "sbom-ntia-conformance"/'`, so putting the rule ids in the +# three-word grammar breaks eight cases the branch is then refused permission to +# fix. `shell edit refused` declares no override route and no `bypass_env`, so +# again the campaign mandates an edit it cannot land. +# +# THE NARROWING IS EXACT AND NOT A JUDGEMENT. Every removed line must pair with +# an added line that is the SAME BYTES either side of one span, that span must +# become an id the authority declares, and what it replaced must not be one. A +# branch renaming no rule has no admitted rewrite; an added line with no removed +# counterpart is refused; so this cannot become a licence to maintain a shell +# rule in place, which is the whole reason the arm above exists. +# +# COULD-NOT-LOOK REFUSES, for the sibling's reason: an unreadable base side makes +# the removed set unknowable, the admission does not hold, and the edit is +# refused. That is the safe direction for an arm whose failure mode is a silent +# licence. +#MUTANT rename-rewrite-unchecked|s@^ not old in head_rule_ids$@ true@|a_rewrite_naming_an_unrenamed_token_is_refused +only_rewrites_a_renamed_id(path) if { + base := delta["base-lines"][path] + base_set := {line | some line in base} + head := {line | some line in input.tree.lines[path]} + removed := {line | some line in base_set; not line in head} + added := {line | some line in head; not line in base_set} + + # An edit that removed nothing is not this case, and one that added nothing + # is the sibling's. + count(removed) > 0 + count(added) > 0 + + # Every removed line becomes one of the added ones by a rename. + count({line | some line in removed; rewritten_by_a_rename(line, added)}) == count(removed) + + # AND EVERY ADDED LINE HAS AN ORIGIN, so nothing rides along. Asked this way + # round rather than by running the predicate backwards: an added line carries + # the NEW id, so looking for the old token in it can never hold, and that + # inversion made the positive case fail while its two negatives passed. + count({ + line | + some line in added + some was in removed + rewritten_by_a_rename(was, {line}) + }) == count(added) +} + +# One line is the other with a declared rule id put where a non-id token was. +# +# READ OFF THE HEAD DOCUMENT, not off `base-lines`. The tree surface populates +# `base-lines` for the paths this module governs — `mise-tasks/**` and +# `tests/**` — and `batten.toml` is neither, so a first version asking what the +# authority declared AT BASE was undefined on every real tree while passing its +# own synthetic case. `input.tree.documents["batten.toml"].rule` is the reader +# two other modules already use, and it is populated. +# +# NO SUBSTRING ENUMERATION, which is what makes this decidable. The NEW id is +# known — the authority declares it — so finding it in the added line fixes the +# prefix and the suffix, and the token it replaced is whatever the removed line +# carries between the same two. Nothing is guessed. +# +# SPLIT-AND-REJOIN, not `replace`: `drops_a_retired_name` above already does span +# surgery with `indexof` and `substring` for the reason a repository gate must — +# `replace` is not a builtin the shipped engine implements, so a body using it is +# UNDEFINED rather than false, and its negative cases then pass vacuously. +rewritten_by_a_rename(was, others) if { + some other in others + some new in head_rule_ids + at := indexof(other, new) + at >= 0 + before := substring(other, 0, at) + after := substring(other, at + count(new), -1) + + # The removed line agrees byte for byte either side of the span. + startswith(was, before) + endswith(was, after) + count(was) >= count(before) + count(after) + + # And what it carried there was NOT itself a live id, so this admits a rename + # and not a rewrite from one live name to another. + old := substring(was, count(before), (count(was) - count(before)) - count(after)) + count(old) > 0 + not old in head_rule_ids +} + +# Every rule id the committed authority declares at head. +head_rule_ids contains id if { + some row in input.tree.documents["batten.toml"].rule + id := row.id +} + +# THE RENAME-REWRITE ARM, over a synthetic input. +test_a_rename_rewrite_is_admitted if { + only_rewrites_a_renamed_id("tests/probe.bats") with input as rename_input( + "\trun awk '/^id = \"old-kebab-name\"/'", + "\trun awk '/^id = \"new three words\"/'", + ) +} + +# ANTI-VACUITY: a rewrite naming a token the authority does not declare is +# refused, which is the whole narrowing. +test_a_rewrite_naming_an_unrenamed_token_is_refused if { + not only_rewrites_a_renamed_id("tests/probe.bats") with input as rename_input( + "\trun awk '/^id = \"unrelated-token\"/'", + "\trun awk '/^id = \"also-unrelated\"/'", + ) +} + +# AND NOTHING RIDES ALONG: an added line with no removed counterpart is refused. +test_an_unrelated_addition_is_refused if { + not only_rewrites_a_renamed_id("tests/probe.bats") with input as object.union( + rename_input( + "\trun awk '/^id = \"old-kebab-name\"/'", + "\trun awk '/^id = \"new three words\"/'", + ), + {"tree": {"lines": {"tests/probe.bats": [ + "\trun awk '/^id = \"new three words\"/'", + "\techo smuggled", + ]}}}, + ) +} + +# One edited governed path, and the authority declaring the id it now names. +rename_input(base_line, head_line) := {"tree": { + "lines": {"tests/probe.bats": [head_line]}, + "documents": {"batten.toml": {"rule": [{"id": "new three words"}]}}, + "base-delta": { + "added": [], + "deleted": [], + "edited": ["tests/probe.bats"], + "base-lines": {"tests/probe.bats": [base_line]}, + }, +}} diff --git a/tests/batten-glob-check.bats b/tests/batten-glob-check.bats index 92581226f..7afde66ea 100644 --- a/tests/batten-glob-check.bats +++ b/tests/batten-glob-check.bats @@ -111,7 +111,7 @@ hooks_with() { @test "a config the gate parses nothing out of is exit 2, not a pass" { # The vacuous green a containment check produces most easily: parse zero # requirements and every list covers them. Distinguished from a violation, - # the same way a missing lockfile is in lock cover partial. + # the same way a missing lockfile is in lock-complete. printf '[epoch]\ntracked = ["batten.toml"]\n' >"$CONFIG" hooks_with "crates/**" diff --git a/tests/closing-key-check.bats b/tests/closing-key-check.bats index 9f4fadf86..25a4498cf 100644 --- a/tests/closing-key-check.bats +++ b/tests/closing-key-check.bats @@ -148,12 +148,12 @@ Refs: CLOUD-593, CLOUD-344, CLOUD-661, CLOUD-103' } @test "a body naming no key at all is the key rule's case, not this one" { - # One rule, one authority. The engine's `review name unnamed` row judges this + # One rule, one authority. The engine's `pr-names-an-issue` row judges this # at `gh pr create`, which is earlier and cheaper. It was `issue-guard` until # CLOUD-446 retired that program. run bash -c "printf 'A body with no key.\n' | $GATE" [ "$status" -eq 0 ] - [[ "$output" == *"review name unnamed rule owns that case"* ]] + [[ "$output" == *"pr-names-an-issue rule owns that case"* ]] } @test "one closed key is enough, even beside a named-but-unclosed one" { @@ -171,7 +171,7 @@ Refs: CLOUD-593, CLOUD-344, CLOUD-661, CLOUD-103' # boundary deleted. run bash -c "printf 'SUBCLOUD-17 is a different system.\n' | $GATE" [ "$status" -eq 0 ] - [[ "$output" == *"review name unnamed rule owns that case"* ]] + [[ "$output" == *"pr-names-an-issue rule owns that case"* ]] } @test "several named keys are each reported, in stable numeric order" { diff --git a/tests/hk-selection.bats b/tests/hk-selection.bats index cd12ad353..53e64dac9 100644 --- a/tests/hk-selection.bats +++ b/tests/hk-selection.bats @@ -39,7 +39,7 @@ status_of() { @test "every path selects batten-check once a rule globs the whole tree" { # SUPERSEDES "a Markdown file that is not an input does not select # batten-check", which asserted `skipped` for README.md on the premise that - # it "carries no rule glob and is in no budget". CLOUD-59's `source carry unsafe` row + # it "carries no rule glob and is in no budget". CLOUD-59's `no-secrets` row # globs `**`, so that premise is simply false now: a credential can be in # README.md, and narrowing the rule would be choosing which files are # allowed to carry one. @@ -82,12 +82,12 @@ status_of() { } @test "every non-crates path a batten.toml rule globs selects batten-check" { - # mise.toml (pin add unsafe), workflows (cargo add loose), - # tests/*.bats (bats count dropped). These are the ones the issue's + # mise.toml (no-source-built-tool), workflows (no-cargo-install-in-ci), + # tests/*.bats (bats-tests-not-deleted). These are the ones the issue's # proposed glob would have dropped. [ "$(status_of batten-check mise.toml)" = "included" ] [ "$(status_of batten-check .github/workflows/ci.yml)" = "included" ] - [ "$(status_of batten-check tests/lock cover partial.bats)" = "included" ] + [ "$(status_of batten-check tests/lock-complete.bats)" = "included" ] } @test "the embedded budget path selects batten-check" { diff --git a/tests/landed-check.bats b/tests/landed-check.bats index 8d0fd9a93..7798ba7a8 100644 --- a/tests/landed-check.bats +++ b/tests/landed-check.bats @@ -29,7 +29,7 @@ setup() { # that is currently checked out, so the same line failed outright the moment # a developer's default was the trunk's own name. Naming the branch makes the # topology explicit instead of inheriting it, and `main` is then a fresh name - # needing no force at all. `branch edit unsafe` in batten.toml keeps the old + # needing no force at all. `no-branch-f-main` in batten.toml keeps the old # form out; the literal is not spelled here, because that row is a substring # rule over this directory and would fire on its own explanation. git init -q -b work "$REPO" diff --git a/tests/ntia-check.bats b/tests/ntia-check.bats index de1bbac42..560f56229 100644 --- a/tests/ntia-check.bats +++ b/tests/ntia-check.bats @@ -207,7 +207,7 @@ EOF @test "a receipt that cannot be written is reported, never a nonconformance" { # THE FALSE VERDICT CI REPORTED (CLOUD-631). `batten receipt record` exits 1 # where the configured transcript is unreadable, which is a runner's ordinary - # state, and `set -e` made that the document's verdict — `manifest cover partial` + # state, and `set -e` made that the document's verdict — `sbom-ntia-conformance` # red over a document sbomcheck had just passed. The receipt is a cache written # after the answer, so its failure costs the next hook a scan and nothing else. : >"$BATS_TEST_TMPDIR/receipt.fails" @@ -422,7 +422,7 @@ EOF # ─── CLOUD-631: the promotion, asserted over the committed bytes ────────────── -@test "THE PROMOTION: the committed batten.toml declares deny on manifest cover partial" { +@test "THE PROMOTION: the committed batten.toml declares deny on sbom-ntia-conformance" { # Asserted over the bytes rather than inferred from behaviour, so the row cannot # be quietly relaxed later — `config-lint`s weakening class covers that shape, # and this pins the value the promotion set. @@ -434,7 +434,7 @@ EOF } @test "the precondition row is STILL deny, and the two are not the same question" { - # `manifest check unread` answers "could we look" and was always deny; the + # `sbom-ntia-precondition` answers "could we look" and was always deny; the # promotion moves the verdict row only. A change that collapsed them would make # an unresolvable checker indistinguishable from a nonconformant document. local toml="$BATS_TEST_DIRNAME/../batten.toml" diff --git a/tests/release-tracking-check.bats b/tests/release-tracking-check.bats index 918db64e0..06b123714 100644 --- a/tests/release-tracking-check.bats +++ b/tests/release-tracking-check.bats @@ -1,6 +1,6 @@ #!/usr/bin/env bats -# subject: mise-tasks/release wire missing.sh .github/workflows/release-plz.yml .github/workflows/linear-release-backfill.yml -# release wire missing's decision table (CLOUD-618). +# subject: mise-tasks/release-tracking-check.sh .github/workflows/release-plz.yml .github/workflows/linear-release-backfill.yml +# release-tracking-check's decision table (CLOUD-618). # # Every case below is a shape that leaves a job GREEN while a shipped tag fails to # reach Linear, which is what makes them worth a gate at all — a dropped @@ -21,7 +21,7 @@ # nothing. setup() { - GATE="$BATS_TEST_DIRNAME/../mise-tasks/release wire missing.sh" + GATE="$BATS_TEST_DIRNAME/../mise-tasks/release-tracking-check.sh" WORKFLOW="$BATS_TEST_TMPDIR/release-plz.yml" BACKFILL="$BATS_TEST_TMPDIR/linear-release-backfill.yml" clean_workflow >"$WORKFLOW" diff --git a/tests/serena-mcp.bats b/tests/serena-mcp.bats index fe7050a63..8b4da6fbc 100644 --- a/tests/serena-mcp.bats +++ b/tests/serena-mcp.bats @@ -98,7 +98,7 @@ shim() { (cd "$REPO" && "$SHIM" "$@"); } @test "the launch args stay in .mcp.json, so the pin gate still reads them" { # The shim moved the COMMAND and deliberately not the args. If the pinned, - # scoped `mise exec` argv migrated into this script, `pin declare wrong` + # scoped `mise exec` argv migrated into this script, `mise-pin-agreement` # would report a clean pass over a file that no longer carries a pin. run jq -r '.mcpServers.serena.args | join(" ")' "$BATS_TEST_DIRNAME/../.mcp.json" [[ "$output" == "exec pipx:serena-agent@"*" -- serena start-mcp-server"* ]] diff --git a/tests/signing-posture.bats b/tests/signing-posture.bats index 058958936..dbe97a213 100644 --- a/tests/signing-posture.bats +++ b/tests/signing-posture.bats @@ -17,7 +17,7 @@ setup() { REPO="$BATS_TEST_TMPDIR/repo" mkdir -p "$REPO" # `main` from the start, so no row has to force a branch into place later: - # `branch edit unsafe` forbids that shape in a suite, and rightly — a `branch -f` + # `no-branch-f-main` forbids that shape in a suite, and rightly — a `branch -f` # that escaped the fixture would move the real trunk. git -C "$REPO" init --quiet --initial-branch=main # Per fixture, never inherited: a CI runner carries no global identity. diff --git a/tests/spawn-census.bats b/tests/spawn-census.bats index 408f15b01..c4e495cf3 100644 --- a/tests/spawn-census.bats +++ b/tests/spawn-census.bats @@ -37,7 +37,7 @@ setup() { # The PINNED toolchain, resolved once from the repository that pins it. The # toy crate lives outside this tree, so a bare `cargo` there would resolve to - # whatever is ambient — which is the exact defect `cargo run loose` refuses, and + # whatever is ambient — which is the exact defect `no-bare-cargo` refuses, and # the exact defect this suite is about. CARGO=$(cd "$BATS_TEST_DIRNAME/.." && mise which cargo) [ -x "$CARGO" ] || skip "no pinned cargo to drive clippy with" @@ -175,7 +175,7 @@ toy_clippy() { @test "at warn the gate reports clean, and at deny the same source is refused" { # CLOUD-822's measurement, reproduced as the argument for where the level - # lives. The escape `cargo run loose`'s own refusal text recommends omits + # lives. The escape `no-bare-cargo`'s own refusal text recommends omits # `-D warnings`; under it a lint left at `warn` reports clean over an # unannotated spawn, and the agent then quotes the clean run as verification. # diff --git a/tests/zizmor-split.bats b/tests/zizmor-split.bats index cc8b88dba..0883408e9 100644 --- a/tests/zizmor-split.bats +++ b/tests/zizmor-split.bats @@ -8,7 +8,7 @@ # dropping `--offline` restores a green-looking gate whose verdict depends on # api.github.com, and nothing else in the tree would notice until a rate limit # stopped a landing again. So the split is pinned as text, the same way -# `hk-version` and `pin declare wrong` pin agreements no runtime check reaches. +# `hk-version` and `mise-pin-agreement` pin agreements no runtime check reaches. # # The measurement behind it: two laps of one `land` run over an unchanged tree — # `No findings to report`, then `403 Forbidden` on From 22ba7c994f916b01fce784c0f2a01e711898c319 Mon Sep 17 00:00:00 2001 From: Alec Wenzowski Date: Wed, 9 Sep 2026 19:34:46 +0000 Subject: [PATCH 11/23] fix(policy): carry the grammar to what main added under the branch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rebase brought two arms this branch had never seen. `policy/run-shape.rego` gained `foreground-mise` and `background-redirect`, both hyphenated, and the grammar this branch installs refuses a name that is not exactly three words. Each is renamed to the class it already raises — `task run blocked` and `redirect write unread` — which is the convention every other arm in that module follows: the finding id and its verdict are the same token, and both classes are already declared. `crates/batten/tests/it/agentic_record.rs` was rewritten on main and hand-rolls a `policy::Vocabulary`. That struct gained `words` on this branch, so the literal no longer compiles; it takes `None`, matching every other test helper that replays one module rather than the committed table. Refs: CLOUD-1638 --- crates/batten/tests/it/agentic_record.rs | 1 + policy/run-shape.rego | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/batten/tests/it/agentic_record.rs b/crates/batten/tests/it/agentic_record.rs index 17048c3c9..5a47d37b8 100644 --- a/crates/batten/tests/it/agentic_record.rs +++ b/crates/batten/tests/it/agentic_record.rs @@ -434,6 +434,7 @@ fn replayed_findings( batten::policy::Vocabulary { patterns: &[], verdicts, + words: None, recorders: &[], }, dir, diff --git a/policy/run-shape.rego b/policy/run-shape.rego index 9a32fda1a..eeb786c8f 100644 --- a/policy/run-shape.rego +++ b/policy/run-shape.rego @@ -67,9 +67,9 @@ rules contains "timer run refused" rules contains "task watch duplicate" -rules contains "foreground-mise" +rules contains "task run blocked" -rules contains "background-redirect" +rules contains "redirect write unread" # CLOUD-613's three, and none of them is over a program NAME — a mutation on the # `sleep` or `git` token survives, because every ALLOW row already fails some @@ -205,7 +205,7 @@ violation contains { # to stop consulting. A predicate with no list cannot be argued with, which is # the property (house style §5). violation contains { - "rule": "foreground-mise", + "rule": "task run blocked", "verdict": "task run blocked", } if { some program in input.call.programs @@ -234,7 +234,7 @@ violation contains { # INPUT redirect is untouched: reading a file into a backgrounded command # discards nothing. violation contains { - "rule": "background-redirect", + "rule": "redirect write unread", "verdict": "redirect write unread", } if { input.call["run-in-background"] == true From 58093e611f8b0d6ce41e2b56e23644d3851bcc3a Mon Sep 17 00:00:00 2001 From: Alec Wenzowski Date: Wed, 9 Sep 2026 19:42:34 +0000 Subject: [PATCH 12/23] fix(policy): finish the grammar over what main added, tests and prose too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `policy/run-shape.rego`'s own cases still asserted `v.rule ==` the hyphenated names, and `policy/ci-cache-declared.rego` gained a case on main asserting `read-family-has-a-warm-writer`, which this branch renamed to `job read empty` — four `policy test` failures, all of them a stale assertion rather than a changed decision. The prose mentions move with them. A backticked `foreground-mise` in AGENTS.md, `batten.toml`, `verdict.rs` and four test files now names an id no surface declares, which is the drift this migration exists to remove; the fixture DIRECTORY names keep their spelling, because a directory is not a name in the grammar. Refs: CLOUD-1638 Admits: 6c51184a2512725bf1ff1285e894ec5e817a6bfe718f4cc830fc74fc1459b1fc Admits-rule: protected-mutation Admits-verdict: path write refused Admits-subject: batten.toml Admits-anchor: call:efc2676b32014299359667de1e8145b02c1c0eae Admits-epoch: bbb3402be6a232c795117be523a3a0849ce538fa107910a987530992e70c69a7 Admits-author: alec@wenzowski.com Admits-prev: c93e2c4fa50cae76bba7e9da076e4cc233628c20991647994998b6be8b31afd5 Admits-answer-lost: The authority's commentary would name two ids that do not exist, which is precisely the drift CLOUD-1638 exists to remove, and it would name them in the file that defines the grammar refusing them. Admits-answer-precondition: No surface can express this change: `batten.toml` carries the [[rule]] and [[verdict]] tables as prose comments that name findings by id, and main added `foreground-mise` and `background-redirect` under this branch. The grammar this branch installs refuses a name that is not exactly three words, so two backticked mentions in the authority's own commentary now point at ids no surface declares. The edit is two token substitutions inside comment prose, visible in the diff it lands in. Admits-answer-rejected-route: `config read first` does not apply: this is not a read that skipped the authority, it is a write to it. `patch run first` does not apply either: it addresses a patch applied before the config was consulted, and here the config IS the subject, with the whole change computed from the ids the same file declares. --- AGENTS.md | 4 ++-- batten.toml | 4 ++-- crates/batten/src/verdict.rs | 2 +- crates/batten/tests/it/cli.rs | 4 ++-- crates/batten/tests/it/gh_guard.rs | 4 ++-- crates/batten/tests/it/hook_skip_local.rs | 2 +- crates/batten/tests/it/pipeline_shapes.rs | 14 +++++++------- crates/batten/tests/it/run_shape.rs | 2 +- policy/ci-cache-declared.rego | 2 +- policy/run-shape.rego | 8 ++++---- 10 files changed, 23 insertions(+), 23 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 4fb90f2d3..6a0cebdda 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -126,7 +126,7 @@ to green — a red run means verify was skipped. ## Background the slow path; never block the foreground **EVERY `mise` call is backgrounded** (`run_in_background`), **and so is anything -else past ~2 minutes**. Gated — `sleep` is blocked, `foreground-mise` the rest, and +else past ~2 minutes**. Gated — `sleep` is blocked, `task run blocked` the rest, and a foreground command is _killed_ at ~2 min. **No fast list, `alive` included.** **The exit notification IS the wake-up; waiting for it costs nothing.** A backgrounded task re-invokes you when it exits (measured 523/524, failures @@ -141,7 +141,7 @@ by `run-shape-guard`. To ask what a live task is _doing_, `mise run alive`. a pager (the exit status becomes the pager's) or detaching it with `nohup`/`&` (the wake-up is lost). Put `run_in_background` on the long command, never a launcher, and **never redirect it** — the harness captures where the HUMAN watches, so `>log -2>&1` writes where nobody reads. `verdict-not-discarded`, `background-redirect`. +2>&1` writes where nobody reads. `verdict-not-discarded`, `redirect write unread`. **Never** use a foreground `sleep`, spin a foreground busy-poll, or end a turn idle "to watch" something — background it, act on its exit, and commit first, since **committed-and-pushed is the only state surviving a reclaim, and that is the TREE's diff --git a/batten.toml b/batten.toml index 11071192e..af6f2298b 100644 --- a/batten.toml +++ b/batten.toml @@ -3312,8 +3312,8 @@ unredirected: The tool result IS the verdict, and the harness captures the output to a file it \ names back — read that in a SEPARATE call. THIS TEXT USED TO PRESCRIBE \ `>/tmp/.log 2>&1` AND THAT FORM IS NOW REFUSED from both sides: \ -`foreground-mise` because the harness kills a foreground call at ~2 minutes, and \ -`background-redirect` because a private log file is one nobody reads while the \ +`task run blocked` because the harness kills a foreground call at ~2 minutes, and \ +`redirect write unread` because a private log file is one nobody reads while the \ pane the human watches stays empty. A pager over a FILE is fine; a pager over a \ live task is not.""" diff --git a/crates/batten/src/verdict.rs b/crates/batten/src/verdict.rs index 1138ba3a5..0b01d1a25 100644 --- a/crates/batten/src/verdict.rs +++ b/crates/batten/src/verdict.rs @@ -1851,7 +1851,7 @@ checked it -- a class a reader believes is worse than one they cannot look up.", passed or failed. A verdict is read from the harness, never inferred from output. Background \ the command and read the exit code the notification carries; a pager over a FILE is fine, a \ pager over a live task is not. DO NOT REDIRECT INSTEAD -- this text prescribed `> file 2>&1` \ -for its whole life, and `background-redirect` now refuses exactly that, because the harness \ +for its whole life, and `redirect write unread` now refuses exactly that, because the harness \ already captures a backgrounded task's output where the human watches.", routes: &[read("rule read first", "rules/toolchain.md")], applicability: Applicability::Advice, diff --git a/crates/batten/tests/it/cli.rs b/crates/batten/tests/it/cli.rs index 68b73e110..8cb097f6b 100644 --- a/crates/batten/tests/it/cli.rs +++ b/crates/batten/tests/it/cli.rs @@ -231,7 +231,7 @@ fn claude_payload(command: &str) -> String { /// Most hosts send no `run_in_background` at all and the engine projects `null`, /// which is why the plain builder carries no key — that absence is the ordinary /// envelope rather than an omission. This one is for the rows that read the -/// posture: `foreground-mise` refuses an unstated one on the strict side, so a +/// posture: `task run blocked` refuses an unstated one on the strict side, so a /// case asserting the allowed shape has to say so out loud. fn claude_payload_backgrounded(command: &str) -> String { serde_json::json!({ @@ -3506,7 +3506,7 @@ fn the_committed_shape_rules_fire_on_every_banned_shape() { ); // THE `mise` CALLS MOVED TO THE BACKGROUNDED FORM, and the move is the rule - // rather than an accommodation of it. `foreground-mise` refuses every + // rather than an accommodation of it. `task run blocked` refuses every // foreground `mise` invocation with no fast list, because the harness kills a // foreground call at ~2 minutes and each of these two can cross that bound // behind a cargo build the caller cannot see coming — `test:cargo` is the diff --git a/crates/batten/tests/it/gh_guard.rs b/crates/batten/tests/it/gh_guard.rs index f98dcf7c3..6ada97b0f 100644 --- a/crates/batten/tests/it/gh_guard.rs +++ b/crates/batten/tests/it/gh_guard.rs @@ -167,7 +167,7 @@ fn allowed(command: &str) { /// [`allowed`], with the call's backgrounding STATED. /// -/// For a command another committed row reads the posture of. `foreground-mise` +/// For a command another committed row reads the posture of. `task run blocked` /// refuses every foreground `mise` invocation with no fast list, so a case whose /// property is something else entirely — wrapper look-through, here — has to /// carry the posture or it measures that row instead of its own. @@ -334,7 +334,7 @@ fn a_task_name_is_not_a_wrapped_program() { // it, so the case names one no lock is ever taken for. That `land` itself is // allowed when unheld is `singleton_gate.rs::an_unheld_task_starts`, where // the lock state is written rather than inherited. - // BACKGROUNDED, because `foreground-mise` now refuses the foreground form of + // BACKGROUNDED, because `task run blocked` now refuses the foreground form of // both and this case is not about that: the property under test is that // `mise run` names a TASK while `mise exec` runs another program, and the // look-through is what decides it. Left foreground, the case would measure diff --git a/crates/batten/tests/it/hook_skip_local.rs b/crates/batten/tests/it/hook_skip_local.rs index 59c94c408..233d2e757 100644 --- a/crates/batten/tests/it/hook_skip_local.rs +++ b/crates/batten/tests/it/hook_skip_local.rs @@ -85,7 +85,7 @@ fn allowed(command: &str) { /// [`allowed`], with the call's backgrounding STATED. /// /// These cases adjudicate against the LIVE root, so every committed row reaches -/// them — `foreground-mise` included, which refuses a foreground `mise` call with +/// them — `task run blocked` included, which refuses a foreground `mise` call with /// no fast list. An anti-vacuity case has to survive on this row's own account /// rather than by another row's silence, so the posture is stated and the /// remaining question is whether THIS row fires. diff --git a/crates/batten/tests/it/pipeline_shapes.rs b/crates/batten/tests/it/pipeline_shapes.rs index 8b4d60ea6..ca805ecbf 100644 --- a/crates/batten/tests/it/pipeline_shapes.rs +++ b/crates/batten/tests/it/pipeline_shapes.rs @@ -64,7 +64,7 @@ fn assert_allowed(command: &str) { /// [`assert_allowed`], with the call's backgrounding STATED. /// -/// These adjudicate against the LIVE root, so `foreground-mise` reaches any case +/// These adjudicate against the LIVE root, so `task run blocked` reaches any case /// naming a `mise` call and refuses it with no fast list. A case whose subject is /// a DIFFERENT row has to state the posture or it measures that one instead. /// [`cause`], with the call's backgrounding STATED. @@ -206,8 +206,8 @@ fn a_redirection_is_not_a_background_ampersand() { /// `verdict-not-discarded` used to recommend `mise run verify >/tmp/verify.log /// 2>&1` — redirect the output rather than pipe it, so the exit status stays the /// task's. Two newer rows make that exact string unrunnable from either side: -/// `foreground-mise` refuses it foreground, because the harness kills a -/// foreground call at ~2 minutes, and `background-redirect` refuses it +/// `task run blocked` refuses it foreground, because the harness kills a +/// foreground call at ~2 minutes, and `redirect write unread` refuses it /// backgrounded, because the harness already captures a backgrounded task's /// output where the human watches and a private log file is one nobody reads. /// @@ -272,8 +272,8 @@ fn a_pager_on_an_earlier_query_does_not_condemn_a_later_command() { // // THE RECOMMENDED FORM MOVED, AND THIS CASE FOLLOWED IT (CLOUD-1722). It used // to read `&& mise run verify >/tmp/v.log 2>&1`, which is now two refusals - // rather than a model answer: `foreground-mise` refuses the foreground call - // because the harness kills one at ~2 minutes, and `background-redirect` + // rather than a model answer: `task run blocked` refuses the foreground call + // because the harness kills one at ~2 minutes, and `redirect write unread` // refuses a backgrounded call that redirects its own output, because the // harness already captures it where the human watches. What is left is the // form both rows agree on — backgrounded, unredirected — and pinning THAT @@ -337,10 +337,10 @@ fn each_shape_renders_its_own_cause() { // same string masks the class under test: measured, `mise run verify >log // 2>&1; ls` read `task run blocked foreground-mise` and this case could no // longer see `verdict carry other` at all. Stating the posture takes - // `foreground-mise` out of the way and leaves the shape's own row to answer. + // `task run blocked` out of the way and leaves the shape's own row to answer. // // The `>log 2>&1` also left the middle shape, for the same reason one layer - // on: backgrounded, `background-redirect` claims it. The redirect was never + // on: backgrounded, `redirect write unread` claims it. The redirect was never // what that case was about — `; ls` is, because it hands the exit status to // `ls` — so dropping it makes the case name its own subject. assert!(cause_backgrounded("mise run verify | tail -1").contains("verdict read dropped")); diff --git a/crates/batten/tests/it/run_shape.rs b/crates/batten/tests/it/run_shape.rs index 0bf96f5fd..dee903473 100644 --- a/crates/batten/tests/it/run_shape.rs +++ b/crates/batten/tests/it/run_shape.rs @@ -826,7 +826,7 @@ fn a_process_read_outside_a_loop_is_not_a_wait() { // it would refuse its own remedy. let root = fixture("reads-a-process-once"); allowed_background(&root, "pgrep -f mise", true); - // BACKGROUNDED, and it was foreground until `foreground-mise` landed: the + // BACKGROUNDED, and it was foreground until `task run blocked` landed: the // probe is still the remedy this class recommends, and it is now a // backgrounded one like every other `mise` call. allowed_background(&root, "mise run alive", true); diff --git a/policy/ci-cache-declared.rego b/policy/ci-cache-declared.rego index c086b81bf..6ec368d27 100644 --- a/policy/ci-cache-declared.rego +++ b/policy/ci-cache-declared.rego @@ -700,7 +700,7 @@ test_a_scheduled_writer_on_another_architecture_still_leaves_the_reader_empty if scheduled_writer, pr_reader_on("ci-", false, "ubuntu-24.04-arm"), ) - finding.rule == "read-family-has-a-warm-writer" + finding.rule == "job read empty" } test_a_job_reaching_no_cargo_needs_no_cache if { diff --git a/policy/run-shape.rego b/policy/run-shape.rego index eeb786c8f..8ae86fcdb 100644 --- a/policy/run-shape.rego +++ b/policy/run-shape.rego @@ -947,7 +947,7 @@ test_a_mention_of_sleep_is_not_a_call if { # backgrounded call keeps its own output. # # `programs` rather than `words[0]`, and these cases are where that matters: -# `foreground-mise` anchors on the engine's RESOLVED program, so a fixture must +# `task run blocked` anchors on the engine's RESOLVED program, so a fixture must # carry the key. `sleeps` above reaches for a segment's program through # `words_program_index`; this family does not, because the mediated document # already publishes the resolution and CLOUD-1382 says a first word is not a @@ -968,7 +968,7 @@ test_a_foreground_mise_run_is_refused if { "programs": [prog("mise", ["run", "verify"])], "segments": [seg(["mise", "run", "verify"], null, false)], }} - v.rule == "foreground-mise" + v.rule == "task run blocked" } # THE STRICT SIDE OF THE THREE-VALUED READ, and the ordinary envelope: most hosts @@ -990,7 +990,7 @@ test_an_unstated_posture_is_refused_too if { "programs": [prog("mise", ["run", "ci"])], "segments": [seg(["mise", "run", "ci"], null, false)], }} - v.rule == "foreground-mise" + v.rule == "task run blocked" } test_a_backgrounded_mise_run_is_allowed if { @@ -1020,7 +1020,7 @@ test_a_backgrounded_call_redirecting_its_own_output_is_refused if { "programs": [prog("cargo", ["build"])], "segments": [seg(["cargo", "build", ">", "/tmp/log", "2>&1"], null, false)], }} - v.rule == "background-redirect" + v.rule == "redirect write unread" } # NOTHING IS CAPTURED FOR A FOREGROUND CALL, so a redirect there discards no From 88755dbfe19a10ace3819c24f97798df010e1645 Mon Sep 17 00:00:00 2001 From: Alec Wenzowski Date: Wed, 9 Sep 2026 20:13:09 +0000 Subject: [PATCH 13/23] fix(commit): an unreadable parent config leaves the commit unjudged MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `arm-self-authorized` refuses a commit that both declares a `[rule.conserves]` arm and spends it. The arm sets come from the config at the commit and at its PARENT, and `conserves_arms` read a parse failure as an empty map — so an unparseable parent made every arm the head declares look INTRODUCED, and the clause fabricated a refusal over a commit that added nothing. THE FUNCTION'S OWN DOC ALREADY SAID SO. Its third section states that a parent revision which will not parse is could-not-look about the comparison rather than a verdict on the commit; the code returned a verdict. An empty map is a comparison the caller can complete, and the two were conflated. `None` is the distinction. MEASURED ON THIS BRANCH, not imagined. This binary refuses a rule id that is not three words, which is what CLOUD-1638 installs; every config predating that grammar is therefore unparseable to it, and `commit-check` walks exactly that history. `b69bd7a3` was refused for `bats count dropped.changed` — an arm whose token, table and `declared_in` are byte-identical either side of the rename it made. The rename half was already handled: the map is keyed by the TOKEN for this exact reason, and that section anticipated a renamed rule. Only the parse failure was not. The premise case still refuses, which is what keeps the four admitting cases from being satisfied by a clause that never fires. `//MUTANT unparseable-parent-reads-empty` restores the empty-map reading against `a_commit_whose_parent_config_cannot_be_parsed_is_unjudged`. Refs: CLOUD-1638 --- crates/batten/src/lib.rs | 47 ++++++++++++++----- .../batten/tests/it/commit_arm_sequencing.rs | 35 ++++++++++++++ 2 files changed, 70 insertions(+), 12 deletions(-) diff --git a/crates/batten/src/lib.rs b/crates/batten/src/lib.rs index 288f68c84..73b8aa881 100644 --- a/crates/batten/src/lib.rs +++ b/crates/batten/src/lib.rs @@ -11674,10 +11674,23 @@ fn commit_admissions( /// since retired, and refusing there would make retiring a key unlandable. The /// head side of every other clause in the same run parses the config strictly, so /// a genuinely broken config is already refused — loudly, and with the key named. -fn conserves_arms(text: &str, source: &str) -> std::collections::BTreeMap { +/// +/// So the answer is `None`, never an empty map. The two were conflated until +/// CLOUD-1638: an empty map is "this revision declares no arm", which is a +/// comparison the caller can complete, and reading a parse failure as one turned +/// could-not-look into a refusal of the very shape this section rules out. +fn conserves_arms(text: &str, source: &str) -> Option> { let mut arms = std::collections::BTreeMap::new(); + // COULD-NOT-LOOK, NEVER AN EMPTY SET (CLOUD-1638). An empty map here reads as + // "the base declared no arm", which makes every arm the head declares look + // INTRODUCED and fabricates a refusal over a commit that added nothing. The + // case is not hypothetical: this binary refuses a rule id that is not three + // words, so every config predating that grammar is unparseable to it, and + // the ratchet ledger is read at each commit's PARENT. `None` is the honest + // answer and the function's own doc already names the direction a miss must + // fail in. let Ok(parsed) = config::parse_base(text, source) else { - return arms; + return None; }; for rule in &parsed.rules { let Some(conserves) = rule.conserves.as_ref() else { @@ -11705,7 +11718,7 @@ fn conserves_arms(text: &str, source: &str) -> std::collections::BTreeMap std::collections::BTreeMap { - let held = conserves_arms(before, "the parent revision's batten.toml"); - conserves_arms(after, config::CONFIG_FILE) - .into_iter() - .filter(|(token, _)| !held.contains_key(token)) - .map(|(token, pointer)| (pointer, token)) - .collect() +/// +/// `None` when either side is unparseable, which leaves the commit unjudged +/// rather than reading every arm the head declares as introduced. +fn introduced_arms( + before: &str, + after: &str, +) -> Option> { + let held = conserves_arms(before, "the parent revision's batten.toml")?; + Some( + conserves_arms(after, config::CONFIG_FILE)? + .into_iter() + .filter(|(token, _)| !held.contains_key(token)) + .map(|(token, pointer)| (pointer, token)) + .collect(), + ) } /// One commit's two sets, resolved. @@ -11892,7 +11915,7 @@ fn arm_sequence( ) -> commit::ArmSequence { let before = git::show(root, parent, config::CONFIG_FILE).unwrap_or_default(); let after = git::show(root, rev, config::CONFIG_FILE).unwrap_or_default(); - let introduced = introduced_arms(&before, &after); + let introduced = introduced_arms(&before, &after).unwrap_or_default(); if introduced.is_empty() { // No arm arrived, so no line can spend one. Returning early keeps the // ledger's blobs unread on every commit that merely edited the config. diff --git a/crates/batten/tests/it/commit_arm_sequencing.rs b/crates/batten/tests/it/commit_arm_sequencing.rs index 64d017079..ef413d808 100644 --- a/crates/batten/tests/it/commit_arm_sequencing.rs +++ b/crates/batten/tests/it/commit_arm_sequencing.rs @@ -66,6 +66,7 @@ /* #MUTANT same-commit-spend-passes|s@ .any(|line| line.trim_start().starts_with(token.as_str()))@ .any(|_unread| false)@|a_commit_that_adds_an_arm_and_spends_it_is_refused +#MUTANT unparseable-parent-reads-empty|s@^ return None;$@ return Some(arms);@|a_commit_whose_parent_config_cannot_be_parsed_is_unjudged */ // Panicking on setup failure is the idiomatic way for a test to fail loudly. #![allow(clippy::unwrap_used, clippy::expect_used)] @@ -240,6 +241,40 @@ fn a_commit_that_spends_an_arm_the_tree_already_carried_passes() { ); } +#[test] +fn a_commit_whose_parent_config_cannot_be_parsed_is_unjudged() { + // COULD-NOT-LOOK, NEVER A FABRICATED REFUSAL (CLOUD-1638). The arm sets are + // read from the config at each commit and its PARENT, and a parent this + // binary cannot parse yields no set at all. Reading that as "the parent + // declared no arm" makes every arm the head declares look introduced, so a + // commit that added nothing is refused for spending what it did not create. + // + // NOT HYPOTHETICAL, AND NOT A FIXTURE'S IDEA. A binary enforcing a rule-id + // grammar refuses every config predating it, which is exactly the history + // this clause walks — measured on this repository's own branch, where the + // migration commit was refused over an arm it had carried unchanged. + let dir = fixture("arm-unparseable-parent", false); + write(&dir, "batten.toml", "version = 1\nthis is not toml\n"); + commit(&dir, "chore(config): break the authority"); + write(&dir, "batten.toml", &config(true)); + write( + &dir, + LEDGER, + &format!("// the successors of the alpha suite\n{ARM_ROW}"), + ); + commit(&dir, "feat(config): add the withdrawn arm and use it"); + let (code, report) = check(&dir, "HEAD~2..HEAD"); + assert_eq!( + code, + Some(0), + "an unreadable parent leaves the commit unjudged rather than refused: {report}" + ); + assert!( + !report.contains("arm-self-authorized"), + "no finding may be fabricated from a set that could not be read: {report}" + ); +} + #[test] fn a_commit_that_only_declares_the_arm_passes() { // Declaring without spending is the whole shape the remedy asks for, so it From b0c2357abf9badff5c23fa30cb2edac53533d3c1 Mon Sep 17 00:00:00 2001 From: Alec Wenzowski Date: Wed, 9 Sep 2026 20:59:12 +0000 Subject: [PATCH 14/23] fix(config): a config read from a ref is compared, not judged by this grammar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `parse_base` exists to answer version skew between a git ref and this build — that is its whole reason, and `RETIRED_KEYS` is the skew it already answers. It funnels into `parse`, so the rule-id grammar this branch installs was being enforced against every historical `batten.toml` too. That makes adopting the grammar unlandable, because the BASE of every comparison is the revision before the adoption. Measured: `config lint --base origin/main` refused with "rule `gh pr merge`: `gh` is not in the declared `subject` list" — a true statement about a config written before the grammar existed, and no verdict at all on the tree being linted. `GrammarReading` names the two readings. `parse` enforces; `parse_base` reads `Skewed` and skips the id grammar, exactly as it already skips a retired key. Normalisation stays on both sides, because that is what makes the two spellings of one id compare equal rather than reading as a rename. Refs: CLOUD-1638 Weakens: rule-removed rule[a move to in review owes an adjudication] Weakens: rule-removed rule[a spawn names few artifacts] Weakens: rule-removed rule[a spawn prompt stays in budget] Weakens: rule-removed rule[a todo promotion owes a ready verdict] Weakens: rule-removed rule[agentic experiment record] Weakens: rule-removed rule[an update owes a recent read] Weakens: rule-removed rule[ancestry decides nothing] Weakens: rule-removed rule[assertions not gutted] Weakens: rule-removed rule[bash surface not growing] Weakens: rule-removed rule[bats invocation] Weakens: rule-removed rule[bats tests not deleted] Weakens: rule-removed rule[cfg gated test] Weakens: rule-removed rule[ci cache declared] Weakens: rule-removed rule[ci hygiene] Weakens: rule-removed rule[ci parity] Weakens: rule-removed rule[ci suite lane] Weakens: rule-removed rule[claim before code] Weakens: rule-removed rule[claim needs receipt] Weakens: rule-removed rule[claim not raced] Weakens: rule-removed rule[claim order is stated] Weakens: rule-removed rule[claude shell not growing] Weakens: rule-removed rule[command task defined] Weakens: rule-removed rule[commit message obtainable] Weakens: rule-removed rule[connector not granted] Weakens: rule-removed rule[delay waivers not growing] Weakens: rule-removed rule[denials outlive the turn] Weakens: rule-removed rule[egress fencing] Weakens: rule-removed rule[evaluator closure io free] Weakens: rule-removed rule[filed here] Weakens: rule-removed rule[filing needs a search] Weakens: rule-removed rule[fix selection complete] Weakens: rule-removed rule[fixture forks] Weakens: rule-removed rule[forge verdict required] Weakens: rule-removed rule[gh pr checks] Weakens: rule-removed rule[gh pr comment fast forward] Weakens: rule-removed rule[gh pr merge] Weakens: rule-removed rule[gh run watch] Weakens: rule-removed rule[harness grant] Weakens: rule-removed rule[harness wiring] Weakens: rule-removed rule[hk contract drift] Weakens: rule-removed rule[hk fix selection] Weakens: rule-removed rule[hk plan required] Weakens: rule-removed rule[hook profile] Weakens: rule-removed rule[hook skip local] Weakens: rule-removed rule[inline task bodies not growing basic] Weakens: rule-removed rule[inline task bodies not growing] Weakens: rule-removed rule[install does one thing] Weakens: rule-removed rule[issue key derivations not growing] Weakens: rule-removed rule[landing loop preset] Weakens: rule-removed rule[landing roster guarded] Weakens: rule-removed rule[leased push] Weakens: rule-removed rule[lock complete] Weakens: rule-removed rule[mcp grant matches something] Weakens: rule-removed rule[memory graph] Weakens: rule-removed rule[mise pin agreement] Weakens: rule-removed rule[mise preset tree] Weakens: rule-removed rule[mise preset] Weakens: rule-removed rule[module layering] Weakens: rule-removed rule[mutation declared case] Weakens: rule-removed rule[nextest slow] Weakens: rule-removed rule[no appeal to authority] Weakens: rule-removed rule[no bare cargo] Weakens: rule-removed rule[no bash4 mapfile] Weakens: rule-removed rule[no bash4 wait n] Weakens: rule-removed rule[no branch f main] Weakens: rule-removed rule[no cargo install in ci] Weakens: rule-removed rule[no conflict markers] Weakens: rule-removed rule[no consumer account literal] Weakens: rule-removed rule[no consumer entity path] Weakens: rule-removed rule[no consumer repo name] Weakens: rule-removed rule[no denied identity prescribed] Weakens: rule-removed rule[no gnu sed in place] Weakens: rule-removed rule[no gnu sed z] Weakens: rule-removed rule[no gnu xargs r] Weakens: rule-removed rule[no key leaves the schema unannounced] Weakens: rule-removed rule[no new ignores] Weakens: rule-removed rule[no origin literal in fixtures] Weakens: rule-removed rule[no pr activity subscription] Weakens: rule-removed rule[no raw issue read] Weakens: rule-removed rule[no rego metadata] Weakens: rule-removed rule[no scheduled self wakeup] Weakens: rule-removed rule[no scheduled trigger] Weakens: rule-removed rule[no secrets] Weakens: rule-removed rule[no source built tool] Weakens: rule-removed rule[no tool substitution] Weakens: rule-removed rule[no tracker key in core] Weakens: rule-removed rule[no tracker key in modules] Weakens: rule-removed rule[no util linux flock] Weakens: rule-removed rule[obligations bound] Weakens: rule-removed rule[opa tracks regorus compliance] Weakens: rule-removed rule[perf assert] Weakens: rule-removed rule[pinned toolchain preset] Weakens: rule-removed rule[plan complete] Weakens: rule-removed rule[policy lint rule tests] Weakens: rule-removed rule[policy modules bind input] Weakens: rule-removed rule[policy modules type check] Weakens: rule-removed rule[pr names an issue] Weakens: rule-removed rule[pr partition restated] Weakens: rule-removed rule[privileged lane tests origin] Weakens: rule-removed rule[prose only] Weakens: rule-removed rule[ready names an issue] Weakens: rule-removed rule[ready needs a review to exist] Weakens: rule-removed rule[ready needs an answered review] Weakens: rule-removed rule[ready needs receipts] Weakens: rule-removed rule[ready needs review] Weakens: rule-removed rule[ready needs the threads answered] Weakens: rule-removed rule[rebase not hand stepped] Weakens: rule-removed rule[release attestation precondition] Weakens: rule-removed rule[release provision parity] Weakens: rule-removed rule[release tag shape] Weakens: rule-removed rule[release tracking check] Weakens: rule-removed rule[remedy authorship] Weakens: rule-removed rule[repetition without progress] Weakens: rule-removed rule[review answered] Weakens: rule-removed rule[review dispatched] Weakens: rule-removed rule[rules drift] Weakens: rule-removed rule[sbom inventory] Weakens: rule-removed rule[sbom ntia conformance] Weakens: rule-removed rule[sbom ntia precondition] Weakens: rule-removed rule[shell hygiene] Weakens: rule-removed rule[shell retirement] Weakens: rule-removed rule[shell write advisory] Weakens: rule-removed rule[spawn adapters] Weakens: rule-removed rule[spawn widening] Weakens: rule-removed rule[stop posture] Weakens: rule-removed rule[suite subject retirable] Weakens: rule-removed rule[task substitution] Weakens: rule-removed rule[test targets] Weakens: rule-removed rule[tests not deleted] Weakens: rule-removed rule[trunk based preset] Weakens: rule-removed rule[validator verdict clean] Weakens: rule-removed rule[verdict not discarded] Weakens: rule-removed rule[verdict routes resolve] Weakens: rule-removed rule[weakens declared] Weakens: rule-removed rule[workspace dep referenced] Weakens: rule-removed rule[worktree registration live] Weakens: waiver-added waiver[claim mint absent] Weakens: waiver-added waiver[issue file held] Weakens: waiver-added waiver[issue file same] Weakens: waiver-added waiver[source carry unsafe][.github/workflows/release-plz.yml] Weakens: waiver-added waiver[task carry other] Weakens: waiver-added waiver[test count dropped] Weakens: waiver-added waiver[test cover unseen][crates/batten/src/provision.rs] Weakens: waiver-added waiver[test cover unseen][crates/batten/tests/it/mutate.rs] --- crates/batten/src/config.rs | 40 ++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/crates/batten/src/config.rs b/crates/batten/src/config.rs index f769fcca4..9b689316a 100644 --- a/crates/batten/src/config.rs +++ b/crates/batten/src/config.rs @@ -1396,7 +1396,9 @@ pub fn parse_base(text: &str, source: &str) -> Result { } let text = toml::to_string(&table) .map_err(|err| UsageError::raise(format!("invalid config {source}: {err}")))?; - parse(&text, source) + let config = parse_ungated_with(&text, source, GrammarReading::Skewed)?; + check_min_version(&config, source)?; + Ok(config) } /// The override surface: exactly what `batten.local.toml` may carry. @@ -1657,7 +1659,12 @@ fn validate_remedy_tables(config: &Config) -> Result<()> { ) } -fn validate_tables(config: &Config, text: &str, source: &str) -> Result<()> { +fn validate_tables( + config: &Config, + text: &str, + source: &str, + grammar: GrammarReading, +) -> Result<()> { // The verb table is validated here, at load, because nothing else validates // it anywhere: `verbs::validate` had no caller outside its own tests, so a // `[[verb]]` row that is inert — `effect = "read"` in a table named for @@ -1748,7 +1755,15 @@ fn validate_tables(config: &Config, text: &str, source: &str) -> Result<()> { .collect(); under(Native::RuleTableRefused, { let mut first = Ok(()); + // SKEW, NOT A VERDICT (CLOUD-1638). A config read from a git ref is + // read to be COMPARED, and an id predating this build's grammar is + // the same shape as a key this build has since retired: refusing it + // makes adopting the grammar unlandable, because the base of every + // comparison is the revision before it. for rule in &config.rules { + if grammar == GrammarReading::Skewed { + break; + } if declared.contains(rule.id.as_str()) { continue; } @@ -3094,7 +3109,26 @@ fn prune_unresolvable(source: &str, behind: bool } } +/// Whether this build's rule-id grammar is a verdict on the config being read. +/// +/// [`parse_base`] reads a config from a git REF, and the reason it exists is +/// version skew between that ref and this build (CLOUD-1638). A rule id that +/// predates the grammar is skew of exactly the shape [`RETIRED_KEYS`] already +/// answers: enforcing it there would make ADOPTING the grammar unlandable, +/// since the base of every comparison is the revision before the adoption. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum GrammarReading { + /// The working tree's own authority: the grammar is a verdict. + Enforced, + /// A config read from a ref, to be compared rather than judged. + Skewed, +} + fn parse_ungated(text: &str, source: &str) -> Result { + parse_ungated_with(text, source, GrammarReading::Enforced) +} + +fn parse_ungated_with(text: &str, source: &str, grammar: GrammarReading) -> Result { // THE COMMON CASE COSTS ONE PARSE, and it used to cost three. // // A config this build fully understands succeeds here and is DONE — it @@ -3159,7 +3193,7 @@ fn parse_ungated(text: &str, source: &str) -> Result { waiver.rule = crate::verdict::normalise_rule_id(&waiver.rule); } } - validate_tables(&config, text, source)?; + validate_tables(&config, text, source, grammar)?; Ok(config) } From 06fd4c770f9cabfd67332fb59d8bc690cc2a0d7f Mon Sep 17 00:00:00 2001 From: Alec Wenzowski Date: Wed, 9 Sep 2026 21:23:19 +0000 Subject: [PATCH 15/23] fix(config): keep `validate_tables` inside the line budget The skew reading is one condition on the block that runs the grammar check rather than a break inside its loop, which is where it belonged anyway: the whole `if` is about a consumer that has adopted the grammar, and a base revision has not adopted it in the sense that matters. 103/100 before, and this is the second time this function has paid for a guard added inside it rather than at its head. Refs: CLOUD-1638 --- crates/batten/src/config.rs | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/crates/batten/src/config.rs b/crates/batten/src/config.rs index 9b689316a..be5b75f2d 100644 --- a/crates/batten/src/config.rs +++ b/crates/batten/src/config.rs @@ -1396,7 +1396,7 @@ pub fn parse_base(text: &str, source: &str) -> Result { } let text = toml::to_string(&table) .map_err(|err| UsageError::raise(format!("invalid config {source}: {err}")))?; - let config = parse_ungated_with(&text, source, GrammarReading::Skewed)?; + let config = parse_ungated_with(&text, source, Grammar::Skewed)?; check_min_version(&config, source)?; Ok(config) } @@ -1659,12 +1659,7 @@ fn validate_remedy_tables(config: &Config) -> Result<()> { ) } -fn validate_tables( - config: &Config, - text: &str, - source: &str, - grammar: GrammarReading, -) -> Result<()> { +fn validate_tables(config: &Config, text: &str, source: &str, grammar: Grammar) -> Result<()> { // The verb table is validated here, at load, because nothing else validates // it anywhere: `verbs::validate` had no caller outside its own tests, so a // `[[verb]]` row that is inert — `effect = "read"` in a table named for @@ -1730,7 +1725,12 @@ fn validate_tables( // `verdict::validate` grants a registry with no vocabulary, and it must be // the same one, or the two names diverge on exactly the trees that have // adopted neither. - if !config.vocabulary.is_empty() { + // SKEW IS NOT A VERDICT (CLOUD-1638). A config read from a git ref is read to + // be COMPARED, and an id predating this build's grammar is the same shape as + // a key this build has since retired: enforcing it there makes ADOPTING the + // grammar unlandable, because the base of every comparison is the revision + // before the adoption. + if !config.vocabulary.is_empty() && grammar == Grammar::Enforced { // A COLLAPSED ID IS GOVERNED BY THE CLASS REGISTRY, NOT BY THIS LIST. // // Where the id IS a class token, the class's own validation already @@ -1755,15 +1755,7 @@ fn validate_tables( .collect(); under(Native::RuleTableRefused, { let mut first = Ok(()); - // SKEW, NOT A VERDICT (CLOUD-1638). A config read from a git ref is - // read to be COMPARED, and an id predating this build's grammar is - // the same shape as a key this build has since retired: refusing it - // makes adopting the grammar unlandable, because the base of every - // comparison is the revision before it. for rule in &config.rules { - if grammar == GrammarReading::Skewed { - break; - } if declared.contains(rule.id.as_str()) { continue; } @@ -3117,7 +3109,7 @@ fn prune_unresolvable(source: &str, behind: bool /// answers: enforcing it there would make ADOPTING the grammar unlandable, /// since the base of every comparison is the revision before the adoption. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum GrammarReading { +enum Grammar { /// The working tree's own authority: the grammar is a verdict. Enforced, /// A config read from a ref, to be compared rather than judged. @@ -3125,10 +3117,10 @@ enum GrammarReading { } fn parse_ungated(text: &str, source: &str) -> Result { - parse_ungated_with(text, source, GrammarReading::Enforced) + parse_ungated_with(text, source, Grammar::Enforced) } -fn parse_ungated_with(text: &str, source: &str, grammar: GrammarReading) -> Result { +fn parse_ungated_with(text: &str, source: &str, grammar: Grammar) -> Result { // THE COMMON CASE COSTS ONE PARSE, and it used to cost three. // // A config this build fully understands succeeds here and is DONE — it From 7a69ff7d51efed0a6934f10772140c9c3e40d06b Mon Sep 17 00:00:00 2001 From: Alec Wenzowski Date: Wed, 9 Sep 2026 23:28:49 +0000 Subject: [PATCH 16/23] fix(policy): retire the last mention of a renamed finding id MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ci-task-parity` was renamed to `job run other` by the finding-id half of this migration, and three backticked mentions of the old spelling survived in `policy/ci-parity.rego`'s commentary and one in `batten.toml`'s. They name an id no module declares, which is the drift this row exists to remove — and it is the authority's own file saying it. Found by `batten-check` on a land lap, off a speculative tree that had re-declared the old id; the declaration was the borrowed commit's, the prose was mine. Refs: CLOUD-1638 Admits: 60cad3d4cc7ea2c7f81430b4eae52eac5c82e10d71216594e2e5f26726f752fe Admits-rule: protected-mutation Admits-verdict: path write refused Admits-subject: batten.toml Admits-anchor: call:81a2144e6760c9b1763f0d287889fb9b1e25f681 Admits-epoch: 25aac548c6efd15f406788fcf372279a5cc640dae8caaac26e9ef3bcd65abbf1 Admits-author: alec@wenzowski.com Admits-prev: 6c51184a2512725bf1ff1285e894ec5e817a6bfe718f4cc830fc74fc1459b1fc Admits-answer-lost: The authority would name a finding id no module declares, in the file that defines the grammar refusing it — the same defect this row exists to close, left in the row's own artifact. Admits-answer-precondition: No surface can express this change: the edit is a backticked mention of `ci-task-parity` inside `batten.toml`'s own commentary, and that finding id no longer exists — this branch renamed it to `job run other`. A comment naming a dead id is exactly the drift CLOUD-1638 removes, and it is one token substitution visible in the diff it lands in. Admits-answer-rejected-route: `config read first` does not apply: this is a write to the authority, not a read that skipped it. `patch run first` does not apply: it addresses a patch applied before the config was consulted, and here the config IS the subject, with the new name taken from the same file's rule table. --- batten.toml | 2 +- policy/ci-parity.rego | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/batten.toml b/batten.toml index af6f2298b..f34a133e4 100644 --- a/batten.toml +++ b/batten.toml @@ -11950,7 +11950,7 @@ target = "crates/batten/src/lib.rs" id = "cargo spelling other" gloss = "a foreign runner's cargo invocation is not the one `test:cargo` declares" class = """ -`ci-task-parity` exempts a foreign runner, and correctly — there is no local \ +`job run other` exempts a foreign runner, and correctly — there is no local \ Windows, so "a free local run would have caught it" is false there. The cost of \ that exemption is this class: the job's command becomes a SECOND SPELLING of \ `test:cargo`'s body, accurate today and only today. Change the task and every \ diff --git a/policy/ci-parity.rego b/policy/ci-parity.rego index 693e75bdc..2e04cba2d 100644 --- a/policy/ci-parity.rego +++ b/policy/ci-parity.rego @@ -94,7 +94,7 @@ rules contains "path reach dead" # use site. Do not re-collapse them. # # WHAT THIS COST, because it is the whole reason the comment is this long. The -# properties in this module are `ci-task-parity`, the required-check roster in +# properties in this module are `job run other`, the required-check roster in # both directions, the fan-in wiring, and the lease-before-spending precondition # — and a gate that passes because it is dead is byte-identical, on the decision # surface, to a gate that passed. The suite could not see it either: the fixture @@ -864,7 +864,7 @@ violation contains { # job's command is a second spelling of `test:cargo`'s body, accurate today and # only today. Change the task and every Linux leg follows it while the foreign # leg keeps running the old command, green on work it no longer covers. -# `ci-task-parity` cannot object, because its exemption is per JOB, not per +# `job run other` cannot object, because its exemption is per JOB, not per # property. # # WHY THIS READS THE MANIFEST RATHER THAN `mise tasks info`, stated because the @@ -1040,7 +1040,7 @@ swap(key, doc) := out if { # --- the foreign cargo spelling ---------------------------------------------- # A foreign leg running something `test:cargo` does not declare is the whole -# defect: the Linux legs follow the task, this one does not, and `ci-task-parity` +# defect: the Linux legs follow the task, this one does not, and `job run other` # cannot see it because its exemption is per job. test_a_foreign_leg_running_a_different_cargo_is_refused if { drifted := object.union(sound_input.tree.lines, {".github/workflows/rust.yml": [" - run: mise exec -- cargo nextest run --workspace --all-features"]}) From ded2e7857aedf29b1d1adeb679376463809b270c Mon Sep 17 00:00:00 2001 From: Alec Wenzowski Date: Thu, 10 Sep 2026 00:16:15 +0000 Subject: [PATCH 17/23] fix(policy): name the two ci-parity arms main added under the old id MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ci-task-parity` is `job run other` on this branch. Main landed two new arms raising `task run duplicate` and `task cover stale` under the old spelling while this branch was in the landing loop, so after the rebase the module declared one finding and two arms raised another — `batten-check` refused the id as two words, which is the grammar working. The three prose mentions in the same module move with them. `policy test` 870/870. Refs: CLOUD-1638 --- policy/ci-parity.rego | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/policy/ci-parity.rego b/policy/ci-parity.rego index 2e04cba2d..69a0308c8 100644 --- a/policy/ci-parity.rego +++ b/policy/ci-parity.rego @@ -292,7 +292,7 @@ names_the_covering_lane(list, covering) if { } violation contains { - "rule": "ci-task-parity", + "rule": "job run other", "verdict": "task run duplicate", "subjects": [{"path": "mise.toml"}, {"artifact": task}], } if { @@ -311,7 +311,7 @@ violation contains { # It fires on a retired task, a renamed one, and a `ci:quick` respelled to # something `hooks` does not subsume. violation contains { - "rule": "ci-task-parity", + "rule": "job run other", "verdict": "task cover stale", "subjects": [{"path": "mise.toml"}, {"artifact": task}], } if { From 5913c0b0a171e45e71f2315a7650a981e0d1e16a Mon Sep 17 00:00:00 2001 From: Alec Wenzowski Date: Fri, 11 Sep 2026 03:02:18 +0000 Subject: [PATCH 18/23] fix(cli): normalise a rule id at the argument boundary, where it was not MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `normalise_rule_id`'s own doc lists the surfaces an id arrives on — "a `[[rule]] id`, a module's `\"rule\":` literal, a `//MUTANT` row and a `policy rule` argument" — and the CLI argument was the one path that never called it. Every stored id is rewritten to the space form at load (`config.rs:3152`, and `:3155` for a waiver's key), so both lookups compared a raw argv entry against a normalised row: batten policy rule shell-retirement batten check --rule branch-write-unsafe Both answered "no `[[rule]]` row ... declares" for rows that ARE declared. WHY THAT PARTICULAR WRONG ANSWER IS THE BAD ONE. It is indistinguishable from a typo. `select_rules`' own header is entirely about a misspelled id silently dropping a gate — "a typo could ride along with a valid sibling ... having silently stopped enforcing the row the caller misspelled" — and this turned a CORRECT id into that same refusal. Measured in use this session: the answer was read as "that name is not a rule" and the lookup abandoned, twice, against rows that existed. Normalised for MATCHING only. Both refusals still quote the caller's own spelling, because a message naming a form they never typed sends them hunting for a row by a name that is not on their command line. SHOWN TO DISCRIMINATE, in both directions: - `a_rule_selection_accepts_every_spelling_of_a_declared_id` drives the three spellings against this repository's committed authority, the way `harness_wiring`'s live case does, because the question is whether the argument reaches a row that really exists. Reverting the normalisation to `id.to_owned()` reddens exactly this case. - `a_misspelled_rule_is_still_refused_under_the_spelling_it_was_given` keeps the vacuous pass shut: without it the fix above is satisfied by matching anything. It stays green under that same revert, so the pair discriminates rather than failing together. Raised by `/code-review` on this branch. Its first pass this session reviewed three releases of landed history because the local `main` ref was stale at v0.0.156 — that ref is now pointed at `origin/main`, and this finding is from the corrected baseline. Refs: CLOUD-1638 --- crates/batten/src/lib.rs | 40 +++++++++++++++++++++--- crates/batten/tests/it/cli.rs | 58 +++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 4 deletions(-) diff --git a/crates/batten/src/lib.rs b/crates/batten/src/lib.rs index 73b8aa881..a48f6b690 100644 --- a/crates/batten/src/lib.rs +++ b/crates/batten/src/lib.rs @@ -5537,6 +5537,18 @@ fn run_policy_rule( out: &mut dyn Write, ) -> Result { let config = resolve::resolve(Path::new("."), overrides)?; + // THE ARGUMENT IS A BOUNDARY, SO IT NORMALISES (CLOUD-1638), and this is + // the surface `normalise_rule_id`'s own doc names — "a `policy rule` + // argument" — while being the one path that never called it. Stored ids are + // rewritten to the space form at load, so matching a raw argv entry made + // `batten policy rule shell-retirement` answer "no `[[rule]]` row and no + // module declares" for a row that is declared. Measured in use: the refusal + // was read as "that name is not a rule" and the lookup abandoned. + // + // Normalised for MATCHING only. The refusal below still quotes the caller's + // own spelling, because a message naming a form they did not type sends + // them hunting for a row by a name that is not on their command line. + let wanted = verdict::normalise_rule_id(id); // BOTH NAMES A LINE CAN CARRY (CLOUD-1638). A `policy` row's finding is // emitted under the MODULE's `"rule":` — `test add duplicate`, not the row // `test fix duplicate` that binds the module — so resolving only `[[rule]]` @@ -5547,8 +5559,8 @@ fn run_policy_rule( let owner = config .rules .iter() - .find(|rule| rule.id == id) - .or_else(|| owning_row(&config, id)); + .find(|rule| rule.id == wanted) + .or_else(|| owning_row(&config, &wanted)); let Some(rule) = owner else { // Named, and the id is the caller's own argument rather than anything // read out of the tree. A list of what IS declared would be every row on @@ -17944,10 +17956,29 @@ fn select_rules( if only.is_empty() { return Ok((declared.to_vec(), policy::ModuleChecks::Run)); } + // THE ARGUMENT IS A BOUNDARY, SO IT NORMALISES (CLOUD-1638). Every stored + // `[[rule]] id` is rewritten to the space form at load (`config.rs`), so + // comparing a raw argv entry against it refuses `--rule shell-retirement` + // for a row that is declared and spelled `shell retirement`. That refusal + // reads exactly like a typo, which is the one reading this function must + // never produce falsely — its whole header is about a misspelled id + // silently dropping a gate. + // + // `normalise_rule_id`'s own doc already claims this surface: "`-` and `_` + // are accepted at the boundary and NOWHERE stored". The boundary was the + // half that never called it. + let wanted: Vec = only + .iter() + .map(|id| verdict::normalise_rule_id(id)) + .collect(); + // The refusal still names what the CALLER typed, not what it normalised to. + // A message quoting a spelling they did not write sends them looking for a + // row by a name that is not on their command line. let unmatched: Vec<&str> = only .iter() - .map(String::as_str) - .filter(|id| !declared.iter().any(|rule| rule.id == *id)) + .zip(&wanted) + .filter(|(_, want)| !declared.iter().any(|rule| rule.id == **want)) + .map(|(typed, _)| typed.as_str()) .collect(); if let Some(first) = unmatched.first() { return Err(error::UsageError::raise(format!( @@ -17955,6 +17986,7 @@ fn select_rules( declared.len() ))); } + let only = &wanted; // Declaration order, never the order the flags were written: findings sort by // the `(path, line, rule)` pointer tuple downstream, and a selection that // reordered the table would make a caller's argv order visible in bytes §6 diff --git a/crates/batten/tests/it/cli.rs b/crates/batten/tests/it/cli.rs index 8cb097f6b..2682893d9 100644 --- a/crates/batten/tests/it/cli.rs +++ b/crates/batten/tests/it/cli.rs @@ -13228,3 +13228,61 @@ fn a_clone_with_no_origin_main_ref_cannot_look_and_allows() { stderr(&output) ); } + +/// A `--rule` selection accepts the three spellings a boundary may carry +/// (CLOUD-1638). +/// +/// THE DEFECT THIS PINS. Every stored `[[rule]] id` is normalised to the space +/// form at load, and `select_rules` compared a raw argv entry against it — so +/// `--rule branch-write-unsafe` was refused as undeclared for a row that is +/// declared. The refusal reads exactly like a typo, which is the one reading +/// this selection must never produce falsely: its own header is about a +/// misspelled id silently dropping a gate, and this turned a CORRECT id into +/// that same silence. +/// +/// Driven against this repository, like `harness_wiring`'s own live case: the +/// row has to be one the committed authority really declares, because the +/// question is whether the argument reaches it. +#[test] +fn a_rule_selection_accepts_every_spelling_of_a_declared_id() { + for spelling in [ + "branch write unsafe", + "branch-write-unsafe", + "branch_write_unsafe", + ] { + let output = batten() + .current_dir(common::at_root(".")) + .args(["check", "--rule", spelling]) + .output() + .expect("run batten check"); + assert!( + output.status.success(), + "`--rule {spelling}` names a declared row and must select it: {}", + stderr(&output) + ); + } +} + +/// And a real typo still refuses, NAMING WHAT THE CALLER TYPED. +/// +/// Both halves matter. Without the refusal the fix above would be "match +/// anything", which is the vacuous pass `select_rules` exists to prevent. +/// Without the spelling in the message, the caller is sent hunting for a row +/// under a normalised name that never appeared on their command line. +#[test] +fn a_misspelled_rule_is_still_refused_under_the_spelling_it_was_given() { + let output = batten() + .current_dir(common::at_root(".")) + .args(["check", "--rule", "branch-write-unsaef"]) + .output() + .expect("run batten check"); + assert!( + !output.status.success(), + "a misspelled id must not select a row" + ); + assert!( + stderr(&output).contains("branch-write-unsaef"), + "the refusal must quote the caller's own spelling: {}", + stderr(&output) + ); +} From 8053f7c60ad0922b6405baab3e97b102c63a4951 Mon Sep 17 00:00:00 2001 From: Alec Wenzowski Date: Fri, 11 Sep 2026 04:55:45 +0000 Subject: [PATCH 19/23] test(preset): hold a preset's finding ids to the vendor's grammar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `policy::load`'s general branch holds a module's `"rule":` literals to the three-word grammar with `check_finding_ids`; its preset branch does not, and must not — a preset's bytes are in the binary, so a consumer refused for one has no edit that fixes it, the same reason the vendored verdict rows are unconditional in `collidable_tokens`. That left the vendored finding ids with no reader at all. The authority moves to the half that can act on a refusal: this repository's own committed `[vocabulary]`, over every preset module's finding ids, with the class-token exemption `check_finding_ids` itself takes. Not "every finding is one of the manifest's classes": measured, that is false by design — one module raises two classes from two sites under one finding id, which is the one-to-many shape the finding/class split exists to allow. Also records, on the `config.rs` exemption set, why a retired consumer row is deliberately NOT filtered there as it is in `collidable_tokens`: `verdict::validate_one` holds every declared row to the grammar, tombstones included, so filtering would route the name to a checker reaching the identical verdict — a second authority over one name. Refs: CLOUD-1638 --- crates/batten/src/config.rs | 12 +++++++ crates/batten/src/preset.rs | 65 +++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/crates/batten/src/config.rs b/crates/batten/src/config.rs index be5b75f2d..d79022811 100644 --- a/crates/batten/src/config.rs +++ b/crates/batten/src/config.rs @@ -1745,6 +1745,18 @@ fn validate_tables(config: &Config, text: &str, source: &str, grammar: Grammar) // matters here: `trunk push forced` is neither a consumer row nor a // native site but a VENDORED PRESET's, and a set built from the first // two refuses it — measured, twice, before this line was written. + // A RETIRED CONSUMER ROW IS NOT FILTERED OUT, and the asymmetry with + // `policy::collidable_tokens` — which does filter one — is deliberate + // rather than an oversight. That set answers "could this name COLLIDE + // with a live class", where a tombstone is not a name in use; this one + // answers "is this name already held to the grammar by the class + // registry", and `verdict::validate_one` holds EVERY declared row to it, + // tombstones included. So a retired consumer token is three declared + // vocabulary words by the time this line reads it, and filtering it here + // would move it to a checker that reaches the identical verdict — a + // second authority over one name, which is the shape the exemption + // itself exists to avoid. The vendored and native halves stay unfiltered + // for the other reason: they are never held to a consumer's lists at all. let vendored = crate::preset::verdict_rows(); let declared: std::collections::BTreeSet<&str> = config .verdicts diff --git a/crates/batten/src/preset.rs b/crates/batten/src/preset.rs index d3fec2971..7869cb405 100644 --- a/crates/batten/src/preset.rs +++ b/crates/batten/src/preset.rs @@ -873,6 +873,71 @@ mod tests { ); } + /// Every FINDING id a preset's modules declare is in the grammar. + /// + /// # The hole this closes + /// + /// The finding-id half of CLOUD-1638 had no reader over the vendored half. + /// `policy::load`'s general branch holds a module's `"rule":` literals to the + /// grammar with `check_finding_ids`; its PRESET branch does not call it, and + /// must not: a preset's bytes are in the binary, so a consumer refused for + /// one has no edit that fixes it, and held to a narrow consumer vocabulary an + /// enabled preset would simply be unloadable. That is the same reasoning that + /// makes the vendored verdict rows unconditional in `collidable_tokens`. + /// + /// So the authority moves to the VENDOR, which is the half that can act on a + /// refusal — this repository's own committed `[vocabulary]`, holding names + /// this repository ships. + /// + /// # Why not "every finding is one of the manifest's classes" + /// + /// Measured, and it is false BY DESIGN: `ci-hygiene` declares `job guard + /// missing`, which raises `cache build loose` at one site and `cache name + /// unknown` at another. A finding id and a class are different names — + /// `check_finding_ids` exempts an id that happens to be a class rather than + /// requiring it — so an assertion of containment would refuse the very + /// one-to-many shape the two-name split exists to allow. + #[test] + fn every_finding_a_preset_declares_is_in_the_grammar() { + let root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../.."); + let text = + std::fs::read_to_string(root.join("batten.toml")).expect("the authority is readable"); + let authority: toml::Value = toml::from_str(&text).expect("the authority parses"); + let vocabulary: crate::verdict::Vocabulary = authority + .get("vocabulary") + .expect("the vendor has adopted the grammar it holds its presets to") + .clone() + .try_into() + .expect("the word lists deserialize"); + + let mut seen = 0_usize; + for manifest in MANIFESTS { + // A CLASS TOKEN IS EXEMPT, exactly as `check_finding_ids` exempts + // one: where the id IS a class the registry governs the name, and + // checking it again here would be the second authority over one name + // that the exemption exists to avoid. + let declared: BTreeSet<&str> = manifest.verdicts.iter().map(|entry| entry.id).collect(); + for module in manifest.modules { + for id in crate::policy::finding_ids(module.source) { + if declared.contains(id.as_str()) { + continue; + } + seen += 1; + crate::verdict::check_rule_id(&id, &vocabulary).unwrap_or_else(|error| { + panic!( + "`{}` declares the finding `{id}` in `{}`: {error}", + manifest.name, module.pointer + ) + }); + } + } + } + assert!( + seen > 0, + "no preset finding id was judged, so this case is green over nothing" + ); + } + /// Rule 1 reaches a manifest as it reaches a preset source. /// /// Asserted rather than assumed, per the row: this file is under `crates/**` From 8ee5ae68b2ca41b8e551a1bda1517e2a4d742e74 Mon Sep 17 00:00:00 2001 From: Alec Wenzowski Date: Fri, 11 Sep 2026 04:56:14 +0000 Subject: [PATCH 20/23] fix(cli): match a rule id by its stored spelling before normalising MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The boundary normalisation added earlier on this branch was unconditional, and the rewrite it mirrors is not: `config.rs` rewrites a stored `[[rule]] id` to the space form only `if !config.vocabulary.is_empty()`. A tree that has not adopted the grammar therefore keeps a kebab id verbatim, and normalising every argument refused that row under the only name it has — the same false typo the normalisation was added to remove, one direction over. Measured: it took out `every_data_channel_verb_emits_one_pure_json_document` for `policy rule`, because every `-J` census fixture declares no vocabulary. Both boundaries now try the typed spelling first and the normalised one only as a fallback, which is a no-op under the grammar — a stored id is already the space form there, so only the hyphen and snake spellings reach it. Refs: CLOUD-1638 --- crates/batten/src/lib.rs | 32 +++++++++++++++++++++++++++++-- crates/batten/tests/it/cli.rs | 36 +++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/crates/batten/src/lib.rs b/crates/batten/src/lib.rs index a48f6b690..2e8035b63 100644 --- a/crates/batten/src/lib.rs +++ b/crates/batten/src/lib.rs @@ -5548,7 +5548,19 @@ fn run_policy_rule( // Normalised for MATCHING only. The refusal below still quotes the caller's // own spelling, because a message naming a form they did not type sends // them hunting for a row by a name that is not on their command line. - let wanted = verdict::normalise_rule_id(id); + // + // THE REWRITE IS CONDITIONAL, SO THE MATCH IS TOO: `config.rs` rewrites + // stored ids only `if !config.vocabulary.is_empty()`, so a tree that has not + // adopted the grammar stores a kebab id verbatim and normalising + // unconditionally would refuse the row under the only name it has. The + // typed spelling is therefore tried first and the normalised one as a + // fallback, which is a no-op under the grammar — a stored id is already the + // space form there, so only the other two spellings reach it. + let wanted = if config.rules.iter().any(|rule| rule.id == id) { + id.to_owned() + } else { + verdict::normalise_rule_id(id) + }; // BOTH NAMES A LINE CAN CARRY (CLOUD-1638). A `policy` row's finding is // emitted under the MODULE's `"rule":` — `test add duplicate`, not the row // `test fix duplicate` that binds the module — so resolving only `[[rule]]` @@ -17967,9 +17979,25 @@ fn select_rules( // `normalise_rule_id`'s own doc already claims this surface: "`-` and `_` // are accepted at the boundary and NOWHERE stored". The boundary was the // half that never called it. + // + // THE REWRITE IS CONDITIONAL, SO THE MATCH IS TOO — the typed spelling is + // tried FIRST and the normalised one only as a fallback. `config.rs` rewrites + // stored ids only `if !config.vocabulary.is_empty()`, so in a tree that has + // not adopted the grammar a kebab id is stored VERBATIM, and normalising + // unconditionally refused `--rule census-rule` for a row declared under + // exactly that name. Measured: it took out every `-J` census fixture, which + // declares no vocabulary. Exact-first is right in both worlds — under the + // grammar a stored id is already the space form, so only the other two + // spellings reach the fallback. let wanted: Vec = only .iter() - .map(|id| verdict::normalise_rule_id(id)) + .map(|id| { + if declared.iter().any(|rule| rule.id == *id) { + id.clone() + } else { + verdict::normalise_rule_id(id) + } + }) .collect(); // The refusal still names what the CALLER typed, not what it normalised to. // A message quoting a spelling they did not write sends them looking for a diff --git a/crates/batten/tests/it/cli.rs b/crates/batten/tests/it/cli.rs index 2682893d9..86fb434fc 100644 --- a/crates/batten/tests/it/cli.rs +++ b/crates/batten/tests/it/cli.rs @@ -13263,6 +13263,42 @@ fn a_rule_selection_accepts_every_spelling_of_a_declared_id() { } } +/// And a tree that has NOT adopted the grammar keeps its stored spelling. +/// +/// The mirror of the case above, and the one the first fix broke. `config.rs` +/// rewrites a stored id to the space form only `if !config.vocabulary +/// .is_empty()`, so a consumer who declares no `[vocabulary]` keeps a kebab id +/// verbatim — and a boundary that normalised unconditionally refused that row +/// under the only name it has, which is the same false typo one direction over. +/// Measured: it took out every `-J` census fixture at once, none of which +/// declares a vocabulary. +#[test] +fn a_stored_kebab_id_is_selectable_in_a_tree_with_no_vocabulary() { + let repo = repo_with_config( + "kebab-id-no-vocabulary", + r#"version = 1 + +[[rule]] +id = "no-hardcoded-banner" +kind = "forbid" +glob = "**/*.txt" +pattern = "ACME CORP" +severity = "warn" +scope = "tree" +"#, + ); + let output = batten() + .current_dir(&repo) + .args(["check", "--rule", "no-hardcoded-banner"]) + .output() + .expect("run batten check"); + assert!( + output.status.success(), + "a kebab id stored verbatim must be selectable under that spelling: {}", + stderr(&output) + ); +} + /// And a real typo still refuses, NAMING WHAT THE CALLER TYPED. /// /// Both halves matter. Without the refusal the fix above would be "match From 72ab9660e8d48a2fb5ade7f92ae023f326ed3967 Mon Sep 17 00:00:00 2001 From: Alec Wenzowski Date: Fri, 11 Sep 2026 04:57:48 +0000 Subject: [PATCH 21/23] fix(config): point four remedies at the id this branch renamed them to MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `issue list unread` is this branch's name for the row that was `filing-needs-a-search`, and four references were left spelling the old one. The costly one is `batten.toml`'s own `reason` text, which a refused reader is handed: it named an id `batten policy rule` can no longer resolve, so the cross-reference dead-ended exactly where it was meant to help. The other three are comments and `rules/scanning.md`'s worked example. `board_receipts.rs` already quoted the cross-reference under the new id, so this brings the tree in line with what its own test comment asserts. Raised by `/code-review` on this branch. Refs: CLOUD-1638 Admits: 60d2039aa908c104bdc99c2c7fc34f297e1eb97e01b59b6ce2c74be57528c1b3 Admits-rule: protected-mutation Admits-verdict: path write refused Admits-subject: batten.toml Admits-anchor: call:d055324a2a99f030d9dc426b3d1bc4798a0b0e75 Admits-epoch: b40d8ce687e7d6f4955be56f25b3ee4d5edfa00445ce793d1bef20896a57f1ed Admits-author: alec@wenzowski.com Admits-prev: - Admits-answer-lost: The authority would hand a refused reader a rule id no row declares, in the file that defines the grammar refusing it — the same drift this migration exists to remove, left in the row's own remedy text. `crates/batten/tests/it/board_receipts.rs` already quotes the cross-reference under the new id, so the tree contradicts its own test's comment. Admits-answer-precondition: No surface can express this change: the edit is a backticked mention of `filing-needs-a-search` inside `batten.toml`'s own `reason` text and two of its comments, and that rule id no longer exists — this branch renamed it to `issue list unread`. The `reason` is what a refused reader is handed, so it names an id `batten policy rule` cannot resolve and the cross-reference dead-ends where it was meant to help. Four token substitutions, visible in the diff they land in. Admits-answer-rejected-route: `config read first` does not apply: this is a write to the authority, not a read that skipped it. `patch run first` does not apply: it addresses a patch applied before the config was consulted, and here the config IS the subject, with the new name taken from the same file's own rule table at line 2756. --- batten.toml | 6 +++--- rules/scanning.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/batten.toml b/batten.toml index f34a133e4..2cccef52f 100644 --- a/batten.toml +++ b/batten.toml @@ -2996,7 +2996,7 @@ row is unchanged, because the tracker offers no precondition on write. Measured 2026-08-13: a session read a row, planned against it for ~51 minutes, and wrote a \ Ready block moving it into the ready queue — another session had marked it a \ duplicate in between, and the write landed anyway (CLOUD-508). Creating an issue \ -is never gated by this row (that is `filing-needs-a-search`).""" +is never gated by this row (that is `issue list unread`).""" # CLOUD-312 row 4: `connector-verb-guard` retires here. THREE ROWS, one per verb, # and every one is an existing AGENTS.md rule rather than a new one this config @@ -4478,7 +4478,7 @@ fields = [ # prohibition is the generalisation CLOUD-1380's own body records itself making. # # `title` and `url` are the identity half — what a human reads to know which row -# matched — and `filing-needs-a-search` is the consumer that must not be blinded: a +# matched — and `issue list unread` is the consumer that must not be blinded: a # reduction emitting only `hasNextPage` would satisfy every size predicate and # destroy the verb's purpose. # @@ -4542,7 +4542,7 @@ fields = [ # # `list_issues` IS DELIBERATELY NOT HERE. It is already projected on every # measured call, and a `shape` row cannot express "without `fields`" — a row -# that refused it outright would deny the search `filing-needs-a-search` +# that refused it outright would deny the search `issue list unread` # requires, which is how a guard gets switched off within a day (CLOUD-199). # # NOR IS `save_issue`, and that absence is recorded rather than pending. The diff --git a/rules/scanning.md b/rules/scanning.md index 2c3a16ccb..3773e76e4 100644 --- a/rules/scanning.md +++ b/rules/scanning.md @@ -160,7 +160,7 @@ ritual is the whole rule: **before a claim about what is filed, decided or measured reaches a durable artifact — a commit message, a PR body, an issue body, or a sentence to a human — search for it.** -**And the gate here only guards the write.** `filing-needs-a-search` refused +**And the gate here only guards the write.** `issue list unread` refused three attempts to file without searching in that same session and was right every time; it cannot reach a claim that something is _unfiled_, because no tool call is being made. That asymmetry is the reason this row is prose: the write From 742f4070a95eaff00b667927810f31b924ab5dbf Mon Sep 17 00:00:00 2001 From: Alec Wenzowski Date: Fri, 11 Sep 2026 17:20:31 +0000 Subject: [PATCH 22/23] chore(claude): allow the Linear connector's calls without a prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `save_comment` was refused with `MCP tool call requires approval` on the one row this session had a finding for, twice, after the connector had already been set to always-allow in the GUI. The project allowlist did not carry the server, so the harness prompted anyway — and a prompt on a tracker write stalls the sink AGENTS.md's sorting rule sends findings to. Server-level, on `mcp__serena`'s precedent two lines down, plus the tools this repository actually reaches. Measured over this session's transcript: 29 Linear calls against 12 for every other connector combined, and nothing Bash-side needed an entry — `git`, `mise`, `batten`, `cargo` and `python3` are already allowed. Refs: CLOUD-1638 --- .claude/settings.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.claude/settings.json b/.claude/settings.json index 7fc3ae0fc..ffcdd5391 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -118,6 +118,14 @@ "Bash(python3:*)", "Bash(hk:*)", "Bash(jq:*)", + "mcp__Linear", + "mcp__Linear__get_issue", + "mcp__Linear__save_issue", + "mcp__Linear__save_comment", + "mcp__Linear__list_issues", + "mcp__Linear__list_comments", + "mcp__Linear__get_issue_status", + "mcp__Linear__list_issue_statuses", "mcp__serena", "mcp__serena__delete_memory", "mcp__serena__edit_memory", From 45ca0a7286376c7ec7d58dca83e2eed86f21dcd0 Mon Sep 17 00:00:00 2001 From: Alec Wenzowski Date: Fri, 11 Sep 2026 17:46:32 +0000 Subject: [PATCH 23/23] revert(claude): drop the Linear grant, the gate that refused it is right MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverts the previous commit. `connector grant loose` refuses any `mcp__Linear*` entry in `permissions.allow`, and the refusal is correct on its own measurement: the grant puts the unreduced payload back on the model's surface, which is the 13.2 MB / 73%-of-tool-output finding the reduction exists to remove. Granting both routes is not a fallback — the cheaper-looking one wins every time. The underlying complaint stands and is not this: a tracker WRITE through `batten mcp call` is refused `requires approval` while the same tool called raw succeeds, so the route `issue read loose` prescribes is the one route that cannot write. That is a defect in the dispatch, not a reason to widen the grant, and it is being filed rather than worked around here. Refs: CLOUD-1638 --- .claude/settings.json | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index ffcdd5391..7fc3ae0fc 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -118,14 +118,6 @@ "Bash(python3:*)", "Bash(hk:*)", "Bash(jq:*)", - "mcp__Linear", - "mcp__Linear__get_issue", - "mcp__Linear__save_issue", - "mcp__Linear__save_comment", - "mcp__Linear__list_issues", - "mcp__Linear__list_comments", - "mcp__Linear__get_issue_status", - "mcp__Linear__list_issue_statuses", "mcp__serena", "mcp__serena__delete_memory", "mcp__serena__edit_memory",