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
|
||||
|
|
|
|||
172
scripts/board.sh
172
scripts/board.sh
|
|
@ -69,8 +69,33 @@
|
|||
# position: it answers which repos to open a tab for today and in what order.
|
||||
# It prints key=value blocks, not prose, because it has two consumers - the
|
||||
# operator pasting commands, and a separate repo driving a terminal from it.
|
||||
# Ordering is deterministic (debt, then in-progress, then planned, then repos
|
||||
# with no declared status) and there is no cutoff, so nothing is hidden.
|
||||
# Ordering is deterministic and there is no cutoff, so nothing is hidden. It is
|
||||
# ONE score, not four buckets, because four groups cannot express "this repo
|
||||
# owes one message and releases two others":
|
||||
#
|
||||
# 40 x repos released transitively (chain-root credit, see below)
|
||||
# 15 x unhandled inbox messages
|
||||
# +10 in-progress / +5 planned / +2 undeclared status
|
||||
# +3 when next-cost names a Sonnet row (cheap to move under quota pressure)
|
||||
#
|
||||
# CHAIN-ROOT CREDIT is the term that needed blocked-on to stop being a display
|
||||
# string. For every blocked repo the edge is followed transitively to the ROOT
|
||||
# of the chain - the first repo not itself blocked - and only the root is
|
||||
# credited. Opening a blocked repo releases nobody, since its own next step is
|
||||
# by definition waiting; opening the root releases everything behind it. A
|
||||
# cycle, a blocked-on naming an unscanned repo, and a blocked repo with no
|
||||
# target all credit NOBODY: inventing a root there would look like a working
|
||||
# plan while sending the operator to the wrong repo.
|
||||
#
|
||||
# Debt is deliberately UNCAPPED. A cap was proposed with 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 what unblocks a chain. 15 per message is above the sum
|
||||
# of every other bonus (13), so one message more still outranks any combination
|
||||
# of status and cost - the bonuses separate repos that owe the SAME.
|
||||
#
|
||||
# why= names the DOMINANT term rather than the first rule that matched, because
|
||||
# with a score there is no single rule that put a repo in the plan - and a why=
|
||||
# that always read `inbox:N` was the same sentence on every block.
|
||||
# Read-only like the rest: --plan writes nothing, in the repo or the mailbox.
|
||||
#
|
||||
# --focus "<prose>" narrows --plan to the repos whose STATE.md DECLARES a
|
||||
|
|
@ -200,7 +225,7 @@ printf '%s\n' "$REPOS" | while IFS= read -r d; do
|
|||
# No plan file, so no plan age: ALDER is blank rather than quietly showing
|
||||
# the commit age under a heading that means something else everywhere else
|
||||
# in the table. The sort key keeps using it - order is unchanged.
|
||||
printf '5|%06d|%s|-|-|%s|%s|-|%s|%s|(ingen STATE.md)\n' \
|
||||
printf '5|%06d|%s|-|-|%s|%s|-|%s|%s|-|(ingen STATE.md)\n' \
|
||||
"$lastd" "$name" "$inbox" "$dirty" "$lastcol" "$d"
|
||||
continue
|
||||
fi
|
||||
|
|
@ -264,12 +289,55 @@ printf '%s\n' "$REPOS" | while IFS= read -r d; do
|
|||
*) bucket=2 ;;
|
||||
esac
|
||||
|
||||
printf '%s|%06d|%s|%s|%s|%s|%s|%sd|%s|%s|%s\n' \
|
||||
"$bucket" "$age" "$name" "$disp" "$cost" "$inbox" "$dirty" "$age" "$lastcol" "$d" "$neste"
|
||||
# blocked-on is a FIELD, not just the display string above: --plan follows the
|
||||
# edge to find who a session would release, and the edge cannot be recovered
|
||||
# from `blocked>X` alone (a repo can be blocked with no target, and a repo can
|
||||
# name a target the scan never produced). Inserted BEFORE `neste`, never after:
|
||||
# `neste` is free prose lifted out of a STATE.md and may contain a literal '|'
|
||||
# from a markdown table, so it has to stay the last field on the line.
|
||||
printf '%s|%06d|%s|%s|%s|%s|%s|%sd|%s|%s|%s|%s\n' \
|
||||
"$bucket" "$age" "$name" "$disp" "$cost" "$inbox" "$dirty" "$age" "$lastcol" "$d" \
|
||||
"${blockedon:--}" "$neste"
|
||||
done > "${TMPDIR:-/tmp}/board.$$"
|
||||
|
||||
RECORDS="${TMPDIR:-/tmp}/board.$$"
|
||||
trap '/bin/rm -f "$RECORDS" 2>/dev/null' EXIT
|
||||
UNBLOCKS="${TMPDIR:-/tmp}/board-unblocks.$$"
|
||||
trap '/bin/rm -f "$RECORDS" "$UNBLOCKS" 2>/dev/null' EXIT
|
||||
|
||||
# --- Chain-root credit -----------------------------------------------------
|
||||
# For every blocked repo, walk `blocked-on` up to the ROOT of the chain - the
|
||||
# first repo that is not itself blocked - and credit that root with one repo
|
||||
# released. The credit is transitive and lands ONLY on the root, because that
|
||||
# is the only node where opening a session changes anything: every other link
|
||||
# is by definition waiting on someone further up.
|
||||
#
|
||||
# Three cases must credit NOBODY rather than credit a guess:
|
||||
# - a cycle (A waits on B waits on A): no root exists, so the hop limit is
|
||||
# the terminating condition, not a safety net that also happens to fire.
|
||||
# - a blocked-on naming a repo the scan never produced: nothing validates
|
||||
# that field, so a typo is the expected case. There is no directory to
|
||||
# open, so a tab for it would be unrunnable.
|
||||
# - a blocked repo with no target at all (`blocked-on=-`).
|
||||
# Silently inventing a root in any of these is worse than skipping it: the plan
|
||||
# would look correct and send the operator to the wrong repo.
|
||||
: > "$UNBLOCKS"
|
||||
awk -F'|' '$4 ~ /^blocked/ && $11 != "-" && $11 != "" {print $11}' "$RECORDS" \
|
||||
| while read -r cr_target; do
|
||||
cr_cur="$cr_target"; cr_hops=0; cr_root=""
|
||||
while [ "$cr_hops" -lt 20 ]; do
|
||||
cr_line="$(awk -F'|' -v n="$cr_cur" '$3==n {print; exit}' "$RECORDS")"
|
||||
[ -n "$cr_line" ] || break # dangling target: no root
|
||||
cr_st="$(printf '%s' "$cr_line" | cut -d'|' -f4)"
|
||||
case "$cr_st" in
|
||||
blocked*)
|
||||
cr_nxt="$(printf '%s' "$cr_line" | cut -d'|' -f11)"
|
||||
[ -n "$cr_nxt" ] && [ "$cr_nxt" != "-" ] || break
|
||||
cr_cur="$cr_nxt"; cr_hops=$((cr_hops + 1)) ;;
|
||||
*) cr_root="$cr_cur"; break ;;
|
||||
esac
|
||||
done
|
||||
[ -n "$cr_root" ] && printf '%s\n' "$cr_root"
|
||||
done | sort | uniq -c | awk '{print $2 "|" $1}' > "$UNBLOCKS"
|
||||
|
||||
hdr() {
|
||||
printf '\n%s\n' "$1"
|
||||
|
|
@ -279,7 +347,7 @@ hdr() {
|
|||
|
||||
rows() {
|
||||
awk -F'|' -v b="$1" '$1==b' "$RECORDS" | sort -t'|' -k2,2n | \
|
||||
while IFS='|' read -r bucket sortkey name status cost inbox dirty age last dir neste; do
|
||||
while IFS='|' read -r bucket sortkey name status cost inbox dirty age last dir blockedon neste; do
|
||||
printf '%-32s %-34s %-14s %4s %4s %6s %6s %s\n' \
|
||||
"$name" "$status" "$cost" "$inbox" "$dirty" "$age" "$last" "$(trunc "$neste" "$NESTE_WIDTH")"
|
||||
done
|
||||
|
|
@ -377,7 +445,7 @@ brief() {
|
|||
|
||||
# Most-owed first: the repo holding up the most other sessions is read first.
|
||||
awk -F'|' '$6+0 > 0' "$RECORDS" | sort -t'|' -k6,6nr | \
|
||||
while IFS='|' read -r bucket sortkey name status cost inbox dirty age last dir neste; do
|
||||
while IFS='|' read -r bucket sortkey name status cost inbox dirty age last dir blockedon neste; do
|
||||
printf ' %-32s INN %-4s %s\n' "$name" "$inbox" "$status"
|
||||
# Wrapped, not cut - the whole line is the point, but a 500-character one
|
||||
# is unreadable in a file nobody watched being written. Locale is set for
|
||||
|
|
@ -558,24 +626,75 @@ plan_cmd() {
|
|||
}
|
||||
|
||||
plan() {
|
||||
# Groups are appended in rank order to one file, then numbered in a single
|
||||
# loop: `while ... < file` keeps the counter in THIS shell, where a pipe into
|
||||
# while would run it in a subshell and reset every tab number to 1.
|
||||
# ONE score, not four hard buckets. Four groups could rank a repo by what it
|
||||
# owes or by whether its own work is live, but could never express "this repo
|
||||
# owes one message and releases two others" - the case the four-bucket order
|
||||
# got exactly backwards, ranking a chain's root below the repos waiting on it.
|
||||
#
|
||||
# Each group prefixes its `why` as a new FIRST field, so the sort keys shift
|
||||
# by one: inbox 6->7, sortkey (ALDER) 2->3. Prefixing rather than appending is
|
||||
# deliberate - `neste` is the last field and is free prose, so anything added
|
||||
# after it could be swallowed by a stray separator in a STATE.md.
|
||||
# Every term is a lookup over a field the scan already read, so the order stays
|
||||
# reproducible and no term needs a model call. The weights are judgement and
|
||||
# are meant to be re-tuned against measurement, which is why they sit here
|
||||
# alone rather than spread through four sort invocations.
|
||||
#
|
||||
# Debt is deliberately NOT capped. A cap was proposed 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. A big mailbox therefore
|
||||
# still outranks a small one, all else equal.
|
||||
#
|
||||
# The score is prefixed as field 1 and stripped again after sorting; `why`
|
||||
# then becomes field 1 exactly as the four-bucket version left it, so the read
|
||||
# loop below and both consumers see an unchanged block. Prefixing rather than
|
||||
# appending is deliberate - `neste` is the last field and is free prose, so
|
||||
# anything added after it could be swallowed by a stray separator.
|
||||
pf="${TMPDIR:-/tmp}/board-plan.$$"
|
||||
: > "$pf"
|
||||
awk -F'|' -v OFS='|' '$6+0 > 0 {print "inbox:" $6, $0}' "$RECORDS" \
|
||||
| sort -t'|' -k7,7nr -k3,3n >> "$pf"
|
||||
awk -F'|' -v OFS='|' '$6+0 == 0 && $4 == "in-progress" {print "in-progress", $0}' "$RECORDS" \
|
||||
| sort -t'|' -k3,3n >> "$pf"
|
||||
awk -F'|' -v OFS='|' '$6+0 == 0 && $4 == "planned" {print "planned", $0}' "$RECORDS" \
|
||||
| sort -t'|' -k3,3n >> "$pf"
|
||||
awk -F'|' -v OFS='|' '$6+0 == 0 && ($4 == "?" || $4 ~ /^MALFORMED:/) {print "uavklart", $0}' "$RECORDS" \
|
||||
| sort -t'|' -k3,3n >> "$pf"
|
||||
awk -F'|' -v OFS='|' '
|
||||
NR==FNR { ub[$1] = $2 + 0; next }
|
||||
{
|
||||
name = $3; status = $4; cost = $5; inbox = $6 + 0
|
||||
# A blocked repo is credited with nothing even if something waits on it:
|
||||
# it cannot move its own next step, so opening it releases no one. The
|
||||
# credit already went to the root of its chain.
|
||||
blockedp = (status ~ /^blocked/)
|
||||
u = (!blockedp && (name in ub)) ? ub[name] : 0
|
||||
|
||||
# 15 per message, not the 12 the order proposed. The weights were
|
||||
# explicitly delegated, and 12 has a measurable defect: the status and
|
||||
# cost bonuses sum to 13, so they could outrank a repo owing one message
|
||||
# MORE than another. That is the debt-ordering rule weakening through a
|
||||
# tiebreak, which is the one thing the declined cap would also have done.
|
||||
# At 15 a single message outweighs every non-unblock bonus combined, so
|
||||
# within the debt group the order is still most-owed-first, and the
|
||||
# bonuses do what they are for: separating repos that owe the same.
|
||||
score = 40 * u + 15 * inbox
|
||||
if (status == "in-progress") score += 10
|
||||
else if (status == "planned") score += 5
|
||||
else if (status == "?" || status ~ /^MALFORMED:/) score += 2
|
||||
# Cheap-to-move first under quota pressure, and only ever a tiebreak.
|
||||
if (cost ~ /[Ss]onnet/) score += 3
|
||||
|
||||
# Membership is unchanged except for one addition: a repo that releases
|
||||
# someone is planned even when it owes nothing and its own status would
|
||||
# otherwise exclude it. Everything else keeps the old rule, so done,
|
||||
# deferred, blocked and no-STATE repos with no debt stay out.
|
||||
keep = (inbox > 0) || (u > 0) || (status == "in-progress") || \
|
||||
(status == "planned") || (status == "?") || (status ~ /^MALFORMED:/)
|
||||
if (!keep) next
|
||||
|
||||
# why= names the DOMINANT term, not the first rule that happened to match:
|
||||
# with a score there is no single rule that "put the repo in the plan",
|
||||
# and a why= that always said inbox: would be the uninformative line this
|
||||
# field was changed to fix.
|
||||
if (u > 0 && 40 * u >= 15 * inbox) why = "unblocks:" u
|
||||
else if (inbox > 0) why = "inbox:" inbox
|
||||
else if (status == "in-progress" || status == "planned") why = status
|
||||
else why = "uavklart"
|
||||
|
||||
print score, why, $0
|
||||
}
|
||||
' "$UNBLOCKS" "$RECORDS" \
|
||||
| sort -t'|' -k1,1nr -k4,4n \
|
||||
| cut -d'|' -f2- >> "$pf"
|
||||
|
||||
# The cutoff, and its disclosure, computed together - they are one feature.
|
||||
FOCUS_SLUGS=""
|
||||
|
|
@ -599,11 +718,12 @@ plan() {
|
|||
|
||||
echo "# PLAN $(date '+%Y-%m-%d %H:%M') - en blokk per tab, i den rekkefolgen"
|
||||
echo "# Kilder: STATE.md (NESTE + route-linje), git, coord-innboks. 0 modellkall."
|
||||
echo "# Rekkefolge: innboksgjeld (INN desc), sa in-progress, sa planned, sa uavklart."
|
||||
echo "# Rekkefolge: score = 40*repo losnet (kjede-rot) + 15*innboks + status + kost."
|
||||
# "uten gjeld" governs the WHOLE list, not just the token nearest to it: a
|
||||
# done or deferred repo that owes mail IS planned, and the real tree has two.
|
||||
# Read the other way this line calls its own tab 4 a bug.
|
||||
echo "# Utelatt naar repoet ikke skylder svar: done, deferred, blocked, uten STATE.md."
|
||||
echo "# Utelatt naar repoet verken skylder svar eller losner noen: done, deferred,"
|
||||
echo "# blocked, uten STATE.md. Et blokkert repo faar aldri kreditt - den gaar til rota."
|
||||
# Emitted as key=value, not as a '#' comment, because the format's second
|
||||
# consumer drops every comment line by rule - a disclosure written as a
|
||||
# comment would reach the operator on the terminal path and vanish on the
|
||||
|
|
@ -643,7 +763,7 @@ plan() {
|
|||
echo ""
|
||||
|
||||
pn=0
|
||||
while IFS='|' read -r why bucket sortkey name status cost inbox dirty age last dir neste; do
|
||||
while IFS='|' read -r why bucket sortkey name status cost inbox dirty age last dir blockedon neste; do
|
||||
[ -n "$name" ] || continue
|
||||
pn=$((pn + 1))
|
||||
printf 'tab=%s\n' "$pn"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue