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:
parent
917a902172
commit
e5e9cbe847
10 changed files with 404 additions and 51 deletions
|
|
@ -601,8 +601,13 @@ check "every repo owing mail is planned before any that owes none" $?
|
|||
[ "$(printf '%s\n' "$PLAN" | grep -c '^repo=repo-a$')" -eq 1 ]
|
||||
check "a repo qualifying under two rules is planned exactly once" $?
|
||||
|
||||
printf '%s\n' "$PLAN" | grep -A3 '^repo=repo-a$' | grep -q '^why=inbox:3$'
|
||||
check "why= names the rule that put the repo in the plan" $?
|
||||
# repo-a owes 3 messages AND is the blocker plug-x and repo-blocked-owes both
|
||||
# name. why= reports the DOMINANT term, so it says what a session there would
|
||||
# release rather than what it owes - the whole reason this field was changed.
|
||||
# Before chain-root credit existed this read `inbox:3`, and that was the line
|
||||
# eleven blocks repeated verbatim.
|
||||
printf '%s\n' "$PLAN" | grep -A3 '^repo=repo-a$' | grep -q '^why=unblocks:2$'
|
||||
check "why= names the dominant reason, releasing over owing when it dominates" $?
|
||||
|
||||
# in-progress outranks planned among the repos owing nothing.
|
||||
n_g="$(printf '%s\n' "$PLAN" | grep -n '^repo=repo-g$' | cut -d: -f1)"
|
||||
|
|
@ -875,6 +880,149 @@ check "--focus with no value exits 2, like every other flag missing its value" $
|
|||
"$BOARD" --help 2>/dev/null | grep -q '\-\-focus'
|
||||
check "--help documents --focus" $?
|
||||
|
||||
# --- 12. Chain-root credit in --plan --------------------------------------
|
||||
# Ordered by what a repo OWES, the plan could never say what OPENING it would
|
||||
# RELEASE. blocked-on was scanned and then used for one display string only.
|
||||
#
|
||||
# The credit goes to the ROOT of the chain and to nothing else, and that is the
|
||||
# whole rule: opening a blocked repo moves nothing, because its own next step is
|
||||
# by definition waiting on someone. Opening the root moves everything behind it.
|
||||
# So the walk is transitive - crediting only the direct blocker would leave a
|
||||
# two-hop chain with its root uncredited, which is the shape the real tree
|
||||
# actually had when this was ordered.
|
||||
#
|
||||
# Three fixtures, one chain: leaf -> mid -> root.
|
||||
mkrepo "$ROOT/chain-root"
|
||||
{
|
||||
echo "# STATE - chain-root"
|
||||
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
|
||||
# Deliberately `planned`, not `in-progress`: under the old four-bucket order
|
||||
# that puts the root BELOW every live repo, so the ranking check below fails
|
||||
# for the right reason instead of passing on fixture order.
|
||||
echo "<!-- board: status=planned; blocked-on=-; next-cost=Opus 5/high -->"
|
||||
printf 'Roten %s ingen gjeld, men holder to repo oppe.\n' "$EMDASH"
|
||||
} > "$ROOT/chain-root/STATE.md"
|
||||
|
||||
mkrepo "$ROOT/chain-mid"
|
||||
{
|
||||
echo "# STATE - chain-mid"
|
||||
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
|
||||
echo "<!-- board: status=blocked; blocked-on=chain-root; next-cost=Opus 5/high -->"
|
||||
printf 'Venter p%s chain-root.\n' "$EMDASH"
|
||||
} > "$ROOT/chain-mid/STATE.md"
|
||||
|
||||
mkrepo "$ROOT/chain-leaf"
|
||||
{
|
||||
echo "# STATE - chain-leaf"
|
||||
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
|
||||
echo "<!-- board: status=blocked; blocked-on=chain-mid; next-cost=Opus 5/high -->"
|
||||
printf 'Venter p%s chain-mid, som venter p%s roten.\n' "$EMDASH" "$EMDASH"
|
||||
} > "$ROOT/chain-leaf/STATE.md"
|
||||
|
||||
# A cycle is not hypothetical: two repos can each declare they wait on the other
|
||||
# after a miscommunication, and nobody would notice until the board hung. The
|
||||
# walk must terminate and credit NOBODY - a cycle has no root by definition.
|
||||
mkrepo "$ROOT/cyc-a"
|
||||
{
|
||||
echo "# STATE - cyc-a"
|
||||
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
|
||||
echo "<!-- board: status=blocked; blocked-on=cyc-b; next-cost=Opus 5/high -->"
|
||||
echo "Venter paa cyc-b."
|
||||
} > "$ROOT/cyc-a/STATE.md"
|
||||
mkrepo "$ROOT/cyc-b"
|
||||
{
|
||||
echo "# STATE - cyc-b"
|
||||
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
|
||||
echo "<!-- board: status=blocked; blocked-on=cyc-a; next-cost=Opus 5/high -->"
|
||||
echo "Venter paa cyc-a."
|
||||
} > "$ROOT/cyc-b/STATE.md"
|
||||
|
||||
# blocked-on names a repo the scan never produced. Nothing validates that field,
|
||||
# so a typo is the expected case, not the exotic one - and it must credit
|
||||
# nothing rather than credit a name that has no directory to open.
|
||||
mkrepo "$ROOT/dangle"
|
||||
{
|
||||
echo "# STATE - dangle"
|
||||
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
|
||||
echo "<!-- board: status=blocked; blocked-on=no-such-repo; next-cost=Opus 5/high -->"
|
||||
echo "Venter paa noe som ikke finnes."
|
||||
} > "$ROOT/dangle/STATE.md"
|
||||
|
||||
P2="$("$BOARD" --roots "$ROOT" --plan 2>/dev/null)"; rc=$?
|
||||
check "--plan still exits 0 with chain, cycle and dangling fixtures present" "$rc"
|
||||
|
||||
printf '%s' "$P2" | grep -q '^repo=chain-root$'
|
||||
check "the root of a blocked chain is planned even though it owes no mail" $?
|
||||
|
||||
printf '%s\n' "$P2" | grep -A3 '^repo=chain-root$' | grep -q '^why=unblocks:2$'
|
||||
check "the root is credited with the WHOLE subtree it holds up, not one edge" $?
|
||||
|
||||
# The exclusion these fixtures must not break: a blocked repo with no debt still
|
||||
# gets no tab. The credit goes to the root precisely so the blocked repos do not
|
||||
# need one - crediting them instead would open two tabs that cannot move.
|
||||
printf '%s' "$P2" | grep -q '^repo=chain-mid$'; [ $? -ne 0 ]
|
||||
check "a blocked middle link with no debt is still not planned" $?
|
||||
printf '%s' "$P2" | grep -q '^repo=chain-leaf$'; [ $? -ne 0 ]
|
||||
check "a blocked leaf with no debt is still not planned" $?
|
||||
|
||||
# Unblocking outranks work that neither owes nor releases anything. This is the
|
||||
# point of the change: repo-h is in-progress and idle, chain-root is in-progress
|
||||
# and holds two repos up.
|
||||
n2_cr="$(printf '%s\n' "$P2" | grep -n '^repo=chain-root$' | cut -d: -f1)"
|
||||
n2_h="$(printf '%s\n' "$P2" | grep -n '^repo=repo-h$' | cut -d: -f1)"
|
||||
[ -n "$n2_cr" ] && [ -n "$n2_h" ] && [ "$n2_cr" -lt "$n2_h" ]
|
||||
check "a chain root ranks above live work that releases nothing" $?
|
||||
|
||||
# A cycle terminates and credits nobody. Both halves matter: hanging is the
|
||||
# failure everyone imagines, but silently inventing a root is the one that would
|
||||
# ship - it looks like a working plan.
|
||||
printf '%s' "$P2" | grep -q '^repo=cyc-a$'; [ $? -ne 0 ]
|
||||
check "a cyclic blocked-on chain credits nobody (cyc-a not planned)" $?
|
||||
printf '%s' "$P2" | grep -q '^repo=cyc-b$'; [ $? -ne 0 ]
|
||||
check "a cyclic blocked-on chain credits nobody (cyc-b not planned)" $?
|
||||
printf '%s' "$P2" | grep -q '^why=unblocks:0$'; [ $? -ne 0 ]
|
||||
check "no repo is ever credited with an empty unblock count" $?
|
||||
|
||||
# A dangling blocked-on must not conjure a tab for a name with no directory.
|
||||
printf '%s' "$P2" | grep -q '^repo=no-such-repo$'; [ $? -ne 0 ]
|
||||
check "a dangling blocked-on target is never planned into existence" $?
|
||||
printf '%s' "$P2" | grep -q '^repo=dangle$'; [ $? -ne 0 ]
|
||||
check "the repo carrying a dangling blocked-on is itself still excluded" $?
|
||||
|
||||
# The format is the API. Adding a scoring rule must not move, rename or drop a
|
||||
# single field - the driver parses these keys positionally-independently, but it
|
||||
# parses THESE keys.
|
||||
n2_tab="$(printf '%s\n' "$P2" | grep -c '^tab=')"
|
||||
n2_rep="$(printf '%s\n' "$P2" | grep -c '^repo=')"
|
||||
n2_dir="$(printf '%s\n' "$P2" | grep -c '^dir=')"
|
||||
n2_why="$(printf '%s\n' "$P2" | grep -c '^why=')"
|
||||
n2_sta="$(printf '%s\n' "$P2" | grep -c '^status=')"
|
||||
n2_nes="$(printf '%s\n' "$P2" | grep -c '^neste=')"
|
||||
n2_cmd="$(printf '%s\n' "$P2" | grep -c '^command=')"
|
||||
n2_cms="$(printf '%s\n' "$P2" | grep -c '^command_missing=')"
|
||||
[ "$n2_tab" -gt 0 ] && [ "$n2_rep" -eq "$n2_tab" ] && [ "$n2_dir" -eq "$n2_tab" ] \
|
||||
&& [ "$n2_why" -eq "$n2_tab" ] && [ "$n2_sta" -eq "$n2_tab" ] \
|
||||
&& [ "$n2_nes" -eq "$n2_tab" ] && [ $(( n2_cmd + n2_cms )) -eq "$n2_tab" ]
|
||||
check "scored plan still carries every documented field exactly once per block" $?
|
||||
|
||||
printf '%s' "$P2" | grep -q '^command=$'; [ $? -ne 0 ]
|
||||
check "scored plan never emits an empty command= line" $?
|
||||
|
||||
# Debt still puts a repo in the plan and still names itself when it is the
|
||||
# dominant reason. Rejecting the proposed cap on debt was a deliberate call:
|
||||
# owing a reply is the other axis, and answering is often what unblocks a chain.
|
||||
printf '%s\n' "$P2" | grep -A3 '^repo=repo-blocked-owes$' | grep -q '^why=inbox:2$'
|
||||
check "debt still names itself in why= when it is the dominant reason" $?
|
||||
|
||||
# --brief is a second RENDERING of the same scan and must survive the change.
|
||||
"$BOARD" --roots "$ROOT" --brief >/dev/null 2>&1
|
||||
check "--brief still exits 0 after the scoring change" $?
|
||||
|
||||
# The plan must still print the scan whole. There is no cutoff by design, so a
|
||||
# consumer's own tab cap is the only thing that ever hides a repo.
|
||||
[ "$n2_tab" -ge "$n_tab" ]
|
||||
check "the scored plan hides nothing the bucketed plan showed" $?
|
||||
|
||||
echo ""
|
||||
echo "board-selftest: $PASS passed, $FAIL failed"
|
||||
[ "$FAIL" -eq 0 ] || exit 1
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue