test(board): pin the "field inserted before neste" class; 16 of 16 mutants die

An independent review ran 16 mutants of the PLAN column against the 461
board checks and 5 survived:
- --row taking neste from a stale index (neste=5d|...)
- --inbox-plan taking neste from a stale index (neste=5d)
- the no-STATE.md record one field short (plan=(ingen STATE.md))
- Nd measured behind NOW instead of behind the last commit
- the '?' branch (a plan with no commit) rendered as '-'

Section 34 part 8 adds a fixture for each: a NESTE with a '|' inside,
pinned exactly in --row and --inbox-plan; a repo with no STATE.md; a repo
whose last commit is 30 days old, so the two definitions give different
numbers; a repo with a plan and no commit. Rebuilt from this tree and run
against the full suite: 16 of 16 mutants now fail it.

The same fixtures exposed two defects, fixed here:
- --inbox-plan took neste as ONE field and cut the prose at its first '|'
  (also in 0.34.0); it now takes that field onward.
- a symlinked PLAN.md was aged by the link's own mtime; it now uses
  stat -L, so the plan's age is the target's.

Docs: CHANGELOG no longer says "field 17 onward" (the index is 18) and
now records the no-STATE.md record fix and the --inbox-plan fix; README's
requirement says macOS, since board.sh uses BSD stat and renders nothing
under GNU stat.

board-selftest 461 -> 478 (badge 978). Suites: coord 257, board 478,
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 17:58:49 +02:00
commit 6f4908a5dc
5 changed files with 145 additions and 15 deletions

View file

@ -14,7 +14,7 @@ Session A in repo X leaves a message for repo Y; the next session in repo Y gets
![Hooks](https://img.shields.io/badge/hooks-1-green)
![Skills](https://img.shields.io/badge/skills-4-orange)
![CLI scripts](https://img.shields.io/badge/CLI_scripts-8-blue)
![Selftest checks](https://img.shields.io/badge/selftest_checks-961-blue)
![Selftest checks](https://img.shields.io/badge/selftest_checks-978-blue)
---
@ -152,7 +152,7 @@ It makes **zero model calls**, which is the point rather than a detail. Under su
**`board.sh --row <repo>` renders one repo's row as `field=value` lines**, a seventh rendering of the same scan and the only one that answers about a single repo. It exists because the table's columns *were* misread: the fixed-width view is for a human, and reading a column off it by eye is how `FLY` was read as `ORDRE` on 2026-09-11 — while every other rendering a program consumes is already `key=value`. `inn`, `ordre` and `fly` are three separate fields because they are three separate facts; `status` is the bare token rather than the table's `blocked>target` display, with `blocked-on` beside it; `neste` is last and uncut. An unknown repo exits **2** and writes nothing, because an empty block would read as a repo whose every column is blank — a real and different state. One field is deliberately *not* a rendering of the scan and is named as such: `upushet` is measured once, for that repo only, from the remote-**tracking** ref rather than the remote, so a repo with no upstream reports `?` and never `0`.
**The table's `PLAN` column shows whether a repo keeps a destination where sessions actually look.** It is a filesystem reading and nothing more: `-` means no file named exactly `PLAN.md` in the repo root, `Nd` means the plan's mtime lies `N` whole days behind the last commit (`0d` when it is as new or newer), and `?` means a plan with no commit to compare against. The file's content is never read, so no wording inside it can move the cell — and the column does not judge whether the criterion inside can be falsified. The name is matched **byte for byte** through a glob rather than with `[ -f PLAN.md ]`, because APFS is case-insensitive by default and that test is true for a `plan.md`. It is display only: never a sort key, never in `--plan`, never summed; `--row` carries it as `plan=`. `touch` and a checkout reset the mtime, the same caveat `ALDER` carries. It replaced nothing: a heuristic on "activity without a release" was measured and rejected first, because a release made it go silent on the one known case it existed to catch.
**The table's `PLAN` column shows whether a repo keeps a destination where sessions actually look.** It is a filesystem reading and nothing more: `-` means no file named exactly `PLAN.md` in the repo root, `Nd` means the plan's mtime lies `N` whole days behind the last commit (`0d` when it is as new or newer), and `?` means a plan with no commit to compare against; a symlinked `PLAN.md` is aged by its target. The file's content is never read, so no wording inside it can move the cell — and the column does not judge whether the criterion inside can be falsified. The name is matched **byte for byte** through a glob rather than with `[ -f PLAN.md ]`, because APFS is case-insensitive by default and that test is true for a `plan.md`. It is display only: never a sort key, never in `--plan`, never summed; `--row` carries it as `plan=`. `touch` and a checkout reset the mtime, the same caveat `ALDER` carries. It replaced nothing: a heuristic on "activity without a release" was measured and rejected first, because a release made it go silent on the one known case it existed to catch.
**`board.sh --plan` renders the day plan**, a *third* rendering of that same scan and the only one that takes a position: which repos to open a tab for today, in what order, and the command to start each. The order is the position, and there is no cutoff — nothing is hidden, and **five ordered groups** decide it, each a lookup over a field the scan already read rather than a weighted score: (1) **chain-root credit**, most repos released first; (2) **debt**, most-owed-first, whatever the status; (3) `planned`; (4) `in-progress`; (5) `?`/`MALFORMED` — undeclared, last and labelled. Within a group, ties break on a cheap Sonnet `next-cost` row, then oldest plan first. A 0.19.0 weighted score (`40 ×` repos released, `15 ×` unhandled inbox messages) briefly stood in this spot and *could* express "this repo owes one message and releases two others" as a single number — but re-tuning those two coefficients would have silently reordered a parser living in another repo, with no test here able to hold a ranking stable for a consumer it can't see; the operator replaced it with the group order in 0.20.0 for that reason. Groups 3 and 4 are `planned` above `in-progress`, inverted from every earlier version by the same decision: turning a decision into motion is the slow step, live work is already moving. **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, since its own next step is by definition waiting. A cycle, or a `blocked-on` naming a repo the scan never produced, credits nobody rather than inventing a root — a plan that looks correct while sending you to the wrong repo is worse than one that says nothing. Repos owing mail still rank high *whatever their status*, and debt is deliberately **uncapped**: excluding `blocked` or `done` is a statement about a repo's own next step, which cannot be moved, while owing an answer is a different axis and answering is often what unblocks it. Repos with no board line come last and labelled — the table already prints a note about those, so a plan that dropped them silently would repeat exactly that defect. `why=` names the group that placed the repo, so a block reads `unblocks:2` rather than the `inbox:N` every block used to repeat. Still zero model calls, still read-only, and still cross-checked against `coord-count.sh`.
@ -194,14 +194,14 @@ Note that raising the inbox's priority (Rule 7) deliberately does **not** widen
## Requirements
- macOS or Linux with bash 3.2+ (the scripts are deliberately bash-3.2-safe and ASCII-only).
- macOS with bash 3.2+ (the scripts are deliberately bash-3.2-safe and ASCII-only). `board.sh` uses BSD `stat` (`stat -f`), so it does not run on Linux as written.
- Node.js >= 18 for the SessionStart and PreToolUse hooks (zero npm dependencies).
- `git` is required to derive repo identity automatically. Without it, pass `--from`/`--repo` explicitly; the engine refuses to guess an identity from the working directory.
## Development
bash scripts/coord-selftest.sh # 257 checks against a throwaway mailbox
bash scripts/board-selftest.sh # 461 checks against a throwaway repo tree
bash scripts/board-selftest.sh # 478 checks against a throwaway repo tree
bash scripts/route-selftest.sh # 73 checks, incl. the route->board round trip
bash scripts/orders-selftest.sh # 116 checks, incl. the 20-way barriered claim race
bash scripts/state-line-guard-selftest.sh # 54 checks, incl. the Edit replace_all projection and the ratchet