#!/usr/bin/env bash
# parity_check.sh — ADR-009 Phase 2 + ADR-005 Gates C/E/F.
#
# Runs hf2q parity checks against locked llama.cpp reference outputs.
# Each prompt is run N times (default 3) at T=0; every run must pass its
# declared min-prefix threshold. This encodes ADR-005 Gate F (deterministic
# reproducibility): a single flaky run fails the whole gate. Gate E is
# partially covered by the min-prefix floors; full divergence-point
# tracking is a follow-up.
#
# Usage:
#   scripts/parity_check.sh <gguf_path>
#   scripts/parity_check.sh <gguf_path> --n-runs 3
#
# Exit codes:
#   0  all checks passed (every run of every prompt met its threshold)
#   1  usage / env error
#   2  parity check failed (at least one run of one prompt failed)

set -euo pipefail

SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd -- "$SCRIPT_DIR/.." && pwd)"
cd "$REPO_ROOT"

HF2Q_BIN="target/release/hf2q"
N_RUNS="3"

err() { echo "error: $*" >&2; exit 1; }

GGUF_PATH=""
while [[ $# -gt 0 ]]; do
  case "$1" in
    --n-runs)
      [[ $# -ge 2 ]] || err "--n-runs requires an argument"
      N_RUNS="$2"; shift 2 ;;
    -*) err "unknown flag: $1" ;;
    *)  [[ -z "$GGUF_PATH" ]] || err "unexpected positional arg: $1"
        GGUF_PATH="$1"; shift ;;
  esac
done

[[ -n "$GGUF_PATH" ]] || { echo "Usage: scripts/parity_check.sh <gguf_path> [--n-runs N]"; exit 1; }
[[ -f "$GGUF_PATH" ]] || err "GGUF not found: $GGUF_PATH"
[[ -x "$HF2Q_BIN"  ]] || err "hf2q binary not found (run: cargo build --release)"

GIT_HEAD="$(git rev-parse --short HEAD 2>/dev/null || echo unknown)"
PASS=0
FAIL=0

# Runs `hf2q parity check` N times for one prompt; every run must pass.
# Encodes Gate F (N≥3 determinism): a single flaky run fails the gate.
# When EXTRA_ARGS is non-empty they're forwarded to `hf2q parity check`
# (used to plumb --self-baseline for Gate D).
#
# ADR-007 post-close 2026-04-24 (commit 7a4d354): byte-exact gates require
# DENSE decode. TQ is the default decode path post-correction; argmax
# divergence is ~0.8% by physical design (Lloyd-Max codebook is lossy).
# Force HF2Q_USE_DENSE=1 explicitly here so the gate keeps asserting
# "hf2q dense == llama.cpp" byte-identity. Mirrors the precedent in
# scripts/sourdough_gate.sh:120-123. The `env -u` clears any inherited
# layer-policy / codebook-bits override that would re-activate TQ.
# Iter-107 reconciliation: this script previously had zero HF2Q_USE_DENSE
# references — that was the half-done migration the iter finishes.
run_parity_n_times() {
  local prompt="$1"
  local min_prefix="$2"
  local label="$3"
  local extra_args="${4:-}"

  echo "--- $label (prompt=$prompt, min-prefix=$min_prefix, n_runs=$N_RUNS) ---"
  local run_pass=0
  local run_fail=0
  for run in $(seq 1 "$N_RUNS"); do
    # shellcheck disable=SC2086
    if env -u HF2Q_LAYER_POLICY -u HF2Q_TQ_CODEBOOK_BITS HF2Q_USE_DENSE=1 \
        $HF2Q_BIN parity check --model "$GGUF_PATH" --prompt "$prompt" \
        --min-prefix "$min_prefix" $extra_args 2>/dev/null >/dev/null; then
      run_pass=$((run_pass + 1))
    else
      run_fail=$((run_fail + 1))
      echo "  run $run: FAIL"
    fi
  done
  if (( run_fail == 0 )); then
    echo "  $run_pass/$N_RUNS PASS"
    # Pre-increment: under `set -e`, post-increment returns the *old* value;
    # the 0→1 transition exits 1 and kills the suite mid-run. Surfaced by
    # ADR-005 iter-104 W7 release-check pass.
    ((++PASS))
  else
    echo "  $run_pass/$N_RUNS PASS, $run_fail/$N_RUNS FAIL (Gate F: determinism violated)"
    ((++FAIL))
  fi
  echo
}

echo "=== ADR-005 Parity Suite (Gates C/D/E + F via N=$N_RUNS rerun) ==="
echo "GGUF: $GGUF_PATH"
echo "hf2q: $HF2Q_BIN"
echo "git:  $GIT_HEAD"
echo

# --- Gates C/E/F: live llama.cpp-anchored parity ---
# Short deterministic — exact byte comparison (full answer)
run_parity_n_times "short_hello"  16   "Check 1: short_hello (exact vs llama.cpp)"
# Sourdough — long-prompt regression-detector floor.
# 2026-05-16 re-anchor: refs re-captured against
# gemma4-ara-2pass-APEX-Q5_K_M.gguf on hf2q HEAD b4005d9d.  The previous
# anchor (2026-05-01) was against a DWQ quant variant whose model dir
# is no longer present locally; the model swap (DWQ → APEX-Q5_K_M)
# accounts for the prior 0/6 parity result, NOT a kernel regression
# (verified: hf2q vs llama-completion on the SAME APEX-Q5_K_M model
# byte-equal on short_hello via scripts/adr022_p18_byte_equal.sh).
#
# Cross-validation captured 2026-05-16 against same-model llama-
# completion (greedy, --temp 0 --top-k 1) shows long-prompt drift
# kicks in after ~117 bytes for sourdough and ~129 for sliding_wrap
# — independent-implementation FP non-associativity cascading through
# argmax tie-breaks at long context.  Both implementations are
# independently correct on short answers (full match on short_hello).
#
# Floors anchored at MEASURED common_prefix so future hf2q kernel
# changes that further reduce divergence point trip the regression
# gate.  See tests/evals/reference/MANIFEST.json for the model + commit
# the refs were captured against.
run_parity_n_times "sourdough"    117  "Check 2: sourdough (long-prompt floor vs llama.cpp)"
# Sliding wrap — same re-anchor; today's measured common_prefix = 129.
run_parity_n_times "sliding_wrap" 129  "Check 3: sliding_wrap (long-prompt floor vs llama.cpp)"

# --- Gate D: frozen hf2q self-baseline (byte-identical required) ---
# min-prefix is unused under --self-baseline (hf2q passes 0 safely as
# a placeholder; the kernel check is byte-equality, not prefix).
run_parity_n_times "short_hello"  0    "Check 4: short_hello (Gate D self-baseline)"   "--self-baseline"
run_parity_n_times "sourdough"    0    "Check 5: sourdough (Gate D self-baseline)"     "--self-baseline"
run_parity_n_times "sliding_wrap" 0    "Check 6: sliding_wrap (Gate D self-baseline)"  "--self-baseline"

# --- Summary ---
TOTAL=$((PASS + FAIL))
echo "=== Parity Summary: $PASS/$TOTAL checks passed ==="
if [[ $FAIL -gt 0 ]]; then
    exit 2
fi
exit 0
