#!/usr/bin/env bash
# Trend-Radar (wöchentlich Sa 05:15, 17.07.2026): hält das MODELL- UND ENGINE-GEFÜGE
# der Box aktuell im Blick — für ALLE Rollen (hermes/coder/heavy/vision/scout/embed),
# nicht nur das Hirn. Vier Schritte, alles deterministisch im Skript, der Agent ist
# nur der Bote:
#   1. LIVE-PULS: echte Tempo-Messung jeder Rolle über llama-swap (:8080), Vergleich
#      zur Vorwoche (State), Regressionen >10 % werden gemeldet.
#   2. ENGINE-A/B: gibt es einen neueren llama.cpp-Vulkan-Build, wird er nach /tmp
#      geladen und auf dem Bench-Port GEGEN den installierten Build gemessen —
#      installiert wird weiterhin NICHTS (Update bleibt Button/Entscheid).
#   3. WATCH-LISTE: mechanisch prüfbare Bedingungen (Issue zu? neuer ROCm-Release?
#      Community-Grid-Zahlen) aus deploy/trend-radar-watchlist.json.
#   4. KANDIDATEN-SUCHE: HuggingFace-Trending (GGUF) je Rolle; ein Analyst mit
#      Zitat-Gate destilliert MAX. 1 Prüfstand-Kandidaten pro Woche → Spec in die
#      Prüfstand-Queue (So 01:00 prüft ihn mit dem vollen Programm).
# Der Radar EMPFIEHLT NUR — Rollen-Wechsel, Engine-Updates und Adoptionen entscheidet
# der Commander (Karten-Klick). Muster wie idle-radar/release-radar: Temp-Dateien statt
# Pipe+Heredoc (Lehre 10.07.), Ein-Schuss-Analyst gegen :8080, stiller Briefkasten.
set -uo pipefail

MC="$HOME/mission-control-v2"
STATE_DIR="$HOME/.hermes/state"
STATE="$STATE_DIR/trend-radar-state.json"
HIST="$STATE_DIR/trend-radar-puls-history.jsonl"   # Langzeit-Verlauf (Lücke 3, 17.07.)
PS_QUEUE="$STATE_DIR/pruefstand/queue"
PS_DONE="$STATE_DIR/pruefstand/done"
WATCHLIST="$MC/deploy/trend-radar-watchlist.json"
ENDPOINT="${RADAR_ENDPOINT:-http://127.0.0.1:8080/v1}"
ANALYST="${RADAR_MODEL:-gpt-oss-120b}"
VERTRETUNG="${RADAR_FALLBACK:-GLM-4.7-Flash}"
BRAIN_GGUF="${BRAIN_GGUF:-/srv/models/Qwen3.6-35B-A3B-MTP-GGUF/Qwen3.6-35B-A3B-UD-Q4_K_M.gguf}"
BENCH_PORT="${BENCH_PORT:-5899}"
VULKAN_DIR=/opt/llamacpp-vulkan

mkdir -p "$STATE_DIR" "$PS_QUEUE" "$PS_DONE"
[ -f "$STATE" ] || echo '{}' > "$STATE"
TMPD="$(mktemp -d /tmp/trend-radar.XXXXXX)"
trap 'rm -rf "$TMPD"' EXIT

briefkasten() { # $1=Betreff $2=Text
  curl -sf -m 5 -X POST "${MC_ANNOUNCE_URL:-http://127.0.0.1:9001/api/voice/announce}" \
    -H 'Content-Type: application/json' \
    --data "$(python3 - "$1" "$2" <<'PY'
import json, sys
print(json.dumps({"text": sys.argv[2], "subject": sys.argv[1],
                  "source": "trend-radar", "priority": "silent"}))
PY
)" >/dev/null 2>&1 || true
}

# ---------- Versionierter Auftrag für den Boten-Agenten ----------
cat "$MC/deploy/trend-radar-prompt.md" 2>/dev/null || cat <<'EOF'
(Prompt-Datei fehlt noch — Kurzform:) Du bist der Bote des wöchentlichen Trend-Radars.
Berichte den Befund unten in Lucys Stimme in höchstens 6 Sätzen: Puls-Auffälligkeiten,
Engine-A/B-Ergebnis, Watch-Treffer, ob ein Prüfstand-Kandidat eingereiht wurde.
Nichts selbst umsetzen — das Skript hat alles Nötige bereits getan.
EOF
echo
echo "## TREND-RADAR-BEFUND (erhoben am $(date '+%d.%m.%Y %H:%M'))"
echo

# =====================================================================
# 1. LIVE-PULS aller Rollen (heavy zuletzt → Analyst findet es noch warm)
# =====================================================================
echo "### 1. LIVE-PULS (echte Messung über llama-swap, Vergleich zur Vorwoche + Langzeit)"
python3 - "$ENDPOINT" "$STATE" "$TMPD/puls.json" "$HIST" <<'PY'
import json, sys, time, urllib.request

endpoint, state_pfad, out_pfad = sys.argv[1], sys.argv[2], sys.argv[3]
hist_pfad = sys.argv[4]
ROLLEN = ["hermes", "coder", "vision", "scout", "heavy"]  # heavy zuletzt (Analyst-Wärme)

def chat(model, timeout=420):
    body = {"model": model, "max_tokens": 160, "temperature": 0, "messages": [
        {"role": "user", "content":
         "Erkläre in drei kurzen Sätzen, was ein Mixture-of-Experts-Modell ist."}]}
    req = urllib.request.Request(endpoint.rstrip("/") + "/chat/completions",
        data=json.dumps(body).encode(), headers={"Content-Type": "application/json"})
    with urllib.request.urlopen(req, timeout=timeout) as r:
        return json.load(r).get("timings") or {}

puls = {}
for rolle in ROLLEN:
    try:
        chat(rolle)          # Lauf 1: lädt/wärmt (on-demand-Modelle brauchen Minuten)
        t = chat(rolle)      # Lauf 2: die eigentliche Messung
        puls[rolle] = {"tg": round(t.get("predicted_per_second") or 0, 1),
                       "pp": round(t.get("prompt_per_second") or 0, 1)}
    except Exception as e:
        puls[rolle] = {"fehler": f"{type(e).__name__}"}
# embed: Funktions- und Latenz-Probe
try:
    t0 = time.time()
    req = urllib.request.Request(endpoint.rstrip("/") + "/embeddings",
        data=json.dumps({"model": "embed", "input": "Puls-Probe"}).encode(),
        headers={"Content-Type": "application/json"})
    with urllib.request.urlopen(req, timeout=60) as r:
        json.load(r)
    puls["embed"] = {"ms": round((time.time() - t0) * 1000)}
except Exception as e:
    puls["embed"] = {"fehler": f"{type(e).__name__}"}

try:
    alt = json.load(open(state_pfad)).get("puls") or {}
except Exception:
    alt = {}
for rolle, w in puls.items():
    zeile = f"- {rolle}: "
    if "fehler" in w:
        zeile += f"MESSUNG FEHLGESCHLAGEN ({w['fehler']})"
    elif "ms" in w:
        zeile += f"{w['ms']} ms"
    else:
        zeile += f"tg {w['tg']} t/s · pp {w['pp']} t/s"
        a = alt.get(rolle) or {}
        if a.get("tg") and w.get("tg"):
            d = 100 * w["tg"] / a["tg"] - 100
            zeile += f" · Vorwoche {a['tg']} ({d:+.0f} %)"
            if d <= -10:
                zeile += "  ⚠️ REGRESSION"
    print(zeile)

# Langzeit-Vergleich (~8 Wochen): fängt SCHLEICHENDE Regression, die jede Woche
# einzeln unter der 10-%-Schwelle bleibt (Lücke 3, 17.07.).
eintraege = []
try:
    with open(hist_pfad, encoding="utf-8") as f:
        for l in f:
            l = l.strip()
            if l:
                try:
                    eintraege.append(json.loads(l))
                except Exception:
                    pass
except FileNotFoundError:
    pass
grenze = time.time() - 49 * 86400
alte = [z for z in eintraege if (z.get("ts") or 0) <= grenze]
ref = alte[-1] if alte else None
if ref:
    treffer = []
    for rolle, w in puls.items():
        a = (ref.get("puls") or {}).get(rolle) or {}
        if a.get("tg") and w.get("tg"):
            d = 100 * w["tg"] / a["tg"] - 100
            if d <= -10:
                treffer.append(f"- LANGZEIT {rolle}: tg {w['tg']} vs. {a['tg']} "
                               f"({ref.get('datum','?')}, Engine b{ref.get('engine','?')}) "
                               f"= {d:+.0f} %  ⚠️ SCHLEICHENDE REGRESSION")
    if treffer:
        print(f"Langzeit-Vergleich (vs. {ref.get('datum','?')}):")
        for t in treffer:
            print(t)
    else:
        print(f"Langzeit-Vergleich vs. {ref.get('datum','?')}: unauffällig.")
json.dump(puls, open(out_pfad, "w"))
PY
PULS_BLOCK="$(cat "$TMPD/puls.json" 2>/dev/null || echo '{}')"
echo

# =====================================================================
# 2. ENGINE-A/B (nur wenn ein neuerer Vulkan-Build existiert)
# =====================================================================
echo "### 2. ENGINE-A/B (neuer llama.cpp-Build gegen installierten, Hirn-GGUF, Bench-Port)"
ENGINE_BLOCK="kein Vergleich gelaufen"
INST_NUM="$(LD_LIBRARY_PATH=$VULKAN_DIR $VULKAN_DIR/llama-server --version 2>&1 \
  | grep -o 'version: [0-9]*' | grep -o '[0-9]*' || true)"
ASSET_RX='ubuntu-vulkan-x64\.tar\.gz$'
REL_JSON="$(curl -sf --max-time 30 'https://api.github.com/repos/ggml-org/llama.cpp/releases?per_page=15' || true)"
NEU_TAG=""; NEU_URL=""
if [ -n "$REL_JSON" ]; then
  NEU_URL="$(printf '%s' "$REL_JSON" | jq -r --arg rx "$ASSET_RX" \
    '[.[] | .assets[]? | select(.name|test($rx))][0].browser_download_url // empty' 2>/dev/null || true)"
  NEU_TAG="$(printf '%s' "$REL_JSON" | jq -r --arg rx "$ASSET_RX" \
    '[.[] | select(any(.assets[]?; .name|test($rx)))][0].tag_name // empty' 2>/dev/null || true)"
fi
NEU_NUM="$(printf '%s' "$NEU_TAG" | grep -o '[0-9]*' | head -1 || true)"
MEM_FREI_GB="$(awk '/MemAvailable/{printf "%d", $2/1048576}' /proc/meminfo)"

if [ -z "$INST_NUM" ] || [ -z "$NEU_NUM" ]; then
  ENGINE_BLOCK="Versionsermittlung unvollständig (installiert: ${INST_NUM:-?}, GitHub: ${NEU_NUM:-?}) — ehrlich: kein Vergleich."
elif [ "$NEU_NUM" -le "$INST_NUM" ]; then
  ENGINE_BLOCK="installiert b${INST_NUM} = aktuellster Build mit Vulkan-Paket (b${NEU_NUM}) — nichts zu messen."
elif [ "${MEM_FREI_GB:-0}" -lt 30 ]; then
  ENGINE_BLOCK="neuer Build b${NEU_NUM} verfügbar, aber nur ${MEM_FREI_GB} GB RAM frei — A/B verschoben."
elif [ ! -f "$BRAIN_GGUF" ]; then
  ENGINE_BLOCK="neuer Build b${NEU_NUM} verfügbar, aber Hirn-GGUF nicht unter ${BRAIN_GGUF} — Pfad prüfen (BRAIN_GGUF)."
else
  mkdir -p "$TMPD/engine"
  if curl -fsSL --max-time 600 -o "$TMPD/engine/neu.tgz" "$NEU_URL" \
     && tar xzf "$TMPD/engine/neu.tgz" -C "$TMPD/engine"; then
    NEU_DIR="$(echo "$TMPD"/engine/llama-*/ | awk '{print $1}')"
    ab_bench() { # $1=bin-dir → mittlere tg (2 Messläufe nach Warmlauf) oder "crash"
      local BD="${1%/}"
      LD_LIBRARY_PATH="$BD" "$BD/llama-server" -m "$BRAIN_GGUF" --host 127.0.0.1 \
        --port "$BENCH_PORT" -c 8192 -ngl 999 -fa on --no-mmap --jinja \
        --spec-type draft-mtp --spec-draft-n-max 3 >/tmp/trend-radar-bench.log 2>&1 &
      local PID=$!
      local OK=0
      for i in $(seq 1 120); do
        curl -s --max-time 2 "http://127.0.0.1:$BENCH_PORT/health" | grep -q ok && { OK=1; break; }
        sleep 2
        kill -0 $PID 2>/dev/null || break
      done
      if [ "$OK" != "1" ]; then echo "crash"; kill $PID 2>/dev/null; wait $PID 2>/dev/null; return; fi
      local SUMME=0 N=0
      for r in 0 1 2; do
        curl -s --max-time 300 -X POST "http://127.0.0.1:$BENCH_PORT/completion" \
          -H "Content-Type: application/json" \
          -d '{"prompt":"Erklaere in drei kurzen Saetzen, was ein Mixture-of-Experts-Modell ist.","n_predict":150,"temperature":0}' \
          > "$TMPD/engine/probe.json" 2>/dev/null
        [ "$r" = "0" ] && continue
        local TG
        TG="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("timings",{}).get("predicted_per_second") or 0)' "$TMPD/engine/probe.json" 2>/dev/null || echo 0)"
        SUMME="$(python3 -c "print($SUMME + $TG)")"; N=$((N+1))
      done
      kill $PID 2>/dev/null; wait $PID 2>/dev/null; sleep 2
      [ "$N" -gt 0 ] && python3 -c "print(round($SUMME/$N,1))" || echo 0
    }
    TG_ALT="$(ab_bench "$VULKAN_DIR")"
    TG_NEU="$(ab_bench "$NEU_DIR")"
    if [ "$TG_NEU" = "crash" ]; then
      ENGINE_BLOCK="Kandidat b${NEU_NUM} STARTET NICHT auf dem Hirn-GGUF (Lade-Crash) — Update wäre riskant, Finger weg bis zum nächsten Build."
    elif [ "$TG_ALT" = "crash" ] || [ "${TG_ALT%.*}" = "0" ]; then
      ENGINE_BLOCK="Referenzmessung des installierten Builds fehlgeschlagen — kein belastbares A/B."
    else
      DELTA="$(python3 -c "print(round(100*$TG_NEU/$TG_ALT-100,1))" 2>/dev/null || echo '?')"
      ENGINE_BLOCK="b${NEU_NUM} (neu) tg ${TG_NEU} t/s vs. b${INST_NUM} (installiert) ${TG_ALT} t/s → ${DELTA} %. Anwendung übernimmt das So-04:30-Auto-Update (Rollback-Fangnetz) bzw. der Cockpit-Knopf — bei klar NEGATIVEM Delta vorher pinnen erwägen."
    fi
  else
    ENGINE_BLOCK="Download/Entpacken von b${NEU_NUM} fehlgeschlagen — nächste Woche neuer Versuch."
  fi
fi
echo "$ENGINE_BLOCK"
echo

# =====================================================================
# 3. WATCH-LISTE (mechanische Bedingungen)
# =====================================================================
echo "### 3. WATCH-LISTE (mechanisch geprüft)"
python3 - "$WATCHLIST" "$STATE" "$TMPD/watch.txt" <<'PY'
import json, re, sys, urllib.request

def hole(url, timeout=30):
    req = urllib.request.Request(url, headers={"User-Agent": "mc2-trend-radar"})
    with urllib.request.urlopen(req, timeout=timeout) as r:
        return r.read().decode("utf-8", "replace")

try:
    eintraege = json.load(open(sys.argv[1], encoding="utf-8")).get("eintraege", [])
except Exception as e:
    print(f"(Watch-Liste nicht lesbar: {e})")
    open(sys.argv[3], "w").write("")
    raise SystemExit
try:
    state = json.load(open(sys.argv[2]))
except Exception:
    state = {}
gesehen = state.get("watch_gesehen") or {}
zeilen = []
for e in eintraege:
    key, typ = e.get("key", "?"), e.get("typ")
    try:
        if typ == "github_issue":
            d = json.loads(hole(f"https://api.github.com/repos/{e['repo']}/issues/{e['nummer']}"))
            status = d.get("state", "?")
            treffer = " ⚠️ BEDINGUNG ERFÜLLT!" if status == "closed" else ""
            zeilen.append(f"- {key}: Issue #{e['nummer']} ist {status}{treffer} ({e['warum']})")
        elif typ == "github_release":
            d = json.loads(hole(f"https://api.github.com/repos/{e['repo']}/releases?per_page=1"))
            tag = (d[0].get("tag_name") if isinstance(d, list) and d else "?") or "?"
            neu = " ⚠️ NEU seit letzter Woche!" if tag != gesehen.get(key) else ""
            zeilen.append(f"- {key}: neuester Release {tag}{neu} ({e['warum']})")
            gesehen[key] = tag
        elif typ == "github_release_major":
            # PC-Watch (17.07.): nur MAJOR-Sprünge melden — Electron released wöchentlich,
            # relevant ist der Chromium-Jahrgang, nicht jeder Patch.
            d = json.loads(hole(f"https://api.github.com/repos/{e['repo']}/releases?per_page=1"))
            tag = (d[0].get("tag_name") if isinstance(d, list) and d else "?") or "?"
            m = re.search(r"(\d+)", tag)
            major = m.group(1) if m else "?"
            neu = " ⚠️ NEUE MAJOR-VERSION!" if major != gesehen.get(key, major) and major != "?" else ""
            zeilen.append(f"- {key}: neuester Release {tag} (Major {major}){neu} ({e['warum']})")
            gesehen[key] = major
        elif typ == "pypi":
            d = json.loads(hole(f"https://pypi.org/pypi/{e['paket']}/json"))
            ver = ((d.get("info") or {}).get("version")) or "?"
            neu = " ⚠️ NEU seit letzter Woche!" if ver != gesehen.get(key, ver) and ver != "?" else ""
            zeilen.append(f"- {key}: PyPI-Version {ver}{neu} ({e['warum']})")
            gesehen[key] = ver
        elif typ == "url_zeilen":
            text = hole(e["url"], timeout=45)
            passend = [z.strip() for z in text.splitlines() if e.get("muster", "") in z]
            passend = passend[: int(e.get("max_zeilen", 20))]
            zeilen.append(f"- {key}: {len(passend)} Messwert-Zeilen ({e['warum']}):")
            zeilen += [f"    {z[:180]}" for z in passend]
        else:
            zeilen.append(f"- {key}: unbekannter Typ {typ}")
    except Exception as ex:
        zeilen.append(f"- {key}: PRÜFUNG AUSGEFALLEN ({type(ex).__name__})")
state["watch_gesehen"] = gesehen
json.dump(state, open(sys.argv[2], "w"))
ausgabe = "\n".join(zeilen) or "(Watch-Liste leer)"
print(ausgabe)
open(sys.argv[3], "w", encoding="utf-8").write(ausgabe)
PY
echo

# =====================================================================
# 4. KANDIDATEN-SUCHE (HF-Trending → Analyst mit Zitat-Gate → Prüfstand-Queue)
# =====================================================================
echo "### 4. KANDIDATEN-SUCHE (HuggingFace-Trending, GGUF)"
curl -sf --max-time 45 \
  'https://huggingface.co/api/models?filter=gguf&sort=trendingScore&direction=-1&limit=40' \
  -o "$TMPD/hf.json" || echo '[]' > "$TMPD/hf.json"
python3 - "$TMPD/hf.json" "$TMPD/hf.txt" <<'PY'
import json, sys
try:
    modelle = json.load(open(sys.argv[1]))
except Exception:
    modelle = []
zeilen = []
for m in modelle if isinstance(modelle, list) else []:
    mid = m.get("modelId") or m.get("id") or "?"
    zeilen.append(f"- {mid} · downloads {m.get('downloads', 0)} · likes {m.get('likes', 0)}"
                  f" · angelegt {(m.get('createdAt') or '')[:10]}")
ausgabe = "\n".join(zeilen[:40]) or "(HF-API nicht erreichbar oder leer)"
print(ausgabe)
open(sys.argv[2], "w", encoding="utf-8").write(ausgabe)
PY
echo

# Schon bekannt (Dedup): gemeldete Keys + Prüfstand-Queue + erledigte Prüfungen
BEKANNT="$(python3 - "$STATE" "$PS_QUEUE" "$PS_DONE" <<'PY'
import glob, json, os, sys
try:
    state = json.load(open(sys.argv[1]))
except Exception:
    state = {}
zeilen = [f"- {k}" for k in (state.get("kandidaten_gemeldet") or [])]
for d in (sys.argv[2], sys.argv[3]):
    for f in glob.glob(os.path.join(d, "*.json")):
        zeilen.append(f"- {os.path.splitext(os.path.basename(f))[0]}")
print("\n".join(sorted(set(zeilen))) or "(noch keine)")
PY
)"

RASTER='Du bist der TREND-RADAR einer lokalen KI-Box (AMD Strix Halo, 128 GB Unified RAM, ~256 GB/s Speicherbandbreite, llama.cpp/Vulkan). Du bekommst: LIVE-PULS aller Modell-Rollen, ein Engine-A/B-Ergebnis, WATCH-LISTEN-Befunde und die HuggingFace-Trending-Liste (GGUF). ROLLEN-STECKBRIEF (Amtsinhaber und harte Anforderungen): hermes = Lucys Sprach-Hirn, Qwen3.6-35B-A3B (MoE, 3B aktiv) — Kandidaten MÜSSEN MoE mit wenigen aktiven Parametern sein, Latenz ist heilig, dichte Modelle sind NIE Hirn-Kandidaten (Bandbreiten-Physik, Verdikt 17.07.); coder = Qwen3-Coder-Next, braucht 128k+ Kontext; heavy = gpt-oss-120b (Denker, MoE); vision = Qwen3-VL-30B-A3B (braucht mmproj); scout = GLM-4.6V-Flash (klein, Vision+Tools); embed = Qwen3-Embedding-0.6B. Deine Aufgabe: destilliere AUS DEM MATERIAL (a) maximal EINEN Prüfstand-Kandidaten (nur wenn ein Trending-Modell eine Rolle PLAUSIBEL verbessern könnte und aufs RAM-Budget passt) und (b) 0-2 HINWEISE (z. B. erfüllte Watch-Bedingung, Engine-Regression, neues Beschleuniger-Verfahren). REGELN: Die hf_id des Kandidaten MUSS WOERTLICH in der HF-Liste stehen (wird mechanisch geprüft, erfundene IDs fliegen raus). NIE einen Amtsinhaber selbst oder ein Repackaging/eine Quantisierung desselben Modells vorschlagen (gleicher Modellname = KEIN Kandidat; wird mechanisch geblockt). Nichts vorschlagen, was unter SCHON BEKANNT steht. Reine Namens-Hypes ohne erkennbare Rollen-Passung sind KEIN Kandidat. KANDIDAT: 0 ist der Normalfall und völlig ok. Antworte auf DEUTSCH, exakt so: erste Zeile "KANDIDAT: <0|1>". Bei 1 folgen vier Zeilen: "HF_ID: <exakte id aus der Liste>" / "ROLLE: <hermes|coder|heavy|vision|scout|embed>" / "BELEG: <die HF-Listenzeile wörtlich>" / "WARUM: <ein Satz mit der erwarteten Verbesserung>". Danach optional: "HINWEISE:" gefolgt von maximal 2 Spiegelstrichen mit je einem Satz + wörtlichem Beleg-Zitat aus dem Material.'

EVID="$(cat <<EOF
LIVE-PULS (JSON): $PULS_BLOCK

ENGINE-A/B: $ENGINE_BLOCK

WATCH-LISTE:
$(cat "$TMPD/watch.txt" 2>/dev/null || echo '(leer)')

HF-TRENDING (GGUF, Top 40):
$(cat "$TMPD/hf.txt" 2>/dev/null || echo '(leer)')

SCHON BEKANNT (nicht erneut vorschlagen):
$BEKANNT
EOF
)"

judge() { # $1=Modell $2=Timeout $3=max_tokens
  local req
  req="$(jq -n --arg m "$1" --arg sys "$RASTER" --arg usr "$EVID" --argjson mt "$3" \
    '{model:$m, messages:[{role:"system",content:$sys},{role:"user",content:$usr}],
      max_tokens:$mt, temperature:0.2}')"
  curl -s -m "$2" "$ENDPOINT/chat/completions" -H 'Content-Type: application/json' \
    --data-binary "$req" | python3 -c '
import json, sys
try:
    d = json.load(sys.stdin)
    msg = d["choices"][0]["message"]
    print((msg.get("content") or msg.get("reasoning_content") or "").strip())
except Exception:
    pass'
}

RICHTER="$ANALYST"
ANALYSE="$(judge "$ANALYST" 420 2600)"
if [ -z "${ANALYSE// /}" ]; then
  RICHTER="$VERTRETUNG (Vertretung — $ANALYST hat nicht geantwortet)"
  ANALYSE="$(judge "$VERTRETUNG" 240 1800)"
fi

echo "### 5. ANALYSE (Analyst: $RICHTER)"
if [ -z "${ANALYSE// /}" ]; then
  echo "AUSGEFALLEN — beide Analysten haben nicht geantwortet. Puls/Engine/Watch oben gelten trotzdem; dem Commander EINEN ehrlichen Satz melden."
  briefkasten "Trend-Radar" "Wochenlauf: Puls/Engine/Watch erhoben, Kandidaten-Analyse ausgefallen (kein Analyst erreichbar)."
  exit 0
fi
echo "$ANALYSE"
echo

# ---------- Zitat-Gate + Spec in die Prüfstand-Queue (deterministisch) ----------
printf '%s' "$ANALYSE" > "$TMPD/analyse.txt"
printf '%s' "$EVID"    > "$TMPD/material.txt"
ERGEBNIS="$(python3 - "$TMPD/analyse.txt" "$TMPD/material.txt" "$STATE" "$PS_QUEUE" <<'PY'
import json, os, re, sys, time, unicodedata

ana = open(sys.argv[1], encoding="utf-8", errors="replace").read()
evid = open(sys.argv[2], encoding="utf-8", errors="replace").read()
state_pfad, queue = sys.argv[3], sys.argv[4]

def norm(s):
    s = unicodedata.normalize("NFKC", s or "")
    s = "".join(ch for ch in s if ch.isalnum() or ch.isspace())
    return " ".join(s.lower().split())

def feld(name):
    m = re.search(rf"^{name}\s*:\s*(.+)$", ana, re.MULTILINE)
    return m.group(1).strip() if m else ""

anzahl = feld("KANDIDAT")
if not anzahl.startswith("1"):
    print("KEIN_KANDIDAT")
    raise SystemExit
hf_id = feld("HF_ID").strip("`").strip()
rolle = feld("ROLLE").lower().strip()
beleg = feld("BELEG").strip()
warum = feld("WARUM").strip()
if rolle not in ("hermes", "coder", "heavy", "vision", "scout", "embed"):
    print(f"VERWORFEN: unbekannte Rolle {rolle!r}")
    raise SystemExit
# Amtsinhaber-Sperre (Testlauf-Befund 17.07.: Radar schlug das eigene Hirn-Repo vor):
# gleicher Modellname = Repackaging, kein Kandidat.
AMTSINHABER = ("qwen3.6-35b-a3b", "qwen3-vl-30b", "gpt-oss-120b",
               "qwen3-coder-next", "glm-4.6v-flash", "qwen3-embedding")
if any(t in hf_id.lower() for t in AMTSINHABER):
    print(f"VERWORFEN: {hf_id} ist ein Amtsinhaber-Repackaging (mechanische Sperre)")
    raise SystemExit
if hf_id not in evid:
    print(f"VERWORFEN: HF_ID {hf_id!r} steht nicht wörtlich im Material (Zitat-Gate)")
    raise SystemExit
if beleg and norm(beleg) not in norm(evid):
    print("VERWORFEN: BELEG nicht wörtlich im Material (Zitat-Gate)")
    raise SystemExit
key = re.sub(r"[^a-z0-9-]+", "-", hf_id.lower())[:60].strip("-")
try:
    state = json.load(open(state_pfad))
except Exception:
    state = {}
gemeldet = state.get("kandidaten_gemeldet") or []
if key in gemeldet:
    print(f"VERWORFEN: {key} schon früher gemeldet")
    raise SystemExit
spec = {"key": key, "rolle": rolle, "hf_id": hf_id, "warum": warum,
        "beleg": beleg, "erstellt": time.strftime("%Y-%m-%d %H:%M"),
        "quelle": "trend-radar"}
with open(os.path.join(queue, key + ".json"), "w", encoding="utf-8") as f:
    json.dump(spec, f, ensure_ascii=False, indent=1)
state["kandidaten_gemeldet"] = gemeldet + [key]
json.dump(state, open(state_pfad, "w"))
print(f"EINGEREIHT: {hf_id} → Rolle {rolle} (Prüfstand prüft in der Nacht So 01:00)")
PY
)"
echo "### 6. PRÜFSTAND-QUEUE"
echo "$ERGEBNIS"

# ---------- Puls in den State (Vorwoche) + an die Langzeit-Historie anhängen ----------
python3 - "$STATE" "$TMPD/puls.json" "$HIST" "${INST_NUM:-}" <<'PY'
import json, sys, time
try:
    state = json.load(open(sys.argv[1]))
except Exception:
    state = {}
try:
    puls = json.load(open(sys.argv[2]))
except Exception:
    puls = None
if puls:
    state["puls"] = puls
    with open(sys.argv[3], "a", encoding="utf-8") as f:
        f.write(json.dumps({"ts": int(time.time()),
                            "datum": time.strftime("%Y-%m-%d"),
                            "engine": sys.argv[4], "puls": puls},
                           ensure_ascii=False) + "\n")
json.dump(state, open(sys.argv[1], "w"))
PY

briefkasten "Trend-Radar" "Wochenlauf fertig. Engine: ${ENGINE_BLOCK}
Queue: ${ERGEBNIS}"
