#!/usr/bin/env bash # vault_audit — Full audit pipeline for one or all declared vaults. # Runs: layout-ensure → index → profile → dedupe → aggregate → doctor # # Usage: # vault_audit.sh # vault_audit.sh --all # vault_audit.sh --skip-profilers # vault_audit.sh --dry-run-layout # vault_audit.sh --all --skip-profilers set -euo pipefail # --- locate FN_REGISTRY_ROOT --- _find_registry_root() { local dir dir="$(pwd)" while [[ "$dir" != "/" ]]; do if [[ -f "$dir/registry.db" ]]; then echo "$dir" return 0 fi dir="$(dirname "$dir")" done return 1 } if [[ -n "${FN_REGISTRY_ROOT:-}" && -f "${FN_REGISTRY_ROOT}/registry.db" ]]; then REGISTRY_ROOT="$FN_REGISTRY_ROOT" elif REGISTRY_ROOT="$(_find_registry_root 2>/dev/null)"; then : # found else echo "ERROR: Cannot locate registry.db. Set FN_REGISTRY_ROOT or run from registry root." >&2 exit 1 fi FN_BIN="${FN_BIN:-${REGISTRY_ROOT}/fn}" if [[ ! -x "$FN_BIN" ]]; then echo "ERROR: fn binary not found at $FN_BIN. Build with: CGO_ENABLED=1 go build -tags fts5 -o fn ./cmd/fn/" >&2 exit 1 fi # --- parse args --- AUDIT_ALL=0 SKIP_PROFILERS=0 DRY_RUN_LAYOUT=0 VAULT_NAMES=() START_TS=$(date +%s) while [[ $# -gt 0 ]]; do case "$1" in --all) AUDIT_ALL=1 ;; --skip-profilers) SKIP_PROFILERS=1 ;; --dry-run-layout) DRY_RUN_LAYOUT=1 ;; -*) echo "ERROR: Unknown flag: $1" >&2 echo "Usage: vault_audit.sh | --all [--skip-profilers] [--dry-run-layout]" >&2 exit 1 ;; *) VAULT_NAMES+=("$1") ;; esac shift done if [[ $AUDIT_ALL -eq 0 && ${#VAULT_NAMES[@]} -eq 0 ]]; then echo "Usage: vault_audit.sh | --all [--skip-profilers] [--dry-run-layout]" >&2 exit 1 fi # --- resolve vault list --- if [[ $AUDIT_ALL -eq 1 ]]; then mapfile -t VAULT_NAMES < <( sqlite3 "${REGISTRY_ROOT}/registry.db" "SELECT name FROM vaults ORDER BY name;" 2>/dev/null || true ) if [[ ${#VAULT_NAMES[@]} -eq 0 ]]; then echo "No vaults registered in registry.db. Run 'fn index' first." >&2 exit 1 fi echo "Found ${#VAULT_NAMES[@]} vault(s): ${VAULT_NAMES[*]}" fi # --- build fn vault flags --- LAYOUT_FLAGS=() if [[ $DRY_RUN_LAYOUT -eq 1 ]]; then LAYOUT_FLAGS+=(--dry-run) fi # --- per-vault audit --- PASS_COUNT=0 FAIL_COUNT=0 declare -A VAULT_STATUS audit_one() { local name="$1" local vault_ok=1 echo "" echo "=== vault: $name ===" # Step 1: layout-ensure echo " [1/5] layout-ensure" if ! "$FN_BIN" vault layout-ensure "$name" "${LAYOUT_FLAGS[@]}" 2>&1 | sed 's/^/ /'; then echo " WARN: layout-ensure failed (non-fatal)" >&2 vault_ok=0 fi # Step 2: index echo " [2/5] index" if ! "$FN_BIN" vault index "$name" 2>&1 | sed 's/^/ /'; then echo " ERROR: index failed" >&2 vault_ok=0 fi # Step 3: profile if [[ $SKIP_PROFILERS -eq 0 ]]; then echo " [3/5] profile" if ! "$FN_BIN" vault profile "$name" 2>&1 | sed 's/^/ /'; then echo " WARN: profile had errors (non-fatal)" >&2 fi else echo " [3/5] profile (skipped)" fi # Step 4: dedupe (informational, non-fatal) echo " [4/5] dedupe" "$FN_BIN" vault dedupe "$name" 2>&1 | sed 's/^/ /' || true # Step 5 deferred — aggregate runs once at the end echo " [5/5] aggregate (deferred)" if [[ $vault_ok -eq 1 ]]; then VAULT_STATUS["$name"]="ok" PASS_COUNT=$((PASS_COUNT + 1)) else VAULT_STATUS["$name"]="warn" FAIL_COUNT=$((FAIL_COUNT + 1)) fi } for vault_name in "${VAULT_NAMES[@]}"; do audit_one "$vault_name" done # --- aggregate (once, after all vaults) --- echo "" echo "=== aggregate ===" "$FN_BIN" vault aggregate 2>&1 | sed 's/^/ /' # --- doctor (read-only health check) --- echo "" echo "=== doctor ===" "$FN_BIN" vault doctor 2>&1 | sed 's/^/ /' || true # --- summary table --- END_TS=$(date +%s) ELAPSED=$(( END_TS - START_TS )) echo "" echo "=== summary ===" printf "%-30s %s\n" "VAULT" "STATUS" printf "%-30s %s\n" "-----" "------" for vault_name in "${VAULT_NAMES[@]}"; do status="${VAULT_STATUS[$vault_name]:-unknown}" printf "%-30s %s\n" "$vault_name" "$status" done echo "" echo "Done: ${PASS_COUNT} ok, ${FAIL_COUNT} warn (${ELAPSED}s)" if [[ $FAIL_COUNT -gt 0 ]]; then exit 4 fi exit 0