Skip to content

feat(dashboard): Versions×PDF-Paarvergleich mit Schnittmengen-Delta#305

Merged
TillQuandel merged 3 commits into
masterfrom
feat/version-pdf-paarvergleich
Jul 15, 2026
Merged

feat(dashboard): Versions×PDF-Paarvergleich mit Schnittmengen-Delta#305
TillQuandel merged 3 commits into
masterfrom
feat/version-pdf-paarvergleich

Conversation

@TillQuandel

@TillQuandel TillQuandel commented Jul 15, 2026

Copy link
Copy Markdown
Owner

Motivation

Multi-Perspektiven-Dashboard-Review 2026-07-15 (3 Statistiker, P1-Empfehlung aller drei, vom adversarialen Statistiker modifiziert). Kernbefund aus #293: gepoolte Versions-Deltas über den vollen PDF-Corpus sind Artefakte, sobald sich der PDF-Mix zwischen zwei Versionen ändert — v0.3.140 vs v0.3.143 teilten nur 3 von 9/5 PDFs, ein +2,7pp-Hall-Delta war größtenteils Corpus-Wechsel, kein echter Versions-Effekt. #293 hat das für ein Sparkline-Delta gefixt (version_delta, Corpus-Overlap-Guard gegen die letzte belastbare Vorversion). Dieser PR erweitert das Prinzip auf eine frei wählbare Versions×PDF-Matrix mit explizitem Paarvergleich.

Datenmodell

generative/eval_dashboard.py (neu, reine Aggregation ohne I/O):

  • _matrix_cell_stats(rows) — Median-Fehlerquote/-Belegrate + n + Min/Max-Spanne für eine (PDF, Version)-Zelle. Dedupliziert selbst per _dedup_latest_per_note (fix(eval-dashboard): Re-Eval-Dedup je Note in KPI-Aggregation + Corpus-Overlap-Guard fuer Versions-Delta #293) — keine Pseudoreplikation durch Re-Evals. n ist Pflichtfeld; None nur wenn nach Dedup gar keine Zeile existiert.
  • _calc_version_pdf_matrix(quality_rows, versions=None) — Matrix-Rohdaten (Versionen × kanonische PDF-Gruppen, _pdf_group_key-SSoT mit dem bestehenden PDF-Filter). Versions-Auswahl ohne expliziten Parameter über _top_versions (dieselbe 15-neueste-mit-n≥3-Deckelung wie das bestehende Dropdown), anschließend chronologisch aufsteigend sortiert.
  • _version_pair_compare(quality_rows, version_a, version_b) — Paarvergleich zweier Versionen:
    • Deltas NUR auf Schnittmengen (Spezifikation Refine-Redesign: iterative Ueberarbeitung statt Neugenerierung #1): overall-Delta poolt ausschließlich Notes der von beiden Versionen geteilten PDFs. Ein Regressionstest belegt das direkt (gepoolt über den vollen Corpus wäre das Delta stark verzerrt, über die Schnittmenge korrekt).
    • Note-Paarung wo möglich (Spezifikation ER-Stage-1-Threshold bei Praefix-dominanten Titeln #2): teilen zwei Versionen für dieselbe PDF echte _note_key-Identität, wird nur über die gemeinsamen Notes verglichen (paired=True, n_paired); sonst PDF-Ebene (paired=False) — der Client kennzeichnet das sichtbar. Eigener Regressionstest gegen eine Scheinpaarung: _note_keys Index-Fallback für Rows ohne echten Identifier darf zwei unabhängige Zeilen aus verschiedenen Versionen NICHT über zufällig gleiche Listenposition als "gepaart" erkennen (_real_note_key filtert das explizit heraus).
  • build_version_pdf_matrix(quality_rows) — Server-Fassade: Matrix + alle paarweisen Vergleiche vorab (bis 15·14/2=105 Paare, für den lokalen Server unproblematisch).

generative/eval_dashboard_server.py: eigene Rohdaten-Basis (_matrix_base_rows, Snapshot direkt nach dem eval_version-Filter) — gescoped auf die aktive eval_version (Spezifikation #5: 1.3/4.1/4.3 dürfen nie gemischt werden), aber bewusst unabhängig von den bestehenden Einzelwert-Filtern (PDF/Version/Modell/Sprache/Lauf) in der Sidebar, sonst kollabiert die Matrix auf 1 Zelle und verfehlt ihren Zweck. Bestehender Archiv-Mechanismus (pipeline_runs_archive/query_archived_pipeline_versions, #193 — bisher nur für Versions-Bump-Kollisionsschutz genutzt) schließt archivierte WIP-Versionen aus und macht das kenntlich (excluded_archived_versions, Spezifikation #4). Ein separates, note-/zeilen-genaues Quarantäne-Flag existiert im Schema NICHT (nur das Versions-Archiv) — die bekannte, noch nicht quarantänierte #232-Artefakt-Zeile (Hrastinski, Lauf 20260712-215118) wird deshalb nicht hartkodiert ausgeschlossen; sie fließt ein, ihre Dominanz wird über Zell-n + Min/Max-Tooltip sichtbar statt versteckt.

internal/dashboard/eval_dashboard.html (Hinweis: der Task-Auftrag nannte generative/eval_dashboard.html — diese Datei existiert nicht; die tatsächliche Frontend-Vorlage ist internal/dashboard/eval_dashboard.html, wird von eval_dashboard_server.py per _HTML_TEMPLATE_PATH geladen): neue Section „Versions×PDF-Paarvergleich" (Nav-Item, Matrix-Tabelle im bestehenden .cmp-Stil, Farbcodierung über die etablierten good-t/warn-t/bad-t-Klassen, Tooltips über das bestehende title-Attribut-Muster). Zellen mit n<3 gedimmt (.pm-low-n, Opacity 0.5), nie versteckt (Spezifikation #3). Zwei Dropdowns für den Versionspaar-Vergleich (Default: die zwei jüngsten Versionen), Delta-Tabelle je gemeinsamer PDF-Quelle inkl. Paarungs-Badge, Hinweis auf nicht geteilte Quellen.

RED-Nachweis

23 neue Tests (generative/tests/test_dashboard_version_pdf_matrix.py) vor der Implementierung geschrieben — Import-Fehler (Funktionen existierten noch nicht), danach 5 Server-Integrationstests zusätzlich RED (KeyError: 'pair_matrix') bis der Server-Wire-up stand. Beide RED-Läufe verifiziert, dann GREEN implementiert.

Suite / ruff / NUL-Byte

  • Volle Suite (pytest generative lib/decision_engine/tests shared/tests): 6014 passed, 3 skipped (identisch zum Stand vor diesem PR — keine Regression).
  • ruff check + ruff format --check auf allen geänderten Dateien: clean.
  • NUL-Byte-Check internal/dashboard/eval_dashboard.html: nach jedem Edit per Python geprüft, konstant 1 (das eine gewollte Byte, unverändert).

Sichtprüfung

Testserver auf Port 8069 (eigenes Bootstrap-Skript ohne webbrowser.open()), Datenbasis: read-only Kopie der echten Produktionsdaten (.cache/atomic_analytics.db + generative/.cache/{quality_history.jsonl,runs,eval/baseline}) in den Worktree kopiert — Original im Hauptrepo nicht angerührt. Playwright (playwright-core + firefox-1532-Build, MCP ist bekanntlich defekt), hell + dunkel, mit echten Klicks:

  • Sidebar-Nav „Paarvergleich" scrollt die Section ins Bild.
  • Matrix zeigt 25 PDF-Quellen × 15 Versionen, 105 vorab berechnete Vergleichspaare.
  • Zell-Tooltip zeigt reale Werte, z. B. n=4 Note(n) · Spanne 0,0–14,3 % (Min–Max über die 4 Notes).
  • 6 Zellen mit n<3 gedimmt, hidden: false bestätigt (nicht versteckt).
  • Dropdown-Wechsel (Version B) ändert die Vergleichstabelle nachweislich.
  • Default-Auswahl beim ersten Render ist v0.3.140/v0.3.143 — reproduziert den fix(eval-dashboard): Re-Eval-Dedup je Note in KPI-Aggregation + Corpus-Overlap-Guard fuer Versions-Delta #293-Befund ohne Interaktion: „3 von 9/5 PDFs gemeinsam · 36 % der Notes von v0.3.143 teilen eine Quelle mit v0.3.140". Hrastinski (2008) erscheint dort notengepaart (n=1) mit Fehlerquote A=0,0 %, B=42,9 % — exakt der im Auftrag genannte Signaturwert des Eval-Verifier: possible_retrieval_miss blaeht Halluzinationsrate auf — 5/6 FP-Anker handverifiziert (Hrastinski, v0.3.143) #232-Artefakts, live aus echten Daten reproduziert, nicht versteckt.
  • Regressionscheck bestehender Funktionen: aktiver PDF-Filter (#global-pdf) lässt die Matrix unverändert den vollen Corpus zeigen (Design-Entscheid bestätigt); bestehender KPI-Kachel-Klick (Spark-Row) weiterhin intakt.
  • 0 Page-Errors in beiden Themes.

Screenshots: C:/tmp/pairview-verify/ (pairmatrix-overview-{LIGHT,DARK}.png, pairmatrix-compare-{LIGHT,DARK}.png, pairmatrix-compare-DEFAULT-140-143-LIGHT.png).

Nachbesserung 1 — D4-Anti-Pattern (Spec-Review, Commit 77fefc8)

Beide neuen Metrik-Funktionen (_matrix_cell_stats, _pair_metric_stats) nutzten coverage_factual or coverage_rate — das D4-Anti-Pattern aus dem Dashboard-Review 2026-07-15: or verschluckt eine echte coverage_factual=0.0 (falsy) und nimmt fälschlich coverage_rate bzw. verwirft die Zeile, wenn coverage_rate fehlt (0%-Coverage-Zeilen existieren real, Jockisch-Fälle). Fix: geteilte Hilfsfunktion _row_coverage()coverage_factual wenn nicht None (auch bei 0.0), nur bei None Fallback auf coverage_rate. 4 neue Regressionstests, RED verifiziert (80.0 statt 0.0 bzw. None statt 0.0). Die Bestands-Stellen mit demselben Muster (D4 nennt 5, u. a. eval_dashboard.py:610/933/1005/1523) bleiben unangetastet (separates Ticket). Suite: 6018 passed, 3 skipped; ruff clean.

Nachbesserung 2 — Adversariale Opus-Statistiker-Abnahme (Commit 73984bb)

B1+B2 (Kernfix): overall gepoolt statt Median + zeilen-konsistente Auswahl. Das Gesamt-Aggregat des Paarvergleichs war doppelt unsauber: (a) Median kollabierte bei der zero-inflated hallucination_rate auf 0,0pp — die Default-Headline 140/143 zeigte „0,0pp", obwohl 2 von 3 gemeinsamen PDFs sichtbar regredieren (hrastinski 0→42,9; schlebbe 9,1→20,0); exakt die Falle, die _pooled_hall_stats für die KPI-Kachel dokumentiert. (b) overall poolte ungepaart (12/8 Notes), während die per-PDF-Zeilen die gepaarte Teilmenge zeigen (4/4) — n-Widerspruch in der Anzeige. Fix: overall nutzt jetzt die ankergewichtet gepoolte Fehlerquote (_pooled_hall_pct, SSoT mit der KPI-Kachel, Mean-Fallback ohne Roh-Counts) über exakt die Zeilen-Auswahl (gepaart wo paired, sonst PDF-Ebene) — Headline-n == Summe der Zeilen-n. Coverage im overall: notengewichtetes Mittel mit demselben Gültigkeitsfilter (kein Anker-Pool für Coverage möglich — note_evals trägt keine Claim-Roh-Counts; dokumentiert). Zell-/Zeilen-Mediane bleiben (mit Min/Max abgenommen); notes_overlap_b bleibt bewusst auf voller PDF-Ebene (Corpus-Überlappungs-Semantik wie version_delta.pdf_overlap, dokumentiert).

Neue Zahlen Default-Paar 140/143 (echte Datenkopie, read-only): Headline vorher „0,0pp (n=12/8)" → jetzt „+14,0pp — 3,3 % → 17,3 % (n=4/4 Notes, Zeilen-Basis)". Begründete Abweichung vom Audit-Wert +4,42pp: das Audit rechnete ankergewichtet auf der ungepaarten 12/8-Basis — reproduziert (5,7 % → 10,2 % = +4,5pp); die vom Audit ebenfalls geforderte zeilen-konsistente (gepaarte 4/4) Basis lässt die Hrastinski-Artefaktzeile (42,9 %) 1 von 4 statt 1 von 8 B-Notes stellen → +14,0pp. n-Konsistenz Headline == Zeilen-Summe (4/4 == 1+1+2) verifiziert.

B3 (Disclosure): PDF-Key-Drift-Hinweis im UI. Leere Schnittmenge (common_pdfs=[]) bei nicht-leeren only_a/only_b zeigt jetzt zusätzlich: „Hinweis: möglicherweise PDF-Key-Drift — dieselbe Quelle kann unter verschiedenen Keys geführt sein (#194/#202); siehe die Listen unten." Keine Heuristik-Zusammenführung (separates Ticket).

B4 (Cap-Disclosure): n_versions_dropped. Der 15er-Cap (_MATRIX_VERSION_LIMIT, min. n≥3) droppte real 35 von 50 Versionen ohne Kennzeichnung. Neu: n_versions_dropped in der Payload (analog excluded_archived_versions: Versionen mit Eval-Zeilen, die nicht gezeigt werden) + Fußnote im UI („35 weitere Version(en) mit Eval-Daten nicht gezeigt (Top-15-Deckelung, min. n≥3 je Version — die neueste immer dabei)").

Tests: 9 neue (RED verifiziert: Median-Kollaps 0,0 statt 10,0; n=2 statt 1; cov-Median 10,0 statt Mean 30,0; 3× KeyError; 2× fehlende HTML-Anker); bestehende Overall-Tests blieben unverändert grün (homogene Fixtures: Mean==Median). Suite: 6027 passed, 3 skipped; ruff clean; NUL-Byte nach HTML-Edits: 1 (unverändert). Sichtprüfung hell+dunkel mit Klick-Pfaden (inkl. Dropdown-Wechsel auf leere Schnittmenge 140 vs. 60): 0 Page-Errors; Screenshots C:/tmp/pairview-verify/nachbesserung-*.png.

Offene Punkte

  • PDF-Key-Fragmentierung (per-PDF-Tabelle: drei Datengrundlagen in einer Zeile, Substring-Doppelzählung, pdf_key-Drift, fehlende PDFs #194/PDF-Filter allein bricht mehrere Panels: leere Charts ohne Meldung, Vergleichstabelle ohne Zeilen, Bates matcht v0.1.0-Kosten #202-Linie, verschärft durch die adversariale Abnahme, B3): dieselbe Quelle wird je nach Pipeline-Ära unter zwei Gruppen-Keys geführt (bates-2017 vs. bates-information-behavior). Konsequenz in dieser Ansicht: bei jedem v0.3.143-vs-Alt-Paar, dessen Überschneidung nur über eine drift-betroffene Quelle liefe, ist common_pdfs leer — kein Overall-Delta berechenbar, Bates fällt still aus jedem Schnittmengen-Vergleich heraus, obwohl die Quelle in beiden Versionen evaluiert wurde. Die Matrix zeigt beide Zeilen und der leere Vergleich trägt seit 73984bb einen Drift-Hinweis, aber die Zusammenführung selbst (Normalisierungs-Heuristik oder manuelles Alias-Mapping) ist ein separates Ticket — bewusst keine Heuristik in diesem PR. Bis dahin sind Paarvergleiche über drift-betroffene Quellen systematisch zu konservativ (Schnittmenge zu klein oder leer).
  • eval_version ist am Panel sichtbar (eval_version <Wert> im Section-Header), aber nicht separat filterbar — der globale eval_version-Dropdown wurde an anderer Stelle bereits bewusst entfernt (Server nutzt default = neueste). Die Matrix folgt derselben Konvention.
  • Till-Entscheid zur Quarantäne der Eval-Verifier: possible_retrieval_miss blaeht Halluzinationsrate auf — 5/6 FP-Anker handverifiziert (Hrastinski, v0.3.143) #232-Artefakt-Zeile (eval-Run 20260712-215118) bleibt bewusst offen — dieser PR ändert daran nichts, macht die Zeile aber sichtbar statt sie zu verstecken. Hinweis: Auf der (neuen) gepaarten Overall-Basis wiegt diese eine Zeile schwerer (1 von 4 statt 1 von 8 Notes im Default-Paar) — der Tooltip/n=1-Ausweis an der Zeile macht das kenntlich.

Tilltime added 3 commits July 15, 2026 22:37
Neue Dashboard-Ansicht "Paarvergleich" (Multi-Perspektiven-Dashboard-Review
2026-07-15, P1-Empfehlung aller 3 Statistiker, vom adversarialen Statistiker
modifiziert): Matrix-Tabelle Median-Fehlerquote/-Belegrate je Pipeline-Version
x PDF-Quelle, plus paarweiser Versions-Vergleich mit Deltas NUR auf der
PDF-Schnittmenge zweier gewaehlter Versionen.

Kernbefund (#293): gepoolte Versions-Deltas ueber den vollen Corpus sind
Artefakte, sobald sich der PDF-Mix zwischen zwei Versionen aendert (v0.3.140
vs v0.3.143 teilten nur 3 von 9/5 PDFs). #293 hat das fuer EIN Sparkline-Delta
gefixt (version_delta Corpus-Overlap-Guard); diese Ansicht erweitert das
Prinzip auf eine frei waehlbare Versions×PDF-Matrix.

Datenmodell (generative/eval_dashboard.py):
- _matrix_cell_stats: Median-Fehlerquote/-Belegrate + n + Min/Max-Spanne je
  (PDF, Version)-Zelle, dedupliziert per _dedup_latest_per_note (#293) --
  keine Pseudoreplikation durch Re-Evals.
- _calc_version_pdf_matrix: Matrix-Rohdaten, Versions-Auswahl per
  _top_versions (SSoT mit dem bestehenden Dropdown), aufsteigend sortiert.
- _version_pair_compare: Paarvergleich zweier Versionen. Deltas NUR ueber
  gemeinsame PDFs (Requirement 1); Note-Paarung ueber echte _note_key-
  Uebereinstimmung wo vorhanden, sonst PDF-Ebene mit paired-Kennzeichnung
  (Requirement 2). Ein Regressionstest haertet gegen den Index-Fallback von
  _note_key (waere sonst eine Scheinpaarung ueber Zeilen ohne Identifier).
- build_version_pdf_matrix: Server-Fassade, berechnet alle Versionspaare
  vorab (bis zu 15x14/2=105, unproblematisch fuer den lokalen Server).

Server (generative/eval_dashboard_server.py): eigene Rohdaten-Basis
(_matrix_base_rows), gescoped auf die aktive eval_version (Requirement 5),
aber bewusst UNABHAENGIG von den bestehenden Einzelwert-Filtern (PDF/Version/
Modell/Sprache/Lauf) -- sonst kollabiert die Matrix auf 1 Zelle. Bestehender
Archiv-Mechanismus (pipeline_runs_archive/query_archived_pipeline_versions,
#193) schliesst archivierte WIP-Versionen aus UND macht das kenntlich
(excluded_archived_versions) -- Requirement 4. Der bekannte, NICHT
quarantaenierte #232-Artefakt-Lauf wird NICHT hartkodiert ausgeschlossen;
Zell-n und Tooltip (Min/Max-Spanne) machen Einzelzeilen-Dominanz sichtbar.

Frontend (internal/dashboard/eval_dashboard.html): neue Section
"Versions×PDF-Paarvergleich" (Nav-Item, Matrix-Tabelle im bestehenden
.cmp-Stil, Farbcodierung ueber die etablierten good-t/warn-t/bad-t-Klassen).
Zellen mit n<3 gedimmt, nie versteckt (Requirement 3). Zwei Dropdowns fuer
den Versionspaar-Vergleich (Default: die zwei juengsten Versionen), Delta-
Tabelle je gemeinsamer PDF-Quelle inkl. Paarungs-Kennzeichnung, Hinweis auf
nicht geteilte Quellen.

Tests: 23 neue (generative/tests/test_dashboard_version_pdf_matrix.py), RED
vor Implementierung verifiziert. Volle Suite 6014 passed/3 skipped (vorher
identisch), ruff clean. Sichtprüfung gegen eine read-only Kopie der echten
Produktionsdaten (Playwright/firefox-1532, hell+dunkel, 0 Page-Errors)
reproduziert den #293-Befund exakt: 3/9/5 PDFs gemeinsam, 36% Notes-Overlap,
und zeigt fuer Hrastinski (2008) eine notengepaarte Zeile (n=1) mit dem im
Task genannten 42,9%-Signaturwert -- der Artefakt-Lauf fliesst unveraendert
ein, seine Dominanz ist ueber n=1 und die Paarungs-Kennzeichnung sichtbar.
…per or verschlucken

Spec-Review-Befund am PR: beide neuen Metrik-Funktionen (_matrix_cell_stats,
_pair_metric_stats) nutzten `coverage_factual or coverage_rate` — das bekannte
D4-Anti-Pattern aus dem Dashboard-Review 2026-07-15. `or` verschluckt eine
ECHTE coverage_factual=0.0 (falsy) und nimmt faelschlich coverage_rate bzw.
verwirft die Zeile komplett, wenn coverage_rate fehlt. 0%-Coverage-Zeilen
existieren real in den Daten (Jockisch-Faelle).

Fix: geteilte Hilfsfunktion _row_coverage() — coverage_factual, wenn NICHT
None (auch bei 0.0), NUR bei None Fallback auf coverage_rate. Beide neuen
Stellen umgestellt; die Bestands-Stellen mit demselben Muster (D4 nennt 5,
u.a. eval_dashboard.py:610/933/1005/1523) bleiben unangetastet (separates
Ticket, ausserhalb dieses PR-Scopes).

Tests: 4 neue Regressionstests, RED vor dem Fix verifiziert (80.0 statt 0.0
bzw. None statt 0.0 an drei Stellen; der legitime None->coverage_rate-
Fallback-Fall war schon gruen und bleibt es):
- coverage_factual=0.0 + coverage_rate=0.8 -> median_cov 0.0 (nicht 80.0)
- coverage_factual=0.0 + coverage_rate=None -> zaehlt als 0.0 (nicht verworfen)
- coverage_factual=None + coverage_rate=0.8 -> 80.0 (Fallback bleibt)
- derselbe 0.0-Fall im Paarvergleichs-Pfad (cov_a/cov_b/cov_delta)

Volle Suite: 6018 passed, 3 skipped. ruff check + format: clean.
HTML unveraendert (kein NUL-Byte-Check noetig).
…p- und Key-Drift-Disclosure

Adversariale Opus-Statistiker-Abnahme zu PR #305, drei Befunde:

B1+B2 (Kernfix) -- overall in _version_pair_compare war doppelt unsauber:
(a) Median kollabierte bei der zero-inflated hallucination_rate auf 0,0pp
(Audit-Beleg Default-Paar 140/143: Headline "0,0pp", obwohl 2 von 3
gemeinsamen PDFs sichtbar regredieren; dieselbe Falle, die _pooled_hall_stats
fuer die KPI-Kachel dokumentiert). (b) overall poolte UNGEPAART (12/8 Notes),
waehrend die per-PDF-Zeilen die gepaarte Teilmenge zeigen (4/4) --
n-Widerspruch in der Anzeige. Fix: overall nutzt jetzt (1) die ankergewichtet
gepoolte Fehlerquote via _pooled_hall_pct (SSoT mit der KPI-Kachel,
Mean-Fallback ohne Roh-Counts) und (2) EXAKT die Zeilen-Auswahl (gepaarte
Teilmenge wo paired, sonst PDF-Ebene) -- Headline-n == Summe der Zeilen-n.
Coverage im overall: notengewichtetes Mittel mit demselben Gueltigkeitsfilter
(inkl. D4-Fix _row_coverage); ein Anker-Pool analog _pooled_hall_stats
existiert fuer Coverage nicht (note_evals traegt keine Claim-Roh-Counts) --
im Docstring dokumentiert. Zell-/Zeilen-Mediane bleiben unveraendert (mit
Min/Max abgenommen). notes_overlap_b bleibt bewusst auf voller PDF-Ebene
(misst Corpus-Ueberlappung, version_delta.pdf_overlap-Semantik, dokumentiert).

Neue Zahlen Default-Paar 140/143 (echte Datenkopie, read-only): Headline
vorher "0,0pp (n=12/8)" -> jetzt "+14,0pp -- 3,3 % -> 17,3 % (n=4/4,
Zeilen-Basis)". Abweichung vom Audit-Wert +4,42pp begruendet: das Audit
rechnete ankergewichtet auf der UNGEPAARTEN 12/8-Basis (reproduziert:
5,7 % -> 10,2 % = +4,5pp); die vom Audit ebenfalls geforderte
zeilen-konsistente (gepaarte 4/4) Basis laesst die Hrastinski-Artefaktzeile
(42,9 %) 1 von 4 statt 1 von 8 B-Notes stellen -> +14,0pp. n-Konsistenz
Headline==Zeilen-Summe verifiziert (4/4 == 1+1+2).

B3 (Disclosure) -- PDF-Key-Drift-Hinweis: common_pdfs=[] bei nicht-leeren
only_a/only_b ist im Bestand oft dieselbe Quelle unter zwei Gruppen-Keys
(bates-2017 vs bates-information-behavior, #194/#202-Linie). Der leere
Vergleich zeigt jetzt einen Hinweistext statt nur "kein Delta". Keine
Heuristik-Zusammenfuehrung (separates Ticket). PR-Body wird entsprechend
geschaerft.

B4 (Cap-Fussnote) -- _MATRIX_VERSION_LIMIT droppte 35 von 50 Versionen ohne
Kennzeichnung. Neu: n_versions_dropped in der Matrix-Payload (analog
excluded_archived_versions; Versionen MIT Eval-Zeilen, die nicht gezeigt
werden) + Fussnote im UI ("35 weitere Version(en) mit Eval-Daten nicht
gezeigt (Top-15-Deckelung, min. n>=3 je Version -- die neueste immer dabei)").

Tests: 9 neue (RED vor Implementierung verifiziert: Median-Kollaps 0,0 statt
10,0; n=2 statt 1 gepaart; cov-Median 10,0 statt Mean 30,0; 3x KeyError
n_versions_dropped; 2x fehlende HTML-Anker). Bestehende Overall-Tests
blieben unveraendert gruen (homogene Fixtures: Mean==Median). Volle Suite:
6027 passed, 3 skipped. ruff check + format: clean. NUL-Byte-Check nach
HTML-Edits: 1 (unveraendert). Sichtpruefung (Playwright/firefox-1532,
hell+dunkel, Klick-Pfade inkl. Dropdown-Wechsel auf leere Schnittmenge):
0 Page-Errors; Screenshots C:/tmp/pairview-verify/nachbesserung-*.png.
@TillQuandel TillQuandel merged commit 477eb07 into master Jul 15, 2026
4 checks passed
TillQuandel added a commit that referenced this pull request Jul 15, 2026
…-Live-Befund) (#310)

Till-Live-Befund am gemergten #305 (master 477eb07): in der Matrix liess
sich nicht ganz nach rechts scrollen — bei voller Versionszahl (15 Spalten
+ PDF-Spalte, ~1500px Mindestbreite via nowrap/min-width) waren die
hinteren Versionen nicht erreichbar.

Ursache (Repro VOR dem Fix, Playwright/firefox-1532, echte Datenkopie):
Die Matrix steckt zwar im etablierten .table-wrap (overflow-x:auto), aber
die #203-P3-Media-Query gibt .table-wrap oberhalb 1200px auf
overflow-x:visible frei — fuer die Bestandstabellen korrekt (die passen
dort ohne Scroll, dafuer sticky-thead), fuer die Matrix nie. Der Ueberlauf
wurde von .app{overflow-x:clip} abgeschnitten. Messwerte 1440px: Wrapper
1162px, Tabelle 1497px, letzte Spalte v0.3.143 endet bei x=1746 (ausserhalb
Viewport), scrollLeft unbeweglich. 1280px analog. Unter 1200px griff der
Basis-Wrapper (auto) und es war scrollbar — deshalb fiel es in der
1440px-Sichtpruefung von #305 nicht auf (Messfehler dort: nur Dropdown-
Klicks, kein Scroll-Versuch in der Matrix selbst).

Fix (minimal, nur die Matrix — #203-Bestand unangetastet):
- Wrapper-Klasse pm-scroll: .table-wrap.pm-scroll { overflow-x:auto }
  haelt den Scroll-Container in ALLEN Viewport-Breiten aktiv (Spezifitaet
  (0,2,0) schlaegt die Media-Query-Regel (0,1,0)).
- Nice-to-have (trivial geblieben): erste Spalte (PDF-Namen) sticky
  (left:0, opaker var(--bg-card)-Hintergrund, z-index:2). Erste Iteration
  hatte ein Light-Theme-Artefakt: die body.light-Zebra-Regel (0,3,4)
  schlug die sticky-Regel (0,3,1) und legte semi-transparentes rgba ueber
  die Spalte — durchgescrollte Zellen schienen in even-Zeilen durch
  (Screenshot-Beleg). Behoben ueber gezielte Opak-Override-Regel (0,5,4)
  fuer Zebra+Hover in genau dieser Spalte; Trade-off (kein Zebra/Hover-Tint
  in der ersten Spalte) im CSS-Kommentar dokumentiert.
- Delta-Tabelle des Paarvergleichs NICHT angefasst: Repro zeigte kein
  Problem (width:100%, wrappt; >=1200 passt sie, <1200 greift der
  Basis-Wrapper).

Verifikation NACH dem Fix (aktives Scrollen via scrollLeft, hell+dunkel,
1440/1280/1100px): letzte Spalte v0.3.143 in allen Konstellationen voll
sichtbar (scrollLeft==maxScroll, lastColFullyVisible=true), sticky-Spalte
opak ohne Durchschein-Artefakte, 0 Page-Errors. Screenshots
C:/tmp/pairview-verify/hscroll-{repro,nachher}-*.png.

Tests: 1 neuer HTML-Anker-Test (RED verifiziert: pm-scroll-Klasse fehlte),
Muster test_dashboard_agent_stats_empty_message (_build_live_html, nie
bash grep/sed wegen NUL-Byte). NUL-Byte-Check nach jedem HTML-Edit: 1
(unveraendert). Volle Suite: 6028 passed, 3 skipped. ruff check + format:
clean.

Co-authored-by: TillQuandel <tillq@live.de>
TillQuandel added a commit that referenced this pull request Jul 16, 2026
#311)

U6-Befund (UX-Review 2026-07-15): dieselbe Quelle (Bates - Information
Behavior) stand unter bis zu 3 Schreibweisen nebeneinander im Dashboard.
B3-Befund (Opus-Statistiker-Abnahme PR #305): _version_pair_compare verlor
Bates deshalb still aus common_pdfs -- jedes v0.3.143-vs-alt-Paar zeigte
"kein gemeinsamer PDF-Corpus", obwohl Bates in beiden Versionen evaluiert
wurde. Ursache: _pdf_filter_key erkennt nur Volltitel-Struktur "Autor - Jahr
- Titel"; "Bates - Information Behavior.pdf" (ab v0.3.143, Jahr verloren)
und bare "Bates" (eval_version 1.3) fallen deshalb NICHT auf denselben Slug
wie "bates-2017"/"Bates - 2017 - Information Behavior.pdf".

Fix: _PDF_GROUP_ALIASES-Map in _pdf_group_key (generative/eval_dashboard.py)
-- Post-Slug-Lookup fuer belegte Drift-Varianten, die die bestehende
Autor-Jahr-Slug-Logik nicht erkennt. Jeder Eintrag mit DB-Beleg (run_id-Join
bzw. exakte Note-Titel-Ueberschneidung, s. PR-Body) dokumentiert -- kein
geratener Merge. Reine Anzeige-seitige Kanonisierung, Bestandsdaten
unangetastet; da _pdf_group_key die SSoT-Gruppierungsfunktion fuer
PDF-Dropdown, -Tabelle, Versions×PDF-Matrix UND den Corpus-Overlap-Guard
(eval_dashboard_server.pdf_notes) ist, wirkt der Fix konsistent auf allen
Konsumenten.

Zwei zusaetzliche, gleich gut belegte Drift-Paare (exakte Note-Titel-Treffer
in beiden Runs) im selben Aufwasch mitgenommen: rl-information-seeking-
quantum.pdf -> jaiswal-2020, cobaltite-paper.pdf -> dhanasekhar-2025.
test-short.pdf trotz thematischer Naehe zu Dhanasekhar NICHT gemerged
(Inhalts-Identitaet ohne Quell-PDF-Diff nicht verifizierbar).

Ein bestehender, davon unabhaengiger Test nutzte "Bates.pdf" nur als
generischen Platzhalter (Docstring: "stellvertretend") fuer die
#232-Artefaktzeilen-Sichtbarkeit -- umbenannt auf "Testquelle.pdf", um die
Kollision mit dem neuen Alias zu vermeiden (keine Verhaltensaenderung am
Getesteten).

Tests: 6 neue + 1 angepasst (RED vor dem Fix verifiziert). Volle Suite
(generative, lib/decision_engine/tests, shared/tests): 6035 passed,
3 skipped (unveraendert ggue. vorher). ruff check + format: clean.
internal/dashboard/eval_dashboard.html nicht angefasst (Konsumenten-Analyse:
keine eigene Gruppierungslogik im Frontend). Sichtpruefung (Playwright/
firefox-1532, read-only Kopie der echten Produktionsdaten): PDF-Dropdown
und Versions×PDF-Matrix zeigen Bates/Jaiswal/Dhanasekhar je genau einmal,
0 Page-Errors.

Co-authored-by: TillQuandel <tillq@live.de>
TillQuandel pushed a commit that referenced this pull request Jul 16, 2026
Nachbesserung Punkt 2 (UX-Review 16.07, Beleg
c-drift-v0.3.140-vs-v0.3.60-1100-light.png): der table-wrap-Wrapper der
Versions-Vergleichstabelle (Paarvergleich-Sektion) hatte keine
pm-scroll-Klasse -- bei 1100px war die letzte Spalte "Paarung" hart
abgeschnitten, bei 1280px blieben nur 2px Restluft (fragil, gleiche
Bugklasse wie #305/Punkt 0 oberhalb 1201px). Zusaetzlich lebte der
laengere PDF-Key-Drift-Hinweis als Teil der zentrierten <td colspan>-
Zeile INNERHALB der Tabelle -- deren Breite wird von den 9 Header-th
bestimmt, unabhaengig vom (leeren) tbody-Inhalt, und ueberstieg die
sichtbare Wrapper-Breite bei 1100px: der Text wurde mitten im Wort
geclippt.

Fix: derselbe pm-scroll-Mechanismus wie an der Matrix (haelt den
Scroll-Container in allen Viewport-Breiten aktiv, pinnt zugleich die
"Quell-PDF"-Spalte links). Der laengere Drift-Hinweis wandert in die
bestehende `summary`-Zeile ausserhalb des Scroll-Containers (Section-
statt Tabellenbreite als Umbruchgrenze); die In-Table-Leerzeile bleibt
kurz genug, um nie clippen zu muessen.

Verifiziert per Playwright (1100/1280/1440px, hell+dunkel): "Paarung"
nach Scroll vollstaendig sichtbar, Drift-Hinweis komplett lesbar ohne
Clipping, 0 pageerrors.
TillQuandel added a commit that referenced this pull request Jul 16, 2026
* fix(dashboard): Matrix-Rendering-Bruch bei wenigen Versionen behoben

Till-Live-Befund auf master 64d8895 (eval_version 4.3 = 6 PDFs x 1
Version v0.3.144): Der Versions-Spaltenkopf "v0.3.144" rendert nicht in
der Kopfzeile, sondern ~49px tiefer und ueberlappt Zeile 1 (Bates), die
Kopfzeile selbst wirkt leer.

Ursache (per Repro auf isoliertem Testserver mit read-only kopierten
Live-Daten + Computed-Style-Diagnose bestaetigt, nicht geraten): die
>=1201px-Sticky-Regel `table.cmp thead th` (#203 P3) trifft ueber die
gemeinsame .cmp-Klasse auch table.pm-matrix. Der bestehende Kommentar
("sticky entfaellt fuer die Matrix ohnehin, da der pm-scroll-Wrapper ihr
eigener Containing-Block ist") war ein Trugschluss: position:sticky
verschiebt die Box bereits im Ruhezustand (ganz ohne Scrollen) um den
top-Offset, sobald die statische Ausgangsposition die Constraint
verletzt -- exakt der beobachtete 49px-Versatz.

Zusaetzlich klebte die einzige Versions-Spalte am rechten Tabellenrand
mit grosser Leerflaeche zur PDF-Spalte: `table.cmp { width:100% }`
stretcht bei wenigen Spalten die Restbreite in die einzige Datenspalte.

Fix:
- `.table-wrap.pm-scroll table.cmp thead th` setzt position/box-shadow/
  border-bottom explizit zurueck (Spezifitaet 0,3,3 schlaegt 0,1,3).
- `table.cmp.pm-matrix { width: auto }` laesst die Matrix auf
  Inhaltsbreite schrumpfen -- Datenspalten ruecken links neben die
  PDF-Spalte, bei vielen Versionen bleibt der bestehende
  Scroll-Mechanismus (.pm-scroll) unveraendert wirksam.
- Minor: Insight-Text zeigte "1 Versionen" statt "1 Version";
  Singular/Plural-Fix gleich fuer "PDF-Quelle(n)" mitgezogen (identische
  Zeile/Technik).

Nachweis: HTML-Anker-Tests (_build_live_html-Muster) fuer CSS-Reset,
width:auto und die Singular/Plural-Ternaries; Playwright-Repro vor/nach
Fix (Screenshot + computed styles: thPosition sticky->static, thRect/
firstCellRect-Ueberlappung aufgeloest, tableWidth 1162->336px) auf
isoliertem Testserver mit read-only kopierten Live-Daten (eval_version
4.3).

* feat(dashboard): eval_version-Dropdown mit Zeilenzahl je Version

Till-Wunsch + Zusatzbefund: Das Dashboard defaultete unveraenderlich auf
die neueste eval_version (4.3, 27 Baseline-Zeilen). Der URL-Param
?eval_version=4.1 hatte im Browser KEINE Wirkung -- der Client-Fetch von
/data.json reichte ihn nie durch (Bestands-Kommentar: "Eval-Version-
Filter entfernt: nie aus URL/State setzen"), obwohl der Server den Query-
Param bereits korrekt verarbeitete. Verifiziert: Seite zeigte unter
beiden URLs identisch 4.3.

Die Client-Funktionen fuer ein Eval-Version-Dropdown (onEvalVerChange,
_initEvalVerDropdown, _getEvalVerFromUrl/_setEvalVerInUrl) existierten
bereits verwaist im Code (Markup + Fetch-Wiring waren entfernt) --
wieder angeschlossen statt neu gebaut:

- Filterbar: neues Dropdown "Eval-Version" (Optionen aus
  available_eval_versions, Anzeige "4.3 (n=27)"), Warn-Badge bei >1
  Version.
- Server: available_eval_versions traegt jetzt {version, n} statt nur
  den Versions-String -- n = Zeilenzahl je Version, ungefiltert
  (dieselbe "Optionen vor allen Filtern"-Konvention wie all_pdfs_opts).
- loadAndRender() haengt eval_version an JEDEN /data.json-Fetch (inkl.
  15s-Auto-Refresh, da derselbe Codepfad) -- /live.json bewusst NICHT
  angefasst: separate, eval_version-fremde Live-Run-Progress-Seite ohne
  eval_version-Konzept im Payload.
- Sichtpruefungs-Fund (Klick-Pfad 4.3->4.1->zurueck): sf-ev-pill blieb
  nach manuellem Wechsel auf dem alten Wert stehen -- der Post-Fetch-
  Early-Return in _initEvalVerDropdown griff faelschlich, weil
  onEvalVerChange() _currentEvalVersion schon VOR dem Fetch setzt.
  Rebuild jetzt nur noch an die Optionsmenge gekoppelt, pill/warn immer
  aktualisiert.
- Sidebar: persistenter eval-Pill (#sf-ev-pill) neben #sf-time,
  konsistent mit #pm-evalver in der Matrix-Sektion (beide aus
  d.eval_version derselben Antwort).

Nachweis: Server-Tests (Zeilenzahl je Version, filterunabhaengig, Query-
Param-Aufloesung), HTML-Anker-Tests (_build_live_html-Muster) fuer
Markup/Fetch/Boot-Seed/Dropdown-Wiring, Node-Ausfuehrungstest fuer den
Pill-Guard-Fund. Playwright-Klick-Pfad auf isoliertem Testserver (Live-
Daten read-only): Dropdown zeigt "1.3 (n=87)/4.1 (n=515)/4.3 (n=27)",
Wechsel 4.3->4.1->zurueck aktualisiert Kopfzeile+Matrix+Pill korrekt,
Deep-Link ?eval_version=4.1 greift, Auswahl bleibt nach 17s (>1 Auto-
Refresh-Zyklus) stabil auf 4.1, 0 pageerrors.

* fix(dashboard): coverage_factual-or-Anti-Pattern an Bestandsstellen gefixt

D4-Bestand (Reviews 15.07.): der #305-Helper `_row_coverage()` fixte das
`coverage_factual or coverage_rate`-Anti-Pattern (verschluckt eine ECHTE
0.0 als falsy, 0%-Coverage-Zeilen existieren real, Jockisch-Faelle) bisher
nur fuer die neuen Matrix-/Paarvergleichs-Funktionen -- der Docstring
markierte die Bestands-Stellen explizit als separates Ticket.

7 Bestandsstellen mit demselben Muster auf _row_coverage umgestellt:
- eval_dashboard.py: _calc_kpis (avg_cov-KPI-Kachel), _calc_pdf_table
  (cov je PDF-Zeile), _chart_scatter + _build_quality_chart_data
  (Legacy-main()-Standalone-Pfad).
- eval_dashboard_server.py: _read_calibration_data (llm_cov),
  quality_by_version-Aggregation (kpi_trend.cov-Sparkline),
  _chart_scatter_versioned (aktiver Scatter-Chart).

_read_calibration_data arbeitet auf sqlite3.Row (kein `.get()`) --
_row_coverage dafuer Mapping-kompatibel gemacht (`in r.keys()` statt
`.get()`, funktioniert fuer dict UND Row identisch) statt die Fallback-
Logik an dieser einen Stelle separat zu duplizieren.

Nicht angefasst (Suche + Pruefung, nicht blind ersetzt): die
"Routing-only"-Zeilen in _calc_pdf_table setzen `cov: None` hartkodiert
(keine Eval-Daten vorhanden, kein Anti-Pattern moeglich).

Nachweis (TDD): 8 RED-Tests je Bestandsstelle (assert 90.0 == 0.0, zeigt
das Verschlucken) + 2 fuer die sqlite3.Row-Kompatibilitaet von
_row_coverage selbst, alle 10 vor dem Fix rot, danach gruen. Sweep
generative/tests -k "dashboard or coverage or calib": 310 passed, keine
Regression.

* fix(dashboard): accept-Delta-Guard auf geroutete statt evaluierte Notes umgestellt

D3 (Reviews 15.07.): version_delta() lief fuer ALLE KPI-Metriken (hall/cov/
n/accept/dur/tokens/cost) mit demselben kpi_trend["n"]-Array als n>=20-
Reliability-Guard. Fuer hall/cov ist das korrekt (n = evaluierte Notes,
das SIND Eval-Metriken). Fuer "accept" (Akzeptanzrate, gepoolt ueber ALLE
gerouteten/generierten Notes, _pooled_accept) ist n der falsche, zu kleine
Nenner -- nur eine Stichprobe wird LLM-evaluiert. Bug war "aktuell
konservativ": kein falsches Delta, aber unnoetig graue/unreliable Chips,
obwohl das Accept-Delta auf hunderten gerouteten Notes beruht.

Fix: version_delta() bekommt optionalen n_field-Parameter (Default "n" --
rueckwaertskompatibel fuer hall/cov/dur/tokens/cost). Server uebergibt fuer
"accept" n_field="accept_n" -- neues kpi_trend-Feld, Summe derselben
(n_vault, n_total)-Paare wie _pooled_accept (SSoT, keine zweite Zaehlung).

Nachweis (TDD): RED zeigte den Bug direkt (accept-Delta reliable=False bei
n=8/9 evaluiert, obwohl 150/160 geroutete Notes vorlaegen) sowie den
fehlenden n_field-Parameter/accept_n-Key. Nach Fix: 5/5 gruen, inkl.
Rueckwaertskompatibilitaets-Test (hall-Delta ohne n_field unveraendert) und
Server-Integrationstest (accept_n aus denselben Log-Runs wie
_pooled_accept). Sweep generative/tests -k "dashboard or delta or accept":
325 passed, keine Regression.

* fix(dashboard): Dropped-KPI zeigt ehrlich "–" statt stiller 0 im Log-Pfad

D5 (Reviews 15.07.): "n_dropped" existiert strukturell NUR im DB-Fallback-
Pfad (eval_dashboard_server.py, pipeline_runs-Tabelle). Der primaere
Log-Pfad (_read_all_log_runs()) baut Zeilen aus [DRY-RUN]->(Vault|Inbox)-
Treffern -- verworfene Kandidaten (nie bis zum Draft gekommen) hinterlassen
dort GAR KEINE Zeile und tauchen im notes-Dict nie auf. `n_dropped` ist an
dieser Quelle nicht zufaellig leer, sondern nicht ermittelbar. Die Kachel
zeigte trotzdem eine stille "0" (`r.get("n_dropped", 0)`), ununterscheidbar
von "wirklich 0 verworfen" -- die bestehende Tooltip-Erklaerung deckte das
zwar schon auf, aber der angezeigte WERT selbst log weiter.

Fix: `_calc_kpis` liefert `total_dropped=None`, wenn KEINE Zeile in
all_log_runs den Key "n_dropped" traegt (Primaer-Pfad/leere Runs) -- die
Summe (inkl. echter 0, wenn die DB-Spalte 0 ist) bleibt unveraendert, sobald
mindestens eine Zeile den Key traegt (DB-Fallback-Pfad). Client zeigt "–"
statt der Zahl, Tooltip-Text an das neue Verhalten angepasst.

Bestehender Test `test_calc_kpis_total_dropped_zero_when_field_missing`
kodierte selbst das Anti-Pattern (`assert ... == 0` bei fehlendem Key) --
auf die korrigierte Erwartung (`is None`) aktualisiert, nicht geloescht
(Historie/Docstring-Verweis auf den D5-Fund ergaenzt).

Nachweis (TDD): RED zeigte `0 is None`-Fehlschlaege an allen 3 betroffenen
Stellen (kein Key, leere Runs, HTML-Anker fuer den JS-Ternary) vor dem Fix.
Nach Fix 5/5 gruen + aktualisierter Bestandstest gruen. Sweep
generative/tests -k dashboard: 312 passed, keine Regression.

* fix(dashboard): Spark-Achsen-Eskalation fuer %- und h-Einheiten konsistent zur $-Eskalation

#292-LOW (Reviews 15.07.): Die Nachkommastellen-Eskalation der Spark-
Hilfslinien (_trendChart, kollidierende Achsenlabels bei enger Wertespanne)
nutzte eine zweite, unit-fremde Formel (`_fmtDE(v, extra) + au.u`) statt
der Basis-Formatierungsfunktion. Das passte zufaellig fuer "$"/generische
Einheiten (deren Basis-fmt strukturell identisch ist), aber nicht fuer:

- "%": Basis-Anzeige (_autoUnit) haengt GAR KEIN "%" an ("3,0" statt
  "3,0%") -- die Eskalationsformel dagegen IMMER (au.u="%"). Erst bei
  einer Werte-Kollision sprang das %-Zeichen ploetzlich auf, inkonsistent
  zur unmittelbar daneben unveraenderten Basis-Anzeige.
- "h": Basis-Anzeige zeigt < 1h als gerundete Minuten ("2 min"), >= 1h als
  Stunden-Dezimalzahl ("1,2 h"). Die Eskalationsformel ignorierte das
  komplett und zeigte bei einer Kollision IMMER rohe Stunden-Dezimalen
  ("0,03h") -- ein Einheiten-Sprung weg von der Minuten-Konvention direkt
  daneben.

Fix: `_autoUnit`s fmt-Funktionen nehmen jetzt einen optionalen Nachkomma-
stellen-Parameter (Default = bisheriges hartkodiertes Verhalten je Zweig);
die Eskalationsschleife ruft `au.fmt(v, extra)` statt der zweiten Formel --
SSoT, jede Einheit eskaliert automatisch in ihrer eigenen Konvention.
"%" haengt dabei neu auch in der Basis-Anzeige den Suffix an (identisch zur
Eskalation). Mischfall (Wertespanne straddled die 1h-Grenze nach Padding)
zeigt weiterhin korrekt gemischte min/h-Einheiten -- kein Bug, dieselbe
Logik wie die unveraenderte Basis-Anzeige, per Test abgesichert.

Nachweis (TDD, Node-Ausfuehrung der echten JS-Funktionen, kein Nachbau):
2 RED-Failures vor dem Fix (fehlender %-Suffix in der Basis-Anzeige,
Einheiten-Sprung auf Stunden-Dezimalen bei sub-1h-Kollision) + 6 bereits
gruene Referenztests (u.a. $-Eskalation unveraendert). Nach Fix 9/9 gruen.
Sweep generative/tests -k dashboard: 318 passed, keine Regression.

* fix(dashboard): ch3/leg3 zeigen etikettierte PDF-Labels statt Rohkeys

#294-Nebenfund (Reviews 15.07.): _chart_scaling() (Chart 3 "Scaling" +
Legende 3) kopierte r["label"] unveraendert aus den all_log_runs-Zeilen.
Im primaeren Log-Pfad (_read_all_log_runs) ist das
`_PDF_LABELS.get(key, key)` -- fuer PDFs ausserhalb der 3 registrierten
_PDF_LABELS-Eintraege (Regelfall) faellt das auf den rohen, klein-
geschriebenen Log-Key zurueck (z. B. "cobaltite-paper" statt "Cobaltite
Paper"). Dieselbe Bugklasse traf bereits Chart 2 (Trade-off,
_chart_longitudinal) und wurde dort per U4-Fix lokal geloest
(_PDF_LABELS.get(key) or re.sub(...).title()) -- ch3/leg3 bekam den Fix
bisher nicht.

Fix: dieselbe Formel in _chart_scaling, aber nur wenn label==key (der
Fallback tatsaechlich griff) -- ein bereits besserer Label-Wert aus dem
DB-Fallback-Pfad (pdf_label/pdf_source-Ableitung in
eval_dashboard_server.py) bleibt unangetastet. Quelle bewusst NICHT
zentral in _read_all_log_runs geaendert, um den Blast-Radius auf andere
Konsumenten (_chart_acceptance u. a., nicht Teil dieses Fundes) nicht
auszuweiten.

Nachweis (TDD): RED zeigte den rohen Key in allen 3 Faellen (unregistrierte
PDF, Multi-Separator-Key) vor dem Fix, GREEN danach (4/4) -- inkl.
Regressionswaechter fuer den DB-Fallback-Fall (bereits guter Label bleibt
erhalten). Sweep generative/tests -k dashboard: 322 passed, keine
Regression.

* feat(dashboard): n-valid-Badge je Modell-Filter-Option

Reviews 15.07.: Modell-Dropdown-Optionen zeigen jetzt die Anzahl VALIDER
Eval-Zeilen (hallucination_rate >= 0) je Modell -- Gemini-Fehllesungs-
Schutz: dort tragen Zeilen den bestehenden -1.0-Sentinel fuer "ungueltig"
(vgl. _chart_scatter/_matrix_cell_stats); n_valid zeigt fuer so ein Modell
korrekt 0, nicht die volle aber wertlose Zeilenzahl.

note_evals traegt kein eigenes "model"-Feld (das lebt in pipeline_runs) --
Join ueber run_id -> token_runs.model (bereits angereichert), derselbe
Mechanismus wie der bestehende Modell-Einzelwert-Filter. Basis
_matrix_base_rows (eval_version-skopiert, VOR pipeline_version/language/
pdf/run-Filtern) statt quality_rows -- dieselbe "Dropdown-Optionen vor
allen Filtern"-Konvention wie die Options-Liste selbst (Bestands-
Kommentar direkt darueber), sonst wuerde z. B. ein aktiver PDF-Filter die
angezeigten Zaehler unerwartet mitverschieben.

all_models wechselt von list[str] auf list[{model, n_valid}] (kein
bestehender Test/Konsument referenzierte die alte Form direkt). Client:
_initGlobalModelFilter zeigt "<Label> (n=X)", Dropdown-value bleibt der
Modellname (_rebuildFilterSelect-Signatur auf .model umgestellt,
Bestandsverhalten fuer die Auswahl-Persistenz ueber Auto-Refresh
unveraendert).

Nachweis (TDD): RED zeigte TypeError (all_models noch string[]) an allen
4 Stellen. Nach Fix 4/4 gruen -- inkl. Sentinel-Ausschluss, None-Werte
nicht als valide gezaehlt, Denylist-Regression, HTML-Anker fuer die
JS-Anzeige. Sweep generative/tests -k dashboard: 326 passed, keine
Regression.

* fix(dashboard): Punkt 1 Nachbesserung — eval_version-Select an Meta-Label statt Filterbar

Design-Iteration nach Till-Feedback (Sichtprüfung vor der ersten Playwright-
Runde): Die urspruengliche Umsetzung von Punkt 1 haengte das eval_version-
Select als eigenes Filterbar-Element an — brach deren Einzeilen-Layout
bereits bei 1440px (6. Filter-Group + "weitere Versionen verfuegbar"-Badge
passten nicht mehr in eine Zeile, Screenshot-Beleg vor dieser Korrektur).

Filterbar-Variante verworfen zugunsten eines Inline-Selects am bestehenden,
dezenten eval_version-Meta-Label in der Sidebar (#sf-ev-select, ersetzt den
reinen Text-Pill #sf-ev-pill): gleiche Typografie/Farbe wie zuvor (Mono,
amber-Pill-Optik unveraendert), appearance:none + eigener kleiner Chevron
statt Systemstil, Optionen weiterhin "4.3 (n=27)" etc. Funktion unveraendert
(Param in allen Fetches inkl. Auto-Refresh, Klick-Pfad-Fix aus der vorigen
Runde bleibt erhalten: Select-Wert-Sync laeuft unabhaengig vom teuren
Innerhtml-Rebuild). #pm-evalver (Matrix-Sektion) bleibt Read-only-Text und
folgt der Auswahl unveraendert -- SSoT, beide lesen d.eval_version aus
derselben Antwort.

Die separate "weitere Versionen verfuegbar"-Warnbadge entfaellt: das Select
selbst (sichtbare Optionen beim Oeffnen) ist bereits die Affordanz, dass es
mehrere Versionen gibt -- kein zusaetzliches Element noetig, passt zum
Ziel "so reduziert wie moeglich".

Verifiziert (Playwright, isolierter Testserver, Live-Daten read-only):
Filterbar wieder einzeilig bei 1440px UND 1100px, hell + dunkel (Screenshot-
Vergleich vor/nach) -- Select fuegt sich unauffaellig neben "eval"-Label und
"auto-refresh 15 s" ein, kein klobiges Systemelement in keinem der beiden
Themes. 0 pageerrors.

Nachweis: HTML-Anker-Tests aktualisiert (kein Filterbar-Element mehr,
#sf-ev-select in der Sidebar, Fetch/Boot-Seed/Dropdown-Wiring-Tests
unveraendert gueltig), Node-Ausfuehrungstest fuer den Select-Wert-Sync-Fund
auf das neue Element umgestellt. generative/tests -k dashboard weiterhin
gruen (separat vor dem naechsten Commit erneut voll durchlaufen).

* fix(dashboard): Responsive-Bruch bei 1100px behoben (U8 KPI-Luecke, U9 Filterbar-Umbruch)

Reviews 15.07.:

U8: kpis-perf (5 Kacheln) brach bei <=1200px auf 4 Spalten bzw. bei
<=900px auf 2 Spalten -- 5 ist durch keine der beiden Spaltenzahlen teilbar
(5%4=1, 5%2=1), die letzte Kachel stand dadurch allein in der letzten Zeile
mit leeren Spalten daneben (Playwright-Diagnose 1100px: Kachel 4 bei
x=249..455 statt bis zum rechten Rand, 3 Spalten sichtbar leer). Fix:
letzte Kachel spannt per grid-column:1/-1 die volle Breite, wenn sie allein
in ihrer Zeile steht -- an beiden Breakpoints.

U9: das Modell-Filter brach als letzte Filter-Group allein in eine neue
Zeile. Kompaktere Gruppierung durch engeres .filter-group-Padding (14px auf
10px je Seite) und schmalere .fselect-Maximalbreite (200px auf 150px) bei
<=1200px -- 3 der 5 Groups stiessen vorher an die 200px-Kappung und
ueberschritten die verfuegbare Breite (~880px) um ~370px. Bei 1100px bleibt
"Modell" trotzdem allein in Zeile 2 (die reine Inhaltsbreite der uebrigen 4
Groups reicht nicht fuer alle 5 in eine Zeile, ohne PDF-/Lauf-Namen bei
echten Filterwerten unlesbar zu stauchen) -- aber jetzt kompakt statt mit
grossem Leerraum, kein Bruch mehr.

Verifiziert (Playwright, isolierter Testserver, Live-Daten read-only,
1100px, hell+dunkel): KPI-Zeile ohne Luecke, Filterbar-Umbruch kompakt,
0 pageerrors.

Nachweis: HTML-Anker-Tests fuer beide Breakpoint-Bloecke (grid-column-Span
an beiden Media-Queries, Padding-/Max-Width-Kompaktierung). Sweep
generative/tests -k dashboard: 330 passed, keine Regression.

* feat(dashboard): Accept×Hall-Overlay in der Fehlerquote-Versions-Trend-Ansicht

S2-C3 (Multi-Perspektiven-Dashboard-Review 2026-07-15): note_evals enthaelt
nur AKZEPTIERTE Notes (Selektions-Bias) -- sinkt die Akzeptanzquote
gleichzeitig mit einer Fehlerquoten-Aenderung, ist Letztere teils
Stichproben-Verschiebung statt reiner Qualitaetseffekt, ohne dass das im
bestehenden Fehlerquote-Spark-Chart sichtbar waere.

Fix: Akzeptanzquote als zweite, gestrichelte/gedaempfte Serie im
BESTEHENDEN Fehlerquote-Spark-Chart (Klick auf die Fehlerquote-KPI-Kachel,
_trendChart) -- kein neues Chart gebaut. _trendChart bekommt zwei optionale
Parameter (points2, label2); Y-Achse deckt beide Serien gemeinsam ab (nur
bei gleicher Einheit %, sonst ergibt eine gemeinsame Skala keinen Sinn --
z. B. die Dauer-Kachel bleibt unveraendert). Ohne points2 (alle anderen
KPI-Kacheln) ist das Verhalten exakt wie zuvor.

Zweite Serie bewusst schlank: eigene kleinere Punkte, aber OHNE Hover-
Tooltip-Kaesten (Zweck ist der visuelle Trend-Vergleich, kein zweiter
voller Interaktionslayer) -- kleine SVG-Legende (gestrichelte Linie +
Label) oben links macht die zweite Serie ohne Rueckgriff auf die Kachel-
Farbtoene erkennbar.

Nachweis: Node-Ausfuehrungstest der echten _trendChart-Funktion (kein
Nachbau) -- Overlay rendert bei uebergebenem points2, faellt bei fehlendem
points2/nicht-%-Einheit/Laengen-Mismatch sauber weg (Regressionswaechter
fuer alle anderen Kacheln), gemeinsame Skala deckt Werte ausserhalb des
Fehlerquote-Bereichs ab. HTML-Anker fuer die Aufrufer-Verdrahtung (nur
Fehlerquote-Kachel). Playwright-Sichtpruefung (eval_version 4.1, 15
Pipeline-Versionen, hell+dunkel): zwei klar unterscheidbare Linien (durch-
gezogen amber + gestrichelt gedaempft) auf gemeinsamer 0-100%-Achse, Legende
sichtbar, 0 pageerrors. Sweep generative/tests -k dashboard: 336 passed.

* fix(dashboard): Sticky-Header-Kollision der Matrix-PDF-Spalte behoben

Nachbesserung Punkt 1 (4 externe Reviews nach PR #312, UX konvergent,
Beleg a04-matrix-41-light-scroll400.png): der pauschale
`.table-wrap.pm-scroll table.cmp thead th`-Reset (Punkt 0) setzte auch
th:first-child ("Quell-PDF") auf position:static zurueck. Beim
Horizontal-Scroll scrollte die Kopfzelle dadurch mit den Versions-Headern
weg, waehrend die per td:first-child gepinnte PDF-Namen-Spalte im tbody
stehen blieb -- der jeweils linkeste sichtbare Versions-Header landete
optisch ueber der gepinnten Spalte.

Fix: Reset gilt nur noch fuer thead th:not(:first-child); th:first-child
bekommt eine eigene Sticky-Left-Regel (top:auto hebt das top:49px der
>=1201px-Media-Query explizit auf, z-index 3 haelt sie ueber den
statischen Versions-Headern und der generischen th:first-child/
td:first-child-Regel).

Verifiziert per Playwright (playwright-core + firefox-1532, Testserver
8069 mit read-only kopierten Live-Daten, eval_version 4.1/15 Versionen):
1440px und 1100px, hell+dunkel, "Quell-PDF" bleibt beim Scroll sauber
gepinnt, kein Versions-Header ueberlappt, 0 pageerrors.

* fix(dashboard): Singular/Plural bei der Matrix-Cap-Fussnote korrigiert

Nachbesserung Punkt 5 (trivial): die Cap-Fussnote "N weitere Version(en)
... nicht gezeigt" war keine echte Singular/Plural-Behandlung -- bei
genau 1 uebrig gebliebenen Version stand weiterhin die Klammer "(en)"
da. Gleiche Technik wie die bereits gefixte Matrix-Insight-Zeile ("1
Version" statt "1 Versionen").

Verifiziert live (eval_version 4.1, 35 gedroppte Versionen): Text zeigt
jetzt "35 weitere Versionen" statt "35 weitere Version(en)".

* fix(dashboard): Paarvergleichstabelle bei 1100px scrollbar gemacht

Nachbesserung Punkt 2 (UX-Review 16.07, Beleg
c-drift-v0.3.140-vs-v0.3.60-1100-light.png): der table-wrap-Wrapper der
Versions-Vergleichstabelle (Paarvergleich-Sektion) hatte keine
pm-scroll-Klasse -- bei 1100px war die letzte Spalte "Paarung" hart
abgeschnitten, bei 1280px blieben nur 2px Restluft (fragil, gleiche
Bugklasse wie #305/Punkt 0 oberhalb 1201px). Zusaetzlich lebte der
laengere PDF-Key-Drift-Hinweis als Teil der zentrierten <td colspan>-
Zeile INNERHALB der Tabelle -- deren Breite wird von den 9 Header-th
bestimmt, unabhaengig vom (leeren) tbody-Inhalt, und ueberstieg die
sichtbare Wrapper-Breite bei 1100px: der Text wurde mitten im Wort
geclippt.

Fix: derselbe pm-scroll-Mechanismus wie an der Matrix (haelt den
Scroll-Container in allen Viewport-Breiten aktiv, pinnt zugleich die
"Quell-PDF"-Spalte links). Der laengere Drift-Hinweis wandert in die
bestehende `summary`-Zeile ausserhalb des Scroll-Containers (Section-
statt Tabellenbreite als Umbruchgrenze); die In-Table-Leerzeile bleibt
kurz genug, um nie clippen zu muessen.

Verifiziert per Playwright (1100/1280/1440px, hell+dunkel): "Paarung"
nach Scroll vollstaendig sichtbar, Drift-Hinweis komplett lesbar ohne
Clipping, 0 pageerrors.

* feat(dashboard): Re-Eval-Serien-Kennzeichnung fuer die Matrix ergaenzt

Nachbesserung Punkt 4 (Statistiker-Empfehlung, Reviews 15./16.07 zu PR
#312): unter eval_version 4.3 haengen alle 27 note_evals-Zeilen an
pipeline_version v0.3.144 -- das ist der Code-Stand des Re-Eval-Sweeps
(reeval_baseline.py), NICHT die Erzeugungsversion der Notes. Der
zugehoerige Lauf in pipeline_runs traegt dafuer den expliziten Marker
pipeline_version="reeval" (verifiziert im Quellcode), verknuepft ueber
run_id. Ohne Kennzeichnung liest die Matrix "v0.3.144" faelschlich als
Erzeugungsversion.

Fix: neue reine Funktion D._is_reeval_series(quality_rows, pipeline_runs)
prueft, ob JEDE Zeile der aktiven eval_version (_matrix_base_rows, vor
Einzelwert-Filtern) an einen pipeline_runs-Eintrag mit
pipeline_version=="reeval" haengt. Server exponiert das Ergebnis als
is_reeval_series im Payload; die Matrix-Insight-Sektion zeigt bei True
die Zeile "Re-Eval-Serie: Werte messen den Eval-Stand, nicht die
Erzeugungsversion der Notes."

RED->GREEN: Server-/HTML-Anker-Tests liefen zuerst gegen den unveraen-
derten Server (5 fehlgeschlagen, KeyError/AssertionError), nach Server-
und HTML-Verdrahtung 10/10 gruen. Verifiziert live gegen die kopierten
Produktionsdaten: eval_version 4.3 (realer #232-Fall) zeigt
is_reeval_series=True + sichtbaren Hinweis, eval_version 4.1 (normale
Generierungs-Runs) korrekt False/kein Hinweis. Playwright hell+dunkel,
0 pageerrors.

* fix(dashboard): Accept×Hall-Overlay zuschaltbar + Tooltip/Legende/Dimmen

Nachbesserung Punkt 3 (Statistiker MEDIUM + UX-P3, Reviews 15./16.07 zu
PR #312): die gemeinsame 0-100%-Achse des Overlays stauchte die
Fehlerquoten-Serie auf ~1/3 der Chart-Hoehe (vorher hall-adaptiv), und
100%-Accept-Punkte aus n=1-3-Laeufen waren optisch nicht von
belastbaren unterscheidbar; der Punkt-Tooltip zeigte nur die
Fehlerquote, die Legende nur die Zweitserie.

Fix (4 Teile):
(a) Overlay zuschaltbar, Default AUS -- kleiner Toggle am Chart-Kopf
    (nur Fehlerquote-Kachel), Zustand in localStorage (analog
    theme-mode). Aus bleibt die Achse hall-adaptiv wie vor dem PR.
(b) Punkt-Tooltip zeigt bei aktivem Overlay beide Werte ("12,0% /
    Akzeptanz 72,2%").
(c) Legende benennt BEIDE Serien (vorher nur die Zweitserie) + Caveat-
    Zeile erklaert die unterschiedliche Basis (geroutete/generierte vs.
    evaluierte Notes).
(d) Accept-Punkte mit accept_n<20 (Payload-Feld aus Punkt 4/D3, bereits
    vorhanden) werden gedimmt statt gleichwertig zu belastbaren Werten
    dargestellt.

Verifiziert per Playwright gegen echte kopierte Produktionsdaten
(eval_version 4.1, 50 Versionen): Default aus (keine Dashed-Linie,
Achse 0-37,4%), nach Zuschalten Achse 0-100% + beide Serien in der
Legende + Caveat-Text + 4/48 Punkte volltonig (accept_n>=20) Rest
gedimmt (0.35 Opacity) + kombinierter Tooltip, Toggle-Zustand bleibt
nach Reload erhalten. Hell+dunkel, 0 pageerrors.

---------

Co-authored-by: TillQuandel <tillq@live.de>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants