Judge Live-Monitor (touchstone judge --web) — Design (Ink. 5)¶
Datum: 2026-06-20
Status: ratifiziert (Brainstorming abgeschlossen, vor Implementierungsplan)
Scope: v1 — read-only Live-Monitor des judge-Laufs im Browser.
Baut auf: 2026-06-20-web-live-monitor-design.md (Ink. 3, eval --web).
1 · Ziel & Motivation¶
Ein judge-Lauf bewertet jede Antwort eines Bundles mit einem LLM-Judge (ein Call pro
Antwort, ~15 s gegen einen lokalen 35B-Judge). Über 24+ Prompts × Modelle × Varianten ist
das eine lange Phase, in der man heute nur sporadische Konsolen-Zeilen sieht. Ink. 5
liefert ein flüchtiges Live-Fenster auf einen laufenden Bewertungslauf — analog zu
eval --web (Ink. 3), aber scorend statt latenz-zentriert:
- Fortschritt X/N + ETA
- Live-Score-Verteilung (Histogramm 1–5) + Red-Flag-Zähler + Ø-Score
- Verdict-Tabelle (Prompt · Modell·Variante · Score · Red? · Begründung)
- am Lauf-Ende die gewichtete Master-Scorecard-Vorschau (%, Safety-K.-o., Empfehlung)
Es ist der „eigene Judge-View", der in der Ink.-3-Spec als deferred notiert wurde („Verdicts = Score/Red-Flag-Verteilung, keine Latenz → passt nicht ins zell-/latenz- geformte Dashboard").
2 · Ratifizierte Entscheidungen (mit Begründung)¶
Zwei Forks wurden mit dem User ratifiziert; der Rest ist von eval --web (Ink. 3, D1–D10)
präzedenzfest übernommen und nicht neu verhandelt.
| # | Entscheidung | Begründung |
|---|---|---|
| J1 | Dashboard „Voll": zusätzlich zur Verdict-Live-Sicht am Lauf-Ende die Master-Scorecard-Vorschau (%, Safety, Empfehlung) | Das ist das eigentliche Geld-Resultat — man sieht das Urteil, ohne scorecard.md zu öffnen. (User-Fork 1) |
| J2 | webmon.py generalisieren (Fork A): webmon wird reiner Transport (SSE + tail + serve + Port-Print); der konkrete View (HTML + Aggregation + getailte Streams) kommt aus einem View-Modul, gewählt per --view eval\|judge |
Ein Fernseher, zwei Sender. Die fehleranfällige Klempnerei (SSE/Lifecycle — Quelle von 6 Review-Bugs in Ink. 3) existiert einmal, wird einmal gefixt. Kein dupliziertes Server-Gerüst. Passt zu D4 (schlank) + „kleine, fokussierte Units". (User-Fork 2) |
| J3 (von D5) | judge_events.jsonl — dedizierter, append-only Event-Kanal, getrennt von judgements.jsonl |
judgements.jsonl wird beim Resume-Finalize komplett neu geschrieben (clean rewrite, cli.py:482) → ein Tailer darauf wäre buggy. judge_events.jsonl ist append+flush, wie events.jsonl/resources.jsonl. |
| J4 (von D6) | Additive Callbacks im Judge-Pfad (Default None → judge ohne --web byte-für-byte unverändert) |
judge_bundle feuert bereits on_verdict(v) (judge.py:201). Start/Master/Done kommen additiv dazu. Null Verhaltensänderung im Default-Pfad. |
| J5 | Kein Load-Panel (RAM/Memory-Pressure/Throttle) im judge-View | Der Judge läuft auf einem anderen Endpoint (LM Studio :1234 / Cloud) — er belastet die Messmaschine nicht repräsentativ. Die resources.jsonl im Bundle stammt vom alten eval-Lauf → wäre irreführend. Der judge-View tailt sie nicht. |
| J6 | Master-%-/Safety-Math bleibt im Hauptprozess — der CLI rechnet weighted_total/passes_ko/_recommendation und schreibt das fertige Ergebnis ins master-Event |
Der Monitor-Subprozess hat keinen Pack (er tailt nur Dateien) und soll keinen bekommen. Monitor bleibt reine Anzeige; die Pack-Math verlässt nie den Hauptprozess. Spiegelt „memory is never estimated from the request thread". |
| J7 (von D7) | v1 = read-only Monitor von judge |
Kleinste Fläche. Kein Start-Formular/Abort/History. |
| J8 (von D9) | Konsole bleibt (Browser ergänzt) | --web ist Opt-in-Garnitur; CI/headless brauchen nie einen Browser. |
3 · Prozess-Topologie¶
touchstone judge --bundle … --judge-config … --web [--port 0] [--no-open]
│
│ (Hauptprozess — Bewertung, im Kern unverändert)
├─ on_judge_start(total, prior) ──▶ Writer-Closure (im CLI):
│ │ append judge_start-Event + die prior Verdicts
│ │ (resume) als verdict-Events ▶ judge_events.jsonl (J3)
│ │
├─ judge_bundle(…, on_verdict=…) ──▶ pro frischem Verdict:
│ │ append judgements.jsonl UND verdict-Event (J4)
│ │
├─ nach Rückkehr: pro (Modell·Variante) % + Safety + Empfehlung im Hauptprozess
│ rechnen (scorecard_mod.weighted_total / passes_ko / _recommendation)
│ ──▶ master-Event je Gruppe + judge_done-Event (J6)
│
└─ spawnt _WebMonitorProcess (wie eval, events_name="judge_events.jsonl", view="judge")
│ python -m touchstone.webmon --bundle <dir> --port <p> --events judge_events.jsonl --view judge
│ tailt judge_events.jsonl (NICHT resources.jsonl — J5), serviert SSE
└─ druckt gebundenen Port auf stdout ▶ Hauptprozess öffnet Browser
4 · Modul-Struktur (Fork A / J2)¶
webmon.py Transport: SSE-Server + tail + Port-Print + View-Dispatch (--view)
├─ events.py eval-View: INDEX_HTML + build_view (Latenz/RAM) ← eval-HTML zieht
│ aus webmon hierher
└─ judge_events.py judge-View: INDEX_HTML + build_view (Scores) ← NEU
Ein View ist ein Modul mit dreierlei:
- INDEX_HTML: str — die gerenderte Dashboard-Seite (+ SSE-Client-JS)
- build_view(events: Iterable[dict]) -> RunView-artig mit .as_dict() — faltet den
Event-Strom in ein render-fertiges View-Model
- TAILS_RESOURCES: bool — ob der Server zusätzlich resources.jsonl für ein load-Event
tailt (eval: True, judge: False)
webmon.make_handler(bundle, events_name, view) wählt per view das Modul; webmon.main
nimmt --view eval|judge (Default eval). Das eval-INDEX_HTML wandert aus webmon.py
nach events.py — webmon.py wird dadurch fokussierter (nur Transport), nicht größer.
5 · Event-Kontrakt (judge_events.jsonl)¶
Parallel zu events.py. Pure (de)serialisierung, kein I/O.
| Event | Felder | Wann |
|---|---|---|
judge_start |
ts, total |
einmal zu Beginn; total = Anzahl zu bewertender Antworten |
verdict |
ts, i, model, variant, prompt_id, repeat, category, score, red_flag, unscored, rationale |
pro Verdict (prior beim Resume vorab; danach frische via on_verdict); rationale auf ~160 Zeichen gekürzt; score 0 wenn unscored |
master |
ts, model, variant, pct, safety_passed, safety_reason, recommendation |
eines pro (Modell·Variante), am Ende — schon fertig gerechnet (J6) |
judge_done |
ts, total, scored |
im finally, auch nach Abbruch |
build_view faltet das zu:
- total, done (= Anzahl Verdicts), eta_s (Ø Δt zwischen verdict-Events × Rest)
- histogram = Counts je Score 1..5; red_flags = Σ red_flag (scored); mean_score
- verdicts = Liste (dedup per Key (model,variant,prompt_id,repeat), last wins → resume-fest)
- masters = Liste der master-Zeilen; finished
Defensiv wie events.parse_line: leere/halbe/kaputte Zeilen → None (toleriert eine
halb-geschriebene Schlusszeile bei laufendem Tail).
6 · Callback-Verdrahtung im CLI¶
Im judge-Command (analog zu _eval_event_writers, neu _judge_event_writers):
- ohne
--web(Default): unverändert —judge_bundle(..., on_verdict=_append)schreibt nurjudgements.jsonl. Keinjudge_events.jsonl, keine neuen Callbacks. - mit
--web:with _live_monitor(bundle, port, no_open, events_name="judge_events.jsonl", view="judge").on_verdictwird zu „appendjudgements.jsonlund verdict-Event".on_judge_start(total, prior)schreibt Start + prior-Verdicts. Nachjudge_bundle-Rückkehr rechnet der CLI die master-Zeilen und schreibt sie +judge_done(Letzteres imfinally).
_live_monitor / _hold_monitor / _WebMonitorProcess bekommen einen additiven
view-Parameter (Default "eval") — sonst unverändert.
7 · Error-Handling¶
Wie eval (Ink. 3): Monitor-Crash kann den Bewertungslauf nie umbringen (entkoppelter
Subprozess); SSE bricht sauber bei Browser-Close (BrokenPipeError/OSError → Stream endet,
Thread lebt); judge_events.jsonl append+flush, halbe Schlusszeile beim Parsen toleriert;
judge_done im finally, damit das Dashboard auch nach Abbruch „fertig" zeigt. Ohne --web:
kein judge_events.jsonl, kein Callback — Default-Pfad byte-identisch.
8 · Teststrategie (TDD)¶
Unit (pure):
- judge_events: Event-Konstruktoren round-trip; parse_line (leer/halb/kaputt → None);
build_view (Histogramm, mean, Red-Count, ETA, Master-Faltung, Key-Dedup über
Resume-Replay).
- CLI-Writer-Closures (_judge_event_writers): prior → verdict-Events; on_verdict →
Event und judgements.jsonl; master/done korrekt.
Integration / Verhalten:
- webmon View-Dispatch: --view judge liefert judge-HTML; --view eval byte-identisch
zu heute (Regression-Guard nach HTML-Umzug).
- Rückwärtskompat: judge ohne --web schreibt kein judge_events.jsonl und ruft
keine neuen Callbacks (Default-Pfad byte-identisch).
Live-Smoke (manuell, am Ende): ein Bundle (z. B. runs/2026-06-20_104844_eval_ndassist,
judgements.jsonl gelöscht) gegen LM Studio :1234 qwen3.6-35b mit --web re-judgen;
prüfen: Dashboard zählt live hoch, Histogramm/Red-Flags füllen sich, Master-Vorschau
erscheint am Ende, Ctrl-C-Shutdown sauber (keine Zombies).
9 · Scope-Grenze (YAGNI)¶
v1 = read-only Monitor von judge. Nicht in v1: Start-Formular, Abort-Knopf, History/alte
Läufe, per-Verdict-Latenz (uninteressant — anderer Endpoint), Live-Token-Stream des Judge.
10 · Berührte Dateien¶
| Datei | Änderung |
|---|---|
touchstone/judge_events.py |
neu — judge-View: Event-Konstruktoren + parse_line + build_view + INDEX_HTML + TAILS_RESOURCES=False |
touchstone/events.py |
eval-INDEX_HTML von webmon hierher ziehen; TAILS_RESOURCES=True ergänzen |
touchstone/webmon.py |
View-Dispatch (--view), HTML aus dem Modul statt hartkodiert, resources.jsonl nur tailen wenn TAILS_RESOURCES |
touchstone/runner.py |
_WebMonitorProcess: additiver view-Parameter → an webmon-Subprozess durchreichen |
touchstone/cli.py |
_live_monitor/_hold_monitor: view-Parameter; neu _judge_event_writers; judge-Command um --web/--port/--no-open + Verdrahtung |
tests/ |
neue Tests je Abschnitt 8 |
AGENTS.md |
judge --web in Befehlsliste + ggf. Gotcha (judge-View tailt nie resources.jsonl) |