Skip to content

fix(kanban): an INVERTED anchor pair is unreadable, not "nothing deployed" (backend#1994) - #259

Merged
LukasWodka merged 1 commit into
developfrom
fix/1994-anchor-order
Aug 14, 2026
Merged

fix(kanban): an INVERTED anchor pair is unreadable, not "nothing deployed" (backend#1994)#259
LukasWodka merged 1 commit into
developfrom
fix/1994-anchor-order

Conversation

@LukasWodka

@LukasWodkaLukasWodka commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Bugbot High on the staging promotion PR .github#258, which held the 2026-08-14 develop -> staging hop. Fixed on develop as an ordinary reviewed PR — nothing was pushed onto the promotion PR.

classify_column validated that both anchors exist and never that they are in order. Its whole thesis is that POSITION decides rather than a name, which makes the board's Status option ORDER load-bearing — and order was the one thing never asserted.

If the Status options get dragged so Prod sorts before On dev, then _d > _p, the range >= _d && <= _p is unsatisfiable, and every column comes back no — including Prod itself. The router then writes Done over shipped state and kanban-reconcile.yml asserts it. That is exactly the fail-open this classification was added to close, reachable by one board reorder nobody would file as risky.

Fix

Inverted anchors classify as noboard, the same fail-closed verdict as a missing anchor:

-if [ "$_d" -lt 0 ] || [ "$_p" -lt 0 ]; then echo noboard; return; fi+if [ "$_d" -lt 0 ] || [ "$_p" -lt 0 ] || [ "$_d" -gt "$_p" ]; then echo noboard; return; fi

A reordered board is unreadable, not evidence that nothing deployed — workspace CLAUDE.md rule 3, "cannot tell" must block. The comment above the helper now says why.

Both workflows' existing noboard policies already do the right thing with that verdict and are unchanged: the router exits 1 loudly, reconcile skips the card. Section 8 of the selftest already covers both. The router's noboard message now names both failure modes ("missing or out of order"), so an operator who hits an inverted board is not sent looking for a deleted column.

Applied character-for-character to both copieskanban-reconcile.yml carries a byte-identical classify_column and case 0 of the selftest asserts that byte-identity. Verified post-edit: both regions hash to e1bf87cba9322626….

Test

The gap in the test had the same shape: case 7 removes an anchor, and nothing covered both anchors present but reversed. New case 7b builds that board by swapping BOARD's two anchors (derived from BOARD, not a restated list), asserts the input really is inverted and otherwise unchanged (an inert input and a working guard produce the same green line), and runs it through the existing classify() helper so both copies are exercised and must agree. Verdict must be noboard for a mid-pipeline column and for Prod itself.

PASS both workflows carry a byte-identical classify_column()
PASS a board with no 'Prod' column refuses rather than guessing
PASS 'FR on staging' on a board whose anchors are INVERTED refuses, not 'no'
-> noboard; an unsatisfiable range must fail closed rather than report every deploy column as free to overwrite
PASS 'Prod' on a board whose anchors are INVERTED refuses, not 'no'
-> noboard; an unsatisfiable range must fail closed rather than report every deploy column as free to overwrite
PASS router policy: noboard -> ::error::
rc=1 (want 1); ::error::the board's 'On dev'/'Prod' anchors are missing or out of order, so a deploy state cannot be recognised - refusing to set Done on #1
PASS reconcile policy: noboard -> _skip=yes
27 passed, 0 failed

make check (ruff + shellcheck + actionlint + house-rules + every selftest): green.

Mutation evidence

Mutation A — revert the -gt clause in BOTH copies (anchor asserted present in both before mutating, so the mutation cannot be inert):

FAIL 'FR on staging' on a board whose anchors are INVERTED refuses, not 'no'
-> no
FAIL 'Prod' on a board whose anchors are INVERTED refuses, not 'no'
-> no
25 passed, 2 failed (exit 1)

Exactly the two new cases redden, and the observed verdict is no — the bug verbatim, with Prod classified as free to overwrite. No pre-existing case notices, which is why 7b was needed.

Mutation B — revert it in ONE copy only (router fixed, reconcile broken), proving both copies are really under test rather than one standing in for the other:

FAIL both workflows carry a byte-identical classify_column()
FAIL 'FR on staging' on a board whose anchors are INVERTED refuses, not 'no'
FAIL 'Prod' on a board whose anchors are INVERTED refuses, not 'no'
24 passed, 3 failed

Restored → 27 passed, 0 failed, and the two classify regions hash equal again.

Fixes tracebloc/backend#1994

🤖 Generated with Claude Code


Note

Medium Risk
Changes kanban automation that guards deploy columns from Done overwrites; scope is small and fail-closed, but mistakes could block legitimate Done routing until the board order is fixed.

Overview
classify_column in kanban-closure-router.yml and kanban-reconcile.yml now returns noboard when On dev sorts after Prod, not only when an anchor is missing. Before that, an inverted Status option order made the deploy range empty so every column (including Prod) looked like “not deployed,” which could let Done overwrite shipped state.

Existing noboard handling is unchanged (router fails the step; reconcile skips the card). The closure router’s error text now mentions anchors missing or out of order.

kanban-deploy-state-selftest.py adds case 7b for inverted anchors so both workflow copies stay in sync.

Reviewed by Cursor Bugbot for commit bf3ef60. Bugbot is set up for automated code reviews on this repo. Configure here.

…oyed" (backend#1994)
`classify_column` checked that both anchors EXIST and never that they are IN
ORDER. Since its whole thesis is that POSITION decides rather than a name, the
board's Status option ORDER is load-bearing -- and it was the one thing never
asserted. Drag "Prod" above "On dev" and the range `>= _d && <= _p` becomes
unsatisfiable, so EVERY column comes back `no`, "Prod" itself included: the
router writes Done over shipped state and kanban-reconcile then asserts it.
That is exactly the fail-open this classification exists to close, reachable by
one board reorder nobody would file as risky.
Inverted anchors now classify as `noboard`, the same fail-closed verdict as a
missing one -- a reordered board is UNREADABLE, not evidence that nothing
deployed (workspace CLAUDE.md rule 3: "cannot tell" must block). Both
workflows' existing noboard policies already do the right thing with that
verdict and are unchanged: the router exits 1 loudly, reconcile skips the card.
The router's noboard message now names both failure modes, so an operator who
hits an inverted board is not sent looking for a deleted column.
Applied character-for-character to both copies -- kanban-reconcile.yml carries
a byte-identical `classify_column` and case 0 of the selftest asserts that.
The gap in the test was the same shape: case 7 removes an anchor, and nothing
covered both anchors present but REVERSED. New case 7b builds that board by
swapping BOARD's two anchors (derived, not restated), asserts the input really
is inverted and otherwise unchanged, and runs it through `classify()` so both
copies must agree. Verdict must be `noboard` for a mid-pipeline column and for
"Prod" itself.
Mutation-proven: reverting the `-gt` clause in both copies reddens exactly the
two new cases, each reporting `-> no` -- the bug verbatim, "Prod" classified as
free to overwrite. Reverting it in only ONE copy additionally reddens case 0
and turns 7b into a DISAGREE, confirming both copies are really exercised.
Restored: 27 passed, 0 failed; `make check` green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@LukasWodkaLukasWodka self-assigned this Aug 14, 2026
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

bugbot run

@cursorcursorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit bf3ef60. Configure here.

@LukasWodka
LukasWodka merged commit 6db91b1 into developAug 14, 2026
14 checks passed
@LukasWodka
LukasWodka deleted the fix/1994-anchor-order branch August 14, 2026 14:22
Sign up for freeto 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

@LukasWodka