gwoe-antragspruefer/docs/adr/0003-citation-property-tests.md

107 lines
4.5 KiB
Markdown
Raw Permalink Normal View History

#62 Phase 1+3: ADRs + Doku-Struktur in webapp/docs/ Architektur-Entscheidung aus Issue #62: Diátaxis-Framework für Doku- Pflege ohne Drift. Pflege im Repo, ADRs immutable, Stale-Snapshots explizit als Archiv markiert. Phase 1 — Architecture Decision Records: - docs/README.md — Diátaxis-Index, Erklärung was wo dokumentiert wird - docs/adr/README.md — ADR-Workflow + Index - docs/adr/template.md — Vorlage für neue ADRs - docs/adr/0001-llm-citation-binding.md — Issue #60 Doppel-Fix-Story (A=ENUM-Anker, B=server-seitige Rekonstruktion, warum Option C verworfen) - docs/adr/0002-adapter-architecture.md — ParlamentAdapter-Basisklasse + Registry, Klassen vs. Strategy vs. Modul-pro-Adapter - docs/adr/0003-citation-property-tests.md — Sub-D Strategie, warum Property-Test gegen echte PDFs statt Schema-Tests oder Online-Verify - docs/adr/0004-deployment-workflow.md — Docker-Compose + Volumes Standard-Workflow + SN-XML-Sonderpfad + Container-UTC-Gotcha Phase 3 — Stale Doku archiviert: - DOKUMENTATION.md (24.März, Skript-Architektur vor Webapp-Migrate) → docs/archive/DOKUMENTATION-2026-03-24.md - STATUS-2026-03-28.md (Tagesstand-Snapshot) → docs/archive/STATUS-2026-03-28.md - README.md (28.März, listet nur NRW-Adapter, vor 16 weiteren BLs) → docs/archive/README-2026-03-28.md - docs/archive/README.md erklärt warum die Files da sind und warum niemand sie überschreiben oder ersetzen sollte Plus neue Top-Level-README.md im Project-Root (außerhalb git, da project-root kein Repo ist) als Folder-Index für den User. CLAUDE.md ergänzt um Doku-Sektion mit Verweis auf docs/adr/. Phase 2 (mkdocs Setup) folgt separat — braucht eine Docker-Image- Erweiterung, die ich nicht autark einrollen will ohne Decision. Tests: 194/194 grün (keine Code-Änderung). Refs: #62
2026-04-10 01:38:03 +02:00
# 0003 — Sub-D Property-Verification: Zitate als Substring der zitierten PDF-Seite
| | |
|---|---|
| **Status** | accepted |
| **Datum** | 2026-04-10 |
| **Refs** | Issues #50, #54, #60; tests/integration/test_citations_substring.py |
## Kontext
Der LLM-Output enthält pro Assessment N Zitate, jedes mit `text`, `quelle`
(z.B. "GRÜNE NRW Wahlprogramm 2022, S. 58") und `url`. Wahrscheinlich
korrekt — aber wie verifizieren wir das, ohne jedes einzeln händisch
nachzuschlagen?
Die naheliegenden Test-Optionen sind alle unbefriedigend:
- **Mock-LLM-Tests**: prüfen das Schema, sagen aber nichts über die
inhaltliche Korrektheit.
- **Snapshot-Tests** der LLM-Outputs: drift mit jedem Modell-Update.
- **Manuelles Stichprobenchecken**: skaliert nicht über mehrere BLs.
## Optionen
### Option A — Schema-only Tests (was wir vorher hatten)
Pydantic validiert dass jedes Zitat die Felder `text`, `quelle`, `url` hat
und `url` mit `/static/referenzen/` beginnt. Erkennt syntaktische
Korruption, aber keine Halluzinationen.
### Option B — Property-Test gegen die echten PDFs
Pro Zitat in der Prod-DB:
1. `quelle` per Token-Coverage-Match auf den `PROGRAMME`-Eintrag mappen.
2. Seitennummer aus `quelle` extrahieren.
3. Per `fitz` die PDF-Seite lesen, Whitespace + Soft-Hyphen normalisieren.
4. `text` muss als Substring (oder 5-Wort-Anker) in der Seite vorkommen.
5. Bug-Klasse 17 (Cross-Bundesland-Zitat): das aufgelöste Programm muss
zum Bundesland des Antrags passen, oder ein Grundsatzprogramm sein.
**Vorteile:** prüft die einzige Eigenschaft die wirklich zählt — "war
das was zitiert wird auch wirklich da". Findet Halluzinationen direkt.
**Nachteile:** braucht eine lokale Kopie der `gwoe-antraege.db` und der
Wahlprogramm-PDFs. Test ist Pydantic-Schema-übergreifend (Integration,
nicht Unit). Skipped sauber wenn DB nicht gemounted ist.
### Option C — Online-Verifikation pro Assessment-Insert
Im `analyze_antrag`-Flow direkt nach LLM-Call jedes Zitat verifizieren
und bei Failure abbrechen oder retry.
**Vorteile:** kein "stale data in DB"-Risiko.
**Nachteile:** fügt Latenz und Komplexität in den Hot-Path. Die
Verifikation ist O(N×M), wo N=Zitate und M=Wahlprogramm-Pages.
## Entscheidung
**Option B als pytest-Integration-Test** — `tests/integration/test_citations_substring.py`,
parametrisiert per `_load_recent_assessments(limit_per_bl=5)` × `_flat_zitate()`.
**Strict substring** als Default-Match (Whitespace + Soft-Hyphen normalisiert,
LLM-Truncation-Marker `...` toleriert), **5-Wort-Anker als Fallback** für
geringfügige Wort-Drift wie "LLM hat mittendrin gekürzt". Min-Length-Guard
von 20 Zeichen verhindert false-positive Matches auf trivialen Snippets.
Marker `pytestmark = pytest.mark.integration` — der Test läuft nicht in
der Default-Suite, sondern explizit per `pytest -m integration`. Skipped
wenn `webapp/data/gwoe-antraege.db` nicht existiert (Dev-Setup ohne
DB-Kopie).
Match-Helpers (`_normalize`, `_is_substring`, `_resolve_quelle_to_programm_id`,
`_extract_page_number`) sind eigene Unit-Tests in `TestHelpers` — die Match-
Logik selbst ist nicht-trivial und braucht ihre Eigenkontrolle.
## Konsequenzen
### Positiv
- **Findet Halluzinationen direkt**: Issue #60 wurde durch den ersten
Live-Lauf dieses Tests entdeckt (3 von 36 Citations failed), ohne
dass ein Mensch Wahlprogramm-PDFs aufmachen musste.
- **Re-runnable als Regression-Gate**: nach jedem Deploy einmal `pytest -m
integration` gegen die DB → 0 Failures = OK.
- **Test-Logik = Production-Logik**: ADR 0001 Option B (`reconstruct_zitate`)
nutzt **identische** Match-Heuristiken (`find_chunk_for_text`,
`_normalize_for_match`). Damit kann der Test nichts fangen, was die
Production nicht auch fangen würde, und umgekehrt — kein Test-/Prod-Drift.
### Negativ
- **Lokale DB-Kopie nötig**: vor jedem Sub-D-Run muss `data/gwoe-antraege.db`
vom Container gepullt werden. CI-Integration steht aus.
- **Test ist langsam-ish**: ~50 Citations × ein PDF-Open pro Programm ist
bei den ~30 indexierten Programmen ~250ms im Ganzen, nicht trivial aber
nicht prohibitiv.
- **Token-Coverage-Heuristik für Quelle-zu-Programm-Mapping** kann false-
positive bei sehr ähnlichen Programmen werden (z.B. CDU NRW 2022 vs. CDU
Niedersachsen 2022 — würde durch Bundesland-Bonus-Check abgefangen).
### Folgen für andere ADRs
- **ADR 0001** ist von ADR 0003 abhängig — wenn dieser Test entfernt würde,
hätte der LLM-Citation-Postprocess keinen Backstop und neue Halluzinations-
Bug-Klassen würden still durchrutschen.