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
107 lines
4.5 KiB
Markdown
107 lines
4.5 KiB
Markdown
# 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.
|