fix(board-roll): reject >1 marker pair instead of spanning across zones
Some checks failed
ci/woodpecker/pr/ci Pipeline failed
Some checks failed
ci/woodpecker/pr/ci Pipeline failed
rev0 (PR #868, Gitea comment #18505) caught a real data-relocation bug: the marker scan locked start_idx to the FIRST START but overwrote end_idx on EVERY END, so a board with two BOARD-ROLL zones collapsed into one first-START..last-END span. That span engulfed the curated `##` content and the intermediate START/END markers between the two zones, silently relocating pinned lines into the LEDGER — the exact failure this tool exists to prevent. Fix (conservative, matches the "no markers = exit 3" posture): count START and END markers; if either appears more than once, refuse with exit 3 and change nothing. Single-zone behavior is unchanged. Adds regression group 8: two zones with a curated CANARY section between them, cap 80 → asserts exit 3, LIVE unchanged, and the canary never leaks into the LEDGER. shellcheck -S warning clean; 8/8 groups pass. Refs #868
This commit is contained in:
@@ -68,10 +68,15 @@ board-roll.sh --live <LIVE.md> --ledger <LEDGER.md> [options]
|
||||
-h, --help show help and exit 0
|
||||
```
|
||||
|
||||
Only **one** roll zone is supported. If a board carries more than one
|
||||
`BOARD-ROLL:START`/`END` pair, `board-roll.sh` refuses (exit `3`, zero changes)
|
||||
rather than span first-START..last-END and relocate the curated content between
|
||||
the zones — consolidate the ticks into a single zone instead.
|
||||
|
||||
Exit codes: `0` LIVE under cap (already, or after rolling) — on `--dry-run`, a
|
||||
plan exists or nothing to do · `2` usage / argument / IO error · `3` cannot meet
|
||||
the cap (no markers, or the pinned sections alone exceed the cap and need a manual
|
||||
trim).
|
||||
the cap (no markers, **more than one marker pair**, or the pinned sections alone
|
||||
exceed the cap and need a manual trim).
|
||||
|
||||
Writes are atomic (temp file + `mv`, LEDGER first) so a failure never leaves a
|
||||
board half-written; line endings are normalized to LF on rewrite. `--dry-run`
|
||||
|
||||
@@ -45,8 +45,9 @@
|
||||
# 0 LIVE is under cap (already, or after rolling); on --dry-run, 0 = a plan exists
|
||||
# (or nothing to do)
|
||||
# 2 usage / argument / IO error (bad flag, missing file, unwritable target)
|
||||
# 3 cannot satisfy the cap: no roll markers present, OR the zone was emptied and
|
||||
# LIVE is still over cap (curated pinned sections need a manual trim)
|
||||
# 3 cannot satisfy the cap: no roll markers present, MORE THAN ONE marker pair
|
||||
# (multiple zones are refused, not guessed), OR the zone was emptied and LIVE
|
||||
# is still over cap (curated pinned sections need a manual trim)
|
||||
#
|
||||
# NOTE: line endings are normalized to LF on rewrite (boards are LF markdown); a
|
||||
# trailing newline is always ensured. Writes are atomic (temp file + mv) so a
|
||||
@@ -116,11 +117,30 @@ if (( orig_size < CAP )); then
|
||||
fi
|
||||
|
||||
# --- locate the roll-zone markers ----------------------------------------------
|
||||
start_idx=-1; end_idx=-1
|
||||
# Exactly ONE marker pair is supported. If a board carries more than one START or
|
||||
# END marker we REFUSE (exit 3, zero changes) rather than guess: a naive
|
||||
# first-START..last-END span would swallow the curated content and the intermediate
|
||||
# markers sitting between two intended zones and silently relocate that pinned text
|
||||
# to the LEDGER — the exact data-loss this tool exists to prevent. Refusing matches
|
||||
# the "no markers = exit 3" conservative posture.
|
||||
start_idx=-1; end_idx=-1; start_count=0; end_count=0
|
||||
for i in "${!LINES[@]}"; do
|
||||
[[ "${LINES[$i]}" == "$START_MARK" && $start_idx -eq -1 ]] && start_idx=$i
|
||||
[[ "${LINES[$i]}" == "$END_MARK" ]] && end_idx=$i
|
||||
if [[ "${LINES[$i]}" == "$START_MARK" ]]; then
|
||||
if (( start_count == 0 )); then start_idx=$i; fi
|
||||
start_count=$(( start_count + 1 ))
|
||||
fi
|
||||
if [[ "${LINES[$i]}" == "$END_MARK" ]]; then
|
||||
end_idx=$i
|
||||
end_count=$(( end_count + 1 ))
|
||||
fi
|
||||
done
|
||||
if (( start_count > 1 || end_count > 1 )); then
|
||||
echo "board-roll: LIVE is ${orig_size}B (>= cap ${CAP}B) but has ${start_count} START / ${end_count} END" >&2
|
||||
echo " markers — only a SINGLE '$START_MARK' … '$END_MARK' roll zone is supported." >&2
|
||||
echo " Multiple zones are refused (not guessed) so content between zones is never relocated." >&2
|
||||
echo " Consolidate the archival ticks into one zone, or trim manually." >&2
|
||||
exit 3
|
||||
fi
|
||||
if (( start_idx < 0 || end_idx < 0 || end_idx <= start_idx )); then
|
||||
echo "board-roll: LIVE is ${orig_size}B (>= cap ${CAP}B) but no usable roll zone" >&2
|
||||
echo " (need '$START_MARK' then '$END_MARK'). Add the markers around the" >&2
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
# 5. --dry-run changes nothing and reports a plan.
|
||||
# 6. Zone emptied but pinned sections alone exceed cap → exit 3.
|
||||
# 7. --help exits 0 and prints usage; an unknown flag exits nonzero (#701 discipline).
|
||||
# 8. More than one marker pair → exit 3, unchanged; curated content between the two
|
||||
# zones is never relocated to the LEDGER (rev0 #868 regression).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
@@ -114,7 +116,41 @@ bash "$SUT" -h >/dev/null 2>&1 || note "-h exited nonzero"
|
||||
if bash "$SUT" --not-a-real-flag >/dev/null 2>&1; then note "unknown flag was accepted"; fi
|
||||
if bash "$SUT" --live "$WORK/live3.md" >/dev/null 2>&1; then note "missing --ledger was accepted"; fi
|
||||
|
||||
# ── 8. multiple marker pairs → exit 3, unchanged (no cross-zone relocation) ─────
|
||||
# Two separately-marked zones with a curated pinned section BETWEEN them. A naive
|
||||
# first-START..last-END span would sweep that curated section (and the intermediate
|
||||
# markers) into the LEDGER. board-roll must refuse (exit 3) and touch nothing.
|
||||
L="$WORK/live8.md"; G="$WORK/ledger8.md"; echo "# LEDGER" > "$G"
|
||||
pad8=$(head -c 300 < /dev/zero | tr '\0' 'x')
|
||||
{
|
||||
echo "# BOARD — LIVE"
|
||||
echo "> pinned protocol blockquote"
|
||||
echo
|
||||
echo '<!-- BOARD-ROLL:START -->'
|
||||
echo "### 2026-07-10 zone-A newest"
|
||||
echo "- detail A2 $pad8"
|
||||
echo "### 2026-07-09 zone-A oldest"
|
||||
echo "- detail A1 $pad8"
|
||||
echo '<!-- BOARD-ROLL:END -->'
|
||||
echo
|
||||
echo "## Curated-between-zones (pinned — must never move)"
|
||||
echo "- CANARY-BETWEEN keep me"
|
||||
echo
|
||||
echo '<!-- BOARD-ROLL:START -->'
|
||||
echo "### 2026-07-08 zone-B newest"
|
||||
echo "- detail B2 $pad8"
|
||||
echo "### 2026-07-07 zone-B oldest"
|
||||
echo "- detail B1 $pad8"
|
||||
echo '<!-- BOARD-ROLL:END -->'
|
||||
} > "$L"
|
||||
before8=$(cat "$L")
|
||||
set +e; bash "$SUT" --live "$L" --ledger "$G" --cap 80 >/dev/null 2>&1; rc=$?; set -e
|
||||
[[ "$rc" -eq 3 ]] || note "multi-pair board should exit 3 (got $rc)"
|
||||
[[ "$(cat "$L")" == "$before8" ]] || note "multi-pair run modified LIVE (must never guess across zones)"
|
||||
grep -q "CANARY-BETWEEN keep me" "$L" || note "multi-pair run relocated curated between-zones content"
|
||||
grep -q "CANARY-BETWEEN" "$G" && note "curated between-zones content leaked into LEDGER"
|
||||
|
||||
if [[ "$fail" -eq 0 ]]; then
|
||||
echo "board-roll regression passed (7 groups)"
|
||||
echo "board-roll regression passed (8 groups)"
|
||||
fi
|
||||
exit "$fail"
|
||||
|
||||
Reference in New Issue
Block a user