repo-mailbox/scripts/board-selftest.sh
Kjell Tore Guttormsen 305f1687b7 fix(board): one spec for next-cost, and it is the rubric's
The board line's cost field had two specifications and one of them was this
script. `--help` prescribed a versionless `sonnet/xhigh`; the global model
rubric it points at spells the model `Sonnet 5 / xhigh`. A session following
either was correct by its own source, so the field drifted with nobody making
a mistake. Measured across a real 44-repo tree: 14 board lines carrying eight
distinct strings, and not one of them the form documented here - the only
spelling nobody used was ours.

The help now prescribes the rubric's, which is also what most lines already
carry. The parser is untouched and stays lenient: it reads to `;` or `-->` so
the rubric's spacing survives, and the fixtures still vary the spelling on
purpose to pin that. Read-side leniency was never the defect; it is what makes
a documentation-only fix sufficient.

No normalization at read, deliberately. Board lines are rewritten every session
by convention, so an unambiguous spec self-heals the existing lines in about one
session per active repo - mapping variants would be permanent code migrating
data that migrates itself. Nothing compares the field mechanically today either:
KOST is a display column and the board sorts on a numeric bucket key, so the
defect was real but latent. Two checks (28 -> 30) pin the documented form,
red first, because cleaning the data cannot fix a spec that regenerates the
divergence every session. Validation at write has no write path to attach to:
nothing in this repo emits a board line outside the fixtures.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U6EixQo6hpoRCVtiAXdnFs
2026-07-28 22:06:45 +02:00

236 lines
11 KiB
Bash
Executable file

#!/bin/bash
# board-selftest.sh - prove board.sh end-to-end against a throwaway repo tree
# and a throwaway mailbox (never touches ~/repos or ~/.claude/coord). Re-run
# after any edit to board.sh. ASCII only, bash 3.2 safe.
#
# Multibyte STATE.md content (em-dash, the NESTE pointing-hand) is generated
# with printf octal escapes so this script's own source stays pure ASCII - a
# literal em-dash in shell source has crashed bash 3.2 under `set -u` before.
set -u
export LC_ALL=C
DIR="$(cd "$(dirname "$0")" && pwd)"
BOARD="$DIR/board.sh"
ROOT="$(mktemp -d)"
CLAUDE_COORD_DIR="$(mktemp -d)"
export CLAUDE_COORD_DIR
cleanup() { /bin/rm -rf "$ROOT" "$CLAUDE_COORD_DIR" 2>/dev/null; }
trap cleanup EXIT
PASS=0; FAIL=0
check() { if [ "$2" -eq 0 ]; then PASS=$((PASS+1)); echo " ok - $1"; else FAIL=$((FAIL+1)); echo " FAIL - $1"; fi; }
# Multibyte building blocks (octal escapes keep this source ASCII).
EMDASH="$(printf '\342\200\224')"
HAND="$(printf '\360\237\221\211')"
OSLASH="$(printf '\303\270')"
mkrepo() { mkdir -p "$1" && git -C "$1" init -q 2>/dev/null; }
echo "board-selftest (root: $ROOT, mailbox: $CLAUDE_COORD_DIR)"
# --- 0. Empty root: no repos, still exits cleanly. ---
out0="$("$BOARD" --roots "$ROOT" 2>/dev/null)"; rc=$?
[ "$rc" -eq 0 ]; check "empty root exits 0" $?
# --- Fixture tree ---------------------------------------------------------
# repo-a: full board line, in-progress, cheap next step, unhandled inbox.
mkrepo "$ROOT/repo-a"
{
echo "# STATE - repo-a"
echo ""
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
echo "<!-- board: status=in-progress; blocked-on=-; next-cost=sonnet/xhigh -->"
printf '**Lukk to MAJOR fra reviewen** %s deretter S3.3 concurrent fan-out.\n' "$EMDASH"
} > "$ROOT/repo-a/STATE.md"
# repo-b: STATE + NESTE prose but NO board line (the un-backfilled majority).
mkrepo "$ROOT/repo-b"
{
echo "# STATE - repo-b"
echo ""
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
echo ""
printf '> Kj%sr forskningstema 1 headless %s hard gate for motorens arkitektur.\n' "$OSLASH" "$EMDASH"
} > "$ROOT/repo-b/STATE.md"
# repo-c: no STATE.md at all (dormant class).
mkrepo "$ROOT/repo-c"
# repo-d: dirty working tree.
mkrepo "$ROOT/repo-d"
{
echo "# STATE - repo-d"
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
echo "<!-- board: status=deferred; blocked-on=-; next-cost=sonnet/high -->"
echo "Parkert med vilje."
} > "$ROOT/repo-d/STATE.md"
# Commit STATE.md first, so the dirty count proves board counts UNCOMMITTED
# files only (2 junk files) rather than every file in a fresh tree.
git -C "$ROOT/repo-d" add STATE.md >/dev/null 2>&1
git -C "$ROOT/repo-d" -c user.email=t@t -c user.name=t commit -qm init >/dev/null 2>&1
: > "$ROOT/repo-d/junk1.txt"
: > "$ROOT/repo-d/junk2.txt"
# repo-e: malformed status token (must be flagged, never silently accepted).
mkrepo "$ROOT/repo-e"
{
echo "# STATE - repo-e"
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
echo "<!-- board: status=aktiv; blocked-on=-; next-cost=opus/high -->"
echo "Ugyldig token."
} > "$ROOT/repo-e/STATE.md"
# repo-g: prose containing a 'board:'-lookalike ABOVE the real board line.
# An unanchored substring grep would match 'dashboard:' first and mis-parse the
# whole repo; the board line is defined as living under the NESTE heading.
mkrepo "$ROOT/repo-g"
{
echo "# STATE - repo-g"
echo "Vi bygde et dashboard: status=done ble diskutert i forrige runde."
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
echo "<!-- board: status=planned; blocked-on=-; next-cost=opus/xhigh -->"
echo "Ekte neste steg her."
} > "$ROOT/repo-g/STATE.md"
# repo-h: next-cost written the way the model rubric actually names the models,
# with a space and capitals ("Sonnet 5/xhigh"). This is spec-conformant
# (<modell>/<effort>) and must parse - a lowercase-only pattern silently drops
# it to "?" and hides the cost column exactly where it is needed.
mkrepo "$ROOT/repo-h"
{
echo "# STATE - repo-h"
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
echo "<!-- board: status=in-progress; blocked-on=-; next-cost=Sonnet 5/xhigh -->"
echo "Neste steg her."
} > "$ROOT/repo-h/STATE.md"
# polyrepo/: NOT a git repo itself, but holds git repos one level down.
mkdir -p "$ROOT/polyrepo"
mkrepo "$ROOT/polyrepo/plug-x"
{
echo "# STATE - plug-x"
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
echo "<!-- board: status=blocked; blocked-on=repo-a; next-cost=opus/high -->"
printf 'Venter p%s amendment-pakken.\n' "$EMDASH"
} > "$ROOT/polyrepo/plug-x/STATE.md"
# repo-wt + wt-feature: a git WORKTREE (and a submodule) has .git as a FILE,
# not a directory. A `git worktree add <root>/feature-x` yields a sibling
# directory that can CARRY its own STATE.md. Testing discovery for a .git
# DIRECTORY only drops it silently - the same silent-loss class as the V6
# marker fix.
mkrepo "$ROOT/repo-wt"
{
echo "# STATE - repo-wt"
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
echo "<!-- board: status=done; blocked-on=-; next-cost=sonnet/high -->"
printf 'Hovedtreet %s ferdig.\n' "$EMDASH"
} > "$ROOT/repo-wt/STATE.md"
git -C "$ROOT/repo-wt" add STATE.md >/dev/null 2>&1
git -C "$ROOT/repo-wt" -c user.email=t@t -c user.name=t commit -qm init >/dev/null 2>&1
git -C "$ROOT/repo-wt" -c user.email=t@t -c user.name=t \
worktree add -q -b feature-x "$ROOT/wt-feature" >/dev/null 2>&1
# Distinct board line, so the check proves board read the STATE INSIDE the
# worktree rather than matching the source repo's row by accident.
{
echo "# STATE - wt-feature"
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
echo "<!-- board: status=in-progress; blocked-on=-; next-cost=fable/xhigh -->"
printf 'Arbeid i worktree %s eget neste steg.\n' "$EMDASH"
} > "$ROOT/wt-feature/STATE.md"
# plain-dir/: no git repo anywhere under it - must be ignored entirely.
mkdir -p "$ROOT/plain-dir/sub"
echo "hei" > "$ROOT/plain-dir/sub/file.txt"
# Coord fixture: 3 unhandled for repo-a, 1 archived (must not be counted).
mkdir -p "$CLAUDE_COORD_DIR/repo-a/inbox" "$CLAUDE_COORD_DIR/repo-a/archive"
for n in 1 2 3; do echo "msg" > "$CLAUDE_COORD_DIR/repo-a/inbox/2026-msg$n-from-x.md"; done
echo "old" > "$CLAUDE_COORD_DIR/repo-a/archive/2026-old-from-x.md"
OUT="$("$BOARD" --roots "$ROOT" 2>/dev/null)"
# --- 1. Discovery ---------------------------------------------------------
printf '%s' "$OUT" | grep -q 'repo-a'; check "discovers top-level git repo" $?
printf '%s' "$OUT" | grep -q 'plug-x'; check "discovers nested polyrepo git repo (depth 2)" $?
printf '%s' "$OUT" | grep -q 'repo-c'; check "lists git repo without STATE.md" $?
printf '%s' "$OUT" | grep -q 'plain-dir'; [ $? -ne 0 ]; check "ignores non-git directory tree" $?
printf '%s' "$OUT" | grep -q 'polyrepo'; [ $? -ne 0 ]; check "polyrepo container itself is not listed as a repo" $?
# Guard the fixture itself: if git ever stops writing a .git FILE for
# worktrees, the next two checks would pass for the wrong reason.
[ -f "$ROOT/wt-feature/.git" ]; check "fixture: worktree .git is a FILE, not a dir" $?
printf '%s' "$OUT" | grep -q 'wt-feature'; check "discovers git worktree (.git is a file)" $?
printf '%s' "$OUT" | grep -qE 'wt-feature.*fable/xhigh'
check "reads STATE.md from inside the worktree, not the source repo" $?
# --- 2. Board line parsing (the B field) ----------------------------------
printf '%s' "$OUT" | grep -q 'in-progress'; check "parses status token from board line" $?
printf '%s' "$OUT" | grep -q 'sonnet/xhigh'; check "parses next-cost from board line" $?
printf '%s' "$OUT" | grep -qE 'repo-h.*Sonnet 5/xhigh'
check "next-cost accepts spaces and capitals (rubric model names)" $?
printf '%s' "$OUT" | grep -qE 'plug-x.*repo-a'
check "blocked repo names its blocker on its own row" $?
printf '%s' "$OUT" | grep -q 'deferred'; check "distinguishes deferred from blocked" $?
# --- 3. Malformed input is flagged, not swallowed -------------------------
printf '%s' "$OUT" | grep -qi 'malformed\|ugyldig\|invalid'; check "malformed status token is flagged" $?
# A 'dashboard:' lookalike earlier in the file must not win over the real line.
printf '%s' "$OUT" | grep -qE 'repo-g.*planned.*opus/xhigh'
check "board-line parse ignores 'board:' lookalikes in prose" $?
# --- 4. Heuristic fallback when board line is absent -----------------------
printf '%s' "$OUT" | grep -q 'repo-b'; check "repo without board line still listed" $?
printf '%s' "$OUT" | grep -qi 'forskningstema\|headless'; check "shows NESTE excerpt when board line absent" $?
# Markdown noise must be stripped: a blockquoted NESTE line renders as prose.
printf '%s' "$OUT" | grep -qE 'repo-b.*[[:space:]]>'; [ $? -ne 0 ]
check "blockquote marker stripped from NESTE excerpt" $?
# --- 5. Git + coord signals ------------------------------------------------
printf '%s' "$OUT" | grep -qE 'repo-d.*[^0-9]2([^0-9]|$)'; check "reports dirty file count" $?
printf '%s' "$OUT" | grep -qE 'repo-a.*[^0-9]3([^0-9]|$)'; check "reports unhandled coord inbox count (archive excluded)" $?
# --- 6. Bucketing ----------------------------------------------------------
printf '%s' "$OUT" | grep -qi 'blocked'; check "blocked repo surfaced" $?
printf '%s' "$OUT" | grep -qiE 'dormant|sovende|uten STATE'; check "STATE-less repos bucketed separately" $?
# --- 7. Robustness ---------------------------------------------------------
# Multibyte prose must not crash the reader nor emit split-character garbage.
[ -n "$OUT" ]; check "produces output over multibyte STATE prose" $?
printf '%s' "$OUT" | iconv -f UTF-8 -t UTF-8 >/dev/null 2>&1
check "output is valid UTF-8 (no split multibyte truncation)" $?
# A STATE.md with a NESTE heading and nothing after it must not hang or crash.
mkrepo "$ROOT/repo-f"
{ echo "# STATE - repo-f"; printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"; } > "$ROOT/repo-f/STATE.md"
"$BOARD" --roots "$ROOT" >/dev/null 2>&1; check "empty NESTE block does not crash" $?
"$BOARD" --help >/dev/null 2>&1; check "--help exits 0" $?
# Unreadable root is a no-op, not a crash.
"$BOARD" --roots "$ROOT/does-not-exist" >/dev/null 2>&1; check "missing root is a clean no-op" $?
# --- 8. The documented next-cost form IS the convention --------------------
# The cost field is free text on purpose: the parser reads to ';' or '-->' so
# the rubric's spacing survives, and the fixtures above deliberately vary the
# spelling to pin that leniency. But leniency on the read side means the ONLY
# thing holding the field uniform is what --help prescribes, and --help once
# prescribed a versionless `sonnet/xhigh` while the global rubric spelled
# `Sonnet 5 / xhigh`. Two specs for one field: measured across the real tree,
# not one board line used ours, and eight distinct strings were in circulation.
# Pinned here because cleaning the DATA cannot fix that - a spec that
# contradicts the rubric regenerates the divergence every session.
HELP="$("$BOARD" --help 2>/dev/null)"
printf '%s' "$HELP" | grep -q 'next-cost=Sonnet 5/xhigh'
check "--help prescribes the canonical next-cost form" $?
# The versionless form is what the rubric does NOT use; no example may show it.
if printf '%s' "$HELP" | grep -qE '(sonnet|opus|fable)/(high|xhigh)'; then rc=1; else rc=0; fi
check "--help shows no versionless model example" "$rc"
echo ""
echo "board-selftest: $PASS passed, $FAIL failed"
[ "$FAIL" -eq 0 ] || exit 1
exit 0