Newer
Older
openstack-caracal-ipv4 / scripts / ledger-scan.sh
#!/usr/bin/env bash
# scripts/ledger-scan.sh -- machine-derived "what is still OPEN" view of the repo.
#
# The session ledger (docs/session-ledger.md) is a human narrative; it can drift.
# This script is the DRIFT CHECK: it greps the repo for the open-work markers that
# can be reliably derived -- PROPOSED/OPEN decisions, OPEN security-ledger rows,
# and the next-free D/DOCFIX/BUNDLEFIX numbers -- so the narrative ledger can be
# reconciled against ground truth at session start and before every handoff.
# Read-only; mutates nothing. Exit: 0 always (it reports; it does not judge).
#
# Reconcile rule: the narrative ledger must not claim CLOSED anything this scan
# shows OPEN, and must not omit a decision/number this scan surfaces.
set -uo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO="${VR_REPO:-$(cd "$HERE/.." && pwd)}"
DD="$REPO/docs/design-decisions.md"
SL="$REPO/docs/security-ledger.md"
SESSL="$REPO/docs/session-ledger.md"

# --- fence validator (delimited-section convention) --------------------------
# Validates STRUCTURAL integrity of the ledger ownership fences:
#   <!-- SECTION: <name> | OWNER: <owner> -->  ...  <!-- END: <name> -->
# FATAL: unmatched / overlapping (no nesting) / duplicate-name / unclosed.
# WARN (non-fatal): unknown section name, missing/empty OWNER. No fences = OK
# (transition state). Separate --fences MODE; default report below is unchanged.
_vf_trim(){ local x="$1"; x="${x#"${x%%[![:space:]]*}"}"; x="${x%"${x##*[![:space:]]}"}"; printf '%s' "$x"; }
validate_fences() {
  local f="$1"
  [ -r "$f" ] || { echo "  HOLD: session-ledger not readable: $f"; return 2; }
  local errs=0 warns=0 nfence=0 ln=0
  local open_name="" open_line=0 seen=" " known=" machine-derived main-chat jumphost shared "
  local line inner name owner ename
  while IFS= read -r line || [ -n "$line" ]; do
    ln=$((ln+1))
    case "$line" in
      *'<!-- SECTION:'*)
        nfence=$((nfence+1))
        inner="${line#*<!-- SECTION:}"; inner="${inner%%-->*}"
        name="$(_vf_trim "${inner%%|*}")"
        if [ "$inner" != "${inner#*OWNER:}" ]; then owner="$(_vf_trim "${inner#*OWNER:}")"; else owner="__none__"; fi
        if [ -n "$open_name" ]; then echo "  ERROR line $ln: SECTION $name opened while $open_name (line $open_line) still open -- no nesting"; errs=$((errs+1)); fi
        if [ -z "$name" ]; then echo "  ERROR line $ln: SECTION marker with unparseable name"; errs=$((errs+1))
        else
          case "$seen" in *" $name "*) echo "  ERROR line $ln: duplicate SECTION name $name"; errs=$((errs+1));; esac
          case "$known" in *" $name "*) : ;; *) echo "  WARN  line $ln: unknown section name $name (known: main-chat jumphost machine-derived shared)"; warns=$((warns+1));; esac
          if [ "$owner" = "__none__" ] || [ -z "$owner" ]; then echo "  WARN  line $ln: SECTION $name has missing/empty OWNER"; warns=$((warns+1)); fi
          open_name="$name"; open_line="$ln"; seen="$seen$name "
        fi
        ;;
      *'<!-- END:'*)
        inner="${line#*<!-- END:}"; ename="$(_vf_trim "${inner%%-->*}")"
        if [ -z "$open_name" ]; then echo "  ERROR line $ln: END $ename with no open SECTION"; errs=$((errs+1))
        elif [ "$ename" != "$open_name" ]; then echo "  ERROR line $ln: END $ename does not match open SECTION $open_name (line $open_line)"; errs=$((errs+1)); open_name=""
        else open_name=""; fi
        ;;
    esac
  done < "$f"
  [ -n "$open_name" ] && { echo "  ERROR: unclosed SECTION $open_name opened at line $open_line"; errs=$((errs+1)); }
  echo "  fences: $nfence  errors: $errs  warnings: $warns"
  if [ "$errs" -gt 0 ]; then echo "  FENCE VALIDATION: FAIL"; return 1; fi
  [ "$nfence" -eq 0 ] && echo "  (no fences yet -- partial/unfenced ledger is OK during transition)"
  echo "  FENCE VALIDATION: OK"; return 0
}

if [ "${1:-}" = "--fences" ]; then
  echo "=== ledger-scan --fences: session-ledger fence integrity ($SESSL) ==="
  validate_fences "$SESSL"; exit $?
fi

echo "=== ledger-scan: open-work ground truth (repo: $REPO) ==="

echo "-- PROPOSED / OPEN decisions --"
if [ -r "$DD" ]; then
  # a decision header line whose status line (within the next few lines) is PROPOSED/OPEN.
  # A decision is OPEN iff the LAST Status line in its block is PROPOSED/OPEN.
  # (A later RESOLVED/CLOSED/SUPERSEDED amendment closes it -- so we track the last
  # status per block, not the first; this is the D-063-amendment drift the first
  # real run surfaced.)
  OPEN_DEC="$(awk '
    /^## D-[0-9]+:/ { if (hdr!="" && laststat ~ /PROPOSED|OPEN/) print "  " hdr; sub(/^## /,"",$0); hdr=$0; laststat="" }
    /\*\*Status:\*\*/ { laststat=$0 }
    END { if (hdr!="" && laststat ~ /PROPOSED|OPEN/) print "  " hdr }
  ' "$DD")"
  if [ -n "$OPEN_DEC" ]; then printf '%s\n' "$OPEN_DEC"; else echo "  (none)"; fi
else echo "  (design-decisions.md not found)"; fi

echo "-- OPEN security-ledger rows --"
if [ -r "$SL" ]; then
  grep -E '^\|[[:space:]]*SEC-[0-9]' "$SL" | awk -F'|' 'toupper($(NF-1)) ~ /OPEN|PENDING/ {gsub(/^ *| *$/,"",$2); gsub(/^ *| *$/,"",$7); print "  " $2 " (" $7 ")"}' || echo "  (none)"
  [ -z "$(grep -E '^\|[[:space:]]*SEC-[0-9]' "$SL" | awk -F'|' 'toupper($(NF-1)) ~ /OPEN|PENDING/')" ] && echo "  (none open)"
else echo "  (security-ledger.md not found)"; fi

echo "-- next-free numbers (highest assigned + 1) --"
# Exclude "next-free" pointer lines from the count -- a "next-free D-072" pointer in
# prose must NOT inflate the highest-assigned (the first real run counted exactly that).
nextfree_mentions() { # <regex> <label> -- highest MENTION excluding next-free pointers
  local pat="$1" label="$2" hi
  hi="$(grep -rhE "$pat" "$REPO/docs" "$REPO/runbooks" 2>/dev/null \
        | grep -viE 'next[- ]free' \
        | grep -oE "$pat" | grep -oE '[0-9]+' | sort -n | tail -1)"
  if [ -n "$hi" ]; then printf '  %s: highest=%s  next-free=%03d\n' "$label" "$hi" "$((10#$hi + 1))"
  else printf '  %s: none found\n' "$label"; fi
}
# D is AUTHORITATIVE from decision headers in design-decisions.md, not prose mentions.
if [ -r "$DD" ]; then
  DHI="$(grep -oE '^## D-[0-9]+' "$DD" | grep -oE '[0-9]+' | sort -n | tail -1)"
  if [ -n "$DHI" ]; then printf '  D: highest-header=%s  next-free=%03d\n' "$DHI" "$((10#$DHI + 1))"; else echo "  D: none"; fi
else echo "  D: design-decisions.md not found"; fi
nextfree_mentions 'DOCFIX-0[0-9]{2}' "DOCFIX"
nextfree_mentions 'BUNDLEFIX-0[0-9]{2}' "BUNDLEFIX"

echo "-- reconcile against docs/session-ledger.md --"
if [ -r "$REPO/docs/session-ledger.md" ]; then
  echo "  ledger present; compare the sections above against its narrative"
else
  echo "  NO docs/session-ledger.md -- create/update it (this session's continuity record)"
fi
exit 0