feat(board): rank --plan on one score, with transitive chain-root credit

blocked-on was scanned and then used for a single display string; it never
reached the ordering. A blocked chain's root could therefore rank below the
repos waiting on it - measured on the real tree, the root opened at tab 14
while the two repos it held up opened at tabs 5 and 8.

--plan now ranks on one score instead of four hard buckets, because four
groups cannot express "this repo owes one message and releases two others":

    40 x repos released transitively + 15 x inbox
    +10 in-progress / +5 planned / +2 undeclared, +3 for a Sonnet row

Chain-root credit follows blocked-on transitively to the first repo that is
not itself blocked and credits only that root. Opening a blocked repo
releases nobody - its own next step is by definition waiting. A cycle, a
blocked-on naming an unscanned repo, and a blocked repo with no target all
credit NOBODY: inventing a root there yields a plan that looks correct and
sends the operator to the wrong repo.

Debt is deliberately uncapped. A cap was proposed alongside the credit and
declined: owing a reply is the other axis from a repo's own next step, and
answering is often what unblocks a chain. The weight carries that decision -
15 per message exceeds the sum of every other bonus (13), so one message
more still outranks any combination of status and cost. At the originally
proposed 12 a tiebreak could flip the debt order, which is the cap arriving
through the back door.

blocked-on becomes a record field, inserted BEFORE neste so no existing field
index moves: neste is free prose lifted from a STATE.md and may carry a
literal '|' from a markdown table, so it stays last.

why= now names the dominant term rather than the first rule that matched, so
a block says what opening it would release instead of repeating inbox:N.

Format is otherwise unchanged for both declared consumers: tab=, repo=, dir=,
why=, status=, neste=, command=/command_missing=, paste= and the fokus_*
header all keep their names and positions.

board-selftest 118 -> 134 (chain, cycle, dangling-edge and format fixtures
written first, all failing before the change).
This commit is contained in:
Kjell Tore Guttormsen 2026-08-02 22:38:40 +02:00
commit e5e9cbe847
10 changed files with 404 additions and 51 deletions

View file

@ -5,6 +5,51 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.19.0] - 2026-08-02
### Added
- **Chain-root credit in `board.sh --plan`.** `blocked-on` was scanned and then
used for one display string (`blocked>X`); it never reached the ordering. For
every `blocked` repo the edge is now followed **transitively** to the root of
the chain — the first repo not itself blocked — and only that root is
credited with the repos it holds up. Opening a blocked repo releases nobody;
opening the root releases everything behind it. Measured on the real tree: the
root of the one live chain moved from tab 14 to tab 1, ahead of the two repos
waiting on it.
- **`why=unblocks:N`.** `why=` now names the *dominant* scoring term rather than
the first rule that matched, so a block says what opening it would release.
Previously every planned block read `inbox:N` — the same sentence repeated.
- **`blocked-on` is a record field**, inserted before `neste` so no existing
field index moves. `neste` is free prose lifted from a STATE.md and may carry
a literal `|` from a markdown table, so it stays last.
### Changed
- **`--plan` ranks on one score, not four hard buckets.** Four groups could not
express "this repo owes one message and releases two others", which is exactly
how a chain's root ended up below the repos waiting on it:
40 x repos released transitively + 15 x inbox
+10 in-progress / +5 planned / +2 undeclared, +3 for a Sonnet row
Every term is a lookup over a field the scan already read, so the order stays
reproducible and costs no model call.
- **`--plan` header lines** now state the score and that a blocked repo never
receives credit. The format is otherwise **unchanged**: `tab=`, `repo=`,
`dir=`, `why=`, `status=`, `neste=`, `command=`/`command_missing=`, `paste=`
and the `fokus_*` header all keep their names and positions.
### Not done, deliberately
- **No cap on inbox debt.** Proposed alongside the credit and declined by the
operator: owing a reply is the other axis from a repo's own next step, and
answering is often precisely what unblocks a chain. The debt weight is 15 —
above the sum of every other bonus (13) — so one message more still outranks
any combination of status and cost, and the bonuses only separate repos that
owe the same. At the proposed 12 they could have flipped it, which would have
weakened the debt order through a tiebreak.
## [0.18.0] - 2026-08-02
### Added