#!/usr/bin/env bash set -euo pipefail # CASAN H1 context validator. # Before a sub-agent trusts pipeline-context.yaml, verify every referenced # artifact / trace file actually exists on disk, and (optionally) is not # staler than CASAN_CONTEXT_TTL_SECONDS relative to the context file itself. # Catches renamed/deleted/missing artifacts before they cause a silent bad read. # # Usage: context-validate.sh # Exit: 0 all good, 2 a referenced path is missing, 3 a referenced path is stale. CTX="${1:-}" if [[ -z "$CTX" || ! -f "$CTX" ]]; then echo "Usage: context-validate.sh " >&2 exit 64 fi CTX_DIR="$(cd "$(dirname "$CTX")" && pwd)" # Resolve paths relative to the repo root (3 levels up from this script). SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/casan-paths.sh" PROJECT_ROOT="$CASAN_APP_ROOT" python - "$CTX" "$PROJECT_ROOT" "${CASAN_CONTEXT_TTL_SECONDS:-0}" <<'PY' import os, re, sys, subprocess ctx, root, ttl = sys.argv[1], sys.argv[2], int(sys.argv[3]) ctx_mtime = os.path.getmtime(ctx) missing, stale, checked = [], [], 0 def path_exists(p): """Check existence using both Python and bash (handles MSYS2/Windows path mismatch).""" if os.path.exists(p): return True # On Windows+MSYS2, /tmp maps to AppData/Local/Temp but Windows Python resolves it as C:\tmp. # Fall back to bash test for absolute paths that Python can't find. try: r = subprocess.run(["bash", "-c", f"test -e {repr(p)}"], capture_output=True, timeout=3) return r.returncode == 0 except Exception: return False with open(ctx, encoding="utf-8") as fh: for line in fh: m = re.match(r"\s*(?:artifact|trace_file|path|data-model|spec|plan):\s*(\S+)", line) if not m: continue ref = m.group(1).strip().strip('"').strip("'") if ref in ("", "null", ""): continue if "<" in ref or ref.endswith(">"): continue # unresolved template placeholder, not a concrete path cand = ref if os.path.isabs(ref) else os.path.join(root, ref) checked += 1 if not path_exists(cand): missing.append(ref) continue if ttl > 0 and (ctx_mtime - os.path.getmtime(cand)) > ttl: stale.append(ref) if missing: sys.stderr.write("CONTEXT_INVALID missing=%d: %s\n" % (len(missing), ", ".join(missing))) raise SystemExit(2) if stale: sys.stderr.write("CONTEXT_STALE stale=%d: %s\n" % (len(stale), ", ".join(stale))) raise SystemExit(3) print(f"CONTEXT_VALID checked={checked} all referenced artifacts present") PY