B2 — Cross-Run-Vergleichsseite¶
Datum: 2026-06-25 · Projekt: B „GUI-Restrukturierung", Sub-Projekt 2 von 3 Status: Design freigegeben, bereit für Implementierungsplan
Kontext & Motivation¶
/compare (compare.html, 53 Z.) zeigt heute automatisch alle bewerteten Läufe als
flache Aggregat-Tabelle: aggregate.aggregate(load_all_scores(runs_dir)) gruppiert jede
scores.csv-Zeile nach (chip, ram_gb, pack, pack_version, model, quant, variant), mittelt
Wiederholungen, sortiert nach Chip+Qualität. Keine Auswahl, kein Fokus — bei vielen Läufen
unübersichtlich, und Wiederholungen desselben Setups verschwinden im Mittel.
Der Projektkern ist Cross-System-Vergleichbarkeit: derselbe Code auf M1 (LM Studio) und M5 (mlx) — „nur die Config ändert sich" — und publizierbare, von vielen beigesteuerte Läufe. Genau dafür fehlt eine kuratierte Vergleichssicht: konkrete Läufe auswählen (auch importierte fremde) und strukturiert gegenüberstellen.
Ziel: /compare wird eine Auswahl-+-Vergleichsseite — Läufe wählen/importieren, dann ein
Auto-Diff-Vergleich der gewählten Zeilen.
Design-Entscheidungen (im Brainstorming ratifiziert)¶
- Universelle Seite, eine Mechanik. Cross-Machine, Cross-Model/Quant und kuratierter Community-Vergleich sind Lesarten derselben „N Zeilen auswählen → gegenüberstellen"- Seite, nicht getrennte Features. Was sie unterscheidet: was variiert und was betont wird.
- Vergleichseinheit = Einzelzeile
(HW × Modell × Quant × Variante)— die feinste vergleichbare Einheit (genau eine Zahl pro Metrik). - Ersetzen/integrieren — eine Seite. B2 wird
/compare: Auswahl-Pool oben, Vergleich unten. Keine zweite Route, keine Duplikation (dieselbe Konsolidierungs-Linie wie B1). - Auto-Diff-Hervorhebung. Die Seite erkennt automatisch, welche Dimensionen über die Auswahl variieren (→ Spalten-Köpfe) und welche konstant sind (→ einmal als „GEMEINSAM" oben). Kein manueller Achsen-Modus.
- Auswahl-Einheit = einzelne Bundle-Zeile (
run_dir × model × variant), nicht die gemittelte Gruppe — so sind zwei Läufe desselben Setups gegenüberstellbar (Varianz/ Regression), und Provenance (Datum, „importiert") bleibt sichtbar. Stabile ID:run_dir|model|variant. - Server-seitiger
?rows=-Vergleich. Checkboxen sammeln clientseitig (Alpine); ein „Vergleichen"-Button schickt die IDs als Query-Param → server rendert (verlinkbar/druckbar, wie B1's Achsen-Umschalter). - Import = simples Upload-Feld über den bestehenden
/import-bundle-Endpoint (Drag-Drop = spätere Politur, YAGNI).
Nicht-Ziele (Scope-Grenzen)¶
- Später, eigene Specs: Effizienz-Scatter cross-run (Wiederverwendung
scatter.js), Provenance-Badges, Drill-down zu einzelnen Antworten, Permalink-/Teilen-Funktion. /compare/{name}(der B1-Redirect) bleibt unberührt.- B3 (Konfig+Start / Pack-Editor) ist nicht Teil von B2.
- Keine neuen Metriken; rein Auswahl-/Darstellungs-/Diff-Logik über den bestehenden
scores.csv-Daten (und damitresult.json/Projekt A als Quelle).
Architektur¶
Seiten-Struktur (eine Scroll-Seite)¶
Zone 1 — Auswahl-Pool (oben):
- Die Tabelle aller verfügbaren Bundle-Zeilen (chip, ram, pack, model, quant, variant +
Datum/Provenance) mit Auswahl-Checkbox je Zeile und stabiler ID run_dir|model|variant.
- Filter (client-seitig, Alpine): Chip / Pack / Modell.
- Import-Feld: Datei-Upload → /import-bundle; nach Erfolg Reload, der Lauf erscheint im
Pool.
- „Vergleichen"-Button: sammelt die angehakten IDs → navigiert zu /compare?rows=….
Zone 2 — Auto-Diff-Vergleich (unten, ab ≥2 gewählten):
- GEMEINSAM-Block: Dimensionen, die über alle gewählten Zeilen konstant sind, einmal oben.
- Spalten = die gewählten Zeilen, Kopf zeigt die variierenden Dimensionen.
- Metrik-Zeilen: Qualität %, TTFT P50, Decode-Median, Peak-RAM, Modell-Delta — mit
Winner-Marker 🏆 pro Metrik (Gleichstand → keiner, wie B1's _winners).
Datenfluss & Code¶
aggregate.pyist heute pure Logik. Die bestehendeaggregate()(über Bundles gemittelt) bleibt unverändert — sie speist den CLI-Pfadtouchstone aggregate(scores_all.csv+ md). B2 fügt eine separate, pro-Bundle-Funktion hinzu:- Eine Liefer-Funktion, die pro Bundle (nicht über Bundles gemittelt) eine Zeile mit
run_dir-Herkunft + stabiler IDrun_dir|model|variantliefert.load_all_scoresmuss dierun_diraus dem Dateipfad anreichern (additiv, ohne die gemittelteaggregate()- Nutzung zu brechen). - Eine pure Funktion
diff_rows(selected_rows) -> (common: dict, varying: list[str], columns: list[Row]): ermittelt konstante vs. variierende Dimensionen + die Spalten. - Eine
winners(columns) -> dict[metric, id|None]-Funktion (oder Wiederverwendung des B1-Musters) für die 🏆-Marker; Gleichstand → None. - Route
GET /comparenimmt optional?rows=<id>,<id>,…: ohne → nur Pool; mit ≥2 gültigen → Pool + Vergleich. Unbekannte/kaputte IDs werden ignoriert (nie 500). compare.htmlneu; Sektionen in Partials (templates/macros/, Muster wie B1's_compare_block.html), damit keine überladene Datei entsteht.
Tests¶
- Pure Logik (unit):
diff_rows— common/varying-Erkennung (alle gleich, eine Dim variiert, mehrere variieren), Einzel-Bundle-Zeilen-IDs (zwei Läufe gleichen Setups bleiben unterscheidbar), Winner inkl. Gleichstand→None, Edge-Cases 0/1/N Zeilen. - Route (TestClient):
/compareohnerows(nur Pool), mit gültigenrows(Vergleich rendert GEMEINSAM + variierende Köpfe), leere/kaputterows(kein 500), Import-Roundtrip (Upload → Lauf erscheint im Pool). - Bestehende
aggregate-Tests bleiben grün (oder werden um dierun_dir-Herkunft erweitert, ohne die Cross-Run-Aggregat-Semantik zu brechen).
Risiken & Gegenmaßnahmen¶
- Zeilen-ID-Stabilität:
run_dir|model|variantmuss URL-sicher sein (run_dir-Namen sind Zeitstempel-Slugs — safe; defensiv encoden). scores.csvträgt keinerun_dir: die Herkunft kommt aus dem Dateipfad beim Laden —load_all_scoresmuss sie anreichern, ohne die bestehende Aggregat-Nutzung zu brechen.- Pool wird groß bei vielen Läufen → client-seitige Filter; Pagination ist Future, nicht MVP.
compare.htmlwächst → Partials/Macros (wie B1).- Verwechslung
/compare(diese Seite) ↔/compare/{name}(B1-Redirect): in Spec + Tests getrennt; der Redirect bleibt unangetastet.