Skip to content

feat(facts): one definition of an issue key, and a gate on the twenty-first - #818

Closed
wenzowski wants to merge 3 commits into
mainfrom
claude/cloud-1142-issue-key-one-definition
Closed

feat(facts): one definition of an issue key, and a gate on the twenty-first#818
wenzowski wants to merge 3 commits into
mainfrom
claude/cloud-1142-issue-key-one-definition

Conversation

@wenzowski

@wenzowski wenzowski commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Superseded by #811, which carries CLOUD-1142's commit replayed onto the same base.

The other two commits here are dropped rather than replayed, and that is the finding: they were a second, independent implementation of CLOUD-1231, written two hours after #817 had already landed one on its own branch. #817's version is the one #811 carries, with the weakens half this one had and that one did not added on top.

…-first

CLOUD-761 measured twenty independent derivations of the key pattern in
nine spellings, diverged on three axes, with a shipped defect behind
them: a body writing the lowercase form is accepted by one gate and
invisible to two others. This is the definition those sites converge on,
plus the gate that stops a twenty-first arriving.

THE CRATE OWNS THE AXES AND NEVER THE TOKEN. `no-tracker-key-in-core`
refuses a derivation anywhere under `crates/**` — the mechanism that
exists because CLOUD-1121 carried the literal in as a `const` and passed
every gate — so the token stays the consumer's, read from the
`[[pattern]]` registry, and `ready::Grammar` owns case, boundary and
anchoring. The tests spell the prefix from parts for the same reason: a
test is not exempt from the rule it is testing.

THREE OPERATIONS, AND SEPARATING THEM REMOVES A DERIVATION RATHER THAN
ADDING ONE. `key_of` answers "is this whole string a key" — the four
shell `case` globs' question, which they get wrong because a glob cannot
anchor, accepting `AB-1`, `Z-9`, `A-1foo` and a key with a trailing
letter. `keys_in` answers "which keys does this text carry". The third
question — "does this text carry key K" — needs no expression at all: it
is `keys_in` compared for equality, so the boundary the two landed sites
commented on is decided once.

The boundary is read off the bytes either side of a match rather than
composed as `(^|[^0-9A-Za-z-])…([^0-9]|$)`. `regex` has no lookahead, so
a trailing class would CONSUME the byte after a match and make adjacent
keys unfindable — and checking bytes means the crate composes no key
expression, so there is nothing here for a twenty-first copy to be a copy
of.

THE GATE RATCHETS RATHER THAN FORBIDS, which is what makes it landable.
34 occurrences survive across the task tree; a `forbid` fires on every
one. Converting them means editing governed programs, which is CLOUD-761's
remaining half. Meanwhile the count cannot grow.

Replayed over 400 commits of origin/main as CLOUD-1142's §7 requires,
before the severity was set: 400 examined, 1 would-fire, 0 false
positives. The one firing is `c64e54a3`, a revert that put four
derivations back — a true positive, and the same false premise this row's
own blocker rests on.

Shown able to fail, and it caught a dead gate doing it: the first draft
wrote the pattern in escaped regex form, but `ratchet_rule` counts with
`str::matches` — a literal substring — so it counted zero at both ends and
could never fire. Driving a twenty-first derivation through it is what
found that; reading it did not.

Admits: f3cdc5c1a49523305bc7451d2cc9bdd05a313d96a00e21164b405c6ce170bdc2
Admits-rule: protected-mutation
Admits-verdict: path write refused
Admits-subject: batten.toml
Admits-head: 9ece058
Admits-epoch: 7dcd0bd54e1da3834c9350ebcdeb40e91dbeb1c8a268d6466d66a26b459753c2
Admits-author: alec@wenzowski.com
Admits-prev: b28fd480c8126febacd3db96118787d32ebab604e9dce98f869210441887098e
Admits-answer-lost: The property whose absence is how twenty derivations arrived. CLOUD-761 measured twenty independent spellings of the issue-key pattern diverged on three axes, with a shipped defect behind them — a body writing the lowercase form is accepted by one gate and invisible to two others. Without this row the count can keep growing and nothing notices, which is exactly the history: replayed over 400 commits of `origin/main` the predicate fires once, on `c64e54a3`, a revert that put four derivations back. That firing is a true positive and there are no false ones.
Admits-answer-precondition: The change adds one `[[rule]]` row, `issue-key-derivations-not-growing`. A rule row IS the committed authority — no verb writes one, and CLOUD-1142's §1 names this file explicitly as where the anti-duplication gate's identity, applicability, scope and severity are declared. It lands beside `no-tracker-key-in-core`, the row that already refuses the same derivation under `crates/**`, so a reviewer reads the two halves of one predicate together: forbidden in the crate, ratcheted in the task tree.
Admits-answer-rejected-route: R-USE-THE-OWNING-SURFACE is rejected because this file is the owning surface for a `[[rule]]` row and CLOUD-1142's §1 says so by name. R-RESTORE-IT is rejected because restoring the committed bytes restores the ungated state, which is the defect rather than a fault to undo.

Refs: CLOUD-1142, CLOUD-761, CLOUD-1121
`mint` has always written line 1 as an id LIST and keyed its `weakens`
lines per issue, so a receipt holding several keys is the shape this file
was built for. Every invocation wrote a fresh one, though, so claiming a
second row on one branch silently discarded the first row's record.

Measured on CLOUD-1295's branch, an hour before this: re-claiming would
have dropped the `weakens` lines `config lint`'s groomed half reads,
which is the difference between a landable branch and a refusal nobody
can explain. That work moved to a branch of its own to avoid it, which is
the wrong reason to open a branch.

ONLY WHEN THE BASE AGREES, and that condition is the whole safety of it.
CLOUD-516's restart case is precisely a receipt outliving the branch it
described: `git checkout -B <name> origin/main` discards the commits and
keeps the filename. Carrying ids across that would let a restarted branch
inherit claims for work it no longer holds, which is the defect that row
exists to close. A differing, absent or unreadable base therefore
REPLACES rather than merges — the direction that forgets rather than the
one that over-claims.

Four cases, and two of them are the ones that keep this honest: the
restart case must still forget, and `Carried::default` must carry
nothing, without which a populated default would pass every other case
without reading a file at all.

Refs: CLOUD-1231, CLOUD-516, CLOUD-1142
@linear-code

linear-code Bot commented Sep 1, 2026

Copy link
Copy Markdown
CLOUD-1142 An issue key gets one definition, in the fact producer — the three axes decided, three-valued, and a gate that fails the twenty-first copy

Why

CLOUD-761 measured twenty independent derivations of the issue-key regex across nine spellings, diverged on three axes — case, boundary, and whether the project prefix is required at all — with a shipped defect behind them: a PR body writing closes cloud-757 is accepted by closing-key-check and then invisible to claimed-keys and released. Two gates disagree today about what an issue key is.

That row decided all three axes and then named the whole remedy as one piece of work: one definition, plus nineteen consumer conversions. The two halves have very different shapes, and only one of them is landable.

Seventeen of the nineteen consumer sites live in governed mise-tasks/ programsready-lint.sh, graph-check.sh, closing-key-check and released among them — and three of those cannot retire at all, being resolved by path from siblings that would themselves need an added line to be repointed. Converting a consumer in place raises V-SHELL-RULE-EDITED, which declares one route, R-PORT-AND-RETIRE, with no override and no bypass_env. So the conversion half stays on CLOUD-761 with that blocker named, and this row is the definition half, split out so it can ship.

Nothing here is a new decision. Every axis this row builds, CLOUD-761 already decided, and its wording is carried rather than re-argued.

Refinement — Ready

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

  • Authority boundary (§1). One definition of an issue key, in the fact producer under crates/batten/src/, declared as a typed board fact. batten.toml declares the identity, applicability, scope and severity of the anti-duplication gate over it; the generated schemas are derived by mise run schema and byte-compared, never hand-written. Nothing under mise-tasks/ and no tests/**/*.bats is added, edited or deleted here — this row converts no consumer, which is exactly what makes it separable from the half that cannot land.
  • Computable predicate (§2). The fact answers the key question once, on the three axes CLOUD-761 decided. Case: sensitiveCLOUD-757 is a key and cloud-757 is not one, and a body writing the lowercase form is refused rather than normalised, because normalising up is what produced the shipped defect. **Boundary: the explicit character-class form **(^|[^0-9A-Za-z-])…([^0-9]|$), not \b, so the rule does not depend on how a given matcher treats -. Project prefix: mandatory — a glob cannot anchor, which is why [A-Z]*-[0-9]* accepts AB-1, Z-9 and A-1foo, and CLOUD-[0-9]* accepts CLOUD-1x. The fact is three-valued: an unreachable tracker resolves to "could not look", never to an empty set that reads as a coherent board. The anti-duplication half is a gate over the tree: a second, independent derivation of the key pattern is a finding, because without one the twenty-first copy reappears and nothing notices, which is how twenty arrived.
  • **Effect (§3). **read. Board state is bounded and cacheable, resolved at verify / config lint time through the existing boundary; no verb changes class and no per-checker spawn is added.
  • Output & exit (§5). Pointer-only: a refusal names the key and the site, never a body. The crate's one 0/1/2/3 table with no per-verb exception — a malformed key is a policy verdict at 2, an unreachable tracker is 3, and 1 is a usage error. The 2/3 split must not collapse: that collapse is the vacuous pass this row exists to close.
  • **Commit / bump (§6). **feat(facts)patch until 0.1.0, since below that release-plz bumps the patch whatever the type says. Not ! for the consumer surface: no exit code or output shape a consumer sees moves, and nothing that is accepted today by the sites this row actually touches stops being accepted, because it touches no existing site. mise run semver decides the library half, which is where a new fact type can move a pub surface without anyone meaning it to.
  • **Test obligation (§7). **crates/batten/tests/*.rs over the compiled binary, each case shown able to fail. The fixed example set is the discriminator and it is written down rather than left to the implementer: CLOUD-757 accepted; cloud-757, AB-1, Z-9, A-1foo and CLOUD-1x refused; and CLOUD-17 not matched inside CLOUD-179. The vacuity case, which a two-valued fact passes silently: an unreachable tracker must be distinguishable from a board with no relations. And the anti-duplication case, without which the whole row decays: a twenty-first independent derivation of the key pattern must fail the gate. Before that gate's severity is set to deny, replay the predicate over git rev-list origin/main and record commits examined, the would-fire count, and how many of those firings were false positives; the number is reported, never graded.
  • Blockers (§8). None on this half — the fact model's cost class and its three-valued contract, which CLOUD-761 waited on, have both shipped. relatedTo CLOUD-761 (the consumer half this was split from, which this row unblocks), CLOUD-757 (the fact model), CLOUD-251 (the vacuous-pass trap), CLOUD-773 (why an exit code cannot carry what the producer already knew) and CLOUD-418.

Acceptance

  • An issue key has exactly one definition in the tree, and a second derivation fails a gate rather than passing unnoticed.
  • All three axes are resolved by the stated behaviour rather than by whichever copy happened to run: case-sensitive, the explicit character-class boundary, project prefix mandatory.
  • The fact is three-valued — an unreachable tracker yields "could not look", never an empty set.
  • No governed shell program and no bats suite is touched, so the row lands without reaching the conversion half.

Not in scope

Converting the nineteen existing derivation sites, and issue-read-check:91's message-versus-glob disagreement along with them. That is CLOUD-761's remaining half and it is blocked on the shell retirement, not on this row. Also out of scope: the Ready-block clause structure beyond §8's relation, which CLOUD-761 already scopes honestly as prose that stays prose.

CLOUD-1231 The claim receipt holds ONE key per branch, but `closing-key-check` expects a PR to close several — so a multi-row branch can only ever record a claim for one of them

Why

Two gates disagree about how many rows a branch may serve.

closing-key-check (CLOUD-674) is built for the multi-key case. Its refusal is "this PR closes some of the keys its commits served and strands the rest", and it enumerates them — so a PR closing three rows is an expected, supported shape.

The claim receipt is not. $GIT_DIR/batten-receipts/claim.<branch> is one file per branch holding one key:

CLOUD-1161
ready-lint pass
claimed-at 2026-08-30T17:44:25Z
base e340f796a07b648a3c0385c9f59b20d9dbd10594
branch claude/retire-shell-gates-t77iuk

Minting a claim for a second row on the same branch overwrites the first. There is no append, and the shape has no room for a second key.

Measured, 2026-08-31 on PR #770

That PR closes CLOUD-1145, CLOUD-1161 and CLOUD-1218 — a shape closing-key-check accepts and, on the last lap, actively demanded a Closes line for each. Its receipt records CLOUD-1161 only.

Claiming CLOUD-1218 by hand would have erased CLOUD-1161's claim, so I did the board half (Todo → In Progress, assigned) and left the receipt alone. That is the right call for the tree and the wrong outcome for the record: the branch performed work for a row whose claim it cannot show.

**Nothing failed, and that is the finding. **claim-needs-receipt only asks whether the branch carries a receipt, so the second and third rows ride the first one's. config-lint's weakens lookup reads ^weakens lines from the same file, so a Weakens clause groomed onto row two would be unreadable unless row two's claim were the one minted — which would then strand row one. The gap is silent in both directions.

Why this is not CLOUD-516's or CLOUD-733's

Both are about the receipt being keyed by branch name — a stale claim inherited after a merge (516), and a rename stranding it (733). Both are now Done. This is a different axis: the key is fine, the arity is one where the workflow's own gate expects many.

CLOUD-502 asked a related board question (a bundle landed as one PR cannot be represented) and was cancelled. This row is narrower and mechanical rather than about WIP accounting: it is about what the receipt file can hold.


Refinement — Ready (let a branch's claim record every row it serves)

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

  • Authority boundary (§1). The claim receipt's format and the code that mints and reads it — batten claim check and config-lint's weakens lookup. The board discipline is untouched: this changes what the receipt can record, not when a row may be pulled.
  • Computable predicate (§2). A branch that has claimed n rows carries all n in its receipt, and a read for any one of them succeeds. Minting a second claim must be additive rather than replacing, and re-claiming the same key must stay idempotent. Every reader that parses the file today — the weakens scan, and whatever asserts a claim exists — must resolve per key rather than positionally.
  • Deliberately not in scope (§2). Whether a branch should serve several rows. closing-key-check already settled that it may; this row only makes the receipt able to say so.
  • **Effect (§3). **write, to $GIT_DIR only, as today. No new surface and nothing tracked.
  • Output & exit (§5). Unchanged and pointer-only: keys, a timestamp, a base. Never issue prose.
  • **Commit / bump (§6). **fix(claim) — patch until 0.1.0.
  • Test obligation (§7). Shown able to fail per CLOUD-418. Red today: claim row A, then row B on the same branch, and read A back — it is gone. Green and staying green: a single-row branch's receipt is byte-identical to what it is now (or migrates cleanly), re-claiming the same key twice does not duplicate it, and config-lint's weakens lookup finds a clause groomed onto the second key. Without that last one the fix records the keys and still cannot serve the reader that most needs them.
  • Blockers (§8). None.

Acceptance

  • A branch serving n rows carries n keys in its receipt, and each is independently readable.
  • config-lint resolves a weakens clause groomed onto any claimed key, not only the first.
  • A single-key branch is unaffected — the common case does not change shape.

Found while landing PR #770: the branch served three rows, could record a claim for one, and no gate noticed.

CLOUD-761 Nineteen issue-key consumers still re-derive the key, and every one of them is a governed shell program the conversion cannot touch

Lifecycle bundle integration

Extend canonical board facts with status, lifecycle revision, semantic body digest, structured relations, claim generation, bundle roster, and PR/main/release evidence. An unreachable tracker must yield explicit unknown facts, never an empty clean payload.
The largest amortisation win in the tree, and the one with a shipped defect already attached.

Board state — issue status, relations, Ready-block clauses, issue keys — is re-extracted from markdown by every checker that needs it. Measured:

Signal Count
Independently re-derived copies of the issue-key regex 20 sites, 9 spellings — diverged on three axes, see below
ready-lint: markdown-dialect extraction → predicate ~95 lines → ~23 lines
claim-race-check: fetching → predicate ~50 lines → ~6 lines
graph-check: total → genuinely relational part ~700 lines → under 60

Those ratios have been read before as "an engine would only improve the small half". That reading is backwards: they are the price of nothing amortising extraction, paid once per checker, forever. Twenty derivations of one regex is not evidence that regexes are expensive — it is evidence that no fact exists.

And they have already diverged — on three axes, not one. Re-counted 2026-08-20 over code lines only (mise-tasks is 43–53% comment, so a naive grep inflates). The original "nine copies, diverged in case-sensitivity" undercounted the sites and missed the worst axis entirely.

Axis 1 — case. Five sites match case-insensitively and normalise up (closing-key-check:68, :98, claimed-keys:59, bot-issue:357, ready-lint:360); nine match case-sensitively (released:97, graph-check:216, :291, :302, ready-lint:314, bot-issue:295, :379, :436, deferral-check:104). Two expressions are mixed within two lines of themselves: bot-issue:357 matches insensitively and :358 re-extracts sensitively.

Axis 2 — boundary. (^|[^0-9A-Za-z-])…([^0-9]|$) (landed-check:63, done-check:108, both commented "so CLOUD-17 does not match CLOUD-179") versus \b…\b (board-move-guard:142, different semantics — - is a word boundary) versus grep -qxF (claim-race-check:127) versus unanchored.

Axis 3 — whether the project prefix is required at all. This was not in the original count, and it is the worst of the three. Four sites are shell case globs, not regexes: board-move-guard:104, issue-read-guard:71 and issue-read-check:88 all test [A-Z]*-[0-9]*; hooks-wiring-check:202 tests CLOUD-[0-9]*. Verified behaviour of [A-Z]*-[0-9]*:

input verdict
CLOUD-757 accept
AB-1 accept
Z-9 accept
A-1foo accept
cloud-757 reject

And issue-read-check:91 prints "is not an issue key (want CLOUD-123)" — an error message asserting a contract its own glob does not enforce. hooks-wiring-check:202 accepts CLOUD-1x for the same reason: a glob, not an anchored pattern.

The shipped defect is concrete. A PR body writing closes cloud-757 is accepted by closing-key-check and then invisible to claimed-keys and released. Two gates disagree today about what an issue key is. That is not a risk.

Cost class: read

Board state arrives over the tracker API — bounded, cacheable, resolvable at verify/config lint time. No 100ms budget question, and no §5 spawn question if resolved through the existing boundary rather than per-checker.

What this issue lands

  • One authority for an issue key, replacing twenty derivation sites. This alone justifies the issue.
  • Typed board facts: status, relations (blockedBy / blocks / relatedTo), milestone membership, and the Ready-block clause structure — as data, not as a markdown substring hunted per checker.
  • The extraction paid once. ready-lint's ~95 lines become a fact producer with one consumer today and several tomorrow; its ~23 lines of predicate stay predicates.

A caution this must respect. CLOUD-623's survey classified invariants by input shape and warned that where the hard part is turning prose into structure, an engine over already-normalized facts cannot help. That warning is right about engines and is exactly why this is a fact issue: the point is to do the extraction once and well, not to move the predicate to a different language. The Ready-block clause structure is the hard case — it is markdown with a dialect — and it should be scoped honestly: some clauses are structural (§8's blockedBy cross-check is already a relation), others are prose and stay prose.

A trap already recorded, not to be rebuilt. CLOUD-251"a rule set with no relations still reports the board coherent" — is the vacuous-pass failure. A board fact that resolves to "no relations" must be distinguishable from "could not look", which is CLOUD-757's three-valued contract doing real work here.

Acceptance

  • An issue key has exactly one definition; the other nineteen sites consume it. Asserted by a gate, or the twenty-first copy reappears.
  • All three divergences are resolved deliberately — one behaviour chosen and stated for case, for boundary, and for whether the project prefix is mandatory — not whichever copy happened to run. The four glob sites are the ones that change behaviour, so they need naming individually.
  • issue-read-check:91's message and its glob agree, in whichever direction is chosen.
  • Board facts are three-valued: an unreachable tracker yields "could not look", never an empty set that reads as coherent (CLOUD-251's trap).
  • At least one existing checker consumes the fact rather than re-extracting, with its extraction half deleted — the proof that amortisation is real rather than asserted.

Two findings filed 2026-08-20 point here. CLOUD-772 holds the volume half of the same defect: 73 independently hand-rolled parsers across nine committed artifacts, report() defined in 39 task files, exit 2 hand-rolled in 76, and zero source/. lines in the whole mise-tasks tree — there is no shared library and never has been. CLOUD-773 supplies the mechanism rather than another count: graph-check spawns ready-lint — the one program in the tree that parses a Ready block and identifies every key it cites — and then re-derives the issue-key regex itself at :216, :291 and :302, because an exit code cannot carry what the producer already knew. That is why the copies exist, and it is why one authority is necessary but not sufficient.

Filed from a subprocess-boundary audit; the ratio data is CLOUD-623's and CLOUD-647's, read the other way round.

Probe plan

  • Unresolved decision: canonical issue-key behavior for case, boundary, and mandatory project prefix.
  • Probe: run the named existing checkers over the recorded key examples, including closes cloud-757, AB-1, A-1foo, and CLOUD-1x, before and after routing one checker through the shared board fact.
  • Record: each checker verdict by example, selected canonical behavior, unreachable-tracker result, and evidence that one consumer no longer re-extracts the fact.
  • Ready when: all three behaviors are explicitly selected, the checker results agree, unreachable resolves to “could not look,” and one extraction half is deleted; then expand consumers from that authority.

Refinement — Ready (one definition of an issue key, and all three axes chosen rather than inherited)

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

The three divergences are decided here, because "resolve them deliberately" is not a predicate until the resolutions are named.

  • **Case: sensitive. **CLOUD-757 is a key and cloud-757 is not one, and a body writing the lowercase form is refused rather than normalised. Normalising up looks friendlier and is what produced the shipped defect: one gate accepts the spelling, two others cannot find it, and nothing says so. The cost is stated rather than hidden — a PR body writing closes cloud-757 fails closing-key-check where today it passes, which is the refusal that should always have been there.
  • **Boundary: the explicit character-class form **(^|[^0-9A-Za-z-])…([^0-9]|$), not \b. It states both sides in one place and does not depend on how a given matcher treats -, which is precisely what let two sites disagree while both looking correct.
  • Project prefix: mandatory. The four shell case globs are replaced outright, not tightened: a glob cannot anchor, which is why [A-Z]*-[0-9]* accepts AB-1, Z-9 and A-1foo, and why CLOUD-[0-9]* accepts CLOUD-1x. issue-read-check:91's message already asserts the prefix is required, so the message is right and the glob is what changes.
  • Authority boundary (§1). The definition itself is no longer this row's — it was split out so it could ship, and this row is the consumer half: the nineteen sites that re-derive the key each stop doing so and read the fact instead, with their extraction halves deleted. That is what makes §1 clean here and the row blocked all the same, because seventeen of those sites are governed mise-tasks/ programs. The Ready-block clause structure is scoped honestly rather than wholesale: §8's cross-check is already a relation and becomes a typed fact; the prose clauses stay prose, per CLOUD-623's warning that an engine over un-normalised prose cannot help.
  • Computable predicate (§2). Two, both decidable. First, every consumer's verdict over a fixed example set — CLOUD-757, cloud-757, AB-1, Z-9, A-1foo, CLOUD-1x, and CLOUD-17 inside CLOUD-179agrees, where today closing-key-check and claimed-keys disagree on the second. Second, board facts are three-valued: an unreachable tracker resolves to "could not look", never to an empty set. That is CLOUD-757's contract doing the work CLOUD-251 recorded as missing, and it is what stops a rule set with no relations reporting the board coherent.
  • **Effect (§3). **read. Board state is bounded and cacheable, resolved at verify/config lint time through the existing boundary rather than per-checker, so no verb changes class and no per-checker spawn is added.
  • Output & exit (§5). Unchanged and pointer-only: a refusal names the key and the site, never a body. Exit follows the one table — a malformed key is a policy verdict at 2, an unreachable tracker is 3, and the two must not be collapsed, since that collapse is the vacuous pass this row exists to close.
  • **Commit / bump (§6). **feat(facts)patch until 0.1.0, since below that release-plz bumps the patch whatever the type says. Not ! for the consumer surface: the change only ever adds refusals — cloud-757, AB-1, A-1foo and CLOUD-1x stop being accepted — which house style §8's raise-only invariant permits, and no exit code or output shape moves. mise run semver decides the library half, which is where a new fact type can move a pub surface without anyone meaning it to.
  • Test obligation (§7). Over the compiled binary and, for each converted consumer, whatever tier survives that consumer's own retirement — which is part of what is unresolved, since a consumer that retires takes its bats suite with it and a consumer that does not cannot gain a case. Each case shown able to fail per CLOUD-418. The discriminator is the shipped defect and it is red today: closes cloud-757 must produce one verdict across closing-key-check, claimed-keys and released, where today the first accepts it and the other two cannot see it. The four glob sites need naming individually because they are the ones whose behaviour changes: AB-1, Z-9, A-1foo and CLOUD-1x are accepted today and refused after. The vacuity case, which a two-valued fact passes silently: an unreachable tracker must be distinguishable from a board with no relations. And the anti-duplication case, without which this whole row decays: a twenty-first independent derivation of the key must fail the gate.
  • Blockers (§8). This row is blockedBy CLOUD-1142, which holds the definition half and must land first, and blockedBy CLOUD-757 for the fact model's cost class and its three-valued contract. relatedTo CLOUD-251 (the vacuous-pass trap), CLOUD-772 and CLOUD-773 (the volume half and the mechanism half of the same defect), CLOUD-623 (the caution that keeps this a fact row rather than an engine row) and CLOUD-418.

Split note, 2026-08-29 — the definition landed elsewhere; what is left here is blocked, and by what.

This row asked for one definition of an issue key and the conversion of nineteen consumers. The first half is landable and the second is not, so they are now two rows and this one keeps the harder half.

CLOUD-1142 carries the definition: the fact producer under crates/batten/src/, the three axes exactly as decided above, the three-valued could-not-look contract, and the gate that fails a twenty-first derivation. It touches no governed path and can be built today.

THE "CANNOT RETIRE" CLAIM IS WITHDRAWN (2026-09-01) — it was false in both halves, and it has already been paid for

The paragraph below said three programs cannot retire at all. Withdrawn rather than softened, because it is disproven twice over and it has already cost a landed retirement.

**Disproven by demonstration. **hooks-wiring-check.sh is one of the three. It is no longer tracked on origin/main. It retired.

Disproven by reading the module. The reasoning cites only_drops_a_retired_reference — the REMOVAL clause — and concludes about ADDITIONS. Additions are governed by admitted_addition, which has four arms; one of them, repoints_at_the_declared_successor, is an ADDED line swapping a retired path for the successor its own ledger declares. Repointing a sibling is exactly that arm's case. CLOUD-1299 has since added a fifth, admitting a caller that names its callee as a TASK rather than a path.

Already paid for. Replaying the key-derivation count over 400 commits of origin/main fires exactly once: c64e54a3, "take back the Ready-grammar retirement, whose successor cannot spell a sibling" — a revert that put four derivations back, on this same false premise.

The lesson generalises past this row: a blocker asserted from one rule while the admitting rule sits beside it is a block reported as a decision, which AGENTS.md already calls a bug. No row should cite "cannot retire" again without naming which admitted_addition arm fails and why.


What stays here is the conversion, and it is blocked on the retirement campaign's ordinary sequencing rather than on any impossibility. Seventeen of the nineteen sites are mise-tasks/ programs — ready-lint.sh, graph-check.sh, closing-key-check, released, landed-check, done-check, bot-issue, board-move-guard, issue-read-guard, issue-read-check, hooks-wiring-check, deferral-check, claim-race-check and claimed-keys among them. Editing one in place raises V-SHELL-RULE-EDITED, which declares one route, R-PORT-AND-RETIRE, with no override and no bypass_env. Retiring them instead is the sanctioned route, and it is available for all of them. ready-lint.sh is resolved by path from graph-check.sh:163 and pipefail-grep-check.sh, and graph-check.sh from board-sweep.sh and released.sh — sibling constructions, which is_retired_reference's arm 2 recognises and repoints_at_the_declared_successor admits repointing. hooks-wiring-check.sh carried a DECLARED table, and drops_a_retired_name (CLOUD-1224) exists for precisely a list that names a program by stem; it has since retired, which settles that one by demonstration rather than by argument.

So this row's acceptance clause — "at least one existing checker consumes the fact rather than re-extracting, with its extraction half deleted" — arrives with whichever consumer retirement lands first, and it belongs to the shell-retirement campaign's sequencing rather than to this row's own effort. It is recorded here so the next reader does not rediscover it as a surprise mid-build.

The definition half has landed (CLOUD-1142): ready::Grammar::key_of and keys_in own the three axes, the token stays the consumer's in [[pattern]] ready-issue-key, and issue-key-derivations-not-growing ratchets the task tree so the count cannot grow while these conversions wait. Replayed over 400 commits before its severity was set: 400 examined, 1 would-fire, 0 false positives.

Review in Linear

@coderabbitai

coderabbitai Bot commented Sep 1, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 30 minutes.

Check out review usage here.

View limit details

Limit 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.
You're only billed for reviews past your plan's rate limits ($0.25/file).

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Free

Run ID: 534068e0-6403-4324-82f4-0e3ce95aae2e

📥 Commits

Reviewing files that changed from the base of the PR and between 9ece058 and b01a2a4.

📒 Files selected for processing (3)
  • batten.toml
  • crates/batten/src/claim.rs
  • crates/batten/src/ready.rs

Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Essentials by visiting https://app.coderabbit.ai/settings/billing.

Comment @coderabbitai help to get the list of available commands.

… demand

The two helpers under `carried_claim_tests` reached for `expect()`, which
`clippy::expect_used` refuses in the lib test target as it does everywhere
else in the crate. The `let Ok(..) = .. else { panic!(..) }` form says the
same thing and is what the neighbouring test module already uses.

Refs: CLOUD-1231
@sonarqubecloud

sonarqubecloud Bot commented Sep 2, 2026

Copy link
Copy Markdown

❌ The last analysis has failed.

See analysis details on SonarQube Cloud

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant