feat(board)!: rank --plan on five ordered groups, planned above in-progress
Replaces the weighted score shipped in 0.19.0 with five lookups: chain-root credit, unhandled inbox, planned, in-progress, undeclared status. Within a group: that group's own quantity, then a Sonnet next-cost, then oldest plan. The score's objection is accepted, not forgotten, and is written into board.sh and CLAUDE.md so a later session reads it as decided rather than as an unfixed defect: a group order cannot express "owes one message AND releases two others" as one quantity. What the score could not do was hold still for the format's second consumer - re-tuning one weight against another silently reorders a parser in another repo, and no test here can catch that. planned now ranks above in-progress, inverted by the same decision: converting a decision into motion is the slow step; live work is already moving. Debt stays uncapped and never excluded. One group below chain-root credit is not the cap declined at 0.19.0 - the debtor keeps its tab, its most-owed-first position, and its why=inbox:N. Pinned by a discriminating fixture the score would fail: a root releasing one repo outranks a repo owing four. board-selftest 134 -> 138. Suite 183 + 138 + 73 = 394. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y6ULuFCPMNYAPNN3pAjsXQ
This commit is contained in:
parent
e5e9cbe847
commit
d0a5ffe515
10 changed files with 287 additions and 118 deletions
161
scripts/board.sh
161
scripts/board.sh
|
|
@ -70,13 +70,25 @@
|
|||
# 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 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":
|
||||
# FIVE ORDERED GROUPS, each a lookup over a field the scan already read:
|
||||
#
|
||||
# 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)
|
||||
# 1. chain-root credit - repos released transitively (see below)
|
||||
# 2. unhandled inbox messages, most-owed first, WHATEVER the status
|
||||
# 3. planned
|
||||
# 4. in-progress
|
||||
# 5. undeclared status ('?' / MALFORMED) - last, and labelled
|
||||
#
|
||||
# Within a group: the quantity that group is about (released, then owed), then a
|
||||
# Sonnet next-cost (cheap to move under quota pressure), then oldest plan first.
|
||||
#
|
||||
# 0.19.0 shipped a weighted score here instead, and 0.20.0 replaced it. The score
|
||||
# expressed one thing this cannot - "owes one message AND releases two others" as
|
||||
# a single quantity - and the operator accepted losing that. What a score could
|
||||
# not do was hold still for the second consumer: re-tuning 40 against 15 here
|
||||
# would silently reorder a parser living in another repo, and no test in THIS one
|
||||
# could catch it. Groups 3 and 4 are inverted from every version before 0.20.0
|
||||
# (planned above in-progress), also by operator decision: a decided but unstarted
|
||||
# step is what converts a decision into motion; live work is already moving.
|
||||
#
|
||||
# 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
|
||||
|
|
@ -87,15 +99,18 @@
|
|||
# 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.
|
||||
# Debt is deliberately UNCAPPED and never excluded. 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. Being one
|
||||
# group below chain-root credit is NOT a cap - the debtor keeps its tab, its
|
||||
# most-owed-first position among the other debtors, and its `why=inbox:N`. A
|
||||
# change that dropped a debtor from the plan would be the declined cap wearing
|
||||
# this decision as a disguise, and section 12 of the selftest pins that half.
|
||||
#
|
||||
# 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.
|
||||
# why= names the GROUP that put the repo in the plan. Under the score it named
|
||||
# the dominant TERM, because a score has no single rule that admitted a repo;
|
||||
# under groups the admitting rule is the group, so the two coincide wherever the
|
||||
# chain-root group applies and the field is unchanged for both consumers.
|
||||
# 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
|
||||
|
|
@ -592,17 +607,25 @@ focus_heldback() {
|
|||
# consumer drops them with one rule.
|
||||
#
|
||||
# ORDER IS THE POSITION THIS RENDERING TAKES, and it is the only one it takes -
|
||||
# there is no cutoff, so nothing is hidden. Four rules, all deterministic over
|
||||
# there is no cutoff, so nothing is hidden. Five groups, all deterministic over
|
||||
# fields the scan already read:
|
||||
# 1. INN > 0, most-owed first, WHATEVER the status. A message owed is an
|
||||
# 1. chain-root credit - repos this one releases transitively, most first.
|
||||
# Above debt because it is the only move that frees sessions other than
|
||||
# your own; a blocked repo is never credited, since its own next step is
|
||||
# by definition waiting.
|
||||
# 2. INN > 0, most-owed first, WHATEVER the status. A message owed is an
|
||||
# obligation to another session. Excluding `blocked` is about a repo's own
|
||||
# next step, which by definition cannot be moved; answering is a different
|
||||
# axis, and is frequently what unblocks it.
|
||||
# 2. in-progress - live work, oldest plan first.
|
||||
# 3. planned.
|
||||
# 4. '?' and MALFORMED - no declared status. Planned LAST, and labelled. The
|
||||
# 3. planned - a decided but unstarted next step.
|
||||
# 4. in-progress - live work, oldest plan first.
|
||||
# 5. '?' and MALFORMED - no declared status. Planned LAST, and labelled. The
|
||||
# table already prints a MERK line about repos with no board line; a plan
|
||||
# that dropped them silently would repeat exactly that defect.
|
||||
# Groups 3 and 4 are in that order by operator decision at 0.20.0, inverted from
|
||||
# every earlier version: turning a decision into motion is the slow step, and
|
||||
# live work is already moving. Deciding it the other way is a policy change, not
|
||||
# a sort fix.
|
||||
# Excluded: done, deferred, blocked-without-debt, and repos with no STATE.md and
|
||||
# no debt - a tab that cannot be moved is not a plan entry.
|
||||
# Two lines for two consumers, and they are not redundant. A driver cd's the
|
||||
|
|
@ -626,25 +649,43 @@ plan_cmd() {
|
|||
}
|
||||
|
||||
plan() {
|
||||
# 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.
|
||||
# FIVE ORDERED GROUPS, not a weighted score. Each group is a lookup over a
|
||||
# field the scan already read, so two readers can disagree about the order but
|
||||
# never about how it was produced - and a selftest can pin the outcome rather
|
||||
# than a coefficient.
|
||||
#
|
||||
# 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.
|
||||
# THE COST THIS FORM ACCEPTS, recorded because it is real and was argued at
|
||||
# length before it was chosen: a group order cannot express "this repo owes one
|
||||
# message AND releases two others" as a single quantity. The 0.19.0 score could,
|
||||
# and that was its point. What it could not do was stay stable for a consumer in
|
||||
# another repo - re-tuning 40 against 15 here silently reorders `morning`,
|
||||
# `--brief` and `brief-nightly`, and no test in THIS repo can hold a weighted
|
||||
# ranking still for a parser in that one. The operator weighed both and chose
|
||||
# the lookup (2026-08-03). This paragraph exists so a later session reads the
|
||||
# objection as ANSWERED rather than as an unfixed defect and "restores" a score.
|
||||
#
|
||||
# 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.
|
||||
# Group 1 is chain-root credit, and it sits ABOVE debt: opening the root of a
|
||||
# blocked chain is the only move that releases sessions other than your own.
|
||||
# Group 3 is `planned` ABOVE group 4 `in-progress` - inverted from every prior
|
||||
# version by operator decision the same day. The reason is that a decided but
|
||||
# unstarted next step is the step that turns a decision into motion, while live
|
||||
# work is already moving. It is a policy call, not a sort that drifted.
|
||||
#
|
||||
# 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
|
||||
# Debt is neither capped nor excluded - it is group 2, ranked most-owed-first,
|
||||
# WHATEVER the status. A cap was proposed and declined; being outranked by a
|
||||
# chain root is not a cap, because the debtor keeps its tab, its position among
|
||||
# the other debtors, and its `why=inbox:N`.
|
||||
#
|
||||
# `cheap` carries the one property 0.19.0 added that no one asked to remove:
|
||||
# under quota pressure a Sonnet row moves for less. It is the LAST tiebreak,
|
||||
# below the group's own magnitude and above age, so it separates repos that are
|
||||
# otherwise equal and can never lift one past another that owes or releases
|
||||
# more.
|
||||
#
|
||||
# The three sort keys are prefixed as fields 1-3 and stripped again afterwards;
|
||||
# `why` then becomes field 1 exactly as every previous 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"
|
||||
|
|
@ -658,43 +699,31 @@ plan() {
|
|||
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.
|
||||
# Membership is unchanged: a repo that releases someone is planned even
|
||||
# when it owes nothing and its own status would otherwise exclude it, and
|
||||
# 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"
|
||||
# The group IS the reason, so why= is the group and needs no separate rule.
|
||||
# `mag` orders within a group by the quantity that group is about - released
|
||||
# in group 1, messages owed in group 2 - and is 0 where the group has no
|
||||
# quantity, leaving those to the tiebreaks below.
|
||||
if (u > 0) { grp = 1; mag = u; why = "unblocks:" u }
|
||||
else if (inbox > 0) { grp = 2; mag = inbox; why = "inbox:" inbox }
|
||||
else if (status == "planned") { grp = 3; mag = 0; why = "planned" }
|
||||
else if (status == "in-progress") { grp = 4; mag = 0; why = "in-progress" }
|
||||
else { grp = 5; mag = 0; why = "uavklart" }
|
||||
|
||||
print score, why, $0
|
||||
# Cheap-to-move first under quota pressure. Sorted ascending, so 0 = cheap.
|
||||
cheap = (cost ~ /[Ss]onnet/) ? 0 : 1
|
||||
|
||||
print grp, mag, cheap, why, $0
|
||||
}
|
||||
' "$UNBLOCKS" "$RECORDS" \
|
||||
| sort -t'|' -k1,1nr -k4,4n \
|
||||
| cut -d'|' -f2- >> "$pf"
|
||||
| sort -t'|' -k1,1n -k2,2nr -k3,3n -k6,6n \
|
||||
| cut -d'|' -f4- >> "$pf"
|
||||
|
||||
# The cutoff, and its disclosure, computed together - they are one feature.
|
||||
FOCUS_SLUGS=""
|
||||
|
|
@ -718,7 +747,7 @@ 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: score = 40*repo losnet (kjede-rot) + 15*innboks + status + kost."
|
||||
echo "# Rekkefolge: 5 grupper - kjede-rot, innboks, planned, in-progress, uavklart."
|
||||
# "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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue