fix(board-roll): reject >1 marker pair instead of spanning across zones
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:
enhance
2026-07-22 04:17:29 -05:00
parent 406a8464f9
commit 676a906649
3 changed files with 69 additions and 8 deletions

View File

@@ -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