feat(board): PLAN column - where a destination is missing or untouched

A display-only table column and a `plan=` field in --row, read from the
filesystem: `-` = no file named exactly PLAN.md in the repo root; `Nd` =
whole days the plan's mtime lies behind the last commit (`0d` when as new
or newer); `?` = a plan with no commit to compare against. The content is
never read, and the legend says the column does not judge whether a
criterion can be falsified. Not a sort key, not in --plan, not in SUM.

The name is matched byte for byte through a glob plus a basename compare:
APFS is case-insensitive by default, so `[ -f PLAN.md ]` is true for a
`plan.md`. The glob needs no subprocess.

The checks were written red in a separate eval before the column existed
and pass unchanged; they now live in board-selftest section 34 and the
eval file is removed (two judges for one column would drift).
Mutation-verified: a case-blind test, sorting on the field, a plan= key
in --plan and a legend without IKKE each turn exactly one check red.

The new record field sits before `neste`, so every "last field onward"
index moved 17 -> 18, and F6's right-counted DRT read moved NF-3 -> NF-4.

board-selftest 428 -> 461 (badge 961). Suites: coord 257, board 461,
route 73, orders 116, guard 54; npm test 12/12, run after git add.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-17 16:17:49 +02:00
commit 87fb78334e
7 changed files with 327 additions and 260 deletions

View file

@ -2379,19 +2379,19 @@ check "F6: fixture ground truth - git really does fail on the broken worktree" $
F6TBL="$(CLAUDE_COORD_DIR="$F6COORD" "$BOARD" --roots "$F6ROOT" 2>/dev/null)"
# DRT is counted from the RIGHT (NF-3: DRT ALDER SISTE NESTE), never from the
# DRT is counted from the RIGHT (NF-4: DRT ALDER SISTE PLAN NESTE), never from the
# left: the KOST field is free text and its realistic spelling ("Sonnet 5/low")
# contains a space, so $7 reads FLY instead. Measured, not assumed - the first
# cut of this section passed its clean-tree control by accident that way.
printf '%s' "$F6TBL" | grep -E '^f6-clean[[:space:]]' | awk '{print $(NF-3)}' | grep -qx '0'
printf '%s' "$F6TBL" | grep -E '^f6-clean[[:space:]]' | awk '{print $(NF-4)}' | grep -qx '0'
check "F6: control - a healthy CLEAN tree still reads DRT 0" $?
printf '%s' "$F6TBL" | grep -E '^f6-dirty[[:space:]]' | awk '{print $(NF-3)}' | grep -qx '1'
printf '%s' "$F6TBL" | grep -E '^f6-dirty[[:space:]]' | awk '{print $(NF-4)}' | grep -qx '1'
check "F6: control - a healthy DIRTY tree still reads DRT 1" $?
printf '%s' "$F6TBL" | grep -E '^f6-broken[[:space:]]' | awk '{print $(NF-3)}' | grep -qx '0'
printf '%s' "$F6TBL" | grep -E '^f6-broken[[:space:]]' | awk '{print $(NF-4)}' | grep -qx '0'
[ $? -ne 0 ]
check "F6: a git failure does NOT read as DRT 0 (clean tree)" $?
printf '%s' "$F6TBL" | grep -E '^f6-broken[[:space:]]' | awk '{print $(NF-3)}' | grep -qx '?'
printf '%s' "$F6TBL" | grep -E '^f6-broken[[:space:]]' | awk '{print $(NF-4)}' | grep -qx '?'
check "F6: a git failure reads as ? (not measured), the same token coord-count uses" $?
# The load-bearing consequence. All three fixtures are status=done with no mail
@ -3455,6 +3455,218 @@ grep -q -- '--row' "$BOARD"; check "row: board.sh actually carries the --row fla
/bin/rm -rf "$ROW_ROOT" "$ROW_COORD" 2>/dev/null
# --- 34. PLAN column: a filesystem reading, display only --------------------
# Operator decision 2026-09-17. The cell answers one question: does this repo
# keep a destination where sessions actually look (a root PLAN.md), and is
# anyone touching it? `-` = no file named exactly PLAN.md in the root; `Nd` =
# whole days its mtime lies behind the last commit. It never reads the file,
# never enters a sort, a --plan block or SUM, and does not judge whether a
# criterion inside it can be falsified - the legend says so.
#
# These checks were written RED in a separate file before the column existed
# (24 of 33 failing because the column was missing, fixture ground truth
# green), kept out of this suite so main stayed green, and moved here once the
# column passed them unchanged. That file is gone: two judges for one column
# would drift. Mutation-verified against four broken variants, each turning
# exactly one check red: a case-blind `[ -f PLAN.md ]`, sorting rows on the
# plan field, a plan= key in the --plan blocks, and a legend without IKKE.
#
# Table cells are located through the HEADER (split on runs of 2+ spaces),
# never a fixed offset, so a later column move does not break this section.
PC_ROOT="$(mktemp -d)"
PC_COORD="$(mktemp -d)"
PC_NOW="$(date +%s)"
PC_DAY=86400
pc_state() {
{
echo "# STATE - $2"
echo "## NESTE - START HER"
echo "<!-- board: status=$3; blocked-on=-; next-cost=Sonnet 5/high -->"
echo "Next step for $2."
} > "$1/STATE.md"
}
# pc_commit_at <dir> <epoch> <msg>: commit everything with a fixed committer date.
pc_commit_at() {
git -C "$1" add -A >/dev/null 2>&1
GIT_AUTHOR_DATE="@$2 +0000" GIT_COMMITTER_DATE="@$2 +0000" \
git -C "$1" -c user.email=t@t -c user.name=t commit -qm "$3" >/dev/null 2>&1
}
pc_touch_at() { touch -t "$(date -r "$2" +%Y%m%d%H%M.%S)" "$1"; }
pc_last_commit() { git -C "$1" log -1 --format=%ct 2>/dev/null; }
pc_mtime() { stat -f %m "$1"; }
# pc_table_cell <table-output> <repo>: the PLAN cell of <repo>'s row, read under
# the nearest preceding header line. Empty when the header has no PLAN column.
pc_table_cell() {
printf '%s\n' "$1" | awk -v repo="$2" '
function split2(s, arr, n) { gsub(/ +/, "\t", s); sub(/^\t/, "", s); return split(s, arr, "\t") }
/^REPO / { n = split2($0, h); col = 0; for (i = 1; i <= n; i++) if (h[i] == "PLAN") col = i; next }
$1 == repo && col > 0 { split2($0, r); print r[col]; exit }'
}
pc_row_plan() { printf '%s\n' "$1" | sed -n 's/^plan=//p'; }
pc_board() { CLAUDE_COORD_DIR="$PC_COORD" "$BOARD" --roots "$PC_ROOT" "$@" 2>/dev/null; }
# --- Fixtures ---------------------------------------------------------------
# p-none: no PLAN.md anywhere.
mkrepo "$PC_ROOT/p-none"; pc_state "$PC_ROOT/p-none" p-none planned
pc_commit_at "$PC_ROOT/p-none" $((PC_NOW - 3 * PC_DAY)) init
# p-fresh: PLAN.md newer than the last commit -> 0d. The file is EMPTY: a value
# computed from content could not produce 0d here, so this also shows that
# the column is a filesystem reading.
mkrepo "$PC_ROOT/p-fresh"; pc_state "$PC_ROOT/p-fresh" p-fresh planned
pc_commit_at "$PC_ROOT/p-fresh" $((PC_NOW - 2 * PC_DAY)) init
: > "$PC_ROOT/p-fresh/PLAN.md"
pc_touch_at "$PC_ROOT/p-fresh/PLAN.md" "$PC_NOW"
# p-stale: PLAN.md 5 days and 3 hours older than the last commit -> 5d. Its
# content looks like a value on purpose ("PLAN=99d"); it must not be read.
mkrepo "$PC_ROOT/p-stale"; pc_state "$PC_ROOT/p-stale" p-stale planned
printf '# PLAN\nPLAN=99d\n' > "$PC_ROOT/p-stale/PLAN.md"
PC_P_STALE_CT=$((PC_NOW - 3600))
pc_commit_at "$PC_ROOT/p-stale" "$PC_P_STALE_CT" init
pc_touch_at "$PC_ROOT/p-stale/PLAN.md" $((PC_P_STALE_CT - 5 * PC_DAY - 3 * 3600))
# p-docs: PLAN.md in docs/ only - the wrong place by convention.
mkrepo "$PC_ROOT/p-docs"; pc_state "$PC_ROOT/p-docs" p-docs planned
mkdir -p "$PC_ROOT/p-docs/docs"; echo "# PLAN" > "$PC_ROOT/p-docs/docs/PLAN.md"
pc_commit_at "$PC_ROOT/p-docs" $((PC_NOW - PC_DAY)) init
# p-lower: lowercase plan.md in the root. On a case-insensitive filesystem
# (APFS default) `[ -f PLAN.md ]` is TRUE here, so a board that asks the
# filesystem alone would report a plan that is not there. The name has to be
# compared byte for byte against what the directory actually stores (e.g. a
# directory listing matched exactly against PLAN.md).
mkrepo "$PC_ROOT/p-lower"; pc_state "$PC_ROOT/p-lower" p-lower planned
echo "# plan" > "$PC_ROOT/p-lower/plan.md"
pc_commit_at "$PC_ROOT/p-lower" $((PC_NOW - PC_DAY)) init
# --- Ground truth for the fixtures (these must be green today) -------------
[ -z "$(git -C "$PC_ROOT/p-stale" status --porcelain)" ]
check "plan: fixture - p-stale is committed clean" $?
PC_P_STALE_EXP="$(( ($(pc_last_commit "$PC_ROOT/p-stale") - $(pc_mtime "$PC_ROOT/p-stale/PLAN.md")) / PC_DAY ))d"
[ "$PC_P_STALE_EXP" != "0d" ]
check "plan: fixture - p-stale's expected value is computed from its timestamps and is not 0d ($PC_P_STALE_EXP)" $?
[ "$(pc_mtime "$PC_ROOT/p-fresh/PLAN.md")" -ge "$(pc_last_commit "$PC_ROOT/p-fresh")" ]
check "plan: fixture - p-fresh's PLAN.md is newer than its last commit" $?
if [ -f "$PC_ROOT/p-lower/PLAN.md" ]; then PC_LOWER_ARMED=0; else PC_LOWER_ARMED=1; fi
check "plan: fixture - the case trap is armed here - [ -f PLAN.md ] is true for plan.md (case-insensitive fs)" "$PC_LOWER_ARMED"
ls -1 "$PC_ROOT/p-lower" | grep -qx 'PLAN.md'; PC_rc=$?
[ "$PC_rc" -ne 0 ]; check "plan: fixture - a byte-exact directory listing does NOT see PLAN.md in p-lower" $?
PC_TABLE="$(pc_board)"
PC_N_REPOS="$(printf '%s\n' "$PC_TABLE" | sed -n 's/^BOARD .*(\([0-9][0-9]*\) repo).*/\1/p' | head -1)"
[ "${PC_N_REPOS:-0}" -eq 5 ]; check "plan: fixture - board scanned the 5 fixture repos (header says ${PC_N_REPOS:-nothing})" $?
# --- 1. Three different values from three repos ----------------------------
printf '%s\n' "$PC_TABLE" | grep -q '^REPO .* PLAN '
check "plan 1: the table header carries a PLAN column" $?
[ "$(pc_table_cell "$PC_TABLE" p-none)" = "-" ]; check "plan 1: no root PLAN.md -> '-'" $?
[ "$(pc_table_cell "$PC_TABLE" p-fresh)" = "0d" ]; check "plan 1: PLAN.md newer than the last commit -> '0d'" $?
[ "$(pc_table_cell "$PC_TABLE" p-stale)" = "$PC_P_STALE_EXP" ]
check "plan 1: PLAN.md older than the last commit -> '$PC_P_STALE_EXP' (from the fixture's own timestamps)" $?
PC_V3="$(pc_table_cell "$PC_TABLE" p-none) $(pc_table_cell "$PC_TABLE" p-fresh) $(pc_table_cell "$PC_TABLE" p-stale)"
[ "$(printf '%s\n' $PC_V3 | sort -u | wc -l | tr -d ' ')" -eq 3 ]
check "plan 1: the three fixtures show three DIFFERENT values ($PC_V3)" $?
# --- 2. Wrong place and wrong case are not a plan ---------------------------
[ "$(pc_table_cell "$PC_TABLE" p-docs)" = "-" ]; check "plan 2: docs/PLAN.md only -> '-'" $?
[ "$(pc_table_cell "$PC_TABLE" p-lower)" = "-" ]; check "plan 2: lowercase plan.md in the root -> '-' (byte-exact name)" $?
# --- 3. --row carries plan= with the table's value ---------------------------
for PC_r in p-none p-fresh p-stale p-docs p-lower; do
PC_RV="$(pc_row_plan "$(pc_board --row "$PC_r")")"
PC_TV="$(pc_table_cell "$PC_TABLE" "$PC_r")"
[ -n "$PC_RV" ] && [ "$PC_RV" = "$PC_TV" ]
check "plan 3: --row $PC_r has plan=${PC_RV:-<missing>} equal to the table cell (${PC_TV:-<missing>})" $?
done
[ "$(pc_board --row p-none | grep -c '^plan=')" -eq 1 ]
check "plan 3: --row emits exactly one plan= line" $?
[ "$(pc_board --row p-none | tail -1 | cut -d= -f1)" = "neste" ]
check "plan 3: neste= is still the last --row line" $?
# --- 4. Not in the sort, not in --plan blocks, not summed --------------------
# Two repos identical in every sorted property; run 1 gives s-a the stale plan
# and s-b the fresh one, run 2 swaps them. Any sort key built on PLAN would
# reverse them in one of the two runs.
PC_SROOT="$(mktemp -d)"
for PC_r in s-a s-b; do
mkrepo "$PC_SROOT/$PC_r"; pc_state "$PC_SROOT/$PC_r" "$PC_r" planned
echo "<!-- route: path=known; verification=strong; reversibility=cheap; scope=local; rationale=x -->" >> "$PC_SROOT/$PC_r/STATE.md"
: > "$PC_SROOT/$PC_r/PLAN.md"
pc_commit_at "$PC_SROOT/$PC_r" $((PC_NOW - PC_DAY)) init
pc_touch_at "$PC_SROOT/$PC_r/STATE.md" $((PC_NOW - 2 * PC_DAY))
done
pc_touch_at "$PC_SROOT/s-a/PLAN.md" $((PC_NOW - 9 * PC_DAY)); pc_touch_at "$PC_SROOT/s-b/PLAN.md" "$PC_NOW"
PC_T1="$(CLAUDE_COORD_DIR="$PC_COORD" "$BOARD" --roots "$PC_SROOT" 2>/dev/null)"; PC_P1="$(CLAUDE_COORD_DIR="$PC_COORD" "$BOARD" --roots "$PC_SROOT" --plan 2>/dev/null)"
pc_touch_at "$PC_SROOT/s-a/PLAN.md" "$PC_NOW"; pc_touch_at "$PC_SROOT/s-b/PLAN.md" $((PC_NOW - 9 * PC_DAY))
PC_T2="$(CLAUDE_COORD_DIR="$PC_COORD" "$BOARD" --roots "$PC_SROOT" 2>/dev/null)"; PC_P2="$(CLAUDE_COORD_DIR="$PC_COORD" "$BOARD" --roots "$PC_SROOT" --plan 2>/dev/null)"
PC_A1="$(pc_table_cell "$PC_T1" s-a)"; PC_A2="$(pc_table_cell "$PC_T2" s-a)"
[ -n "$PC_A1" ] && [ -n "$PC_A2" ] && [ "$PC_A1" != "$PC_A2" ]
check "plan 4: precondition - the swap really changed s-a's PLAN value (${PC_A1:-<missing>} -> ${PC_A2:-<missing>})" $?
PC_O1="$(printf '%s\n' "$PC_T1" | awk '$1 ~ /^s-/ {print $1}' | tr '\n' ' ')"
PC_O2="$(printf '%s\n' "$PC_T2" | awk '$1 ~ /^s-/ {print $1}' | tr '\n' ' ')"
[ -n "$PC_A1" ] && [ "$PC_O1" = "$PC_O2" ]; check "plan 4: table order is identical across the swap ($PC_O1| $PC_O2)" $?
PC_Q1="$(printf '%s\n' "$PC_P1" | sed -n 's/^repo=//p' | tr '\n' ' ')"
PC_Q2="$(printf '%s\n' "$PC_P2" | sed -n 's/^repo=//p' | tr '\n' ' ')"
[ -n "$PC_A1" ] && [ -n "$PC_Q1" ] && [ "$PC_Q1" = "$PC_Q2" ]; check "plan 4: --plan order is identical across the swap ($PC_Q1| $PC_Q2)" $?
if printf '%s\n' "$PC_P1" | grep -qi '^plan='; then PC_rc=1; else PC_rc=0; fi
[ -n "$PC_A1" ] && [ "$PC_rc" -eq 0 ]; check "plan 4: --plan blocks carry no plan= key" $?
PC_S1="$(printf '%s\n' "$PC_T1" | grep '^SUM:')"; PC_S2="$(printf '%s\n' "$PC_T2" | grep '^SUM:')"
if printf '%s' "$PC_S1" | grep -qi 'plan'; then PC_rc=1; else PC_rc=0; fi
[ -n "$PC_A1" ] && [ "$PC_rc" -eq 0 ] && [ "$PC_S1" = "$PC_S2" ]; check "plan 4: the SUM line neither mentions PLAN nor moves with it" $?
/bin/rm -rf "$PC_SROOT" 2>/dev/null
# --- 5. Replay: the shape C failed on ----------------------------------------
# Many commits, no root PLAN.md, the destination written in docs/*.local.md,
# and a release tag late in the window. A heuristic keyed on "activity without
# a release" went silent at the tag; this column must read '-' at every step.
PC_LROOT="$(mktemp -d)"
PC_L="$PC_LROOT/loop-repo"
mkrepo "$PC_L"; pc_state "$PC_L" loop-repo in-progress
mkdir -p "$PC_L/docs"; echo "destination lives here" > "$PC_L/docs/destination.local.md"
PC_BASE=$((PC_NOW - 60 * PC_DAY)); PC_HITS=0; PC_STEPS=0
PC_i=0
while [ "$PC_i" -lt 6 ]; do
echo "step $PC_i" > "$PC_L/work.txt"
pc_commit_at "$PC_L" $((PC_BASE + PC_i * 10 * PC_DAY)) "step $PC_i"
[ "$PC_i" -eq 4 ] && git -C "$PC_L" -c tag.gpgSign=false tag v9.9.0 >/dev/null 2>&1
PC_LT="$(CLAUDE_COORD_DIR="$PC_COORD" "$BOARD" --roots "$PC_LROOT" 2>/dev/null)"
PC_STEPS=$((PC_STEPS + 1))
[ "$(pc_table_cell "$PC_LT" loop-repo)" = "-" ] && PC_HITS=$((PC_HITS + 1))
PC_i=$((PC_i + 1))
done
[ -n "$(git -C "$PC_L" tag -l v9.9.0)" ]; check "plan 5: fixture - the release tag exists in the replay" $?
[ "$PC_HITS" -eq "$PC_STEPS" ]; check "plan 5: '-' at every step of the replay, before and after the tag ($PC_HITS of $PC_STEPS)" $?
/bin/rm -rf "$PC_LROOT" 2>/dev/null
# --- 6. Denominator: one value per scanned repo ------------------------------
PC_WITH=0
for PC_r in p-none p-fresh p-stale p-docs p-lower; do
[ -n "$(pc_table_cell "$PC_TABLE" "$PC_r")" ] && PC_WITH=$((PC_WITH + 1))
done
# A non-empty, non-zero denominator first: with an empty scan both sides are 0
# and the comparison alone would pass on nothing.
[ "${PC_N_REPOS:-0}" -gt 0 ] && [ "$PC_WITH" -eq "$PC_N_REPOS" ]
check "plan 6: every scanned repo has a PLAN value ($PC_WITH of ${PC_N_REPOS:-?})" $?
printf '%s\n' "$PC_TABLE" | grep -q '^undersoekt: '
check "plan 6: the scan's own denominator line is still printed" $?
# --- 7. Legend -----------------------------------------------------------------
PC_LEG="$(printf '%s\n' "$PC_TABLE" | awk '/^PLAN = /{on=1} on && /^[A-Z]+ = / && !/^PLAN = /{on=0} on')"
[ -n "$PC_LEG" ]; check "plan 7: the legend has a 'PLAN = ' entry" $?
printf '%s' "$PC_LEG" | grep -q 'IKKE' && printf '%s' "$PC_LEG" | grep -qi 'felle'
check "plan 7: the legend says the column does NOT judge whether the criterion can be falsified" $?
printf '%s' "$PC_LEG" | grep -q 'touch' && printf '%s' "$PC_LEG" | grep -q 'checkout'
check "plan 7: the legend says touch/checkout reset the mtime" $?
if printf '%s' "$PC_LEG" | LC_ALL=C grep -q '[^ -~]'; then PC_rc=1; else PC_rc=0; fi
[ -n "$PC_LEG" ] && [ "$PC_rc" -eq 0 ]; check "plan 7: the legend is ASCII like the rest of the table" $?
/bin/rm -rf "$PC_ROOT" "$PC_COORD" 2>/dev/null
echo ""
echo "board-selftest: $PASS passed, $FAIL failed"
[ "$FAIL" -eq 0 ] || exit 1