gwoe-antragspruefer/docs/adr/0003-citation-property-tests.md
Dotty Dotter 45379a2639 #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

4.5 KiB
Raw Permalink Blame History

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-Testtests/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.