Skip to content

fix(board): the column vocabulary stops being an engine constant - #911

Draft
wenzowski wants to merge 6 commits into
mainfrom
claude/rule-1-seam-bundle-0nsnym
Draft

fix(board): the column vocabulary stops being an engine constant#911
wenzowski wants to merge 6 commits into
mainfrom
claude/rule-1-seam-bundle-0nsnym

Conversation

@wenzowski

Copy link
Copy Markdown
Contributor

Closes CLOUD-1623.

Non-negotiable rule 1 binds MECHANISMS, not only string literals, and landed
and claim decided over one board's COLUMN NAMES. Linear ships
Todo/In Progress/In Review/Done; Jira ships To Do/In Development; a GitHub
Project ships whatever the owner typed.

This is rule 1's violation in its worst form, because the failure is silent in
the direction that matters. Off this board every comparison is false, so
is_started never fires and the landed-honesty sweep reports zero findings
over a board full of dishonest columns
, while claim never refuses. A gate
that cannot fire and a gate that found nothing emit the same bytes and the same
exit code.

The seam

The words move to a [board] table read through board::Columns.
board_columns() is board_grammar()'s sibling, for the reason CLOUD-1100 gave
one table over.

Absent is could-not-look, never a default. An undeclared column refuses BY
NAME rather than falling back to this repository's words: a default would
reinstate the violation with one more step in front of it and restore exactly the
property that let the constants survive. Columns::started reads an EMPTY set as
undeclared for the same reason — a set matching nothing reports every row as
not-advanced, which is the silent all-clear.

The half that nearly shipped the same defect back

claim::judge refuses an undeclared board, but landed check and
landed abandoned initially did not — every predicate answered false, so they
returned zero findings and exit 0 over a board they had never read. That is
byte-identical to a clean sweep. Both verbs now DEMAND the columns they decide
over: landed check needs both in_progress and started, not merely whichever
the payload happens to exercise, because a run answering half the disjunction
silently is the halved sweep CLOUD-1458 already paid for.

The duplicate predicate, resolved in the direction the tree already argued

claim required the literal github.com/; landed carried a host-free copy
whose doc argues forge URL SHAPE is not consumer vocabulary. That reasoning is
right, so the host-free one is now the single spelling. This also fixes a live
defect: off GitHub, live_pull_request answered None for every real pull
request, so the open-competitor check was dead.

Recorded rather than silently widened: a GitLab merge request is
/merge_requests/<n> and still does not match. Narrower than the host literal it
replaces — matching every /<word>/<digits> would admit /issues/12.

The gate (rule 2)

no-board-column-in-core refuses the next column literal. It matches the
COMPARISON rather than the vocabulary, because banning the four words fires on
126 occurrences across 18 files — nearly all legitimate fixtures and prose — and
would land carrying an exemption list longer than itself. Shown able to fail: a
planted s == "In Progress" exits 2 naming path:line; reverting returns it to 0.

Sites the row did not carry

Found on re-read: a second independent is_in_progress in impl Claim, and
demand()'s refusal prose, which named a column a consumer's board may not have.

Fixtures

Three suites invoke these verbs and each now declares the board it is judged
against, read from the committed table via common::declared_board() rather than
re-spelled — one authority for this repository's columns, per the same doctrine
the seam itself enforces.

Non-negotiable rule 1 binds MECHANISMS, not only string literals, and
`landed` and `claim` decided over one board's COLUMN NAMES. Linear ships
Todo/In Progress/In Review/Done; Jira ships To Do/In Development; a
GitHub Project ships whatever the owner typed.

This is rule 1's violation in its worst form, because the failure is
silent in the direction that matters. Off this board every comparison is
false, so `is_started` never fires and the landed-honesty sweep reports
ZERO FINDINGS over a board full of dishonest columns, while `claim` never
refuses. A gate that cannot fire and a gate that found nothing emit the
same bytes and the same exit code.

The words move to a `[board]` table. `board::Columns` resolves it, and
`board_columns()` is `board_grammar()`'s sibling for the same reason
CLOUD-1100 gave one table over.

## Absent is could-not-look, never a default

The load-bearing decision. An undeclared column refuses BY NAME rather
than falling back to this repository's words: a default would reinstate
the violation with one more step in front of it and restore exactly the
property that let the constants survive — the dead path and the working
path answering identically. `Columns::started` therefore reads an EMPTY
set as undeclared rather than as "no column means started", since a set
matching nothing reports every row as not-advanced, which is the silent
all-clear.

`judge` refuses the RUN when the queue is undeclared, not every row: a
claim gate that cannot name the queue has not decided nothing is
pullable. `Reason::wants` returns `Option`, so an undeclared column costs
the ` -> <column>` remedy and never the finding itself.

## The duplicate pull-request predicate, resolved

`claim` required the literal `github.com/`; `landed` carried a host-free
copy whose doc argues forge URL SHAPE is not consumer vocabulary. That
reasoning is right, so the host-free one is now the single spelling. This
also fixes a live defect: off GitHub `live_pull_request` answered `None`
for every real pull request, so the open-competitor check was dead.

Recorded rather than silently widened: a GitLab merge request is
`/merge_requests/<n>` and still does not match. Narrower than the host
literal it replaces — matching every `/<word>/<digits>` would admit
`/issues/12` and turn an issue link into a claim.

Two sites CLOUD-1623's body does not carry, found on re-read: a second
independent `is_in_progress` in `impl Claim`, and `demand()`'s refusal
prose, which named a column a consumer's board may not have.

`batten.toml`'s `[board]` table was written under admission
8b378ecbdd0e721328b63d4e7661c18704d71af2534b07a81665dcdfa9280835.

Refs: CLOUD-1623

Admits: 8b378ecbdd0e721328b63d4e7661c18704d71af2534b07a81665dcdfa9280835
Admits-rule: protected-mutation
Admits-verdict: path write refused
Admits-subject: batten.toml
Admits-anchor: call:ea307944cd9ec0c1e4f52720f78ce49d08aa7761
Admits-epoch: ed0cc67ec403a5849f97cbe66586e656ab21f7b6d2015725b2d51a2a2c6f5b91
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: The engine half without the config half is a dead gate. `landed check`, the abandonment drain and `claim check` all resolve their columns through `board::Columns`, and an undeclared table is could-not-look by design — so shipping the Rust change alone turns three working gates into three that decide nothing, while `mise run verify` reports the tree clean. That is precisely the failure CLOUD-1623 exists to remove, reintroduced by landing half of its fix.
Admits-answer-precondition: No batten surface writes a config table. `batten config` inspects only, and `batten init` refuses to overwrite an existing batten.toml, so neither can add a `[board]` table to a file that already exists. The change is the config half of CLOUD-1623 (claimed, receipt minted on this branch): four column names moving out of `crates/batten/src/landed.rs` and `claim.rs`, where they were non-negotiable rule 1's violation. It is a 43-line addition to one table in a draft PR, visible in the diff a reviewer reads before it lands.
Admits-answer-rejected-route: `patch`. Tried and refused identically (`path write refused batten.toml patch protected-mutation`), which is correct: the guard is on the path, not the program, so routing the same bytes through a second writer would be evading the class rather than satisfying it. `config read first` does not apply either — the file was read directly and through `batten config` before this write, and reading cannot add a table. `run first` names no task that authors config.
Admits: 827500be1882ead25b401784a3c9845c5120c7b9cc906f58d9fc993b3d2bec68
Admits-rule: protected-mutation
Admits-verdict: path write refused
Admits-subject: .serena/memories/core.md
Admits-anchor: call:ea307944cd9ec0c1e4f52720f78ce49d08aa7761
Admits-epoch: 24cb2114988016256569172b8027e2e9b6019765eb89934424819c2aa9ac155b
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: `module-map-check` refuses the commit outright, so CLOUD-1623 cannot land at all. Beyond that, its stated defect is the point: `mem:core` is the single authority on what each module owns, and a module added without its row leaves that authority silently incomplete and the rule pointing at it untrue — measured once already on `severity.rs`, which landed past a green gate with no row. A new module carrying the `[board]` seam is exactly the row a later reader needs.
Admits-answer-precondition: The sanctioned writer for `.serena/memories/**` is the Serena MCP tools, and that server failed to connect this session (CONNECTION_CLOSED), so no surface can express the change. The change is one row naming `board.rs` in the module map, required by `module-map-check`, which refused the commit until it existed: every tracked `crates/*/src/*.rs` must appear in `mem:core`. It is 13 lines in a draft PR, visible in the diff a reviewer reads before it lands.
Admits-answer-rejected-route: `patch`. Refused identically on the sibling path this commit also writes (`path write refused batten.toml patch protected-mutation`); the guard is on the path rather than the program, so routing the same bytes through a second writer evades the class instead of satisfying it. `config read first` does not apply — this file is not config, and reading cannot add a row. `run first` names no task that authors the module map; `module-map-check` only reports the gap.
Non-negotiable rule 2: a rule without a runnable gate is half a change.
32f9876 took the board's column names out of the engine; nothing yet
refused the next one, and they arrived in the first place because nothing
refused them. This file already carries the measured cost of that shape —
twenty issue-key derivations accumulated because nothing refused the
twenty-first before it was typed.

## Why this matches the comparison and not the vocabulary

The obvious row is unlandable, and that is a measurement. Banning the four
column WORDS under `crates/**` fires on 126 occurrences across 18 files,
nearly all legitimate: integration fixtures building board payloads
(`landed_check.rs` 19, `claim.rs` 25, `board_receipts.rs` 12), unit-test
vocabulary, and prose recording why the released column is in the started
set. That row either never lands or lands carrying an exemption list
longer than itself — and an exemption is where the next violation hides.

The violation was never the word. It was COMPARING A STATUS AGAINST A
LITERAL, because that is the line that decides, and the line that answers
false on every other board while reading as a clean sweep. So the regex is
the comparison shape in both operand orders, and it reaches exactly the
construct `board::Columns` replaced.

It lands at ZERO with no `exclude`. The only match after the seam commit
was `board.rs`'s own anti-vacuity test asserting a foreign board's set
lacks this repository's word; that assertion now compares the whole set,
which is stronger anyway — a negative assertion also passes for a resolver
that dropped every column.

`checks_green.rs`'s `status != "completed"` and `hk.rs`'s `!= "included"`
are deliberately not reached. They compare a check run's state and a
plan's state — a forge's and this engine's own vocabularies, not a
tracker's. A row keyed on the bare field name would have caught both and
taught the next reader that the ban is about the field.

## Shown able to fail

The acceptance CLOUD-1626 records as missing from the last rule-1 gate.
Clean tree exits 0; one planted `s == "In Progress"` in `receipt.rs` exits
2 reporting `crates/batten/src/receipt.rs:2477 no-board-column-in-core`;
reverting returns it to 0. Pointer-only per rule 4 — `path:line` and the
id, never the matched line.

BREAKING CHANGE: `claim::judge`, `landed::decide` and `landed::drain` each
take the board's column vocabulary as a parameter, introduced by the seam
commit on this branch. The vocabulary cannot be defaulted inside them —
an undeclared column is could-not-look, and a default would restore the
dead-path-reads-as-clean property the seam exists to remove — so the
caller resolves `board::Columns` and passes it in.

Refs: CLOUD-1623
Admits: a953ddc5aa7b792a4f31157caa36d9f1ef8de40802c4d0d2b9c3e592b60d5c94
Admits-rule: protected-mutation
Admits-verdict: path write refused
Admits-subject: batten.toml
Admits-anchor: call:32f98767e586a4476159245512fc3d72c618f899
Admits-epoch: 24cb2114988016256569172b8027e2e9b6019765eb89934424819c2aa9ac155b
Admits-author: alec@wenzowski.com
Admits-prev: 8b378ecbdd0e721328b63d4e7661c18704d71af2534b07a81665dcdfa9280835
Admits-answer-lost: CLOUD-1623 stays half-done and silently re-openable. The column literals are out of the engine as of 32f9876, but nothing refuses the next one: the constants originally arrived because no gate refused them, and non-negotiable rule 2 is explicit that a rule shipped without its mechanism is prose. Measured precedent in this file: twenty issue-key derivations accumulated for exactly this reason, and `issue-key-derivations-not-growing` exists because nothing refused the twenty-first before it was typed.
Admits-answer-precondition: No batten surface authors a `[[rule]]` row. `batten config` inspects only and `batten init` refuses to overwrite an existing file, so writing `batten.toml` directly is the only route to add the gate. This is the rule-2 half of CLOUD-1623 (claimed, receipt minted on this branch): the seam landed in 32f9876 and a rule without a runnable mechanism is half a change. It is one `[[rule]]` row plus its comment, in a draft PR, visible in the diff a reviewer reads before it lands.
Admits-answer-rejected-route: `patch`. Refused identically on this same path earlier in this session (`path write refused batten.toml patch protected-mutation`); the guard is on the path rather than the program, so routing the same bytes through another writer evades the class instead of satisfying it. `config read first` does not apply — the file was read directly before this write and reading cannot add a row. `run first` names no task that authors a rule; `batten check` only evaluates rows that already exist.
Three census gates refused the seam commit, all correctly and all the
same class: a new module and a new config key arrived without the rows
that make them decidable. None was a code defect.

## The table moves into the module that reads it

`Board` was declared in `config.rs` while `Columns` — the resolved form
and every predicate over it — lived in `board.rs`. That is the split
`mcp::McpConfig`, `recorder::Declared` and `deferral` each avoid: a
module that exists owns its own declaration, so the type and the
predicate reading it cannot drift apart across a module boundary.

It also settles the layering. `board` now reaches NOTHING in this crate,
not even `error`. `config` reads it at load; `landed`, `claim` and `lib`
read it at decision time; it reads none of them. That is `secret`'s
placement arrived at from the same direction, and it is the placement
rather than an accident of size: this module exists so a gate's verdict
about a board is stated in the CONSUMER's words, and an edge from here to
any decider would make the honesty of those words conditional on that
decider's own layer.

## `trust::CENSUS` — not policy-bearing, for the structural reason

`board` is read by gates, so the verdict is not that it lacks policy
weight. It is that an override cannot speak to it: the key is absent from
`OverrideConfig` and `resolve` reads the table from the committed
authority alone, which is `contract`'s guarantee for `epoch`'s reason.
That makes the obvious attack unwritable — an uncommitted layer renaming
`ready` to a column every row already sits in would make every row
pullable at once.

A weakening row would be the wrong instrument, which is `mcp`'s point: it
reports a DIRECTION, and a column name has none. Renaming a queue is not
more or less permissive as config; which rows it admits depends on where
the board has put them, which is tracker state rather than a bar this
file sets.

## `module-layering` — the placement the rule asked for

Its coverage clause states absence is an error rather than an allow, and
it named `board` on the gate before landing, as it has for every module
since `preset`. The entry is additive placement, not an exemption: it
adds no allowance and removes no refusal, and a module violating a
forbidden edge would still be refused.

Written under admission
06891c5cdc3e1e55ce23c216053798934226f5904bb2ef87543efa256874e5a2.
`policy/**` is protected by derivation — enabling a module protects it,
so an agent's context cannot influence the rules it is judged by — and
that grant is this edit alone, not a standing one.

Refs: CLOUD-1623
Admits: 06891c5cdc3e1e55ce23c216053798934226f5904bb2ef87543efa256874e5a2
Admits-rule: protected-mutation
Admits-verdict: path write refused
Admits-subject: policy/module-layering.rego
Admits-anchor: call:1aeaf9af0e5e2be0788f653ae068221893c97952
Admits-epoch: c99c0afd250a1f693c40bca4e07d5d52d391ef1ecd443fa225f2d10f4629573f
Admits-author: alec@wenzowski.com
Admits-prev: -
Admits-answer-lost: `module-layering` refuses the tree, so CLOUD-1623 cannot land. The refusal is the rule's own coverage clause — "absence is an error, not an allow" — firing on `board.rs`, a module I added and did not place. Not overriding does not leave the rule enforcing more; it leaves a correct finding unanswered and the branch stuck, because the only answer the rule accepts is the placement it is asking for.
Admits-answer-precondition: No surface writes a policy module. `policy/**` is protected by derivation rather than by the `protected` list — enabling a module protects it, so an agent's context cannot influence the rules it is judged by — and its `[[redirect]]` names a pull request, not a command. This IS that pull request: branch `claude/rule-1-seam-bundle-0nsnym`, draft, reviewed before it lands. The change is one name added to `declared_modules` plus its placement comment, and `mise run policy-test` checks it.
Admits-answer-rejected-route: `patch`. Refused identically on the two sibling protected paths this branch already writes, and correctly: the guard is on the path rather than the program, so routing the same bytes through another writer evades the class instead of satisfying it. `config read first` does not apply — a policy module is not config, and reading cannot add a row. `run first` names no task that authors the table; `module-layering` only reports the gap. NOTE THE NARROWNESS: this is additive PLACEMENT, not an exemption — the same entry `preset`, `agent`, `recorder`, `secret` and `deferral` each received on arrival. It adds no allowance and removes no refusal; a module that violated a forbidden edge would still be refused.
`claim check` now refuses the run when `board.ready` is undeclared, so
two cases in this suite went red against a fixture repository that
declares no `[board]`. The refusal is correct and the fixture was
incomplete: a repository that has declared no board HAS no ready queue,
and answering "nothing is pullable" over one would be the guess the seam
exists to remove.

This is the same sentence the fixture already carries one table over.
CLOUD-1100 made the Ready grammar the consumer's and left this comment
here: "Without these rows `claim check` reports could-not-look naming the
first missing id — which is the correct answer for a repository that has
declared no Ready grammar, and not what this suite is about." The columns
are that sentence again, and the two now sit together because neither the
grammar nor the vocabulary is the engine's to assume.

Refs: CLOUD-1623
…ng it clean

**The seam reintroduced its own defect one layer up, and this removes it.**
`claim::judge` was made to refuse when the board is undeclared; `landed`
was left to decide with the columns it had. Undeclared, it has none —
every comparison against a row's status is false, so `decide` returns
ZERO FINDINGS and the verb exits 0 over a board it never read.

That is byte-identical to a clean sweep, which is the exact failure this
row exists to remove. A gate that cannot fire and a gate that found
nothing emit the same bytes and the same exit code, and shipping the seam
with this in place would have moved the dead path rather than closing it.

Both verbs now demand the columns they decide over, before deciding.

## Both directions, not the one the payload happens to exercise

`landed check` demands `in_progress` AND `started`. Demanding only the
arm a given payload reaches would leave a run that answered half the
disjunction and said nothing about the other half — the silently-halved
sweep CLOUD-1458 already paid for once.

`landed abandoned` demands `in_progress`, where the edge is sharper:
every candidate is SELECTED by that column, so undeclared yields an empty
candidate set and the drain reports no abandoned claims over a board full
of them. An over-reporting drain gets switched off; a silently empty one
is never noticed.

## The fixtures declare the board they are judged against

`common::declared_board` reads the committed `[board]` table, mirroring
`declared_patterns` and for its reason: this repository's columns have
one authority, and a fixture holding a second copy drifts the day the
board is renamed. `landed_check.rs` shadows `scratch` so all eleven cases
build on a declared board rather than asserting about a missing column.

Refs: CLOUD-1623
`claim check` declares the `-J` data channel, and a data channel emits
its document UNCONDITIONALLY — one that is sometimes absent is
unparseable for the caller that asked for it. The board refusal lands
before any output, so over a fixture declaring no `[board]` the verb
emitted nothing at all and the purity census went red.

The fixture is the fix rather than the refusal, and this fixture's own
doc already says why: it exists to be "a config every data-emitting verb
can actually answer from", because "the census is about the output
contract; supplying each verb's minimum input is the fixture's job".
A repository that declares no board HAS no ready queue, so refusing there
is correct and simply not what this census asks about.

That makes `[board]` the fifth minimum input this fixture carries, beside
the Ready grammar it already carried for the identical reason (CLOUD-1100):
both are the consumer's vocabulary, and neither is the engine's to assume.
Read from the committed table rather than re-typed, so the fixture cannot
drift from the columns it exercises.

Refs: CLOUD-1623
@coderabbitai

coderabbitai Bot commented Sep 8, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 35 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 7fb24d30-a1f7-48e9-9743-3175a96f749a

📥 Commits

Reviewing files that changed from the base of the PR and between a4b1548 and 9f9d9ad.

⛔ Files ignored due to path filters (1)
  • hk.pkl is excluded by !**/*.pkl
📒 Files selected for processing (15)
  • .serena/memories/core.md
  • batten.toml
  • crates/batten/src/board.rs
  • crates/batten/src/claim.rs
  • crates/batten/src/config.rs
  • crates/batten/src/landed.rs
  • crates/batten/src/lib.rs
  • crates/batten/src/resolve.rs
  • crates/batten/src/trust.rs
  • crates/batten/tests/it/claim.rs
  • crates/batten/tests/it/cli.rs
  • crates/batten/tests/it/common/mod.rs
  • crates/batten/tests/it/landed_check.rs
  • policy/module-layering.rego
  • schema/batten.schema.json

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@sonarqubecloud

sonarqubecloud Bot commented Sep 8, 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