Skip to content

fix: Code-Hygiene-Buendel B (#315/#316/#318/#319/#331/#332) - #340

Merged
TillQuandel merged 7 commits into
masterfrom
fix/bundle-b-code-hygiene
Jul 17, 2026
Merged

fix: Code-Hygiene-Buendel B (#315/#316/#318/#319/#331/#332)#340
TillQuandel merged 7 commits into
masterfrom
fix/bundle-b-code-hygiene

Conversation

@TillQuandel

Copy link
Copy Markdown
Owner

Sammel-PR "Bündel B Code-Hygiene"

Sechs NIEDRIG-Prio-Issues aus der Eval-Doku-Auditserie (#313), je ein Commit pro Issue. Format wie Sammel-PR #312.

Sperrzone beachtet: internal/dashboard/eval_dashboard.html und generative/eval_dashboard_server.py wurden nicht angefasst (paralleler Bündel-A-Builder). generative/eval_dashboard.py (Legacy-Modul, nicht der Live-Server) wurde für #315 editiert — laut Auftrag erlaubt.

#315 — Sentinel-Filter im Legacy-Slope-Chart

_build_quality_chart_data() (generative/eval_dashboard.py, aktuelle Zeile 1831, nicht mehr 1671/1611 wie im Issue) filterte hallucination_rate/coverage_rate beim Aufbau der Slope-Datasets nur auf is not None, nicht auf den -1.0-Sentinel. Ein Sentinel wurde nach *100-Rundung zu -100.0 und rutschte in die Slope-Mediane ein. Fix: derselbe >= 0-Guard wie in _chart_scatter/_chart_scatter_versioned. Betrifft ausschließlich den Legacy-Einmal-Render-Pfad, nicht den Live-Server.

Test: generative/tests/test_dashboard_slope_sentinel_filter.py — 3 Tests. RED vor dem Fix: -100.0/-40.0 statt None/20.0 im Slope-Median.

#316 — Coverage-Fallback-Bugklasse außerhalb des Dashboards

.get("coverage_factual", r.get("coverage_rate", 0)) fällt bei explizit gespeichertem None NICHT auf coverage_rate zurück (dict.gets Default greift nur bei fehlendem Key). Betraf drei Stellen außerhalb des Dashboards (die Dashboard-eigenen Stellen sind bereits über _row_coverage/#305 gefixt, separates Ticket, hier nicht angefasst):

  • eval_progress.py:63 — crashte real mit TypeError beim Prozent-Formatieren (reproduziert).
  • orchestrator.py (Stage-8-Run-Ende-Print) — derselbe TypeError-Crash bei r.get(...) >= 0 (reproduziert). In _stage8_report_averages() extrahiert (vorher inline in main(), jetzt testbar).
  • db.py::query_kpi_trendAVG(coverage_factual) ganz ohne Fallback, liefert NULL für avg_cov bei jeder eval_version=4.x-Zeile (coverage_factual seit v4 strukturell NULL, coverage_factual: abgeschaffte v1.3-Metrik — Kalibrierungs-View zeigt LLM-Coverage seit v4 leer (stiller NULL-Leser) #233). Fix: AVG(COALESCE(coverage_factual, coverage_rate)).

Neuer geteilter Helper coverage_value() in eval_common.py (dependency-frei von eval_dashboard.py).

Tests: test_eval_common_coverage_value.py (5), test_eval_progress_coverage_fallback.py (2), test_orchestrator_stage8_report_averages.py (4), test_db.py (+2) — 13 neue Tests. RED jeweils gezeigt (TypeError-Crash bzw. None statt Fallback-Wert).

#318 — decision_source-Vokabular vereinheitlichen

Historische eval_version=4.1-Zeilen tragen decision_source="audit" (67 JSONL-Zeilen, Eval-Doku-Audit #313) — aktueller Code schreibt nur noch "audit_override". Neuer Lese-Seiten-Normalizer normalize_decision_source() (lib/decision_engine/models.py, über lib/decision_engine exportiert) mit dokumentiertem Alias-Mapping {"audit": "audit_override"}. Angewendet an der einzigen Stelle im Code, die einen decision_source-String wieder in ein ClaimDecision-Objekt liest (eval_quality_v4.py::_aggregate). Keine Mutation der Bestandsdaten — JSONL/DB bleiben unverändert.

Test: lib/decision_engine/tests/test_models.py — 3 Tests für den Normalizer.

#319 — Agent-Label v3→v4

eval_quality_v4.py nutzte intern weiterhin agent="eval_quality_v3_..." für Cache-/Kostenzuordnung (JSON-Repair-Call und Judge-Call je Variante). Umbenannt auf eval_quality_v4_....

Cache-Invalidierungs-Nebenwirkung geprüft (wie im Issue gefordert): agent ist Teil des LLM-Call-Cache-Keys (agents/base.py::_cache_key). Die Umbenennung invalidiert einmalig den Disk-Cache für Judge-/Repair-Calls innerhalb der aktuellen eval_version — eine inhaltlich unveränderte Note trifft dort beim nächsten Lauf einen Cache-Miss statt -Hit (ein zusätzlicher Sonnet-Call). Der davon unabhängige Re-Eval-Hash-Guard (content_hash+eval_version+pipeline_version in quality_history.jsonl) ist nicht betroffen — bereits gespeicherte Eval-Ergebnisse bleiben gültig. _canonical_agent() im Dashboard (Sperrzone, nicht angefasst) matcht generisch auf den "eval_quality"-Präfix und bricht durch die Umbenennung nicht.

Test: test_eval_agent_label_v4.py — 2 Tests. RED vor dem Fix: Label trug noch "eval_quality_v3_...".

#331 — confirmation_rate-Zählartefakt

confirmation_rate im Verifier-Trace konnte > 100 % werden (Coverage-Serie 2: 2.0 mit Refine, 3.0 ohne Refine — widerlegt die ursprüngliche Refine-Hypothese). Root Cause: _log_anchor_stats() teilte confirmed (gezählt aus final_anchors, NACH dem Lauf) durch total_in (Momentaufnahme VOR dem Lauf). _run_inner() hängt an jedem Ausgang über sync_anchors_from_body() neue, bereits bestätigte Anker an — Zähler und Nenner bezogen sich auf unterschiedliche Mengen, auch ohne Refine-Zyklus. Fix: Nenner = len(final_anchors), dieselbe Menge wie der Zähler — Rate ist damit mathematisch garantiert ≤ 1.0. total_in bleibt als separates Feld im Trace erhalten.

Test: test_verifier_confirmation_rate_bound.py — 1 Test, reproduziert exakt den Kok-Fall (1 pre-pass-bestätigter Original-Anker + 2 body-synced Anker / total_in=1 → 3.0 vor dem Fix).

#332 — figure_alt-Logmeldung kontextneutral

Die "0 Anker-Matches"-Diagnosemeldung nannte unbedingt einen --load-drafts-Kontext als Erklärung, obwohl bind_figures_to_drafts() den Aufrufkontext gar nicht kennt (Kok-Beleg: 0 Anker-Matches trat auch mit --fresh-run auf). Reine Textänderung auf eine neutrale Formulierung, keine Logikänderung.

Test: test_figure_alt_neutral_no_match_message.py — 1 Test.


Suite / Lint

  • uv run pytest generative lib/decision_engine/tests shared/tests -q6244 passed, 3 skipped, 9 deselected, 45 Warnings (alle vorbestehende DeprecationWarning, kein Bezug zu diesem PR), 446s.
  • uv run ruff check . → All checks passed.
  • uv run ruff format --check . → 314 files already formatted.

Closes #315
Closes #316
Closes #318
Closes #319
Closes #331
Closes #332

Tilltime added 7 commits July 17, 2026 14:40
_build_quality_chart_data() filterte hallucination_rate/coverage_rate beim
Aufbau der Slope-Datasets nur auf `is not None`, nicht auf den -1.0-Sentinel,
den eval_quality_v4.py bei ungueltigen Laeufen schreibt. Ein Sentinel -1.0
wurde nach der *100-Rundung zu -100.0 und rutschte als valider Wert in die
Median-Berechnung ein.

Fix: derselbe >= 0-Guard wie in _chart_scatter/_chart_scatter_versioned.
Betraf ausschliesslich den Legacy-Einmal-Render-Pfad (main()/_build_html) --
der Live-Server (eval_dashboard_server.py) ruft diese Funktion nicht auf.

3 neue Tests (RED vor dem Fix: -100.0/-40.0 statt None/20.0 im Slope-Median).
`.get("coverage_factual", r.get("coverage_rate", 0))` faellt bei einem
explizit gespeicherten None-Wert NICHT auf coverage_rate zurueck --
dict.get's Default greift nur bei fehlendem Key. Betraf drei Stellen:

- eval_progress.py:63 -- crashte mit TypeError beim Prozent-Formatieren
  (None statt Fallback-Wert), reproduziert vor dem Fix.
- orchestrator.py (Stage-8-Run-Ende-Print) -- derselbe TypeError-Crash bei
  `r.get(...) >= 0`, reproduziert vor dem Fix. In `_stage8_report_averages()`
  extrahiert (testbar, vorher inline in main()).
- db.py::query_kpi_trend -- AVG(coverage_factual) ganz ohne Fallback, liefert
  NULL fuer avg_cov bei jeder eval_version=4.x-Zeile (coverage_factual seit
  v4 strukturell NULL, #233). Fix: AVG(COALESCE(coverage_factual, coverage_rate)).

Neuer geteilter Helper `coverage_value()` in eval_common.py (dependency-frei
von eval_dashboard.py, das dieselbe Logik bereits fuer die Dashboard-eigenen
Stellen in _row_coverage hat -- separates, aelteres Ticket, hier nicht
angefasst). 13 neue Tests, RED vor dem Fix jeweils gezeigt (TypeError-Crash
bzw. None statt Fallback-Wert).
Historische eval_version=4.1-Zeilen tragen decision_source="audit" (67
JSONL-Zeilen, Eval-Doku-Audit #313) -- aktueller Code schreibt nur noch
"audit_override" (rules.py::rule_audit_stricter_override). Leser die nach
decision_source filtern/gruppieren muessten sonst beide Werte kennen.

Neuer Lese-Seiten-Normalizer normalize_decision_source() (models.py, ueber
lib/decision_engine exportiert) mit dokumentiertem Alias-Mapping. Angewendet
an der einen Stelle im Code, die einen decision_source-String wieder in ein
ClaimDecision-Objekt liest (eval_quality_v4.py::_aggregate). KEINE Mutation
der Bestandsdaten -- JSONL/DB bleiben unveraendert.

3 neue Tests fuer den Normalizer.
eval_quality_v4.py nutzte intern weiterhin agent="eval_quality_v3_..." fuer
Cache-/Kostenzuordnung (JSON-Repair-Call und Judge-Call je Variante) -- Altlast
aus dem v3-Vorgaenger. Verwirrend bei Auswertungen nach Agent-Label.

Cache-Invalidierungs-Nebenwirkung geprueft: agent ist Teil des LLM-Call-
Cache-Keys (agents/base.py::_cache_key). Die Umbenennung invalidiert einmalig
den Disk-Cache fuer Judge-/Repair-Calls innerhalb der aktuellen eval_version --
eine inhaltlich unveraenderte Note trifft dort einen Cache-Miss statt -Hit
(ein Sonnet-Call statt Cache-Treffer). Der Re-Eval-Hash-Guard (separater
Mechanismus ueber content_hash+eval_version+pipeline_version in
quality_history.jsonl) ist NICHT betroffen -- bereits gespeicherte
Eval-Ergebnisse bleiben gueltig, nur der darunterliegende LLM-Response-Cache
wird kalt.

2 neue Tests (RED vor dem Fix: agent-Label trug "eval_quality_v3_...").
confirmation_rate im Verifier-Trace konnte > 100 % werden -- Coverage-Serie 2
belegte 2.0 (Poege, Lauf 3, mit Refine) und 3.0 (Kok, Lauf 5, OHNE Refine).
Der Kok-Beleg widerlegte die urspruengliche Refine-Hypothese.

Root Cause: _log_anchor_stats() teilte `confirmed` (gezaehlt aus
final_anchors, NACH dem Lauf) durch `total_in` (Momentaufnahme VOR dem Lauf,
in run()). _run_inner() haengt an JEDEM Ausgang ueber sync_anchors_from_body()
neue, bereits bestaetigte Anker an draft.source_anchors an (Body-Zitate der
Form „..." (S. N)) -- Zaehler und Nenner bezogen sich dadurch auf
unterschiedliche Mengen, auch ohne Refine-Zyklus.

Fix: Nenner = len(final_anchors), dieselbe Menge wie der Zaehler. Rate ist
damit mathematisch garantiert <= 1.0. total_in bleibt als separates Feld im
Trace fuer Transparenz erhalten.

1 neuer Test (RED vor dem Fix: confirmation_rate=3.0, exakt reproduziert per
1 pre-pass-bestaetigtem Original-Anker + 2 body-synced Ankern / total_in=1).
Die "0 Anker-Matches"-Diagnosemeldung nannte unbedingt einen
--load-drafts-Kontext als Erklaerung -- bind_figures_to_drafts() kennt den
tatsaechlichen Aufrufkontext gar nicht. Coverage-Serie 2, Lauf 5 (Kok) belegt
0 Anker-Matches auch mit --fresh-run woertlich im Log -- dieselbe Fehlklasse
wie das bereits gefixte #288.

Reine Textaenderung (wie im Issue vorgeschlagen): neutrale Formulierung ohne
unbelegte Kontext-Annahme, keine Logikaenderung.

1 neuer Test (RED vor dem Fix: Meldung nannte --load-drafts bedingungslos).
Zwei Zeilenlaengen-Reformatierungen (ruff format), keine funktionale
Aenderung. Betrifft orchestrator.py::_stage8_report_averages (#316) und den
neuen Verifier-Test (#331).
@TillQuandel
TillQuandel merged commit f63468f into master Jul 17, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment