#!/bin/bash
# run-detectors.sh — the SQL-detector runner that refuses to let an ABORTED detector
# masquerade as a CLEAN one. Built sweep #456.
#
# ─────────────────────────────────────────────────────────────────────────────────────
# WHY THIS EXISTS — the FIFTH incarnation of the silent-empty trap.
#
# The skill already warns about four ways a detector lies to you with a confident EMPTY:
#   1. wrong column      -> ERROR 1054   (loud, IF you drop 2>/dev/null)
#   2. split collation   -> ERROR 1267   (loud, IF you drop 2>/dev/null)
#   3. bad anchor        -> fake rows / fake zeroes
#   4. DEAD COLUMN       -> silent; only a positive-case canary catches it
#
# The fifth is the one `2>&1` CANNOT save you from, because there is no error at all:
#   5. THE DETECTOR NEVER FINISHED. (N) covert-evasion-scan.sql takes >2 minutes. Run it
#      through a transport with a shorter timeout (the MCP ssh_execute path, a CI step, a
#      `claude -p` tool call) and the mysql process is KILLED mid-query. stdout contains
#      the section header and NOTHING ELSE — no rows, no canary, no error, no exit code.
#      That is byte-for-byte what "clean" looks like. Sweep #456 hit exactly this: (N)
#      returned nothing, and the honest reading was "no covert evasion." The truth was
#      "(N) did not run."
#
# THE DEFENCE: a detector's verdict is trustworthy ONLY IF ITS CANARY LINE IS PRESENT in
# the output. The canary is the proof-of-execution. Absence of the canary line means the
# detector DID NOT RUN -> verdict UNKNOWN, never CLEAN.
#
# Corollary, and it is a real open gap: a detector with NO canary can never be trusted to
# be clean, because an empty result is indistinguishable from a broken/aborted one. This
# script reports those as NO_CANARY (UNVERIFIABLE) rather than quietly implying they passed.
# ─────────────────────────────────────────────────────────────────────────────────────
#
# Usage (on the droplet, where mysql + the live schema live):
#   bash run-detectors.sh <db_password> [detector_dir]
# Exit: 0 = every detector with a canary PASSED and produced it.
#       1 = at least one detector ABORTED, FAILED its canary, or errored. DO NOT record a
#           clean sweep. Re-run the named detector before believing any zero.

set -uo pipefail

DBPW="${1:?usage: run-detectors.sh <db_password> [detector_dir]}"
DIR="${2:-$(dirname "$0")}"
TIMEOUT="${DETECTOR_TIMEOUT:-900}"
FAILED=0

# detector file : expected canary token ("-" = has no canary, so an empty result is UNVERIFIABLE)
DETECTORS=(
  "sanction-hold-scan.sql:predicate_breaches"
  "covert-evasion-scan.sql:N3_CANARY"
  "covert-evasion-scan.sql:N4_CANARY_LEGACY_BAN_SUPPRESSION"
  "new-account-scan.sql:CANARY_O"
  "new-account-scan.sql:O4_TENURE_CANARY"
  "banned-relogin-scan.sql:P3_CANARY"
  "banned-relogin-scan.sql:P4_CANARY_PROMOTED_ROW"
  # (R) #463 — the LEGACY ban surface (players_*.status='ban2', 1,716 accounts) that (P) is
  # structurally blind to: (P) anchors only on mod_sanctions. R3 asserts BOTH directions —
  # it still reaches the (P)-invisible actor 4204, AND it does not launder a transient ban
  # code (3/5/6) into a fabricated hard-ban bypass (the fake P0 this detector shipped-and-fixed).
  "legacy-ban-relogin-scan.sql:R3_CANARY"
  "payload-repetition-scan.sql:canary_h4"
  "aw3-upgrade-scan.sql:AW3_UPGRADE_CANARY"
  "evasion-scan.sql:E9_CANARY"
  "subcap-velocity-scan.sql:G9_CANARY"
  "guard-fp-scan.sql:-"
  "step-function-scan.sql:-"
  # (T) #595 — login-burst-then-score scan (jjaquinta's #moderation suggestion).
  # Catches accounts with repeated login rejections (ban IN {1,2,3}) inside a 30-min
  # window preceding an admit + score-post. Confirmed 1/7d (3909 on 2026-07-18,
  # pre-freeze delta=974 — that was the visible sample). Pairs a T3 positive canary
  # (3909 must stay findable) with a T4 negative canary (Dany87380 must stay spared)
  # so the threshold is held to both controls.
  "login-burst-then-score-scan.sql:T2_POS_CANARY"
  "login-burst-then-score-scan.sql:T3_NEG_CANARY"
  "login-burst-then-score-scan.sql:T4_STRUCTURE_CANARY"
)

run_one() {
  local file="$1" out rc
  out="$(timeout "$TIMEOUT" mysql -uroot -p"$DBPW" martian_games < "$DIR/$file" 2>&1)"
  rc=$?
  printf '%s' "$out" > "/tmp/detector-$(basename "$file" .sql).out"
  return $rc
}

echo "=============================================================================="
echo " DETECTOR RUN — canary-asserted (an aborted detector is NOT a clean detector)"
echo "=============================================================================="

declare -A DONE
for entry in "${DETECTORS[@]}"; do
  file="${entry%%:*}"
  [[ -n "${DONE[$file]:-}" ]] && continue
  DONE[$file]=1

  if [[ ! -f "$DIR/$file" ]]; then
    echo "  [MISSING ] $file — detector file not found. Cannot claim clean."
    FAILED=1
    continue
  fi

  start=$(date +%s)
  run_one "$file"; rc=$?
  elapsed=$(( $(date +%s) - start ))
  out="$(cat "/tmp/detector-$(basename "$file" .sql).out")"

  if [[ $rc -eq 124 ]]; then
    echo "  [ABORTED ] $file — TIMED OUT after ${TIMEOUT}s. Its empty output is MEANINGLESS."
    FAILED=1
    continue
  fi
  if grep -qE '^ERROR [0-9]+' <<<"$out"; then
    echo "  [ERROR   ] $file — mysql error (${elapsed}s):"
    grep -E '^ERROR [0-9]+' <<<"$out" | sed 's/^/             /'
    FAILED=1
    continue
  fi

  # Every canary this detector is supposed to emit must actually appear.
  canaries=()
  for e2 in "${DETECTORS[@]}"; do
    [[ "${e2%%:*}" == "$file" && "${e2#*:}" != "-" ]] && canaries+=("${e2#*:}")
  done

  if [[ ${#canaries[@]} -eq 0 ]]; then
    rows=$(grep -cve '^$' <<<"$out")
    echo "  [NO_CANARY] $file — ran OK (${elapsed}s, ${rows} lines) but has NO positive-case"
    echo "              canary. An EMPTY result here is UNVERIFIABLE — it cannot be"
    echo "              distinguished from a broken detector. Do not bank a clean on it alone."
    continue
  fi

  ok=1
  for c in "${canaries[@]}"; do
    if ! grep -q "$c" <<<"$out"; then
      echo "  [NO PROOF] $file — canary '$c' ABSENT from output. The detector did not reach"
      echo "              its canary, so it did not run to completion. Verdict: UNKNOWN, not clean."
      ok=0; FAILED=1
    elif grep -qE "$c.*FAIL" <<<"$out"; then
      echo "  [CANARY FAIL] $file — '$c' reports FAIL. The detector is BLIND. Do not act on it."
      grep -E "$c" <<<"$out" | sed 's/^/             /'
      ok=0; FAILED=1
    fi
  done
  [[ $ok -eq 1 ]] && echo "  [PASS    ] $file (${elapsed}s) — canaries present and passing; its zeroes mean something."
done

echo "------------------------------------------------------------------------------"
if [[ $FAILED -eq 0 ]]; then
  echo "RUN_DETECTORS: PASS — every canaried detector executed and proved itself."
else
  echo "RUN_DETECTORS: FAIL — at least one detector did not prove it ran. A clean sweep"
  echo "               CANNOT be recorded on this run. Re-run the named detectors."
fi
exit $FAILED
