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.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/batten.toml b/batten.toml index 504c8e5a4..2cccef52f 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" @@ -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 @@ -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" @@ -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.""" @@ -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" @@ -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 @@ -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 = "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 = "mise-preset" +id = "task reach loose" 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,10 +5849,16 @@ 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" +# 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 @@ -5931,7 +5937,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 +5972,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 +5995,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 +6009,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 +6018,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 +6039,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 +6049,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 +6073,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 +6129,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 +6144,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 +6221,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 +6248,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 +6256,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 +6290,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 +6337,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 +6367,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 +6410,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 +6441,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 +6555,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 +6604,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 +6644,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 +6680,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 +6743,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 +6778,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 +6812,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 +6820,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 +6879,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 +6908,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 +6916,7 @@ module = "policy/weakens-declared.rego" severity = "deny" [[rule]] -id = "harness-wiring" +id = "hook wire missing" kind = "policy" scope = "tree" documents = [ @@ -6967,7 +6973,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 +6988,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 +7034,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 +7077,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 +7103,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 +7151,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 +7174,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 +7183,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 +7227,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 +7251,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 +7288,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 +7322,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 +7338,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 +7347,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 +7356,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 +7364,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 +7382,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 +7414,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 +7478,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 +7487,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 +7496,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 +7523,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 +7558,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 +7589,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 +7598,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 +7607,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 +7643,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 +7690,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" @@ -7722,13 +7728,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 +7810,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,12 +7845,12 @@ 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" [[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" @@ -11944,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 \ @@ -13399,7 +13405,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 +13745,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/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/config.rs b/crates/batten/src/config.rs index 16aa5bb8a..d79022811 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, Grammar::Skewed)?; + check_min_version(&config, source)?; + Ok(config) } /// The override surface: exactly what `batten.local.toml` may carry. @@ -1657,7 +1659,7 @@ 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: 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 @@ -1708,6 +1710,75 @@ 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. + // 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 + // 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. + // 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 + .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), @@ -3042,7 +3113,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 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. + Skewed, +} + fn parse_ungated(text: &str, source: &str) -> Result { + parse_ungated_with(text, source, Grammar::Enforced) +} + +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 @@ -3083,7 +3173,31 @@ fn parse_ungated(text: &str, source: &str) -> Result { config.version ))); } - validate_tables(&config, text, source)?; + // 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. + // 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, grammar)?; Ok(config) } @@ -3395,7 +3509,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/hook.rs b/crates/batten/src/hook.rs index adf64dd04..c0ca09b5a 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, @@ -9330,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 @@ -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/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 5a01abecb..2e8035b63 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, @@ -5540,19 +5537,69 @@ 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 { + // 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. + // + // 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]]` + // 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 == 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 // 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 @@ -5829,7 +5876,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()` @@ -5890,11 +5937,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 +5971,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 +6197,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, @@ -6676,7 +6721,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. @@ -7568,7 +7613,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 @@ -9832,7 +9877,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, @@ -11653,10 +11698,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 { @@ -11684,7 +11742,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. @@ -11871,7 +11939,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. @@ -14662,7 +14730,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. @@ -15121,6 +15189,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 @@ -15232,7 +15301,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"; @@ -17899,10 +17968,45 @@ 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. + // + // 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| { + 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 + // 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!( @@ -17910,6 +18014,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 @@ -18108,6 +18213,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/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 a2532a761..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": @@ -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 @@ -705,6 +730,13 @@ 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 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 @@ -850,7 +882,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)?; - emitted.extend(emitted_verdicts(&bundle)); + record_raised(rule, &bundle, &mut emitted, &mut per_rule); } claim_ids(&mut ids, &declared, source_key)?; bundles.push(bundle.with_severity(rule)); @@ -863,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 @@ -879,7 +914,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)?; - emitted.extend(emitted_verdicts(&bundle)); + record_raised(rule, &bundle, &mut emitted, &mut per_rule); } claim_ids(&mut ids, &declared, where_it_came_from)?; @@ -889,6 +924,7 @@ pub fn load( if checks == ModuleChecks::Run { check_registry_is_exhausted(verdicts, &emitted)?; + check_collapse(rules, &per_rule, &tokens)?; } Ok(bundles) } @@ -1603,6 +1639,240 @@ 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 { + // 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() + .map(|token| (*token).to_owned()), + ) + .collect() +} + +/// Every finding id a module declares is a name in the grammar (CLOUD-1638). +/// +/// # Why the SOURCE and not the compiled set +/// +/// `Bundle::declared` is read back from the engine, and it is not a clean list +/// of finding ids: a module with a nested `rules` key — `module-layering`'s +/// `forbidden["rules"] contains "hook"` — surfaces `hook` there, so holding +/// that set to the grammar refuses a name no reader ever meets. The literals +/// are exact and every one of the 136 in this tree is written out, so the +/// source is both the simpler reader and the correct one. +/// +/// # Why here at all +/// +/// 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. Migrating the row ids alone would leave half +/// the names a reader meets as free-text kebab. +/// +/// 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, since a vendored preset may spell a word no consumer +/// declares. +/// +/// # Errors +/// +/// A [`UsageError`] (exit `1`) naming the module and the id. +fn check_finding_ids( + sources: &[(String, String)], + vocabulary: Option<&crate::verdict::Vocabulary>, + 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. +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..)?; + 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 +/// +/// 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. +//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>, + 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 { + // 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()) { + (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/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/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/**` 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/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/src/resolve.rs b/crates/batten/src/resolve.rs index 57bf2754c..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", @@ -2684,7 +2700,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/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/src/verdict.rs b/crates/batten/src/verdict.rs index 067b79ad8..0b01d1a25 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, @@ -1814,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, @@ -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!( 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..02285f48e 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", @@ -1246,8 +1246,8 @@ 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 -/// `filed-here` row — were issued, spent, committed, and honoured by neither +/// `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. /// /// # 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..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 — `filed-here` 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). @@ -103,6 +103,7 @@ fn load_fixture(root: &Path, rows: &[Rule]) -> Vec { // consumer supplies. patterns: &[], verdicts: &verdicts, + words: None, recorders: &[], }, policy::ModuleChecks::RunOverSelection, @@ -182,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] @@ -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, @@ -212,9 +214,9 @@ 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(["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..5a47d37b8 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}"] @@ -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], @@ -434,6 +434,7 @@ fn replayed_findings( batten::policy::Vocabulary { patterns: &[], verdicts, + words: None, recorders: &[], }, dir, @@ -502,7 +503,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/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..d4a7ec37f 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"], @@ -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, @@ -351,7 +352,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..160eaf3e8 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 @@ -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/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..176e270eb 100644 --- a/crates/batten/tests/it/cfg_gated_test.rs +++ b/crates/batten/tests/it/cfg_gated_test.rs @@ -38,10 +38,10 @@ 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"; +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`. @@ -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", @@ -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 40d18af22..9d6fd70d4 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"], @@ -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 9e62d7d87..ac0483e7f 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", @@ -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 ea58be753..4445b48e9 100644 --- a/crates/batten/tests/it/ci_parity.rs +++ b/crates/batten/tests/it/ci_parity.rs @@ -31,13 +31,13 @@ //! 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 //! 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": [ @@ -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 3eb5ef554..99b368676 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"], @@ -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.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..531d8d0b3 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"], @@ -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/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 ce96741e7..86fb434fc 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" @@ -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!({ @@ -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. @@ -3491,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 @@ -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,22 +3761,22 @@ 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 - // 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. - "ready-needs-the-threads-answered", - "ready-needs-a-review-to-exist", - "review-unanswered", - "review-absent", + "review answer partial", + "review list unread", + "review answer missing", + "review read absent", ] .iter() .any(|row| stderr.contains(row)), @@ -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,10 +12187,10 @@ 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 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 @@ -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!( @@ -13213,3 +13228,97 @@ 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 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 +/// 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) + ); +} diff --git a/crates/batten/tests/it/commit_arm_sequencing.rs b/crates/batten/tests/it/commit_arm_sequencing.rs index 413705a21..ef413d808 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 @@ -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 @@ -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 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..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: &[], } } @@ -100,7 +101,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..345fe9e2a 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", @@ -248,6 +248,7 @@ fn scan(root: &Path) -> rules::Scan { batten::policy::Vocabulary { patterns: &patterns, verdicts: &verdicts, + words: None, recorders: &declared, }, root, @@ -273,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 @@ -325,7 +326,7 @@ fn a_branch_name_with_a_slash_finds_its_record() { } // --------------------------------------------------------------------------- -// `filed-unrefined`. +// `issue file unclear`. // --------------------------------------------------------------------------- #[test] @@ -405,7 +406,7 @@ fn the_refusal_carries_the_id_and_no_prose_from_the_row() { } // --------------------------------------------------------------------------- -// `filed-over-own-diff`. +// `issue file same`. // --------------------------------------------------------------------------- #[test] @@ -423,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] @@ -693,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 @@ -842,7 +843,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..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"]);"#; @@ -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", @@ -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/forced_push.rs b/crates/batten/tests/it/forced_push.rs index 8d7d71df0..9bc513b74 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 @@ -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 @@ -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..6ada97b0f 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. @@ -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. @@ -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, @@ -328,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 @@ -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_fix_selection.rs b/crates/batten/tests/it/hk_fix_selection.rs index 630982fb1..2370590a1 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"], @@ -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, @@ -208,7 +209,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 +309,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..233d2e757 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}" ); } @@ -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. @@ -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_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/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.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/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..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"; @@ -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"], @@ -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/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 1d0200ceb..9f8ab189e 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 { @@ -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}" ); } @@ -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..e7881915e 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()], @@ -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 053a2ff22..61eab29f2 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": [ @@ -87,6 +87,7 @@ fn verdicts(root: &Path) -> Vec { batten::policy::Vocabulary { patterns: &[], verdicts: &verdicts, + words: None, recorders: &[], }, root, @@ -98,7 +99,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..4a604d5b0 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 @@ -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"; @@ -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], @@ -101,6 +101,7 @@ fn scan(root: &Path) -> rules::Scan { batten::policy::Vocabulary { patterns: &[], verdicts: &verdicts, + words: None, recorders: &[], }, root, @@ -134,7 +135,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 +217,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..1874e3a34 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 //! @@ -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 @@ -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", @@ -157,6 +157,7 @@ fn verdicts(root: &Path) -> Vec { batten::policy::Vocabulary { patterns: &[], verdicts: &verdicts, + words: None, recorders: &declared, }, root, @@ -168,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 @@ -341,7 +342,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..aa716108b 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"] @@ -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/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/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/pipeline_shapes.rs b/crates/batten/tests/it/pipeline_shapes.rs index 51a91a331..ca805ecbf 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`, @@ -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. @@ -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 @@ -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 @@ -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"), @@ -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")); @@ -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..720d5af17 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 //! @@ -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 @@ -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", @@ -130,6 +130,7 @@ fn scan(root: &Path) -> rules::Scan { batten::policy::Vocabulary { patterns: &[], verdicts: &verdicts, + words: None, recorders: &[], }, root, @@ -153,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 @@ -196,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"]), ); @@ -258,7 +259,7 @@ fn the_refusal_carries_no_entry_prose() { } // --------------------------------------------------------------------------- -// `plan-unrecorded` — the anti-vacuity arm. +// `plan declare absent` — the anti-vacuity arm. // --------------------------------------------------------------------------- #[test] @@ -310,7 +311,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..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}" @@ -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); @@ -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_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/policy_test_suite.rs b/crates/batten/tests/it/policy_test_suite.rs index 0afb983d1..f0c80f9aa 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: &[], } } @@ -120,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, " ") @@ -132,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"}} } @@ -143,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" } "#; @@ -155,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") @@ -166,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"}} } "#; @@ -497,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 @@ -509,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": {}}}} } "#; @@ -662,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/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/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..ab1e28143 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", @@ -76,6 +76,7 @@ fn findings(root: &Path) -> Vec { batten::policy::Vocabulary { patterns: &[], verdicts: &[], + words: None, recorders: &[], }, root, @@ -138,14 +139,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 +163,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 +173,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..413cbd86c 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. //! @@ -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}" ); } @@ -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 d1d1dac39..4312f7c23 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", @@ -114,6 +114,7 @@ fn findings(root: &Path) -> Vec { batten::policy::Vocabulary { patterns: &[], verdicts: &verdicts, + words: None, recorders: &[], }, root, @@ -129,8 +130,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" ); } @@ -383,7 +384,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. @@ -431,8 +432,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" ); } } @@ -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/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/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/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..e3c4e109c 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"], @@ -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 958839742..4bc660a5f 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, @@ -93,6 +93,7 @@ fn scan(root: &Path, rule: Rule) -> rules::Scan { batten::policy::Vocabulary { patterns: &[], verdicts: &verdicts, + words: None, recorders: &[], }, root, @@ -128,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)" ); } @@ -155,7 +156,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" ); @@ -234,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 @@ -300,7 +301,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..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 = "repetition-without-progress" +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/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..7af9ae45f 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 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 //! //! # 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}"); @@ -511,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 @@ -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..4d64840e6 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"; @@ -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/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..ade915e78 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 @@ -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/rules_drift.rs b/crates/batten/tests/it/rules_drift.rs index 60c2006b9..7b83cdaa4 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 `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. /// @@ -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 } @@ -342,7 +340,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 +389,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 +425,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 +466,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 +527,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 +566,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 +597,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}" ); @@ -608,7 +606,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 @@ -703,7 +701,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,8 +739,8 @@ 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"; +/// the anchor `input name missing` keys on. +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() { @@ -765,7 +763,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,6 +860,173 @@ 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" + ); +} + +// --- 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 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. + +/// 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 "probe read absent" + +violation contains { + "rule": "probe read absent", + "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 = "", + ), + ); + 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"]); + (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/run_shape.rs b/crates/batten/tests/it/run_shape.rs index 811c7ec1d..dee903473 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 @@ -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!( @@ -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/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..8fd11895e 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" @@ -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/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..a8b0f33be 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", @@ -129,6 +129,7 @@ pub(crate) fn scan(root: &Path) -> rules::Scan { batten::policy::Vocabulary { patterns: &patterns, verdicts: &verdicts, + words: None, recorders: &[], }, root, @@ -588,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" ); @@ -624,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" ); @@ -658,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" ); } @@ -693,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] @@ -706,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. @@ -757,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. @@ -774,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] @@ -790,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] @@ -803,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] @@ -820,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] @@ -836,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). @@ -859,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 @@ -948,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()]); } // --------------------------------------------------------------------------- @@ -986,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" ); } @@ -1082,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] @@ -1103,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] @@ -1125,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()]); } // --------------------------------------------------------------------------- @@ -1408,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) ); @@ -1442,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) ); @@ -1480,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) ); @@ -1514,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) ); @@ -1556,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: {:?}", @@ -1598,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) @@ -1635,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) @@ -1766,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" ); @@ -1783,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" ); } @@ -1874,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/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 ef3c35d4b..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,14 +216,14 @@ 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 /// 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 @@ -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/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/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/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..8cf4d1712 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", @@ -127,6 +127,7 @@ fn verdicts(root: &Path) -> Vec { batten::policy::Vocabulary { patterns: &patterns, verdicts: &declared, + words: None, recorders: &[], }, root, @@ -165,14 +166,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 +194,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 +296,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..5ed6be62e 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,11 +565,11 @@ 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 - // and `lock-platform-uninstallable` is what decided — so asserting the row + // the `violation` object declares — `lock cover partial` is what `--rule` selects + // 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/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..004f3b826 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"], @@ -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/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..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 @@ -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", @@ -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/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..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, @@ -435,7 +436,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"], @@ -453,6 +454,7 @@ fn route_findings(name: &str, authority: &str, manifest: &str) -> Vec { Vocabulary { patterns: &[], verdicts: &verdicts, + words: None, recorders: &[], }, &root, @@ -489,7 +491,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/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/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..1adf19891 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. @@ -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: &[], } } @@ -999,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" } "#; @@ -1016,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" ); @@ -1026,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/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/mise.toml b/mise.toml index 9f7f786da..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. @@ -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/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/ancestry-decides-nothing.rego b/policy/ancestry-decides-nothing.rego index 55bec4560..6f71197b8 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", @@ -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/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..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 { @@ -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 @@ -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..6ec368d27 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: @@ -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 { @@ -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..69a0308c8 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 ---------------------------------------------- @@ -94,7 +94,7 @@ rules contains "cache-path-is-rebase-stable" # 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 @@ -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 { @@ -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 { @@ -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 { @@ -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 @@ -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 { @@ -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"]}) 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/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/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/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/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/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/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/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/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..fde73655c 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 @@ -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/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/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/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/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/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-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/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/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..ba42ba7b5 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 { @@ -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 270089529..32cef10b0 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 @@ -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 @@ -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/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/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..8ae86fcdb 100644 --- a/policy/run-shape.rego +++ b/policy/run-shape.rego @@ -57,19 +57,19 @@ 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" +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 @@ -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 { @@ -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 @@ -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 @@ -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 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 b47890915..936d59546 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 { @@ -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:"` @@ -275,13 +275,14 @@ declares_it_stays_bash(path) if { # --------------------------------------------------------------------------- violation contains { - "rule": "shell-rule-retired", + "rule": "shell retire other", "verdict": "shell edit refused", "subjects": [{"path": path}], } if { 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 @@ -704,7 +705,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 @@ -1071,7 +1072,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 +1082,7 @@ violation contains { } violation contains { - "rule": "shell-rule-retired", + "rule": "shell retire other", "verdict": "shell retire unclear", "subjects": [{"path": path}], } if { @@ -1105,7 +1106,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 +1119,7 @@ violation contains { } violation contains { - "rule": "shell-rule-retired", + "rule": "shell retire other", "verdict": "test port missing", "subjects": [{"path": path}], } if { @@ -1144,7 +1145,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 +1166,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 +1197,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 +1262,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 +1280,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 +1310,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 +1325,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 +2435,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 @@ -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/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/spawn-adapters.rego b/policy/spawn-adapters.rego index 6a67805bf..a09a37e74 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 @@ -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 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/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 { 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 { 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..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 @@ -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 diff --git a/tests/ntia-check.bats b/tests/ntia-check.bats index b2a5905e3..560f56229 100644 --- a/tests/ntia-check.bats +++ b/tests/ntia-check.bats @@ -427,7 +427,7 @@ EOF # 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"' ] @@ -438,7 +438,7 @@ EOF # 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/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) }