GUI-Steuerzentrale (touchstone gui) — Design (Ink. 6)¶
Datum: 2026-06-21
Status: ratifiziert (Brainstorming abgeschlossen) + nach adversarialer Code-Review geschärft — vor Implementierungsplan
Scope: v1 — Walking Skeleton einer persistenten, lokalen Benchmarking-Steuerzentrale: ein dünner, durchgehender Faden durch alle 7 Stationen (konfigurieren → starten → live zusehen → auswerten → vergleichen → exportieren), der die Architektur end-to-end beweist.
Baut auf: dem eval --web / judge --web Monitor-Fundament (2026-06-20-web-live-monitor-design.md, 2026-06-20-judge-web-monitor-design.md) — tail.read_new, events.build_view, judge_events.build_view, loadview.latest_load, das _SamplerProcess/_WebMonitorProcess-Spawn-Muster.
1 · Ziel & Motivation¶
Heute ist touchstone ein CLI-Werkzeug: Config von Hand editieren, uv run touchstone eval/judge, danach scorecard.md / aggregate.md lesen, Ergebnis ins Vault übernehmen. Drei Dinge kann dieser Workflow genuin nicht, und genau sie rechtfertigen ein WebUI (reine Textanzeige täte es nicht):
- Verknüpfte Transparenz — die Bewertungslogik ist über Pack-YAML, Judge-Code und drei MD-Dateien verstreut; den Zusammenhang („dieser Prompt → gegen diese Flags geprüft → fließt mit Gewicht ×3 in Dimension Q6 → Q6 ≤ 2 ist die K.-o.-Regel") sieht niemand. Das ist ein verlinkter Graph, kein Fließtext.
- Echte Daten-Visualisierung —
resources.jsonlist eine 2-Hz-Zeitreihe (Speicherdruck/Throttle über die Laufzeit = ein Plot); TTFT-P50/P95-Verteilungen, Score-Histogramme, die Cross-Machine-Matrix — alles heute platte Zahlen in MD-Tabellen, dabei sind es Verläufe und Verteilungen, die man sehen will. - Steuerung — Läufe konfigurieren + starten/stoppen aus dem UI statt Config-Handarbeit +
uv run-Befehle.
Die Steuerzentrale macht aus touchstone ein kohärentes Werkzeug mit einem durchgehenden Workflow:
konfigurieren → starten → live zusehen → auswerten → vergleichen → exportieren
— mit dem Pack-Innenleben transparent, Perf + Quali + Cross-Machine an einem Ort, echten Plots statt Zahlen-Tabellen und Start/Stop aus dem Browser.
Zielgruppe (ratifiziert: c): primär das persönliche Cockpit (M5, bald M1↔M5 cross-machine) — darf Vorwissen voraussetzen, optimiert für Effizienz. Aber das publizierbare Ziel (Jays Vision: viele Leute steuern vergleichbare Läufe bei) wird von Anfang an nicht verbaut: das Tool soll sich selbst erklären können, Ergebnisse bleiben sauber teilbar.
2 · Ratifizierte Entscheidungen (mit Begründung)¶
| # | Entscheidung | Begründung |
|---|---|---|
| G1 | Ein kohärentes Produkt, kein „Read- vs. Write-Hälfte". Die 7 Stationen sind ein Workflow, nicht zwei Lager. | Transparenz und Steuerung sind keine Gegensätze — ein gutes Werkzeug hat beides. Künstliche Hälften-Wahlen verworfen. |
| G2 | Stack B — Python-rich, build-frei: FastAPI/Starlette + HTMX + Alpine.js + Tailwind. Neue Deps isoliert in einem optionalen [gui]-extra. |
Erfüllt #5 („best practices Frontend") ohne npm/Vite-Toolchain; bleibt Python-zentrisch (eine Sprache, eine uv-Build-Story); volle Design-Kontrolle (am besten für das publizierbare Ziel); HTMX+SSE ist die direkte Evolution des webmon-Patterns. |
| G3 | Out-of-process Control-Plane: die GUI spawnt touchstone eval/judge als Subprozesse ([sys.executable, "-m", "touchstone", …]), wie heute die CLI Sampler/webmon spawnt. |
Präzise Invariante (nicht „zero-dep im Hot-Path" pauschal): der Host-Sampler bleibt entkoppelt, Memory wird nie aus dem Request-Thread geschätzt, und GUI-Deps laden nie im Mess-Prozess. (Der pro-Zelle Event-Write existiert schon heute bei --web und liegt zwischen Zellen, nicht im Token-Stream.) In-process (GUI ruft run_eval direkt) würde den Mess-Thread mit Webserver-Event-Loop/GC/GUI-Deps teilen → genau das, was das Tool vermeiden will. |
| G4 | runs/ = SSOT (MD/CSV/JSONL-Ledger). Die GUI persistiert keine Mess-Wahrheit. Ihr Steuer-Zustand lebt in einer in-memory Lauf-Registry, gespiegelt auf Platte als Run-Sentinel run.json im aktiven run_dir (transient, non-SSOT, siehe §6). |
Wahrt „persistierter Output = nur MD/CSV". Das Sentinel ist transienter Steuer-State (kein Mess-Artefakt) — crash-/restart-robust auf Platte, damit G8/Discovery einen GUI-Neustart überleben. |
| G5 | Walking Skeleton als v1: dünner Faden durch alle Stationen, riskanter greenfield-Kern (Steuerung + Live-Tail, Stationen 3+4) zuerst durchgestochen. Read-Stationen rendern vorhandene pure Funktionen/Dateien. | Hält das Produkt kohärent, beweist die Architektur end-to-end, vermeidet die Hälften-Falle. |
| G6 | judge-Start gleich in v1. |
Mechanisch identisch zu eval (gleiches spawn/tail). Erst mit judge ist der Workflow rund — eine Scorecard entsteht durch judge. |
| G7 | Geteilte Datenschicht mit webmon, getrennte Präsentation. CLI eval --web / judge --web bleibt unangetastet. Die GUI verwendet das SSE-Datenmodell verbatim (tail.read_new + events/judge_events.build_view().as_dict()). |
Aggregations-Mathematik (Histogramm, ETA, Dedup) wird nicht dupliziert. Aber: die Präsentation ist net-new — webmon rendert eine hartkodierte INDEX_HTML-Vollseite mit Inline-JS; die HTMX/Alpine/Tailwind-Fragmente sind Neubau (siehe G11). |
| G8 | Nur ein Mess-Lauf gleichzeitig (verfassungs-relevant), durchgesetzt per Filesystem-Lock (das Run-Sentinel run.json), nicht nur durch die in-memory Registry. |
Parallele Läufe streiten um RAM/CPU und verfälschen sich gegenseitig — Mess-Sauberkeit. Ein reiner in-memory Guard bräche bei GUI-Neustart (verwaister Subprozess + leere Registry → zweiter Lauf). Der Lock überlebt den Neustart. |
| G9 | GUI ist Opt-in über [gui]-extra + Befehl touchstone gui. Ohne Extra läuft touchstone unverändert (CLI/Tests/CI brauchen FastAPI nie); touchstone gui ohne Extra → freundlicher Installations-Hinweis statt Crash. |
Kern bleibt der thin harness; GUI lädt nie im Hot-Path. |
| G10 | Event-Dateien sind transient, non-SSOT. events.jsonl / judge_events.jsonl sind Monitor-/Tail-Instrumentierung, nicht Teil des Bundle-Ledgers — aus Discovery & Export ausgeschlossen. Der GUI-gespawnte eval schreibt events.jsonl truncate-per-spawn (wie judge es schon tut), nicht append. |
Verhindert, dass ein transientes File zur „Wahrheit" promotet wird (AGENTS.md). Truncate-per-spawn behebt zugleich den finished=True-Latch-Bug auf dem Resume-Live-Pfad (siehe §9). |
| G11 | Frontend-Präsentation ist net-new Arbeit und der eigentliche Umfang des Skeletts; wiederverwendet werden nur die puren Daten-/Render-Funktionen der Logikschicht. | Ehrlichkeit über den Aufwand: Templates, htmx-Swaps, Alpine-Cards, Tabellen-CSS sind neu. „Reuse" gilt für build_view/scorecard_mod/aggregate, nicht für UI. |
3 · Prozess-Topologie¶
touchstone gui [--port 0] [--no-open]
│
│ GUI-Server (FastAPI · langlebig · [gui]-extra) — Browser: HTMX/Alpine/Tailwind über HTTP/SSE
│
├─ Stationen-Routen (7) ── rendern HTMX-Fragmente (Jinja2) [neu, G11]
├─ Lese-/Render-Schicht ── load_pack · scorecard_mod · aggregate · Loader [wiederverwendet]
├─ Live-View-Adapter ── tail.read_new + events/judge_events.build_view → SSE [Datenmodell wiederverwendet, G7]
└─ Control-Plane + Lauf-Registry ── run_dir host-seitig wählen · Run-Sentinel · ein Lauf [neu, G3/G4/G8]
│ spawn: [sys.executable, "-m", "touchstone", "eval", │ stop: SIGTERM→wait(5)→kill
│ "--pack", …, "--config", …, "--run-dir", <host-gewählt>, ▼
│ "--emit-events"] (kein webmon-Spawn) ┌────────────────────────────────────┐
▼ │ runs/<ts>_eval_<pack>/ = SSOT │
┌──────────────────────────────┐ write (SSOT-Ledger) │ bundle.json · responses.jsonl · │
│ Mess-Subprozess │ ───────────────────────────▶ │ judgements.jsonl · resources.jsonl ·│
│ reiner Mess-Loop, Sampler │ write (transient, G10) │ perf.csv · scorecard.md · scores.csv │
│ entkoppelt, GUI-dep-frei │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ▶ │ + events.jsonl (transient) │
└──────────────────────────────┘ │ + run.json (Sentinel, transient) │
▲ GUI tailt events.jsonl + liest Ledger ───────────────┘ │
Warum --run-dir + --emit-events (zwei neue, additive CLI-Argumente):
- --run-dir <path> (eval & judge): heute erfindet eval_cmd das finale run_dir selbst (base_out / f"{ts}_eval_{pk.id}"); --out setzt nur den Parent, und das run_dir wird nur via Rich-console.print ausgegeben (nicht maschinenlesbar). Damit die GUI den Tail-Pfad beim Spawn kennt (Station 3→4), wählt der Launcher das run_dir host-seitig, legt es an, schreibt das Sentinel und reicht es per --run-dir exakt durch. (Behebt den Blocker.)
- --emit-events (eval & judge): aktiviert die Event-Writer ohne webmon zu spawnen — die GUI tailt selbst. Das ist neue strukturelle Arbeit, kein bloßes „Flag aktivieren" (siehe §6).
Der Subprozess wird nicht mit --web gestartet (kein zweiter HTTP-Server). Der Live-Fortschritt kommt aus events.jsonl, das der GUI-Server selbst tailt.
4 · Die 7 Stationen — Walking-Skeleton-Schnitt¶
⚡ = greenfield-Kern (zuerst). Read-Stationen rendern vorhandene pure Funktionen — aber Status & Urteil werden berechnet, nicht aus bundle.json gelesen (siehe §8).
| Station | v1 — Skeleton | Vertiefung (spätere Specs) |
|---|---|---|
| 1 · Übersicht | Discovery über runs/ (§8) → Tabelle: Pack, Modelle, Datum, abgeleiteter Status (running/judged/eval-only/crashed), berechnetes Urteil-Badge (via scorecard_mod, nur wenn judged); Klick → Ergebnis |
Filter/Suche/Sortierung, Live-Status-Badges |
| 2 · Pack-Explorer | load_pack() → Dimensionen/Gewichte/K.-o./Prompt-Varianten/Prompts+Green-Red-Flags als Read-Tabellen |
verlinkter Graph (Prompt→Flag→Dimension→K.-o.), Varianten-Diff |
| 3 · Konfig + Steuerung ⚡ | vorhandenes Pack + Config aus Dropdown → eval starten / stoppen / fortsetzen (--resume); judge-Config wählen → judge starten (G6) |
Config-Felder im UI editieren, Modell-Auswahl, Live-Validierung, Pack-Editor |
| 4 · Live-Monitor ⚡ | events.jsonl / judge_events.jsonl tailen → Fortschritt (done/total, Balken; eval zusätzlich Last via loadview) via SSE |
volles Score-Histogramm, Verdict-Stream, ETA-Feinschliff |
| 5 · Ergebnis-Ansicht | ein Bundle: Kern-Zahlen (Tech-Specs, Master-%, Urteil, K.-o.) via scorecard_mod in Jinja-Tabellen (kein .md-Render — siehe §11) |
echte Plots: RAM/Throttle-Zeitreihe, TTFT-Verteilung, Score-Histogramm; rohe scorecard.md-Ansicht |
| 6 · Vergleich | aggregate.load_all_scores + aggregate über runs/ → Hardware×Qualität-Tabelle |
interaktive Matrix, Cross-Machine-Charts |
| 7 · Export | bestehende scorecard.md / scores.csv / aggregate.md als Download (Event-/Sentinel-Dateien ausgeschlossen, G10) |
Export aus jeder Ansicht, weitere Formate, gefilterte Exporte |
App-Shell: Sidebar-Navigation (7 Stationen) + Hardware/Endpoint-Status; laufende Messung als Karte oben (Fortschritt + Stop + Live-Last); Bundle-Liste mit Urteil-Badges (inkl. K.-o.-Markierung). Look: Tailwind, dicht genug fürs Cockpit.
5 · Modul-Struktur¶
Neues Sub-Package touchstone/gui/ — nur importiert, wenn der GUI-Server läuft (das [gui]-extra liefert die Deps). Der Kern (runner, qualrun, judge, scorecard, …) importiert gui nie.
touchstone/gui/
__init__.py
app.py FastAPI-App-Factory: Routen der 7 Stationen, mountet static/ + templates/
control.py Control-Plane: Lauf-Registry (Zustandsmaschine) + ProcessLauncher-Protocol
(injizierbar → im Test ein Fake); run_dir host-seitig wählen; Run-Sentinel
schreiben/lesen; spawn/stop/resume; erzwingt G8 via Sentinel-Lock
bundles.py Discovery + Detail-Aufbereitung (read): klassifiziert run_dirs (§8),
berechnet Status + Urteil via geteiltem master_rows-Helper (aus cli.py
herausfaktorisiert), liest Ledger via vorhandene Loader/Renderer
live.py Live-View-Adapter: tail.read_new + events/judge_events.build_view → SSE-Fragmente (G7)
templates/ Jinja2 (HTMX-Fragmente, App-Shell) [net-new, G11]
static/ vendored htmx.min.js, alpine.min.js, CSS (offline-fähig, kein Build-Schritt)
cli.py bekommt einen gui-Befehl (lazy import von touchstone.gui.app; ImportError → „installiere pip install -e .[gui]"), startet uvicorn und öffnet den Browser (--no-open unterdrückt).
pyproject.toml: [project.optional-dependencies] gui = ["fastapi", "uvicorn", "jinja2", "python-multipart"]. Markdown-Renderer bewusst nicht (Station 5 rendert Zahlen, nicht .md — §11). Vendored JS/CSS statt CDN (offline-fähig, build-frei).
Refactor in cli.py (v1-Voraussetzung, kein Detail):
- Event-Writer von Monitor-Spawn entkoppeln: Heute werden _eval_event_writers / _judge_event_writers nur innerhalb des with _live_monitor(...)-Blocks (hinter if web:) verdrahtet. Für --emit-events (Writer ohne Monitor) müssen eval_cmd/judge so umstrukturiert werden, dass „Event-Writer öffnen" von „webmon spawnen" trennbar ist. Garantie: ohne Flag (weder --web noch --emit-events) bleibt der Pfad byte-identisch (kein Event-File, keine Callbacks) — abgesichert durch die existierenden test_eval_web / test_cli_judge_web-Regressionstests.
- --run-dir <path> (eval & judge): exaktes run_dir von außen vorgeben statt selbst-erfinden.
- master_rows-Helper herausfaktorisieren (heute cli._master_rows): wird von Station 1 und 5 gebraucht, um das Urteil pro Bundle zu (re)berechnen → in ein wiederverwendbares, GUI-importierbares Modul (z. B. scorecard.py oder ein kleines report-Helper) heben.
6 · Control-Plane, Run-Sentinel & Lauf-Registry (der greenfield-Kern)¶
run_dir host-seitig (Blocker-Fix): ProcessLauncher.start_eval berechnet das run_dir selbst (output_path()/f"{ts}_eval_{pack_id}"), legt es an, schreibt das Sentinel, und spawnt mit --run-dir <dieses>. Damit ist RunHandle.run_dir beim Spawn bekannt und der live.py-Tail-Pfad deterministisch.
Run-Sentinel run.json (transient, G4/G10) — host-seitig ins run_dir geschrieben, bevor der Subprozess startet, gelöscht/als beendet markiert beim sauberen Ende:
{ kind: "eval"|"judge", run_dir, pid, pack_path, config_path|judge_config_path, started_ts, state }.
Trägt drei Funktionen zugleich: (1) run_dir-Handle, (2) Cross-Process-Lock für G8, (3) Discovery-Anker für laufende/abgestürzte Läufe (§8).
Lauf-Registry (in-memory, gespiegelt aufs Sentinel): hält höchstens einen aktiven RunHandle; state ∈ {running, finished, failed, stopped}.
ProcessLauncher-Protocol (injizierbar, wie Sampler) — Felder pro kind explizit:
- start_eval(pack_path, config_path, *, resume_dir: Path|None) -> RunHandle — Popen [sys.executable, "-m", "touchstone", "eval", "--pack", …, "--config", …, "--run-dir", <host>, "--emit-events"] (+ --resume <resume_dir>); RunHandle{kind="eval", run_dir, pid, pack_id (aus pack), config_path}.
- start_judge(bundle, judge_config_path) -> RunHandle — […, "judge", "--bundle", …, "--judge-config", …, "--emit-events"]; judge hat kein --resume (Resume = erneuter Aufruf, überspringt schon bewertete Zellen) und kein pack_id-Input (Pack kommt aus bundle.json); RunHandle{kind="judge", run_dir=bundle, pid, judge_config_path}. --judge-config ist Pflicht — fehlt sie, beendet sich judge mit Exit-Code 1; die UI muss eine judge-Config wählen lassen (Station 3).
- stop(handle) — SIGTERM → wait(timeout=5) → kill (das _SamplerProcess-Teardown-Muster).
- poll(handle) — proc.poll(); None→running, 0→finished, sonst→failed (Exit 1 ohne judge-Config wird als „failed: judge-config fehlt" gemeldet, nicht als stiller Crash).
G8-Durchsetzung (Sentinel-Lock): start_* lehnt ab, wenn ein aktives Sentinel existiert (run.json mit state=running und lebendem PID) — auch nach GUI-Neustart, weil das Sentinel auf Platte liegt. Stale Sentinel (PID tot) wird beim Start aufgeräumt.
7 · Live-View — Datenmodell wiederverwendet, Präsentation neu (G7/G11)¶
Der GUI-Live-Endpoint ist ein eigener SSE-Stream (FastAPI StreamingResponse), der pro Tick:
1. tail.read_new(events_path, offset) → neue Zeilen + Offset,
2. events.build_view(all_events) bzw. judge_events.build_view(...) (die schon puren Falter: Histogramm, ETA, Dedup-per-Key, Master-Faltung) — verbatim wiederverwendet,
3. das .as_dict()-View-Model in ein neues HTMX/Jinja-Fragment (oder JSON→Alpine) rendert.
Für eval zusätzlich loadview.latest_load(resources.jsonl) (RAM/Pressure/Throttle); für judge nicht (anderer Endpoint, Bundle-resources.jsonl stale — TAILS_RESOURCES=False-Logik). Tail-Offset: eval events.jsonl ist truncate-per-spawn (G10) → Offset startet bei 0 pro Lauf; judge judge_events.jsonl ist ebenfalls truncate-per-run mit prior-Replay → Offset 0 bei (Neu-)Start. Kein Byte der Aggregations-Mathematik wird dupliziert; das gerenderte HTML ist net-new (G11).
8 · Datenfluss, Discovery & Status-Ableitung¶
Die GUI ist downstream-Leser von runs/. Discovery (bundles.py) ist keine flache bundle.json-glob, sondern eine Klassifikation je run_dir (weil bundle.json erst beim erfolgreichen finalize geschrieben wird und runs/ auch run/embed-Dirs enthält):
| Befund im run_dir | Klassifikation |
|---|---|
aktives Run-Sentinel (run.json, PID lebt) |
running (Live-Tail verfügbar) |
Sentinel vorhanden, PID tot, kein bundle.json |
crashed → resumebar (--resume <run_dir>) |
bundle.json + responses.jsonl, kein scores.csv |
eval-only (judge ausstehend) |
bundle.json + scores.csv |
judged (Urteil berechenbar) |
nur raw.csv/report.md bzw. *_embed/ |
legacy run/embed → in v1 ignoriert |
Urteil-Badge (nur „judged"): berechnet, nicht gelesen — load_pack(pack_path) + load_responses_jsonl + load_judgements_jsonl, dann scorecard.weighted_total / red_flagged_prompts / passes_ko / recommendation (= der herausfaktorisierte master_rows-Helper, §5). bundle.json liefert nur das Manifest (pack/models/host/sampling/date), kein Urteil und keinen Status.
Gelesene Kontrakte je Station:
- Übersicht/Vergleich: Sentinel + bundle.json; aggregate.load_all_scores/aggregate über scores.csv.
- Ergebnis: load_responses_jsonl, load_judgements_jsonl, merge.load_samples_jsonl (die 2-Hz-Zeitreihe), Math via scorecard_mod.* (+ load_pack).
- Pack-Explorer: load_pack(pack_path).
- Live: events.jsonl / judge_events.jsonl (Tail), resources.jsonl (eval-Last).
- Export: Ledger-Dateien direkt ausliefern bzw. Renderer (render_scorecard_md, render_aggregate_md, scores_csv_rows) erneut aufrufen. Event-/Sentinel-Dateien sind ausgeschlossen (G10).
Die GUI schreibt nur das Sentinel (transienter Steuer-State); ins Ledger schreiben ausschließlich die Mess-Subprozesse.
9 · Error-Handling¶
- Nur ein Lauf (G8): zweiter Start bei aktivem Sentinel verweigert — überlebt GUI-Neustart (Lock auf Platte). Stale Sentinel (PID tot) wird aufgeräumt.
- Subprozess-Crash (OOM, Endpoint weg):
poll()≠None/0→state=failed; Sentinel bleibt → Discovery zeigt crashed/resumebar. Dank crash-safe Ledger (responses.jsonlappend+flush) bietet die UI „Fortsetzen": eval via--resume <run_dir>; judge durch erneutes Starten auf demselben Bundle (kein Flag — überspringt schon bewertete Zellen). - Resume-Live-Korrektheit: der GUI-gespawnte eval schreibt
events.jsonltruncate-per-spawn (G10), wie judge — behebt denfinished=True-Latch (ein altesrun_donewürde sonst die Live-View fälschlich „fertig" zeigen, während der Resume noch läuft). Schon erledigte Zellen erscheinen in der Ergebnis-Ansicht (ausresponses.jsonl), nicht im Live-Tail. - Stop: SIGTERM → wait(5) → kill; Lauf bleibt fortsetzbar (try/finally + per-cell-Persistenz existieren). Sentinel →
stopped. - GUI-Neustart bei laufendem Lauf: Subprozess läuft eigenständig weiter, schreibt sauber nach
runs/; das Sentinel auf Platte stellt Sicht (Discovery=running) und G8-Lock nach Neustart wieder her. Kein in-memory-PID-Recovery nötig. - Port belegt /
--port 0: uvicorn auf gewähltem Port; für Auto-Port den gebundenen Port aus dem uvicorn-Server-Socket lesen, bevor der Browser geöffnet wird (in-process, kann nicht wie webmon den eigenen stdout lesen — siehe §13). - SSE-Robustheit: Browser-Close beendet den Stream sauber (wie webmon); Server-Task lebt weiter; halbe Schlusszeile beim Tail toleriert.
- Ohne
[gui]-Extra:touchstone gui→ freundlicher Installations-Hinweis, kein Traceback.
10 · Teststrategie (TDD)¶
Konsistent mit der Repo-Philosophie (I/O dependency-injected, pure Logik unit-getestet, mypy strict, ruff). Kern-Tests laufen ohne FastAPI; GUI-Route-Tests skippen, wenn [gui] fehlt.
Unit (pure, kein Server):
- control.py: Registry-Zustandsmaschine; G8 via Sentinel (zweiter Start bei aktivem Sentinel abgelehnt; stale Sentinel mit totem PID aufgeräumt + Start erlaubt; Lock überlebt simulierten Neustart = neue Registry liest Sentinel); stop-Teardown-Sequenz; poll-Mapping inkl. judge-config-fehlt→failed — gegen einen Fake-ProcessLauncher.
- control.py run_dir/Sentinel: Launcher wählt run_dir host-seitig, schreibt Sentinel vor Spawn, reicht --run-dir durch (Fake verifiziert die Argv).
- bundles.py: Klassifikation aller fünf Discovery-Fälle (§8) gegen synthetische run_dirs; Urteil-Recompute stimmt mit scorecard_mod/master_rows überein; legacy run/embed ignoriert; robuste Toleranz fehlender/halber Dateien.
- live.py: Tick faltet getailte Events korrekt (delegiert an die getesteten build_view); eval tailt Last, judge nicht; truncate-per-spawn → Offset-Reset (kein stale finished).
- cli.py-Refactor: --emit-events schreibt Event-File ohne webmon-Spawn; Default ohne Flag byte-identisch (kein Event-File, keine Callbacks) — bestehende Regressionstests grün; --run-dir respektiert.
Integration / Verhalten:
- FastAPI TestClient: jede Stationen-Route 200 + erwartetes Fragment; Start/Stop/Resume rufen den (Fake-)Launcher korrekt; SSE-Endpoint streamt View-Updates.
- Rückwärtskompat: Kern ohne [gui] voll funktionsfähig; touchstone ohne gui-Befehl unverändert.
Live-Smoke (manuell, am Ende): touchstone gui → ndassist-Pack + M5-Config wählen → eval starten; Dashboard zählt live hoch, Stop sauber (kein Zombie), Bundle erscheint; judge-Config wählen → judge starten → Scorecard in Ergebnis-Ansicht; Vergleich zeigt Aggregat; Export lädt scorecard.md. Zusätzlich: eval stoppen → „Fortsetzen" → Live zeigt nicht fälschlich „fertig"; GUI neu starten während Lauf → Discovery zeigt running + zweiter Start verweigert.
11 · Scope-Grenze (YAGNI)¶
In v1 (Skeleton): App-Shell + 7 Stationen je in der „v1"-Spalte aus §4; --run-dir + --emit-events + Event-Writer-Entkopplung + master_rows-Helper-Extraktion (§5); Run-Sentinel + Sentinel-Lock (G8); eval+judge start/stop/resume; Tail-Live-Fortschritt; Ergebnis-Zahlen via scorecard_mod in Jinja-Tabellen (nicht .md-Render); Aggregat-Tabelle; Datei-Download-Export.
Bewusst NICHT in v1 (spätere Specs): .md→HTML-Render (kein Markdown-Dep; v1 rendert die Zahlen direkt); Config-/Pack-Editor im UI; echte interaktive Plots (Zeitreihe/Verteilung/Histogramm); verlinkter Transparenz-Graph; Filter/Suche/Sortierung; parallele Läufe; in-memory-PID-Recovery jenseits des Sentinels; resources.jsonl-Offset-Tailing (v1 nutzt loadview.latest_load wie webmon — Voll-Datei-Read akzeptiert); Auth/Multi-User/Remote (bleibt 127.0.0.1-lokal); Live-Token-Stream.
12 · Berührte / neue Dateien¶
| Datei | Änderung |
|---|---|
touchstone/gui/__init__.py |
neu — Package-Marker |
touchstone/gui/app.py |
neu — FastAPI-App-Factory, Routen der 7 Stationen, static/templates mount |
touchstone/gui/control.py |
neu — Lauf-Registry + ProcessLauncher-Protocol + run_dir-Wahl + Run-Sentinel + G8-Lock |
touchstone/gui/bundles.py |
neu — Discovery/Klassifikation (§8) + Urteil-Recompute (geteilter master_rows-Helper) |
touchstone/gui/live.py |
neu — Live-View-Adapter (tail + build_view → SSE-Fragment), G7 |
touchstone/gui/templates/, touchstone/gui/static/ |
neu — Jinja2-Fragmente + vendored HTMX/Alpine/CSS (G11) |
touchstone/cli.py |
gui-Befehl (lazy import); --run-dir (eval+judge); --emit-events + Event-Writer von _live_monitor entkoppeln (Default byte-identisch); _master_rows-Helper herausfaktorisieren |
pyproject.toml |
[gui]-optional-dependency-Gruppe (fastapi/uvicorn/jinja2/python-multipart) |
tests/ |
neue Tests je §10 (Kern ohne FastAPI; GUI-Routen optional/skip ohne Extra) |
AGENTS.md |
touchstone gui in Befehlsliste; Architektur-Notizen: GUI = optionales [gui]-extra spawnt Mess-Subprozesse mit --run-dir/--emit-events; runs/=SSOT; Run-Sentinel run.json (transient, G8-Lock); event-Files non-SSOT; ein Lauf zur Zeit |
13 · Offene Detailpunkte (für den Implementierungsplan)¶
- Tailwind build-frei: vendored Utility-CSS vs. Play-CDN vs. handgeschriebenes CSS im Tailwind-Stil — Abwägung Offline-Fähigkeit ↔ Aufwand.
- uvicorn Auto-Port: bei
--port 0den OS-zugewiesenen Port aus demuvicorn.Server-Socket (server.servers[*].sockets[0].getsockname()) lesen, bevorwebbrowser.open— der webmon-Trick (eigenen stdout lesen) geht in-process nicht. - SSE-Render-Form: HTMX-Fragment-Swap (server-rendert HTML) vs. JSON→Alpine (client-rendert) für die Live-Karte — vermutlich HTMX fürs Skelett, Alpine wo Client-State nötig.
master_rows-Zielmodul: inscorecard.py(zur restlichen Scorecard-Math) vs. eigenes kleines Helper-Modul — so dass weder GUI noch CLI zirkulär importieren.