#!/usr/bin/env bash
# tests/ledger-scan/run-tests.sh -- ledger-scan.sh against fixture docs.
# Verifies: PROPOSED/OPEN decisions surfaced (ADOPTED not), OPEN security rows
# surfaced (CLOSED not), next-free computed from highest, missing-ledger notice.
set -uo pipefail
SD="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"; SCRIPT="$SD/../../scripts/ledger-scan.sh"
P=0; F=0; ok(){ echo "PASS: $1"; P=$((P+1)); }; no(){ echo "FAIL: $1"; F=$((F+1)); }
W="$(mktemp -d)"; trap 'rm -rf "$W"' EXIT; mkdir -p "$W/docs" "$W/runbooks"

cat > "$W/docs/design-decisions.md" <<'DD'
## D-001: Something adopted
**Status:** ADOPTED 2026-01-01.
## D-050: PROPOSED / OPEN -- keystone policyd override
**Status:** PROPOSED / OPEN (recorded 2026-06-17; no action taken).
## D-063: tighten SG
**Status:** RESOLVED / CLOSED (applied 2026-07-03).
## D-068: Vault substrate hardening
**Status:** PROPOSED / OPEN 2026-07-02.
## D-070: snapshots
**Status:** ADOPTED 2026-07-03.
## D-063b: reopened then closed via amendment
**Status:** PROPOSED / OPEN (recorded).
Some text.
**Status:** RESOLVED / CLOSED (amendment 2026-07-03).
## D-052: partially ruled -- open sub-decisions, OPEN off the status line
**Status:** PARTIALLY RULED. Sub-decision 1 RULED 2026-07-21; the word for the rest
falls on this continuation line: sub-2..4 OPEN (the D-131 blind-spot shape, fixture-numbered below high-water).
## D-055: PROPOSED -- tail decision pinned to next deployment
**Status:** PROPOSED / OPEN (pinned).
## D-063 -- AMENDMENT (detached): a different decision's amendment appended AFTER D-055
**Status:** RESOLVED (this Status belongs to D-063, NOT to D-055 -- the D-132 false-close shape).
## D-056: PROPOSED -- later closed by its own detached amendment
**Status:** PROPOSED / OPEN (recorded).
## D-056 -- AMENDMENT (detached): closes its own decision
**Status:** RESOLVED / CLOSED (amendment).
DD
cat > "$W/docs/security-ledger.md" <<'SL'
| id | date | item | src | owner | status |
|---|---|---|---|---|---|
| SEC-001 | 2026-06-26 | libvirt cred | x | operator | OPEN -- rotate |
| SEC-002 | 2026-06-17 | token rule | x | operator | STANDING RULE |
| SEC-003 | 2026-07-03 | unseal custody | D-069 | operator | OPEN -- assign |
| SEC-004 | 2026-05-27 | repo public | x | operator | CLOSED -- flipped private |
| SEC-005 | 2026-07-16 | forward drop | x | operator | CLOSED -- verified; the --check guards the fail-open case (rule keyed on absent iface) |
SL
# next-free fixtures: mention up to DOCFIX-085, BUNDLEFIX-009 in a doc
printf 'refs: DOCFIX-084 DOCFIX-085 BUNDLEFIX-008 BUNDLEFIX-009 D-070\n' > "$W/runbooks/refs.md"
printf 'Next-free: D-071, DOCFIX-099, BUNDLEFIX-050\n' > "$W/docs/pointer.md"  # must NOT inflate
# illustrative-example fixture: a changelog narrating a bug reproduction with a
# literal high-looking example number must NOT inflate next-free (found live
# 2026-07-10 -- DOCFIX-173's own changelog did exactly this, pushing next-free
# to 1005 by mentioning the reproduction example "DOCFIX-1004")
printf 'Reproduced: echo "DOCFIX-1004" | grep -oE DOCFIX-[0-9]{3} yields 100\n' > "$W/docs/repro-example.md"

OUT="$(VR_REPO="$W" bash "$SCRIPT" 2>&1)"; RC=$?
chk(){ grep -q "$1" <<<"$OUT" && ok "$2" || no "$2 (missing: $1)"; }
nochk(){ grep -q "$1" <<<"$OUT" && no "$2 (should be absent: $1)" || ok "$2"; }
[ "$RC" = 0 ] && ok "exit 0" || no "exit 0 (got $RC)"
chk 'D-050' "surfaces D-050 PROPOSED"
chk 'D-068' "surfaces D-068 PROPOSED"
nochk 'D-001' "omits D-001 ADOPTED"
nochk 'D-063' "omits D-063 CLOSED"
nochk 'D-070' "omits D-070 ADOPTED (from decisions section)"
nochk 'D-063b' "omits amendment-CLOSED decision (last-status wins)"
chk 'D-052' "surfaces D-052 PARTIALLY RULED (open sub-decisions; D-131 blind-spot regression)"
chk 'D-055' "tail decision stays OPEN when ANOTHER decision's detached amendment follows it (D-132 false-close regression, 2026-07-23)"
nochk 'D-056' "detached amendment closes its OWN decision (number-keyed attribution)"
chk 'SEC-001' "surfaces SEC-001 OPEN"
chk 'SEC-003' "surfaces SEC-003 OPEN"
nochk 'SEC-004' "omits SEC-004 CLOSED"
nochk 'SEC-005' "omits SEC-005 CLOSED whose disposition says fail-open (terminology, not status -- SEC-010 close 2026-07-20)"
grep -q 'SEC-002' <<<"$OUT" && no "omits SEC-002 STANDING (not open)" || ok "omits SEC-002 STANDING"
# GA-R4/F3: pointer + count, never row-level dispositions
chk '2 open: SEC-001 SEC-003' "SEC count + ID list (GA-R4/F3)"
chk 'register of record: docs/security-ledger.md' "SEC section points at the register of record"
nochk 'rotate' "no row-level disposition text in SEC section (GA-R4/F3)"
nochk 'OPEN -- assign' "no row-level disposition text for SEC-003 either (GA-R4/F3)"
chk 'next-free=071' "D next-free 071 (header-authoritative)"
chk 'next-free=086' "DOCFIX next-free 086 (next-free pointer 099 excluded, reproduction example 1004 excluded)"
chk 'next-free=010' "BUNDLEFIX next-free 010"
rm -f "$W/docs/repro-example.md"
# boundary: mentions >= 100 must count -- the old 0[0-9]{2} pattern went blind
# past 099 (next-free frozen at 100; hit live 2026-07-06)
printf 'shipped: DOCFIX-101 BUNDLEFIX-100\n' > "$W/runbooks/refs2.md"
OUT3="$(VR_REPO="$W" bash "$SCRIPT" 2>&1)"
grep -q 'DOCFIX: highest=101  next-free=102' <<<"$OUT3" && ok "DOCFIX counts past the 099 boundary" || no "DOCFIX counts past the 099 boundary"
grep -q 'BUNDLEFIX: highest=100  next-free=101' <<<"$OUT3" && ok "BUNDLEFIX counts past the 099 boundary" || no "BUNDLEFIX counts past the 099 boundary"
rm -f "$W/runbooks/refs2.md"
# GA-F15 wrap-aware exclusion: a next-free/reproduc line suppresses itself AND
# the immediately following line (wrapped quote), window exactly one line
printf 'the current next-free\nDOCFIX-777 landed here after a wrap, with BUNDLEFIX-777\n' > "$W/docs/wrap-quote.md"
printf 'Reproduced the counter bug:\nDOCFIX-888 was the example token\n' > "$W/docs/wrap-repro.md"
OUT4="$(VR_REPO="$W" bash "$SCRIPT" 2>&1)"
grep -q 'DOCFIX: highest=085  next-free=086' <<<"$OUT4" && ok "wrapped next-free quote excluded (GA-F15)" || no "wrapped next-free quote excluded (GA-F15)"
grep -q 'BUNDLEFIX: highest=009  next-free=010' <<<"$OUT4" && ok "wrapped BUNDLEFIX quote excluded (GA-F15)" || no "wrapped BUNDLEFIX quote excluded (GA-F15)"
rm -f "$W/docs/wrap-quote.md" "$W/docs/wrap-repro.md"
printf 'next-free words up here\n\nDOCFIX-091 assigned two lines below\n' > "$W/docs/wrap-window.md"
OUT5="$(VR_REPO="$W" bash "$SCRIPT" 2>&1)"
grep -q 'DOCFIX: highest=091  next-free=092' <<<"$OUT5" && ok "exclusion window is ONE line (two-below counts)" || no "exclusion window is ONE line (two-below counts)"
rm -f "$W/docs/wrap-window.md"
# missing ledger notice
OUT2="$(VR_REPO="$W" bash "$SCRIPT" 2>&1)"; grep -q 'NO docs/session-ledger.md' <<<"$OUT2" && ok "flags missing session-ledger" || no "flags missing session-ledger"
# with a ledger present, notice changes
echo "ledger" > "$W/docs/session-ledger.md"
OUT3="$(VR_REPO="$W" bash "$SCRIPT" 2>&1)"; grep -q 'ledger present' <<<"$OUT3" && ok "detects present session-ledger" || no "detects present session-ledger"


# ---------------- --fences mode: fence integrity validation ----------------
ec(){ [ "$1" = "$2" ] && ok "$3" || no "$3 (rc=$1 want=$2)"; }
has(){ grep -q "$1" <<<"$2" && ok "$3" || no "$3 (missing: $1)"; }
FW="$(mktemp -d)"; mkdir -p "$FW/docs"
mkledgerf(){ printf '%s\n' "$1" > "$FW/docs/session-ledger.md"; }
runf(){ VR_REPO="$FW" bash "$SCRIPT" --fences 2>&1; }

mkledgerf '# Ledger
<!-- SECTION: main-chat | OWNER: main claude.ai chat stream only -->
stuff
<!-- END: main-chat -->
<!-- SECTION: jumphost | OWNER: Claude Code jumphost stream only -->
more
<!-- END: jumphost -->'
OUT="$(runf)"; ec "$?" 0 "fences well-formed exit0"; has 'FENCE VALIDATION: OK' "$OUT" "fences OK message"

mkledgerf '<!-- SECTION: jumphost | OWNER: x -->
no end'
OUT="$(runf)"; ec "$?" 1 "fences unclosed exit1"; has 'unclosed SECTION' "$OUT" "reports unclosed"

mkledgerf 'text
<!-- END: jumphost -->'
OUT="$(runf)"; ec "$?" 1 "fences orphan-END exit1"; has 'with no open SECTION' "$OUT" "reports orphan END"

mkledgerf '<!-- SECTION: main-chat | OWNER: x -->
<!-- END: jumphost -->'
OUT="$(runf)"; ec "$?" 1 "fences mismatch exit1"; has 'does not match open SECTION' "$OUT" "reports mismatch"

mkledgerf '<!-- SECTION: main-chat | OWNER: x -->
<!-- SECTION: jumphost | OWNER: y -->
<!-- END: jumphost -->
<!-- END: main-chat -->'
OUT="$(runf)"; ec "$?" 1 "fences nested exit1"; has 'no nesting' "$OUT" "reports nesting"

mkledgerf '<!-- SECTION: jumphost | OWNER: x -->
<!-- END: jumphost -->
<!-- SECTION: jumphost | OWNER: x -->
<!-- END: jumphost -->'
OUT="$(runf)"; ec "$?" 1 "fences duplicate exit1"; has 'duplicate SECTION name' "$OUT" "reports duplicate"

mkledgerf '<!-- SECTION: bogus | OWNER: x -->
<!-- END: bogus -->'
OUT="$(runf)"; ec "$?" 0 "fences unknown-name non-fatal exit0"; has 'unknown section name' "$OUT" "warns unknown name"

mkledgerf '# Ledger
prose only, no fences'
OUT="$(runf)"; ec "$?" 0 "fences none exit0"; has 'no fences yet' "$OUT" "transition-OK note"

mkledgerf '<!-- SECTION: shared | OWNER: -->
<!-- END: shared -->'
OUT="$(runf)"; ec "$?" 0 "fences missing-owner non-fatal exit0"; has 'missing/empty OWNER' "$OUT" "warns missing/empty owner"

OUT="$(VR_REPO="$W" bash "$SCRIPT" 2>&1)"; has 'next-free' "$OUT" "default mode intact (next-free present)"

echo; [ "$F" = 0 ] && { echo "ALL PASS ($P checks)"; exit 0; } || { echo "FAILURES: $F"; exit 1; }
