diff --git a/packages/mosaic/framework/tools/orchestrator/README.md b/packages/mosaic/framework/tools/orchestrator/README.md index d57804d7..3ee7f104 100644 --- a/packages/mosaic/framework/tools/orchestrator/README.md +++ b/packages/mosaic/framework/tools/orchestrator/README.md @@ -68,10 +68,15 @@ board-roll.sh --live --ledger [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` diff --git a/packages/mosaic/framework/tools/orchestrator/board-roll.sh b/packages/mosaic/framework/tools/orchestrator/board-roll.sh index 1c69fe4c..ac0e2008 100644 --- a/packages/mosaic/framework/tools/orchestrator/board-roll.sh +++ b/packages/mosaic/framework/tools/orchestrator/board-roll.sh @@ -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 diff --git a/packages/mosaic/framework/tools/orchestrator/test-board-roll.sh b/packages/mosaic/framework/tools/orchestrator/test-board-roll.sh index ca75c789..bf1ccb62 100644 --- a/packages/mosaic/framework/tools/orchestrator/test-board-roll.sh +++ b/packages/mosaic/framework/tools/orchestrator/test-board-roll.sh @@ -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 '' + echo "### 2026-07-10 zone-A newest" + echo "- detail A2 $pad8" + echo "### 2026-07-09 zone-A oldest" + echo "- detail A1 $pad8" + echo '' + echo + echo "## Curated-between-zones (pinned — must never move)" + echo "- CANARY-BETWEEN keep me" + echo + echo '' + echo "### 2026-07-08 zone-B newest" + echo "- detail B2 $pad8" + echo "### 2026-07-07 zone-B oldest" + echo "- detail B1 $pad8" + echo '' +} > "$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"