Skip to content

feat(generative): Stage-Outcome-Events + Drop-Gründe an den Gate-Punkten (#197 Schritt 1+2)#220

Open
TillQuandel wants to merge 2 commits into
masterfrom
feat/197-stage-outcome-events
Open

feat(generative): Stage-Outcome-Events + Drop-Gründe an den Gate-Punkten (#197 Schritt 1+2)#220
TillQuandel wants to merge 2 commits into
masterfrom
feat/197-stage-outcome-events

Conversation

@TillQuandel

@TillQuandel TillQuandel commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Schritte 1+2 aus #197; der Funnel-Chart folgt separat, sobald stage_outcome-Events aus echten Läufen vorliegen. Kein closes #197.

Schritt 1 — stage_outcome-Event an den Gate-Punkten

Ein konsistentes Trace-Event pro Note, das ihren Weg durch die Pipeline maschinenlesbar macht. Ergänzt das bestehende Event-Vokabular (note_outcome/score_result/anchor_stats/plan_stats) um genau eine Klasse über denselben trace_event()-Pfad — kein zweites Tracing-System. Verifier und Critic tragen ihr per-Note-Urteil bereits via anchor_stats bzw. score_result; dieses Event füllt die bisher stummen Gates: Faithfulness (hatte gar kein Event) und alle Drop-Klassen (vorher nur aggregierte Zähler / stderr-Prints, aus dem Trace nicht rekonstruierbar).

Event: trace_event("orchestrator", "stage_outcome", {title, stage, outcome, drop_reason?, detail?})

Schema (stage × outcome × drop_reason)

Emit-Ort stage outcome drop_reason detail
_drop_artifacts artifact dropped absence_artifact
dedup_exact dedup dropped exact_dup
resolve_sibling_dups dedup dropped sibling_neardup Survivor-Titel
_collect_stage6_results stage6 dropped stage6_crash step/phase
_apply_faithfulness_gate (Fail) faithfulness downgraded faithfulness_fail N failed
_apply_faithfulness_gate (Pass) faithfulness passed N supported
_apply_faithfulness_gate (action≠create) faithfulness skipped action_not_create action

Damit sind die im Issue genannten fehlenden Drop-Klassen Artifact-Drop und Sibling-Dedup abgedeckt, plus Exact-Dedup, Stage-6-Crash und der bisher event-lose Faithfulness-Verdikt.

Bewusst ausgelassen (dokumentierter Follow-up): Extractor-Empty-Drops (bereits in n_dropped gezählt, und der Loop ist die direkte Konfliktfläche von #216 — Function bleibt unangetastet) und Entity-Resolution-Cluster-Merges (content-erhaltender Merge, kein Drop; bräuchte Embedding-Mocks zum Testen). Beide sind für den Funnel über vorhandene Zähler/Prints ableitbar.

stage_outcome trägt (wie note_outcome) kein model-Feld → wird von _is_llm_call_record korrekt als Bookkeeping klassifiziert und von eval_agent_stats.aggregate/run_totals sauber übersprungen (keine bestehende Aggregation betroffen).

Schritt 2 — n_generated: neues Feld statt Semantikwechsel

Entscheidung: n_generated bleibt semantisch unangetastet (= geschriebene Notes; historische Zeilen in pipeline_runs behalten ihre Bedeutung, Alt/Neu bleiben vergleichbar). Ein neues, korrekt benanntes Feld n_extracted (= „nach Planner/Extractor generiert", Funnel-Top = len(drafts) vor Artifact-/Dedup-/Stage-6-Drops) wird additiv ergänzt — exakt das Muster von n_dropped/cost_usd: SCHEMA_SQL (frische DBs) + init_db-ALTER TABLE ADD COLUMN (bestehende DBs) + insert_run. Keine Migration/Mutation bestehender Zeilen, kein stiller Semantik-Bruch. Fehlt der Key (Alt-Aufrufer), wird 0 geschrieben (kein NULL, kein Crash); insert_run ist im Orchestrator best-effort (try/except).

RED-Beweis (TDD)

Vor der Implementierung, generative/tests/test_stage_outcome_events.py:

10 failed, 2 passed
  FAILED test_drop_artifacts_emits_stage_outcome - assert 0 == 1
  FAILED test_dedup_exact_emits_stage_outcome - assert 0 == 1
  FAILED test_resolve_sibling_dups_emits_stage_outcome - assert 0 == 1
  FAILED test_collect_stage6_results_emits_stage_outcome - assert 0 == 1
  FAILED test_faithfulness_downgrade_emits_stage_outcome - assert 0 == 1
  FAILED test_faithfulness_pass_emits_stage_outcome - assert 0 == 1
  FAILED test_faithfulness_skip_emits_stage_outcome - assert 0 == 1
  FAILED test_pipeline_runs_has_n_extracted_column - assert 'n_extracted' in [...]
  FAILED test_insert_run_stores_n_extracted_independent_of_n_generated - no such column: n_extracted
  FAILED test_insert_run_n_extracted_defaults_zero - no such column: n_extracted

Die 2 anfangs grünen Tests sind die Negativ-Assertions (kein Event erwartet: _drop_artifacts behält, Faithfulness global deaktiviert). Nach Implementierung: 12 passed. Traces in Tests via echtes JsonlBackend auf tmp_path umgebogen (Muster aus test_per_agent_tracking.py); DB-Tests via tmp_path/"test.db" — echte .cache-DB/JSONL nie angefasst.

Suite

pytest generative lib/decision_engine/tests shared/tests -q
5705 passed, 3 skipped, 4 deselected  (Baseline 5693 + 12 neue Tests)

ruff format --check . clean · ruff check . clean. Keine Pipeline-Läufe; keine Eval-Daten berührt.

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

Sauberer Durchlauf, keine realen Funde. Verifiziert: (1) n_generated/n_dropped-Semantik unverändert; (2) n_extracted vor Drops gesetzt und korrekt persistiert; (3) stage_outcome bricht keine bestehende Aggregation, thread-sicher (bestehendes trace_event + JsonlBackend-Lock); (4) SQL-Spalten/VALUES/Params konsistent; (5) Abwärtskompatibilität (Default 0). Nichts zu fixen.

Merge-Reihenfolge & Konfliktflächen (empirisch via git merge-tree)

Empfohlen: #216#218 → dieser PR.


Nachbesserung (2026-07-12) — Funnel-Lücken geschlossen + Dashboard-/DB-Event-Robustheit

Extern verifizierte Funde (Mistral-Resample + Fable) abgearbeitet. Der Gate-Funnel ist damit oben dicht: jeder Punkt, an dem eine Note verschwindet, trägt jetzt ein stage_outcome-Event.

Funnel-Lücken (orchestrator.py, TDD — RED→GREEN)

Gate stage outcome drop_reason detail
entity_resolution Cluster-Merge (consumed-Set) dedup dropped entity_resolution_merge Survivor-Titel
_collect_stage6_results BaseException-Zweig stage6 dropped stage6_crash Exception-Typ
run_extractors_per_concept Konzept nicht im Volltext extractor dropped empty_extraction not in fulltext
run_extractors_per_concept Extractor-None (leer) extractor dropped empty_extraction
run_extractors_per_concept Extractor-Exception extractor dropped call_failed Fehlertext (gekappt)
_apply_faithfulness_gate Gate aktiv, aber leerer page_index faithfulness skipped no_page_index
  • ER-Merge: Nicht-Repräsentanten eines Embedding-Clusters verschwanden bisher ohne Event — strukturell derselbe Vorgang wie resolve_sibling_dups (das im PR bereits eines bekam). Test mit gemockten Embeddings/merge_cluster (Muster aus test_semantic_dedup.py) — die im PR-Body genannte Embedding-Mock-Hürde ist damit erledigt.
  • BaseException (z. B. asyncio.CancelledError): droppte die Note bisher ohne Event UND ohne Crash-Report — der Nachbar-Zweig (_Stage6Failure) hatte beides. Jetzt konsistent: Crash-Report (Payload ohne per-Note-Titel, "?") + Event.
  • Faithfulness no_page_index: Bei aktivem Gate + action=create, aber leerem page_index (PDF ohne [S. N]-Marker, dokumentierter Fall) griff weder Gate- noch Skip-Zweig → gar kein Event.

Dashboard-/DB-Event-Robustheit (minimale Hunks)

  • eval_dashboard._read_token_runs (Punkt 5): zählte jede nicht-gecachte JSONL-Zeile als „call". Die neuen (und bestehenden) Bookkeeping-Events tragen kein modelif "model" not in r: continue (inline statt Import — eval_dashboard_server importiert eval_dashboard, ein Rück-Import wäre zirkulär). Verhindert aufgeblähte calls und die 0-Token-Phantomzeile bei Runs mit 0 echten LLM-Calls.
  • eval_dashboard_server model-Backfill (Punkt 6): scannte nur die ersten 5 Trace-Zeilen nach model; mit stage_outcome-Events am Trace-Anfang lief der Backfill leer. Fenster auf 50 Zeilen erweitert (Zeilen ohne model wurden ohnehin übersprungen, break nur bei Treffer).
  • db.init_db (Punkt 7): PRAGMA busy_timeout=5000 nach connect (kein WAL-Umbau) + neuer Helper _add_column, der nur „duplicate column name" verschluckt und jeden anderen OperationalError (v. a. „database is locked" bei Parallel-Prozessen) re-raised. Der frühere pauschale except OperationalError: pass deutete einen Lock-Fehlschlag als „Spalte existiert" fehl — die Spalte fehlte danach und spätere Inserts crashten.

Tests & Suite

  • 8 neue Tests: 5 in test_stage_outcome_events.py (ER-Merge, BaseException+Report, Extractor-empty-ctext, Extractor-None/Exception, Faithfulness-no-page-index — TDD, RED vor Implementierung gezeigt), 3 in neuem test_stage_outcome_robustness.py (Punkt 5 Call-Filter + Phantomzeile, Punkt 7 _add_column-Diskriminierung).
  • pytest generative lib/decision_engine/tests shared/tests -q5713 passed, 3 skipped, 4 deselected (Nachbesserungs-Baseline 5705 + 8). ruff format --check . clean · ruff check . clean. Keine Pipeline-Läufe, keine Eval-Daten berührt.
  • Punkt 6 ist ein Ein-Konstanten-Hunk im inline-Backfill (nicht eigenständig aufrufbar) — durch die volle Server-Test-Suite gedeckt, kein dedizierter Test.

Konfliktflächen — aktualisiert

…ten (#197 Schritt 1+2)

Schritt 1: Neues `stage_outcome`-Trace-Event (ein konsistentes Event pro Note
an jedem Gate-Punkt) mit stage/outcome/drop_reason/detail. Ergänzt das
bestehende Event-Vokabular (note_outcome/score_result/anchor_stats/plan_stats)
um genau eine Klasse über denselben trace_event()-Pfad — kein zweites
Tracing-System. Füllt die bisher stummen Gates: Faithfulness (kein Event) und
die Drop-Klassen Artifact-Drop, Stage-6-Crash, Exact-Dedup, Sibling-Dedup
(vorher nur aggregierte Zähler/stderr-Prints, aus dem Trace nicht
rekonstruierbar). Verifier/Critic tragen ihr per-Note-Urteil bereits via
anchor_stats bzw. score_result.

Schritt 2: Neues DB-Feld n_extracted ("nach Planner/Extractor generiert",
Funnel-Top) additiv ergänzt (SCHEMA_SQL + init_db-ALTER + insert_run, Muster
wie n_dropped/cost_usd). n_generated (= geschriebene Notes) bleibt semantisch
unangetastet — Alt- und Neu-Zeilen bleiben vergleichbar, keine Migration/
Mutation bestehender Zeilen.

Nur Schritt 1+2 aus #197; Funnel-Chart folgt separat, sobald Events aus echten
Läufen vorliegen.

Tests: generative/tests/test_stage_outcome_events.py (12 Tests, alle Gates +
DB-Feld). Suite 5705 passed (Baseline 5693 + 12).
…/Extractor/Faithfulness-Skip-Events + Dashboard-Event-Robustheit (#197 Nachbesserung)

Extern verifizierte Funde (Mistral-Resample + Fable). Der Gate-Funnel ist oben
dicht: jeder Note-Verlust traegt jetzt ein stage_outcome-Event.

Funnel-Luecken (orchestrator.py, TDD):
- entity_resolution Cluster-Merge (consumed-Set): dedup/dropped/
  entity_resolution_merge, Survivor als detail (strukturell wie sibling-Dedup).
- _collect_stage6_results BaseException-Zweig: droppte ohne Event UND ohne
  Crash-Report — jetzt beides (Crash-Report ohne per-Note-Payload + Event).
- run_extractors_per_concept: empty-ctext (Konzept nicht im Volltext), None
  (leere Extraktion) und Exception (call_failed) → extractor/dropped-Events.
- _apply_faithfulness_gate: Gate aktiv + create, aber leerer page_index (PDF
  ohne [S. N]-Marker) → faithfulness/skipped/no_page_index.

Dashboard-/DB-Event-Robustheit (minimale Hunks):
- eval_dashboard._read_token_runs: Event-Records (kein model) vom call-Zaehler
  ausschliessen — verhindert aufgeblaehte calls + 0-Token-Phantomzeile.
- eval_dashboard_server model-Backfill: Scan-Fenster 5 -> 50 Zeilen (Events am
  Trace-Anfang tragen kein model).
- db.init_db: PRAGMA busy_timeout=5000 (kein WAL-Umbau) + _add_column-Helper,
  der nur 'duplicate column name' verschluckt und andere OperationalError
  (z.B. 'database is locked') re-raised.

Tests: 8 neu (5 orchestrator TDD in test_stage_outcome_events.py, 3 in neuem
test_stage_outcome_robustness.py). Suite 5705 -> 5713 passed, 0 failed.
ruff format/check clean.

Konflikt: #216 fasst run_extractors_per_concept Exception-Zweig an (failures) —
beim Merge beide Zeilen behalten. #222/#212 konfliktfrei zu Punkt 5/6.
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