feat(dashboard): Versions×PDF-Paarvergleich mit Schnittmengen-Delta#305
Merged
Conversation
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
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>
This was referenced Jul 16, 2026
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.nist 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: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_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_keyfiltert 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 nanntegenerative/eval_dashboard.html— diese Datei existiert nicht; die tatsächliche Frontend-Vorlage istinternal/dashboard/eval_dashboard.html, wird voneval_dashboard_server.pyper_HTML_TEMPLATE_PATHgeladen): neue Section „Versions×PDF-Paarvergleich" (Nav-Item, Matrix-Tabelle im bestehenden.cmp-Stil, Farbcodierung über die etabliertengood-t/warn-t/bad-t-Klassen, Tooltips über das bestehendetitle-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
pytest generative lib/decision_engine/tests shared/tests): 6014 passed, 3 skipped (identisch zum Stand vor diesem PR — keine Regression).ruff check+ruff format --checkauf allen geänderten Dateien: clean.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:n=4 Note(n) · Spanne 0,0–14,3 % (Min–Max über die 4 Notes).hidden: falsebestätigt (nicht versteckt).#global-pdf) lässt die Matrix unverändert den vollen Corpus zeigen (Design-Entscheid bestätigt); bestehender KPI-Kachel-Klick (Spark-Row) weiterhin intakt.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) nutztencoverage_factual or coverage_rate— das D4-Anti-Pattern aus dem Dashboard-Review 2026-07-15:orverschluckt eine echtecoverage_factual=0.0(falsy) und nimmt fälschlichcoverage_ratebzw. verwirft die Zeile, wenncoverage_ratefehlt (0%-Coverage-Zeilen existieren real, Jockisch-Fälle). Fix: geteilte Hilfsfunktion_row_coverage()—coverage_factualwenn nicht None (auch bei 0.0), nur bei None Fallback aufcoverage_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):
overallgepoolt statt Median + zeilen-konsistente Auswahl. Das Gesamt-Aggregat des Paarvergleichs war doppelt unsauber: (a) Median kollabierte bei der zero-inflatedhallucination_rateauf 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_statsfür die KPI-Kachel dokumentiert. (b)overallpoolte ungepaart (12/8 Notes), während die per-PDF-Zeilen die gepaarte Teilmenge zeigen (4/4) — n-Widerspruch in der Anzeige. Fix:overallnutzt jetzt die ankergewichtet gepoolte Fehlerquote (_pooled_hall_pct, SSoT mit der KPI-Kachel, Mean-Fallback ohne Roh-Counts) über exakt die Zeilen-Auswahl (gepaart wopaired, sonst PDF-Ebene) — Headline-n == Summe der Zeilen-n. Coverage imoverall: notengewichtetes Mittel mit demselben Gültigkeitsfilter (kein Anker-Pool für Coverage möglich —note_evalsträgt keine Claim-Roh-Counts; dokumentiert). Zell-/Zeilen-Mediane bleiben (mit Min/Max abgenommen);notes_overlap_bbleibt bewusst auf voller PDF-Ebene (Corpus-Überlappungs-Semantik wieversion_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-leerenonly_a/only_bzeigt 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_droppedin der Payload (analogexcluded_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
bates-2017vs.bates-information-behavior). Konsequenz in dieser Ansicht: bei jedem v0.3.143-vs-Alt-Paar, dessen Überschneidung nur über eine drift-betroffene Quelle liefe, istcommon_pdfsleer — 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 seit73984bbeinen 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 <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.