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:
parent
87fb78334e
commit
6f4908a5dc
5 changed files with 145 additions and 15 deletions
17
CHANGELOG.md
17
CHANGELOG.md
|
|
@ -12,7 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- **`PLAN` column in the board table, and `plan=` in `--row`.** A filesystem
|
||||
reading of the repo root: `-` = no file named exactly `PLAN.md`; `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
|
||||
newer); `?` = a plan with no commit to compare against. A symlinked
|
||||
`PLAN.md` is aged by its target. The content is never
|
||||
read, and the column does not judge whether a criterion can be falsified -
|
||||
the legend says so. The name is matched byte for byte, because APFS's
|
||||
case-insensitivity makes `[ -f PLAN.md ]` true for `plan.md`. Display only:
|
||||
|
|
@ -25,8 +26,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
The index that takes "the last field onward" was written when the record had
|
||||
14 fields and was never moved as ORDRE, FLY and VOY were inserted before
|
||||
`neste`, so a dispatch block read `neste=0|-|0|<the NESTE prose>`. It now
|
||||
takes field 17 onward, and a selftest check pins the exact start of the
|
||||
prose so the next inserted field turns it red.
|
||||
takes the field after the last column onward (field 18, since the `PLAN`
|
||||
column below moved it again), and a selftest check pins the exact start of
|
||||
the prose so the next inserted field turns it red.
|
||||
- **`--row` and `--dispatch` for a repo with no STATE.md carried shifted
|
||||
values.** That record is written by its own printf, which lacked the `VOY`
|
||||
field, so `--row` read `voy=(ingen STATE.md)` and an empty `neste=`. The
|
||||
record is now complete; a fixture with no STATE.md pins it.
|
||||
- **`--inbox-plan` cut NESTE at its first `|`.** It read `neste` as one field
|
||||
instead of "that field onward"; the whole prose now comes through, as it
|
||||
already did in `--row` and `--dispatch`.
|
||||
- **README said "macOS or Linux".** `board.sh` uses BSD `stat` throughout and
|
||||
renders no rows under GNU `stat`; the requirement now says macOS.
|
||||
|
||||
## [0.34.0] - 2026-09-12
|
||||
|
||||
|
|
|
|||
21
CLAUDE.md
21
CLAUDE.md
|
|
@ -467,7 +467,7 @@ marketplace plugin. Three components, one boundary:
|
|||
- **Board (`scripts/board.sh`):** cross-repo attention board. Reads STATE.md
|
||||
next-step blocks + board lines, `git status`, and mailbox pending counts, and
|
||||
prints one line per repo. Read-only by construction: it writes to no repo, no
|
||||
STATE.md and no mailbox. Pinned by `board-selftest.sh` (461 checks).
|
||||
STATE.md and no mailbox. Pinned by `board-selftest.sh` (478 checks).
|
||||
|
||||
**It lives here because the mailbox is one of its three inputs, and it carries
|
||||
the same axis distinction the mailbox does.** A pending count means *others
|
||||
|
|
@ -1489,6 +1489,23 @@ marketplace plugin. Three components, one boundary:
|
|||
"last field onward" index now moves with the record (17 -> 18 here). F6
|
||||
counts DRT from the right, so its `NF-3` became `NF-4` for the same reason.
|
||||
|
||||
**An independent review then ran 16 mutants against the 461 checks and 5
|
||||
survived** - three of them the same class as the dispatch defect (`--row`
|
||||
and `--inbox-plan` reading `neste` from a stale index, and the no-STATE.md
|
||||
record one field short), one the column's DEFINITION (days behind now
|
||||
instead of behind the last commit - every fixture's commit was close enough
|
||||
to now that the two coincided), and one the `?` branch. Section 34 part 8
|
||||
now has a fixture for each, and all 16 die. Every "last field onward"
|
||||
reader is pinned on the EXACT start of the prose with a `|` inside it,
|
||||
because a reader taking one field instead of "onward" was a second,
|
||||
pre-existing defect in `--inbox-plan` that the same fixture exposed.
|
||||
A symlinked PLAN.md is aged by its target (`stat -L`). Stated, not closed:
|
||||
the 34-character nested key still shifts its row (the section's own header
|
||||
splitter would misread such a row; fixture names are short on purpose), and
|
||||
the "trap is armed" fixture check is red on a case-sensitive volume - this
|
||||
repo is macOS-only (BSD `stat`), and a case-sensitive APFS volume is the
|
||||
remaining gap.
|
||||
|
||||
- **Skills (`skills/coord-send/`, `skills/board/`, `skills/route/`, `skills/dispatch/`):** natural-language front
|
||||
doors mapping user intent to engine invocations. No mailbox logic lives here
|
||||
either. `board` additionally owns the *ranking* — which repo wins and why —
|
||||
|
|
@ -1537,7 +1554,7 @@ obligations in another repo.
|
|||
builtins only in hook and tests.
|
||||
- TDD: no behavior change without a failing selftest check first.
|
||||
`bash scripts/coord-selftest.sh` must exit 0 (257/257),
|
||||
`bash scripts/board-selftest.sh` must exit 0 (461/461),
|
||||
`bash scripts/board-selftest.sh` must exit 0 (478/478),
|
||||
`bash scripts/route-selftest.sh` must exit 0 (73/73),
|
||||
`bash scripts/orders-selftest.sh` must exit 0 (116/116) and
|
||||
`bash scripts/state-line-guard-selftest.sh` must exit 0 (54/54).
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ Session A in repo X leaves a message for repo Y; the next session in repo Y gets
|
|||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -3483,7 +3483,7 @@ 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."
|
||||
echo "${4:-Next step for $2.}"
|
||||
} > "$1/STATE.md"
|
||||
}
|
||||
# pc_commit_at <dir> <epoch> <msg>: commit everything with a fixed committer date.
|
||||
|
|
@ -3524,7 +3524,14 @@ 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
|
||||
# Its NESTE carries a literal '|' and it holds one unhandled message, so the
|
||||
# same repo pins where every "last field onward" reader starts AND that it
|
||||
# really reads onward (section 8 below).
|
||||
mkrepo "$PC_ROOT/p-stale"
|
||||
pc_state "$PC_ROOT/p-stale" p-stale planned "Next step for p-stale | then the rest."
|
||||
mkdir -p "$PC_COORD/p-stale/inbox"
|
||||
printf -- '---\nfrom: x\nto: p-stale\nsubject: s\nreply-expected: yes\n---\nm\n' \
|
||||
> "$PC_COORD/p-stale/inbox/20260901T101010Z-1-from-x.md"
|
||||
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
|
||||
|
|
@ -3544,6 +3551,46 @@ 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
|
||||
|
||||
# The next four fixtures exist because an independent review ran 16 mutants of
|
||||
# the column against this suite and 5 survived. Each fixture below is the one
|
||||
# a surviving mutant could not get past.
|
||||
|
||||
# p-nostate: a repo with NO STATE.md. Its record is written by a separate
|
||||
# printf, which is exactly where a field was once missing (0.34.0 printed
|
||||
# voy=(ingen STATE.md) and an empty neste=), so a short record must show here.
|
||||
# The last commit is 40 days old.
|
||||
mkrepo "$PC_ROOT/p-nostate"
|
||||
: > "$PC_ROOT/p-nostate/PLAN.md"
|
||||
PC_NS_CT=$((PC_NOW - 40 * PC_DAY))
|
||||
pc_commit_at "$PC_ROOT/p-nostate" "$PC_NS_CT" init
|
||||
pc_touch_at "$PC_ROOT/p-nostate/PLAN.md" $((PC_NS_CT - 3 * PC_DAY - 3600))
|
||||
|
||||
# p-old: the column's DEFINITION. The last commit is 30 days old, so "days
|
||||
# behind the last commit" (10) and "days behind now" (40) are different
|
||||
# numbers; every fixture above has a commit close enough to now that the two
|
||||
# readings coincide.
|
||||
mkrepo "$PC_ROOT/p-old"; pc_state "$PC_ROOT/p-old" p-old planned
|
||||
: > "$PC_ROOT/p-old/PLAN.md"
|
||||
PC_OLD_CT=$((PC_NOW - 30 * PC_DAY))
|
||||
pc_commit_at "$PC_ROOT/p-old" "$PC_OLD_CT" init
|
||||
pc_touch_at "$PC_ROOT/p-old/PLAN.md" $((PC_OLD_CT - 10 * PC_DAY - 5 * 3600))
|
||||
|
||||
# p-nocommit: a plan and no commit at all. There is nothing to lag behind, so
|
||||
# the honest cell is '?' - never '-', which would claim there is no plan.
|
||||
mkrepo "$PC_ROOT/p-nocommit"; pc_state "$PC_ROOT/p-nocommit" p-nocommit planned
|
||||
: > "$PC_ROOT/p-nocommit/PLAN.md"
|
||||
|
||||
# p-link: PLAN.md is a symlink. The cell must read the PLAN's age, i.e. the
|
||||
# target's mtime; the link's own mtime is when the link was made, and reading
|
||||
# it turns an 8-day-old plan into 0d.
|
||||
mkrepo "$PC_ROOT/p-link"; pc_state "$PC_ROOT/p-link" p-link planned
|
||||
mkdir -p "$PC_ROOT/p-link/docs"; echo "# PLAN" > "$PC_ROOT/p-link/docs/real-plan.md"
|
||||
ln -s docs/real-plan.md "$PC_ROOT/p-link/PLAN.md"
|
||||
PC_LINK_CT=$((PC_NOW - 2 * 3600))
|
||||
pc_commit_at "$PC_ROOT/p-link" "$PC_LINK_CT" init
|
||||
pc_touch_at "$PC_ROOT/p-link/docs/real-plan.md" $((PC_LINK_CT - 8 * PC_DAY - 3600))
|
||||
touch -h "$PC_ROOT/p-link/PLAN.md"
|
||||
|
||||
# --- 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" $?
|
||||
|
|
@ -3559,7 +3606,7 @@ ls -1 "$PC_ROOT/p-lower" | grep -qx 'PLAN.md'; PC_rc=$?
|
|||
|
||||
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})" $?
|
||||
[ "${PC_N_REPOS:-0}" -eq 9 ]; check "plan: fixture - board scanned the 9 fixture repos (header says ${PC_N_REPOS:-nothing})" $?
|
||||
|
||||
# --- 1. Three different values from three repos ----------------------------
|
||||
printf '%s\n' "$PC_TABLE" | grep -q '^REPO .* PLAN '
|
||||
|
|
@ -3577,7 +3624,7 @@ check "plan 1: the three fixtures show three DIFFERENT values ($PC_V3)" $?
|
|||
[ "$(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
|
||||
for PC_r in p-none p-fresh p-stale p-docs p-lower p-nostate p-old p-nocommit p-link; 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" ]
|
||||
|
|
@ -3645,7 +3692,7 @@ done
|
|||
|
||||
# --- 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
|
||||
for PC_r in p-none p-fresh p-stale p-docs p-lower p-nostate p-old p-nocommit p-link; 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
|
||||
|
|
@ -3665,6 +3712,55 @@ 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" $?
|
||||
|
||||
# --- 8. What the surviving mutants got past --------------------------------
|
||||
# neste= start, pinned EXACTLY in the two renderings that take "the last field
|
||||
# onward" besides --dispatch (which has its own check in section 18). A field
|
||||
# inserted before `neste` with an index left behind shows up as a prefix
|
||||
# ("5d|Next step ..."), and a reader that takes ONE field instead of "onward"
|
||||
# cuts the prose at its first '|'.
|
||||
PC_NESTE='Next step for p-stale | then the rest.'
|
||||
pc_board --row p-stale | grep -qxF "neste=$PC_NESTE"
|
||||
check "plan 8: --row neste= is the NESTE prose exactly, '|' included" $?
|
||||
PC_IP="$(pc_board --inbox-plan)"
|
||||
printf '%s\n' "$PC_IP" | grep -qx 'repo=p-stale'
|
||||
check "plan 8: fixture - --inbox-plan has a block for p-stale" $?
|
||||
printf '%s\n' "$PC_IP" | awk 'BEGIN{RS=""} /(^|\n)repo=p-stale(\n|$)/' | grep -qxF "neste=$PC_NESTE"
|
||||
check "plan 8: --inbox-plan neste= is the NESTE prose exactly, '|' included" $?
|
||||
|
||||
# A repo with no STATE.md still gets its measured value, and its neste= is the
|
||||
# placeholder rather than an empty line or a value shifted from another field.
|
||||
PC_NS_EXP="$(( (PC_NS_CT - $(pc_mtime "$PC_ROOT/p-nostate/PLAN.md")) / PC_DAY ))d"
|
||||
PC_NS_ROW="$(pc_board --row p-nostate)"
|
||||
printf '%s\n' "$PC_NS_ROW" | grep -qx "plan=$PC_NS_EXP"
|
||||
check "plan 8: no STATE.md - --row plan= is the measured value ($PC_NS_EXP)" $?
|
||||
printf '%s\n' "$PC_NS_ROW" | grep -qxF 'neste=(ingen STATE.md)'
|
||||
check "plan 8: no STATE.md - --row neste= is the placeholder, not empty or shifted" $?
|
||||
[ "$(pc_table_cell "$PC_TABLE" p-nostate)" = "$PC_NS_EXP" ]
|
||||
check "plan 8: no STATE.md - the table cell is the measured value too" $?
|
||||
|
||||
# The definition: behind the LAST COMMIT, not behind now.
|
||||
PC_OLD_EXP="$(( (PC_OLD_CT - $(pc_mtime "$PC_ROOT/p-old/PLAN.md")) / PC_DAY ))d"
|
||||
PC_OLD_NOW="$(( (PC_NOW - $(pc_mtime "$PC_ROOT/p-old/PLAN.md")) / PC_DAY ))d"
|
||||
[ "$PC_OLD_EXP" != "$PC_OLD_NOW" ]
|
||||
check "plan 8: fixture - p-old separates 'behind commit' ($PC_OLD_EXP) from 'behind now' ($PC_OLD_NOW)" $?
|
||||
[ "$(pc_table_cell "$PC_TABLE" p-old)" = "$PC_OLD_EXP" ]
|
||||
check "plan 8: Nd counts days behind the LAST COMMIT, not behind now ($PC_OLD_EXP)" $?
|
||||
|
||||
# No commit: '?' in both renderings.
|
||||
git -C "$PC_ROOT/p-nocommit" log -1 >/dev/null 2>&1
|
||||
[ $? -ne 0 ]; check "plan 8: fixture - p-nocommit really has no commit" $?
|
||||
[ "$(pc_table_cell "$PC_TABLE" p-nocommit)" = "?" ]
|
||||
check "plan 8: a plan with no commit reads '?', never '-'" $?
|
||||
pc_board --row p-nocommit | grep -qxF 'plan=?'
|
||||
check "plan 8: --row carries the same '?'" $?
|
||||
|
||||
# Symlink: the target's age, not the link's.
|
||||
PC_LINK_EXP="$(( (PC_LINK_CT - $(pc_mtime "$PC_ROOT/p-link/docs/real-plan.md")) / PC_DAY ))d"
|
||||
[ "$(stat -f %m "$PC_ROOT/p-link/PLAN.md")" -ge "$PC_LINK_CT" ] && [ "$PC_LINK_EXP" != "0d" ]
|
||||
check "plan 8: fixture - the link itself is newer than the commit, its target is $PC_LINK_EXP behind" $?
|
||||
[ "$(pc_table_cell "$PC_TABLE" p-link)" = "$PC_LINK_EXP" ]
|
||||
check "plan 8: a symlinked PLAN.md reads the TARGET's age ($PC_LINK_EXP), not the link's" $?
|
||||
|
||||
/bin/rm -rf "$PC_ROOT" "$PC_COORD" 2>/dev/null
|
||||
|
||||
echo ""
|
||||
|
|
|
|||
|
|
@ -838,7 +838,9 @@ printf '%s\n' "$REPOS" | while IFS= read -r d; do
|
|||
plancol="-"
|
||||
for pl_f in "$d"/PLAN.m[d]; do
|
||||
[ "${pl_f##*/}" = "PLAN.md" ] && [ -f "$pl_f" ] || continue
|
||||
pl_m="$(stat -f %m "$pl_f" 2>/dev/null)"
|
||||
# -L: the PLAN's age is the target's mtime; a symlink's own mtime is only
|
||||
# when the link was made.
|
||||
pl_m="$(stat -L -f %m "$pl_f" 2>/dev/null)"
|
||||
if [ -z "$pl_m" ] || [ -z "$lastct" ]; then
|
||||
plancol="?"
|
||||
elif [ "$pl_m" -ge "$lastct" ]; then
|
||||
|
|
@ -1982,7 +1984,11 @@ inbox_plan() {
|
|||
FILENAME==RCF {
|
||||
name = $3
|
||||
rc_bucket[name] = $1; rc_status[name] = $4; rc_cost[name] = $5
|
||||
# neste is the LAST field and may itself contain a pipe: take field 18
|
||||
# ONWARD, never field 18 alone - a single field cuts the prose at its
|
||||
# first pipe. The index moves with the record.
|
||||
rc_dir[name] = $10; rc_neste[name] = $18
|
||||
for (i = 19; i <= NF; i++) rc_neste[name] = rc_neste[name] "|" $i
|
||||
next
|
||||
}
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue