Skip to content

fix(generative): pipeline_runs-Insert vom Inline-Eval entkoppeln + Test-Traces nach tmp (#198 P1+P2)#218

Merged
TillQuandel merged 2 commits into
masterfrom
fix/198-trace-db-buchfuehrung
Jul 12, 2026
Merged

fix(generative): pipeline_runs-Insert vom Inline-Eval entkoppeln + Test-Traces nach tmp (#198 P1+P2)#218
TillQuandel merged 2 commits into
masterfrom
fix/198-trace-db-buchfuehrung

Conversation

@TillQuandel

@TillQuandel TillQuandel commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Setzt Punkte 1+2 aus #198 um. Punkt 3 wartet auf eine Maintainer-Entscheidung (siehe unten), Punkt 4 läuft über #187 — daher bewusst kein closes #198.

Problem

P1 — Insert-Lücke (strukturell): In generative/orchestrator.py stand der pipeline_runs-Insert INNERHALB von Stage 8 (Inline-Eval). Ist inline_eval_enabled() False (Profil fast/balanced oder ATOMIC_AGENT_INLINE_EVAL=0), kehrte main() VOR dem Insert zurück. Solche Läufe schrieben einen vollständigen Trace, bekamen aber keine DB-Zeile — waren also keiner Pipeline-Version zuordenbar und verschwanden aus allen versions-gefilterten Ansichten.

P2 — Test-Verschmutzung: pytest nutzt dieselbe _trace()-Infrastruktur wie die Pipeline. Tests, die agents.base.call_claude_full(...) mit gestubbtem Backend aufrufen (z. B. test_call_record, test_eval_cache_namespace), schrieben echte Trace-Dateien ins produktive generative/.cache/runs/ (agent=test/verifier/critic, <ts>.jsonl).

Fix

P1: Token-Berechnung + run_meta + DB-Insert laufen jetzt unbedingt vor dem inline_eval-skip-Return. Der Insert hängt an nichts Eval-Spezifischem (nur Trace-Tokens + Run-Zähler); die eval-abhängigen note_evals bleiben Stage-8-exklusiv. get_db(_db.DB_PATH) statt get_db() liest den Modul-Pfad zur Laufzeit (wie calibration.collect), damit Tests ihn auf eine tmp-DB umbiegen können. Kein Doppel-Insert.

Präzisierung — der Verhaltenswechsel gilt auch bei aktivem Inline-Eval, nicht nur im deaktivierten Fall: Der Insert lag zuvor INNERHALB des Stage-8-try, NACH der Eval-Schleife. Schlug die Eval früh fehl (Exception vor dem Insert), ging die DB-Zeile auch bei aktivem Inline-Eval verloren — sie wurde nur vom except als [eval-warn] geschluckt. Jetzt läuft der Insert VOR Stage 8 und überlebt Stage-8-Frühausfälle. Der Eval-Overhead bleibt weiterhin außerhalb von run_meta/DB (nur am Run-Ende geprintet).

P2: Neue autouse-Fixture isolate_pipeline_side_effects in generative/conftest.py biegt tracing._backend, base._RUN_DIR und db.DB_PATH pro Test auf ein isoliertes tmp_path_factory-Verzeichnis um (bewusst NICHT tmp_path — viele Tests listen ihr eigenes tmp_path). Analog zum bestehenden Sicherheitsnetz in generative/gui/tests/conftest.py. Tests, die selbst einen Backend/DB-Pfad setzen, überschreiben die Fixture per monkeypatch (last-wins).

Reichweite der P2-Aussage: Die Fixture wirkt in-process. Der slow-gated Subprozess-Test tests/test_e2e_baseline.py startet die Pipeline als eigenen Prozess — dort greifen die Monkeypatches prinzipiell nicht (siehe Nachbesserung Fix 3). „Kein Test schreibt ins produktive .cache/" gilt also für die in-process-Suite; der e2e-Subprozess isoliert seine DB-Senke separat per ATOMIC_DB_PATH.

RED-Beweis (TDD)

P1test_pipeline_run_persist_no_eval.py fährt main() mit fast-Profil (Inline-Eval AUS) auf dem echten Beispiel-PDF (LLM/Netz gestubbt, Harness aus test_ci_smoke_e2e). Auf master-Stand (Fix gestasht):

[8/8] Qualitäts-Eval übersprungen (Profil: fast, inline_eval deaktiviert) — retro via reeval_baseline.py.
FAILED ... - AssertionError: Erwartet: genau 1 pipeline_runs-Zeile (Insert-Lücke #198 P1), gefunden: []

Mit Fix: 1 Zeile mit korrekter pipeline_version → grün.

P2 — Im frischen Worktree existierte kein runs/-Verzeichnis. Nach zwei Polluter-Tests OHNE Fixture:

generative/.cache/runs/20260712-022522.jsonl   (agent="verifier"/"critic", ...)

test_trace_isolation.py beweist die GREEN-Seite: ein getracter Call landet in tmp, das produktive runs/ bleibt unberührt.

Suite-Ergebnis

  • pytest generative lib/decision_engine/tests shared/tests -q5697 passed, 3 skipped, 4 deselected, 0 failed (Baseline master: 5693 passed; +4 neue Tests: 2× P1/P2, 2× Nachbesserung).
  • ruff format --check · ruff check → clean.

Nebenbefund (nicht Teil dieses PRs): main(--no-llm) mutiert config.ENABLE_LLM = False als Modul-Global ohne Restore — ein bestehender Prozess-State-Leak. Der neue P1-Test snapshottet/restauriert den Wert selbst, damit er keine Folge-Tests (z. B. test_verifier_prepass) in den no-LLM-Modus zieht. Eine saubere Kapselung in main() wäre ein eigener kleiner Folge-Fix.

Nachbesserung (Commit 58227e6, extern verifizierte Funde)

Kontrollreview (Fable) + Runtime-Funktionsprüfung deckten Rest-Lücken in der P2-Isolation und einen Crash-Pfad auf. orchestrator.py bleibt unangetastet (Konfliktschonung ggü. #216/#220) — Fix 4 sitzt an der Wurzel in eval_agent_stats.py.

  • Fix 1 [MITTEL] — LLM-Cache-Senke: Die Fixture deckte base._LLM_CACHE_DIR (.cache/llm/) nicht ab. Der in-process-main()-Test persistierte Fake-Backend-Antworten unter ECHTEN Cache-Keys im produktiven LLM-Cache. → _LLM_CACHE_DIR in die autouse-Fixture aufgenommen (tmp).
  • Fix 2 [MITTEL] — Rotations-Lösch-Senke: rotate_run_caches() am main()-Ende rotiert das ECHTE config.CACHE_DIR (Caps llm=2000 / runs=600, ältestes wird gelöscht). Ein Suite-Lauf auf einer gefüllten Maintainer-Maschine hätte reale Cache-Dateien gelöscht. → Fixture bindet cache_rotation.rotate_run_caches per Wrapper auf ein tmp-cache_root (echte Rotations-Logik bleibt exerziert, nur Zielverzeichnis ist tmp).
  • Fix 3 [MITTEL] — e2e-Subprozess: tests/test_e2e_baseline.py (slow-gated) startet die Pipeline als Subprozess ohne DB-Override → in-process-Fixture greift nicht. → dem Subprozess-Env ATOMIC_DB_PATH=tmp mitgegeben (ENV existiert bereits, steuert db.DB_PATH zur Import-Zeit). Trace-/LLM-Cache-/Rotations-Senke kennen keine ENV-Steuerung; ein ATOMIC_CACHE_DIR-Umbau in config.py hätte breiten Import-Rippel → bewusst als Residuum im Testkommentar + hier dokumentiert (echter Lauf → legitime, rotations-gedeckelte Laufdaten, keine schädliche Verschmutzung).
  • Fix 4 [NIEDRIG] — run_totals defensiv: Der Token-Block am main()-Ende ruft eval_agent_stats.run_totals() ungeschützt; trace_path.read_text() konnte OSError/UnicodeDecodeError werfen und den Lauf am Ende crashen. → Datei-Lesen in run_totals toleriert jetzt Lese-/Decode-Fehler (→ Nullen, wie der Docstring-Contract behauptet). Deckt beide Aufrufstellen ab, ohne orchestrator.py anzufassen.
  • Fix 5 [NIEDRIG, dokumentiert] — Late-Binding-Falle: Die db-Query-Helfer binden DB_PATH als Default-Argument zur DEF-Zeit; no-arg-Aufrufer (query_*()) umgehen den db.DB_PATH-Monkeypatch (lesen die reale DB, read-only). Vorbestehend — jetzt im conftest-Docstring präzise abgegrenzt (abgedeckte vs. nicht abgedeckte Senken).

TDD: test_cache_isolation.py — zwei Tests snapshotten das REALE .cache/llm + .cache/runs rein lesend und beweisen, dass ein gecachter Call bzw. eine Rotation ausschließlich das tmp-Ziel trifft und die produktiven Datei-Mengen (davor == danach) unverändert bleiben.

Cross-Model-Review (Mistral magistral-medium-latest)

Voller Diff geprüft. Keine belastbaren Funde — jede aufgeworfene Sorge (Doppel-Insert, undefinierte Variablen im Eval-Block, run_totals-Exception, get_db-Laufzeitpfad, Fixture-Teardown/init_db, pdf_key-split) wurde vom Modell selbst als sicher bzw. unverändert ggü. Original verifiziert. Fazit wörtlich: „es gibt keine konkreten Bugs".

Offener Punkt 3 (Maintainer-Entscheidung, nicht in diesem PR)

total_tokens-KPI im Dashboard: Lifetime-Summe nur DB-gejoint vs. Orphans ausweisen. Betrifft eval_dashboard*.py/.html (parallele Arbeiten — hier bewusst nicht angefasst).

  • Option A (nur DB-Join): SUM(pipeline_runs.tokens_total). Konsistent mit den übrigen KPIs. Dieser PR schließt die größte Orphan-Klasse (eval-lose Voll-Läufe); verbleibende Orphans sind echte Leerläufe (0 Notes, frühe Returns bei „keine Konzepte"/„alles Artefakte") — deren Tokens aus einer Note-Produktions-KPI auszuschließen ist vertretbar.
  • Option B (Orphans ausweisen): zusätzlich die Trace-Tokens der DB-losen Läufe scannen und getrennt annotieren (z. B. „Lifetime X (+Y in Läufen ohne DB-Zeile)").
  • Empfehlung: Option A — nach diesem Fix ist die Orphan-Menge klein und semantisch „leer". Option B nur, falls volle Token-Transparenz gewünscht ist; dann als kleine Annotation, nicht als KPI-Umbau.

Merge-Reihenfolge

Nach #216 (fix/210-cli-timeout-retry) mergen. #216 ändert orchestrator.py ebenfalls (Extractor-Failure-Tracking, main()-Exit-Code 3, return exit_code am eval-skip, sys.exit(main())).

Konfliktflächen-Einschätzung: gering. Meine zwei Hunks — (1) Insert-Block eingefügt zwischen frühem Token-Print und # --- Stage 8-Kommentar, (2) alter Token/Insert-Block INNERHALB des Eval-try entfernt — überlappen keine der #216-Hunks (Fertig-Warnblock ~L2412, returnreturn exit_code ~L2496, sys.exit(main()) ~L2614). #216s eval-skip-Return liegt im unberührten Spalt zwischen meinen beiden Hunks → erwartetes sauberes 3-Wege-Auto-Merge. Semantisch nach beidem korrekt: der eval-skip-Pfad macht return exit_code, NACHDEM mein Insert-Block gelaufen ist.

…st-Traces nach tmp (#198 P1+P2)

P1 (Insert-Lücke): Bei deaktiviertem Inline-Eval (Profil fast/balanced oder
ATOMIC_AGENT_INLINE_EVAL=0) kehrte orchestrator.main() VOR dem pipeline_runs-Insert
zurück. Der Lauf schrieb einen vollständigen Trace, bekam aber keine DB-Zeile — war
also keiner Pipeline-Version zuordenbar und verschwand aus versions-gefilterten
Ansichten. Token-Berechnung + run_meta + DB-Insert laufen jetzt UNBEDINGT vor dem
inline_eval-skip-Return; die eval-abhängigen note_evals bleiben Stage-8-exklusiv.
get_db(DB_PATH) statt get_db() liest den Modul-Pfad zur Laufzeit (Test-Isolation).

P2 (Test-Verschmutzung): Neue autouse-Fixture in generative/conftest.py biegt
Trace-Backend, base._RUN_DIR und db.DB_PATH pro Test auf ein isoliertes tmp-Verzeichnis
um — kein Test schreibt mehr ins produktive .cache/runs/ bzw. atomic_analytics.db.

Tests: test_pipeline_run_persist_no_eval (P1, RED auf master: 0 statt 1 Zeile),
test_trace_isolation (P2). Suite generative/lib/decision_engine/shared grün.
…-Override, run_totals defensiv (#198 Nachbesserung)
@TillQuandel TillQuandel merged commit 31c655a into master Jul 12, 2026
4 checks passed
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