#!/usr/bin/env bash
# embed_parity — verify Rust /ai/embed (now direct Ollama post-sidecar
# drop) and Go /v1/embed (via embedd) produce equivalent 768-dim
# vectors for identical input.
#
# Why: today's sidecar drop (lakehouse commit ba928b1) changed Rust's
# embed transport. Go's embedd has always been direct. A drift would
# break retrieval semantics — same query, different vector → different
# top-K matches → different staffing recommendations. This probe is
# the regression gate.
#
# Match metric: cosine similarity > 0.99999. Byte-equal comparison
# isn't realistic — Go represents embeddings as []float32 and Rust
# as Vec<f64>, so JSON round-trip introduces small float drift. What
# matters for retrieval correctness is cosine direction, which both
# runtimes preserve when calling the same Ollama with the same model.
#
# Forces both endpoints to use `nomic-embed-text` so the v1-vs-v2-moe
# default difference doesn't pollute the comparison.
#
# Outputs: reports/cutover/gauntlet_2026-05-02/parity/embed_parity.md

set -uo pipefail
cd "$(dirname "$0")/../../.."

RUST_GW="${RUST_GW:-http://127.0.0.1:3100}"
GO_GW="${GO_GW:-http://127.0.0.1:4110}"
MODEL="${EMBED_MODEL:-nomic-embed-text}"
THRESHOLD="${EMBED_COSINE_THRESHOLD:-0.99999}"
OUT_DIR="reports/cutover/gauntlet_2026-05-02/parity"
mkdir -p "$OUT_DIR"
OUT="$OUT_DIR/embed_parity.md"

# Fixtures cover the staffing-domain text shapes plus stress shapes
# (unicode, very short, very long).
FIXTURES=(
  "forklift operator"
  "Welder, Toledo OH, 2nd shift"
  "OSHA-30 certified driver"
  "dental hygienist with 3 years experience"
  "CNC machine operator graveyard"
  "Café résumé ⭐ 你好"
  "x"
  "$(printf 'long fixture: %.0s' {1..200})end"
)

probe() {
  local gw="$1" path="$2" text="$3"
  curl -sf -m 15 -X POST "$gw$path" \
    -H 'Content-Type: application/json' \
    -d "$(jq -nc --arg t "$text" --arg m "$MODEL" '{texts:[$t], model:$m}')" 2>/dev/null
}

# Cosine via inline python3 (bash arithmetic doesn't have sqrt).
cosine() {
  local rust_json="$1" go_json="$2"
  python3 - <<EOF
import json, math, sys
r = json.loads('''$rust_json''')
g = json.loads('''$go_json''')
# Rust /ai/embed returns {embeddings: [[...]]}, dimensions field
# Go /v1/embed returns {vectors: [[...]]}, dimension field
rv = r.get("embeddings", r.get("vectors", []))[0]
gv = g.get("vectors", g.get("embeddings", []))[0]
if len(rv) != len(gv):
    print(f"DIM_MISMATCH:rust={len(rv)},go={len(gv)}")
    sys.exit(0)
dot = sum(a*b for a,b in zip(rv,gv))
nr = math.sqrt(sum(a*a for a in rv))
ng = math.sqrt(sum(b*b for b in gv))
if nr == 0 or ng == 0:
    print("ZERO_NORM")
    sys.exit(0)
print(f"{dot/(nr*ng):.10f}")
EOF
}

TOTAL=0; MATCH=0; DIFF=0
DIFF_DETAIL=""

for text in "${FIXTURES[@]}"; do
  TOTAL=$((TOTAL+1))
  rust_json=$(probe "$RUST_GW" "/ai/embed" "$text")
  go_json=$(probe "$GO_GW" "/v1/embed" "$text")
  if [ -z "$rust_json" ] || [ -z "$go_json" ]; then
    DIFF=$((DIFF+1))
    label=$(echo "$text" | head -c 60)
    DIFF_DETAIL="$DIFF_DETAIL"$'\n'"- \`$label\`: helper failed (rust_empty=$([[ -z "$rust_json" ]] && echo y || echo n) go_empty=$([[ -z "$go_json" ]] && echo y || echo n))"
    continue
  fi
  cos=$(cosine "$rust_json" "$go_json")
  label=$(echo "$text" | head -c 60 | tr '\n' ' ')
  case "$cos" in
    DIM_MISMATCH:*|ZERO_NORM)
      DIFF=$((DIFF+1))
      DIFF_DETAIL="$DIFF_DETAIL"$'\n'"- \`$label\` → $cos"
      ;;
    *)
      # Compare cos vs threshold via awk (bash can't compare floats).
      if awk -v c="$cos" -v t="$THRESHOLD" 'BEGIN{exit !(c >= t)}'; then
        MATCH=$((MATCH+1))
      else
        DIFF=$((DIFF+1))
        DIFF_DETAIL="$DIFF_DETAIL"$'\n'"- \`$label\` → cos=$cos (below threshold $THRESHOLD)"
      fi
      ;;
  esac
done

{
  echo "# /v1/embed cross-runtime parity probe"
  echo
  echo "**Date:** $(date -u +%Y-%m-%dT%H:%M:%SZ)"
  echo "**Rust:** \`$RUST_GW/ai/embed\`  ·  **Go:** \`$GO_GW/v1/embed\`"
  echo "**Model:** \`$MODEL\` (forced — overrides each side's default)"
  echo "**Match metric:** cosine similarity ≥ \`$THRESHOLD\`"
  echo
  echo "Identical text → both endpoints. Cosine compares vector"
  echo "DIRECTION (the operationally-meaningful property for HNSW"
  echo "retrieval); byte-equal isn't expected because Go round-trips"
  echo "through float32 internally while Rust stays at f64."
  echo
  echo "**Tally:** $MATCH match · $DIFF diff (out of $TOTAL fixtures)"
  if [ -n "$DIFF_DETAIL" ]; then
    echo
    echo "## Divergences"
    echo "$DIFF_DETAIL"
  else
    echo
    echo "_Cosine ≥ $THRESHOLD on every fixture — embed parity holds_"
    echo "_post-sidecar-drop. Rust and Go produce vectors that point in_"
    echo "_the same direction in 768-dim space._"
  fi
} > "$OUT"

echo "[parity] embed: $MATCH match / $DIFF diff (out of $TOTAL) → $OUT"
[ "$DIFF" -eq 0 ]
