fix(policy): name the DRY home, and make a retirement declare what engine source is (CLOUD-1239, CLOUD-1182) - #789
Conversation
CLOUD-1176 The retirement campaign has one disposition — "port it into the core" — so it launders bash-era scope creep into `crates/batten`, and the ratchet's landed WITHDRAWAL arm has never been used
Why Every row in this campaign assumes the successor is a thing built in That is a machine for moving bash-era scope creep into the core, and it What the spec says, and what the corpus actually contains§2's command surface is a DECLARED INTENT, not a closed list — and this row said the opposite. §2's own CLOUD-244 note settles it: " Measured against the emitted spec, 2026-08-30 — 18 rows disagree, so "closed" is false empirically as well as textually:
So the refusal this row is built on is §9 and rule 1, not §2. A new verb is not forbidden; a verb carrying consumer-specific behaviour is. Every conclusion this row draws survives on that footing — the board gates are And §2 ALREADY NAMES the verbs this campaign has been re-inventing. §9 and the document's closing line. "Consumer-specific behaviour is Non-negotiable rule 1. No consumer-specific identifiers in The scope reminder. Batten is "not a hook runner, file-shape linter,* And §11 undercuts the single largest row. Completions and man pages are The mechanism already exists and the campaign does not use it
So DELETE is spellable today. What is missing is the doctrine that makes The five homesA disposition is chosen before a successor is designed:
"Port into the core" is not on the list. Core work is confined to the fact* Refinement — Ready (choose the disposition before designing the successor) Refinement gate: Definition of Ready & Done. This body carries only specializations.
Acceptance
Found by pressure-testing the campaign's own framing against the house style: every CLOUD-1239 `has_policy_surface` cannot name a PRESET, so the one generic-by-construction home is unspellable — 0 of 609 landed arms name one, and the gate's incentive runs into the core
Why
has_policy_surface(path) if { ... startswith(name, "policy/"); endswith(name, ".rego") }
has_policy_surface(path) if { ... startswith(name, "crates/batten/src/"); endswith(name, ".rs") }A preset lives at Measured over the landed ledger at That distribution is not a taste for the core; it is a gate that had one. An author choosing the DRY home was refused and an author choosing The prose half, which is how this stayed invisibleThree sites describe the obligation as "a policy surface and a compiled-binary test", full stop — Under that reading CLOUD-1199's whole disposition — eleven gates retiring onto an existing verb, no module — has no spellable arm, and the author's only passing move is the false Distinct from CLOUD-1182, which is the adjacent rowCLOUD-1182 makes a retirement declare its successor KIND so a verb port is reviewable. This row makes the preset shape expressible at all. 1182 assumes the arm set is right and adds a declaration on top of it; without this row its Rust arm still cannot distinguish a preset from engine source, because a preset never reaches that arm. They compose and neither subsumes the other. Refinement — Ready (make the DRY home spellable, and say which home is which) Refinement gate: Definition of Ready & Done. This body carries only specializations.
Acceptance
Found while pressure-testing a dispatch prompt: the prompt said "no module needed", a reader objected that the ledger demands a policy surface, and reading CLOUD-1182 A retirement does not declare its successor KIND, so porting a gate as a new CLI verb satisfies `shell-retirement` exactly as well as porting it as a policy module — which is why nine ports became nine top-level nouns
Why
Nine ports took the first route, and the surface records it: The campaign is not finished. The alternative already exists and is documented.
Nineteen Why this is the row that mattersCLOUD-1176 names this finding — "the retirement campaign has one disposition" — and a tree-wide search for The recorded taxonomy is also at the wrong granularity to help. Refinement — Ready (declare the successor kind, and gate on it) Refinement gate: Definition of Ready & Done. This body carries only specializations.
Acceptance
Found while designing the surface redesign: the nine singleton nouns were traced to their cause, and the cause was that nothing distinguished the two successor shapes. |
|
Warning Review limit reachedNext included review available in 42 minutes. View limit detailsLimit details: You’ve used the included review currently available. Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. Review configuration: ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Free Run ID: 📒 Files selected for processing (20)
Note 🎁 Summarized by CodeRabbit FreeYour organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Pro by visiting https://app.coderabbit.ai/settings/billing. Comment |
`mem:workflow/board-states` carried the facts this follows from — "In Review means already merged", "Done means released" — and never drew the consequence for grooming: a Todo row has shipped nothing, so its body is the spec and you correct it in place; an In Review or Done row's body describes code already on main, so a wrong design there gets a new row and a superseded reference, never a rewrite. The cut is landed-ness, not readership. Rewriting a shipped row's body makes the record disagree with the code, invisibly — the CLOUD-994 class. The opposite error had no name until now, and it cost a session two wrong turns on an Urgent, unclaimed, Todo row whose central premise was false: twice left uncorrected in favour of a spin-off row plus a supersede note, inventing a second authority over a design nothing had built yet and leaving the ready queue holding the wrong spec. An earlier revision of this commit carried NO key trailer, on the reasoning that a `Refs:` would drag the row it cites into In Progress on the PR event. That reasoning was sound about the row it had in mind and wrong as a conclusion: `commit-lint` refuses a commit claiming no issue at all, and the original branch never learned that because it stayed a draft and CI never ran on it. The trailer names CLOUD-1239 instead — a row this branch actually serves, already In Progress and closed by this PR — so the citation is honest and moves nothing that should not move. CLOUD-1152 stays uncited for the original reason. Refs: CLOUD-1239
…s which `has_policy_surface` admitted a consumer module or engine source and nothing else, so a preset -- which lives at `crates/batten/src/policy/presets/**` and is a `.rego` -- failed the first arm on its prefix and the second on its suffix. The one successor shape `.claude/rules/policy-modules.md` calls generic-by-construction could not be spelled at all. The gate's incentive therefore ran the wrong way. Measured over the landed ledger at this branch's base: of 725 arms, 0 name a preset while 113 name engine source, 21 of those retiring a whole file. An author who picked the DRY home was refused and one who picked the core was not -- CLOUD-1176's scope creep arriving through the mechanism meant to bound it, and the reason those 113 are a habit rather than 113 considered choices. The shape is stable as the ledger grows: the same count taken at 5d38e0c read 609/0/110/18, so the preset column has stayed empty across 116 further arms. So this adds the preset arm, and names the choice where the author meets it. `.claude/rules/toolchain.md` carried "each names a policy surface and a compiled-binary test" with no discriminator, which reads as "a Rego module" and makes every retirement onto an existing verb look unspellable -- a misreading that cost a session two wrong turns in one turn, including a false alarm that a dispatched bundle was broken. It now names three homes in preference order and gives the test for the third: engine source is mechanism only, and non-negotiable rule 1 refuses a predicate there that names a consumer fact. `retirement_doctrine.rs` pins both directions, as it already does for the classifiers: the prose names three homes and the module declares three arms. The clause checks are whitespace-normalised, because these are prose and `mise run fmt` reflows them. Refs: CLOUD-1176
…t exists The arm landed with textual coverage only: `retirement_doctrine.rs` counts three `has_policy_surface` definitions and greps the module for the presets prefix. Both pass over an arm carrying the WRONG prefix, so what shipped was evidence that a third arm exists and none that it decides anything. `test_a_mapping_naming_only_a_preset_is_admitted` is the behavioural half: a retirement whose only policy surface is `crates/batten/src/policy/presets/**` is admitted. `test_mapping_without_a_policy_surface_is_refused` directly above is its anti-vacuity mirror, so an arm admitting everything fails one or the other. Shown able to fail rather than asserted: inverting the expectation names the case and moves the tier to 312 passed / 1 failed, which is also what proves the case runs at all -- `policy test` prints counts and never lists a passing rule, so a green tier is not evidence that a newly added rule was among them. Refs: CLOUD-1176
…rb or mechanism `has_policy_surface` admits `crates/batten/src/*.rs`, and until now it could not say WHAT that source is. A port landing a new top-level CLI verb and a port landing mechanism something else reads spell the successor identically, so the ledger recorded both the same way and the count of which happened was readable only by inspecting SURFACE and comparing by hand. That is why nine ports became nine singleton nouns without anyone deciding they should -- CLOUD-1176's "one disposition" arriving through the mechanism meant to bound it. So an engine-source arm now carries `kind:verb` or `kind:mechanism`, marked and validated the way CLOUD-1219's invocation field is, and excluded from `path_successors_for` for the same reason: the token can never satisfy `has_policy_surface`, never satisfy `has_binary_test`, and never be substituted in where a path belongs. Every obligation a retirement already owed it still owes. `kind:module` is NOT a value, and the Ready block asked for one. It reached for module-versus-verb on the premise that "a module's Rust second tier also lives there" -- it does not. A module's second tier is `crates/batten/tests/*.rs`, which is `has_binary_test`'s column and a different field on the same row. The honest split for `crates/batten/src/` is the one `.claude/rules/toolchain.md` already draws for its third home: mechanism, or a verb. The derive-where-you-can half relieves nothing, which is worth recording rather than assuming: of the 113 landed engine-source arms, ZERO also name a module, so there is no arm whose kind the path already decides. All 113 are annotated here -- 77 `kind:verb`, 36 `kind:mechanism` -- which is what makes CLOUD-1176's finding a number a reviewer reads off the ledger instead of an impression. The gate does not forbid a verb, and both tiers carry the anti-vacuity mirror that proves it: a declared verb successor is admitted, a declared mechanism successor is admitted, and a module successor owes no field at all. Without those the refusal is satisfied by a rule refusing every engine-source retirement, which this row puts explicitly out of scope -- a gate needing stdin, spawning with its own arguments, or performing a write cannot be a tree-scoped module. No declared #MUTANT row: `shell-retirement` carries `#MUTANT-EXEMPT CLOUD-931`, because `mutant` resolves a gate's suite as `tests/$gate.bats` and this rule's whole subject is that a migration ships no new bats suite. There is no named case a mutation could turn red, so the discriminating obligation is met by the anti-vacuity pair over the compiled binary instead. `V-SUCCESSOR-NO-SURFACE`'s class text is corrected in the same change: it named two successor shapes where the arm now admits three. Refs: CLOUD-1176
61a4e91 to
540b84b
Compare
|
❌ The last analysis has failed. |
|
/fast-forward |
has_policy_surfacecould not name a preset, so the one generic-by-construction successor home was unspellable in a retirement ledger — and once it could, the ledger still could not say whether an engine-source successor was a new CLI verb or mechanism. Both halves are the same defect at two depths, so they land together.The finding
policy/shell-retirement.regoadmitted two successor shapes:A preset lives at
crates/batten/src/policy/presets/**and is a.rego: wrong prefix for the first arm, wrong suffix for the second. It satisfied neither.Measured over the landed ledger at this branch's base — restated from the tree rather than quoted, per CLOUD-1239's acceptance: of 725 arms, 0 name a preset, 113 name engine source, 21 of those retiring a whole file. The same count at
5d38e0c2read 609/0/110/18, so the preset column has stayed empty across 116 further arms. That is not a taste for the core — it is a gate that had one.Why the second half is not a separate PR
Widening the arm changes no landed decision on its own. CLOUD-1239 puts re-dispositioning the 113 explicitly out of scope, so shipped alone it is an enabling change nothing yet uses.
CLOUD-1182 is what uses it, and it needs 1239 first: its whole predicate is "declare what an engine-source successor is", and while a preset never reaches that arm the distinction cannot be drawn. Measured while implementing it: of the 113 engine-source arms, 0 also name a module, so the derive-where-you-can half relieves none of them and the declaration is the only thing that can answer.
What changed
policy/shell-retirement.regokinds_for/names_engine_source;V-SUCCESSOR-KIND-UNDECLARED; 6 newtest_casesbatten.tomlretirement-kind-fieldpattern, theV-SUCCESSOR-KIND-UNDECLAREDverdict and its route, and a staleV-SUCCESSOR-NO-SURFACEclass naming two shapes where the arm now admits three.claude/rules/toolchain.mdcrates/batten/tests/shell_retirement.rscrates/batten/tests/retirement_doctrine.rskind:verb, 36kind:mechanismkind:moduleis not a value, and CLOUD-1182's body asked for it. That row reached for module-versus-verb on the premise that "a module's Rust second tier also lives there" — it does not. A module's second tier iscrates/batten/tests/*.rs, which ishas_binary_test's column and a different field on the same row. So the honest split forcrates/batten/src/is the one.claude/rules/toolchain.mdalready draws for its third home: mechanism, or a verb. The Ready block's intent is unchanged; only the value names are.77 of 113. That is CLOUD-1176's "one disposition" finding as a number rather than an impression, and it is now readable off the ledger instead of reconstructed from
SURFACE. It also closes that row's zero-hits finding —CLOUD-1176is now cited where a gate can reach it.Verification
mise run verify: fast-forward-green — rebased on latestmain, ci + cross + commit-lint all pass.policy test: 408 passed, 0 failed (404 before the 1182 cases).test_a_mapping_naming_only_a_preset_is_admittedgives 403 passed, 1 failed naming that case — which is also what proves it runs at all, sincepolicy testprints counts and never names a passing rule.No declared
#MUTANTrow, and the reason is the module's own exemption.shell-retirementcarries#MUTANT-EXEMPT CLOUD-931:mutantresolves a gate's suite astests/$gate.bats, and this row's whole subject is that a migration ships no new bats suite — so there is no named case a mutation could turn red. CLOUD-1182 §7's discriminating-mutation obligation is met by the anti-vacuity pair above instead, which is the same discrimination decided over the compiled binary.Two things this PR corrects about its own history
It is not #751 and not that branch.
claude/stage-2-3-grooming-uqk71kalready headed merged #722 (2026-08-28, an unrelatedfix(mise)change) and was never deleted; a third story on one name isbranch-age-check'sreusedproperty, which only branch deletion clears. #751 was also 50 commits behindmainand had never had CI run on it — every check on its head wasskipped, which is draft behaviour, not a failure.commit-lintthen refused its first commit for carrying no key trailer at all, which is a defect the draft had been hiding.Refs: CLOUD-1176stays on the commits and does not close that row. An earlier revision of #751's body carried one, and the tracker attached the PR and set CLOUD-1176 — an Urgent Todo row — to Done at 04:36:53 with nothing merged. It was restored to Todo. AGENTS.md is explicit that Done means released and is never the merge's to set. The citation is honest and stays; theDO-NOT-CLOSEline below is what stopsclosing-key-checkstranding it.DO-NOT-CLOSE CLOUD-1176 — this branch cites that row and mechanises part of its finding, but does not implement it.
Closes CLOUD-1239
Closes CLOUD-1182
Generated by Claude Code