ADRs: - 0006 Embedding-Modell-Migration v3->v4 (#123) - 0008 DDD-Lightweight-Migration (#136) Analysen: - ddd-bewertung.md (1237 Zeilen) — vollstaendige DDD-Analyse mit Tages-Roadmap - protokoll-parser-v6-machbarkeit.md (418 Zeilen) — #106 Phase 2 Vorbereitung Reference: - zugriffsrechte.md — 63 Routes x 3 User-Status, UI-Sichtbarkeits-Matrix Ops: - scripts/deploy.sh — mit Uptime-Kuma-Wartungsmodus (#149) - scripts/run-digest.sh — taeglicher Mail-Digest-Cron - scripts/run-monitoring-scan.sh — Monitoring-Scan-Cron (noch nicht aktiv) - scripts/smoke-test.sh — Gesamt-Funktionspruefung - pytest.ini: integration/slow/e2e Markers, addopts not-integration Tests/integration/: Live-Adapter-Tests + Frontend-XRef + Citation-Substring + Wahlprogramm-Indexed (4 Live-Test-Suites, marker-opt-in) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
17 KiB
Plenarprotokoll-Parser v6 — Machbarkeits-Analyse
| Status | Exploration |
| Datum | 2026-04-20 |
| Refs | Issue #106 (Abstimmungsverhalten), #126 (BL-übergreifender Parser), MEMORY reference_nrw_protokoll_parser.md |
| Autor | Claude Opus 4.7 (1M) — strukturelle Exploration, kein Code |
Zweck
Strukturierte Bewertung, ob ein Parser v6 für das Abstimmungsverhalten in Plenarprotokollen sinnvoll ist — und wenn ja, in welcher Architektur. Entscheidungsgrundlage, keine Implementierungs-Vorgabe.
1. Bestandsaufnahme: v5 heute
1.1 Wo lebt der Parser?
Befund: Im Produktionscode (webapp/app/) existiert kein
protokoll_parser.py oder vergleichbares Modul. Grep über alle
app/*.py auf parse_protokoll, Plenarprotokoll, MMP, Wer stimmt,
Damit ist der Antrag liefert null Treffer in Produktions-Python —
nur ein inzidentelles Plenarprotokoll im Kommentar zu
parlamente.py:986.
Der v5-Parser ist also POC-Code aus einer früheren Session, der
nicht ins Repo gelangt ist. Die einzige belastbare Referenz ist die
Memory-Datei
~/.claude/projects/…gwoe…/memory/reference_nrw_protokoll_parser.md
(95 Zeilen, 12 Apr. 2026).
Konsequenz: v6 ist kein Refactor, sondern ein Neuaufbau. Das reduziert Altlast-Risiko, bedeutet aber auch: es gibt keine Baseline-Test-Fixtures im Repo, keine bestehenden Call-Sites, die zu brechen wären. Saubere grüne Wiese.
1.2 Was v5 laut Memory kann
Aus reference_nrw_protokoll_parser.md (zur Orientierung, nicht als
Autorität — Memory ist 8 Tage alt):
Input: PDF-Text von landtag.nrw.de/.../MMP{WP}-{N}.pdf, nach
Worttrennungs-Auflösung (-\s*\n\s* → "") und
Whitespace-Normalisierung.
Anchor-Phrasen für Ergebnis-Klassifikation:
Damit ist der Antrag [Drucksache X] angenommen|abgelehnt
Damit ist der Gesetzentwurf [Drucksache X] angenommen|abgelehnt
Damit ist dieser Antrag Drucksache X angenommen|abgelehnt
Damit ist (diese|die) Überweisungsempfehlung … angenommen → überwiesen
Somit ist dieser Antrag Drucksache X abgelehnt
Segment-Boundaries (trennen Abstimmungen im selben TOP):
Damit kommen wir zur Abstimmung
Wir kommen (somit )?zur Abstimmung
Wir stimmen (?!zu)
Somit kommen wir (direkt )?zu den Abstimmungen
Vote-Pattern (Ja/Nein/Enthaltung):
Wer stimmt … zu? – Das (ist|sind) [Fraktionen]→ Ja-StimmerWer stimmt dagegen?/Wer lehnt … ab?→ Nein-StimmerWer enthält sich?/Gibt es Enthaltungen?→ Enthaltungen
Negations-Antworten (= leere Liste): niemand, Keine Gegenstimmen, nicht der Fall, Auch nicht.
Sonderfälle:
- Re-Vote (Präsident unterbricht): „Vielleicht sind sich dann alle einig." → letzte Instanz im Segment zählt.
- Beschlussempfehlung-vs-Gesetzentwurf: abgestimmt wird über den
Gesetzentwurf, die Empfehlungs-DS ist
nicht_gesondert_abgestimmt. - Protokoll-Typo (MMP18-115): Anchor nennt falsche DS → Segment-Entry-DS hat Vorrang (v6-Forderung).
- Petitions-Sammelüberweisung (Drucksache 18/33 = Übersicht) → Einzelpetitionen haben kein Ergebnis.
1.3 Validierungs-Stand v5
| Protokoll | Treffer | Bemerkung |
|---|---|---|
| MMP18-119 (Training) | 19/19 | 100 % Precision und Recall |
| MMP18-115 | 10/32 | fehlende Anchor-Varianten |
| MMP18-110 | 17/6 | Sammel-Petitions-Einzelansprache (False Positives) |
| MMP18-100 | 8/25 | ähnlich wie MMP18-115 |
Interpretation: v5 ist overfit auf MMP18-119. Präzision bleibt hoch (keine falschen Abstimmungs-Ergebnisse), aber der Recall-Einbruch auf ~30-50 % schon innerhalb NRW zeigt: die Anchor-Liste deckt nicht einmal NRW-interne Variation ab. Cross-BL wird drastisch schlechter.
2. Cross-BL-Strukturanalyse
2.1 Sample-Verfügbarkeit im Repo
Befund: Keine Plenarprotokoll-Samples im Repo.
Geprüfte Pfade:
webapp/tests/fixtures/→ leer (nur Sub-Dir-Marker, keine Dateien)webapp/app/static/→ nur Wahlprogramm-PDFswebapp/data/→ nurgwoe-antraege.db(SQLite)antraege/(Projekt-Root) → 80+ PDFs, aber alle Drucksachen (Anträge), keine MMP-ProtokolleTEMP/→ HAR-Captures der Dokumentations-Frontends, keine PDFs
Keine einzige MMP*.pdf oder PlPr*.pdf im gesamten Projekt-Tree.
2.2 Strukturelle Annahmen aus Doku-Systemen
Ohne Samples ist eine echte Struktur-Analyse nicht möglich. Aus
app/bundeslaender.py und app/parlamente.py lassen sich nur
Plausibilitäts-Hypothesen ableiten:
| Familie | BL | Protokoll-Quirk (Hypothese) |
|---|---|---|
| OPAL | NRW | MMP-PDF, stereotyp (v5-Referenz) |
| StarWeb/portala | HE, SH, BB, RP, LSA, BE | Plenarprotokolle typischerweise als PDF mit Zweispalten-Layout |
| ParlDok | MV, HH, TH | eigene Redaktionsrichtlinie pro LT |
| PARiS | HB | Bürgerschaft, Kleinst-Parlament |
| Eigensystem | BY, SL, SN | je eigene Typografie |
| DIP (BUND) | BUND | Plenarprotokolle als strukturiertes XML verfügbar — Ausnahme! |
Wichtig: Die Adapter in parlamente.py liefern bisher Drucksachen
(Anträge), nicht Plenarprotokolle. Für Protokoll-Download müssten die
Adapter erweitert werden — das ist Teil des Scopes von #106.
2.3 Fraktions-Namen-Normalisierung — vorhanden
app/parteien.py:187 bietet normalize_partei(raw, *, bundesland=None),
das laut Docstring die vier Adapter-eigenen _normalize_fraktion()
ersetzt. Das ist entscheidend, weil Protokolle Fraktionen in ihrer
LT-spezifischen Schreibweise nennen (BÜNDNIS 90/DIE GRÜNEN,
GRÜNE, B'90/Grüne). Die Komponente ist da und getestet — v6 muss
sie nicht neu bauen.
3. Lösungs-Ansätze
3.1 Option A — Rule-based expand (v5 pro BL)
Pro Bundesland eine abstimmung_rules_<bl>.yaml mit BL-spezifischen
Anchors, Segment-Boundaries, Vote-Patterns. Ein generischer Engine-
Kern lädt die Rules und parst.
Pro:
- Deterministisch, reproduzierbar, offline.
- Null Laufzeit-Kosten, Millisekunden pro Protokoll.
- Präzision bleibt bei 100 % (nur bekannte Anchors matchen).
Contra:
- Recall-Problem überträgt sich linear auf 17 BL. v5 schafft in NRW schon nur 30-50 % auf unbekannten Protokollen — 17-fach skaliert heißt: pro BL eigene Overfit-Runde.
- Format-Drift zwischen Wahlperioden (neues Redaktionsteam, neuer Stilführer) bricht still.
- Wartungs-Aufwand: geschätzt 2-3 Tage Reverse-Engineering pro BL, plus ~0,5 Tage Nachpflege pro WP-Wechsel.
Aufwand: 2-3 Tage pro BL × 17 = 34-51 Personentage bis Vollabdeckung. Pro BL-Nachpflege alle 4-5 Jahre.
Risiko: Stille Recall-Einbrüche, die nur durch Ground-Truth-Tests entdeckt werden. Ohne kontinuierliches Fixture-Update degradiert die Datenqualität.
3.2 Option B — LLM-Extraction pro Seite
Ein strukturiertes Prompt nimmt eine Protokoll-Seite (oder einen 10-Seiten-Block) und liefert JSON:
{
"abstimmungen": [
{
"drucksache": "18/17492",
"titel": "…",
"ergebnis": "angenommen|abgelehnt|überwiesen",
"ja": ["CDU", "GRÜNE"],
"nein": ["SPD", "AfD", "FDP"],
"enthaltung": [],
"namentlich": false
}
]
}
Pro:
- Ein Prompt für alle 17 BL, Format-Drift-robust.
- Nutzt bestehende LLM-Pipeline (
analyzer.py, Qwen-Plus). - Neue BL kosten null Entwicklungs-Zeit nach Grund-Integration.
- Zitat-Binding-Pattern aus ADR 0001 direkt übertragbar (Post-LLM-Match gegen Protokoll-Text, genau wie bei Wahlprogramm-Zitaten).
Contra:
- Kosten: ein Protokoll hat 120-200 Seiten. Bei 10 Seiten pro Prompt-Call: ~15-20 Calls × ~0,03 €/Call bei Qwen-Plus = 0,45-0,60 € pro Protokoll. NRW-WP18 hat ~170 Plenarsitzungen → ~85 € allein für NRW, ~1500 € für alle 17 BL × 1 WP. Erträglich, aber nicht trivial.
- Laufzeit: 15-20 Calls × 10 s = 2,5-3 min pro Protokoll.
- LLM-Halluzinationen: ADR 0001 zeigt, dass Zitate cross-mixen. Hier: Gefahr, dass LLM ein Abstimmungs-Ergebnis halluziniert, das im Protokoll nicht vorkommt. Citation-Binding gegen Protokoll-Text ist Pflicht.
Aufwand: Grund-Integration 3-5 Tage (Prompt-Engineering + Citation- Binding analog ADR 0001 + Cache-Layer, damit dasselbe Protokoll nicht zweimal abgefragt wird). Pro BL null zusätzlich.
Risiko: LLM-Halluzinationen bei stillem Citation-Binding-Bug; Kosten-Explosion wenn alle WPs voll nachgefahren werden.
3.3 Option C — Hybrid: Rules-Pre-Filter + LLM-Strukturierung
Stufe 1 (Rules): Ein universelles Regex-Set (wenige, stabile
Anchors) segmentiert das Protokoll in Abstimmungs-Kandidat-Blöcke.
Kandidat: Text zwischen zwei Abstimmung|stimmen ab|stimmen … zu-
Vorkommen.
Stufe 2 (LLM): Jeder Kandidat-Block (~500-2000 Tokens) wird an den LLM gegeben mit dem Prompt „Extrahiere Drucksache, Ergebnis, Ja/Nein/ Enthaltung-Fraktionen. Nur was im Text steht."
Stufe 3 (Verifier): Jede extrahierte Fraktion wird gegen
normalize_partei() geprüft und gegen BUNDESLAENDER[bl] .landtagsfraktionen gefiltert. Halluzinierte Fraktionen (nicht im LT
vertreten) werden gedroppt oder loggen einen Warn.
Pro:
- Kosten ~80 % niedriger als Option B (nur Kandidat-Blöcke, nicht ganze Seiten).
- BL-übergreifend, weil Stufe 1 nur grobe Anchors braucht (
stimmen,Abstimmungals Lemma-Formen sind in allen dt. Parlamenten üblich). - Verifier fängt die meisten Halluzinationen ab.
- Skaliert auf neue BL mit nahezu null Zusatz-Aufwand.
Contra:
- Drei Stufen = drei Stellen, an denen Bugs sein können.
- Stufe 1 kann Kandidaten übersehen (stille Recall-Lücke). Debuggable über „kein Kandidat gefunden"-Log.
- Komplexer zu testen als reine Rules oder reines LLM.
Aufwand: 5-7 Tage Grund-Integration. Pro BL: 0,5 Tag (einmal Kalibrierung der Stufe-1-Anchors mit einem Protokoll-Sample).
Risiko: Mittel. Komplexität ist höher, aber jede Stufe isoliert testbar. Analog zur LLM-Citation-Binding-Architektur aus ADR 0001.
3.4 Bewertungs-Matrix
| Kriterium | A (Rules) | B (LLM-voll) | C (Hybrid) |
|---|---|---|---|
| Precision (erwartet) | 95-100 % | 85-95 % | 90-98 % |
| Recall (erwartet) | 30-60 % pro BL | 85-95 % | 80-92 % |
| Entwicklung (Tage) | 34-51 | 3-5 | 5-7 |
| Laufzeit-Kosten/Prot. | ~0 € | ~0,50 € | ~0,10 € |
| Skaliert auf neue BL | nein (linear) | ja (gratis) | ja (halbgratis) |
| Format-Drift-Robustheit | niedrig | hoch | mittel-hoch |
| Debug-Transparenz | hoch | niedrig | mittel |
| Offline-fähig | ja | nein | nein |
4. Entscheidungs-Baum
flowchart TD
Start([Issue #106: Abstimmungsverhalten]) --> Samples{Plenarprotokoll-<br/>Samples<br/>beschaffbar?}
Samples -->|nein| Stop1[STOP:<br/>Sample-Beschaffung zuerst]
Samples -->|ja, ≥3 BL| Scope{Scope<br/>klar?}
Scope -->|nur NRW, kurzfristig| OptA[Option A<br/>v5 härten auf NRW]
Scope -->|alle 17 BL| OptFamily{Kosten-Bereitschaft<br/>für LLM-Calls?}
OptFamily -->|nein, offline nötig| OptA_scaled[Option A:<br/>Rule-Engine pro BL<br/>34-51 Tage]
OptFamily -->|ja, 1500 €/WP OK| OptC[Option C Hybrid:<br/>5-7 Tage Grund<br/>+ 0,5 Tag/BL]
OptC --> POC[POC auf 2 BL:<br/>NRW + HE<br/>Ground-Truth-Set je 3 Prot.]
POC --> Eval{Recall<br/>≥ 80 % auf HE?}
Eval -->|ja| Rollout[Rollout auf 17 BL]
Eval -->|nein| Rethink[LLM-Prompt iterieren<br/>oder auf B eskalieren]
OptA_scaled --> NRW_first[NRW zuerst,<br/>dann nach Output-Ranking]
5. Empfehlung
5.1 Familie
Option C (Hybrid) als Primärweg. Begründung:
- Die bestehende Architektur passt:
analyzer.pynutzt schon LLM, ADR 0001 liefert das Binding-Pattern,parteien.py:normalize_parteigibt den Verifier. - Option A skaliert nicht auf 17 BL ohne Personen-Aufwand, den das Projekt realistisch nicht hat (siehe Issue-Backlog).
- Option B hat das Halluzinations-Risiko und Kosten ohne Gewinn gegenüber C.
5.2 Minimum-Viable-Scope
- Protokoll-Download-Erweiterung der Adapter:
get_plenarprotokoll (wp, sitzung)als optionale Methode. Zuerst nur NRW + 1 weiteres BL (Vorschlag: BUND, weil dort XML statt PDF → erleichtert Text-Extraktion). - Stufe-1-Anchors als einzige YAML-Datei (nicht pro BL), mit den
~5 stabilsten Formulierungen (
stimmen ab,Abstimmung über,wer stimmt zu,wer ist dagegen,wer enthält sich). - Stufe-2-Prompt mit Zitat-Binding-Anforderung (jede Fraktion muss als Exakt-Substring im Kandidat-Block vorkommen).
- Stufe-3-Verifier via
normalize_parteiundBUNDESLAENDER[bl].landtagsfraktionen. - Persistenz: neue Tabelle
abstimmungen (drucksache, wp, sitzung, ergebnis, ja_fraktionen, nein_fraktionen, enthaltung_fraktionen, source_url, extracted_at, extractor_version).
5.3 Abbruch-Kriterien
- Stop 1: Wenn POC auf 2 BL nach 7 Tagen unter 70 % Recall bleibt, ist C nicht erreicht. Fallback: B (voller Seiten-Scan) oder Parken.
- Stop 2: Wenn Nutzer-Interview zeigt, dass Abstimmungsverhalten pro Drucksache keinen Dashboard-Mehrwert gegenüber dem bereits aktiven Score hat, ist v6 unpriorisiert. Issue #106 hängt seit April offen — die Frage „braucht das jemand?" ist vor Implementierung zu klären.
- Stop 3: Wenn eine offene Datenquelle (z.B.
abgeordnetenwatch.de /api) Abstimmungsverhalten bereits strukturiert ausliefert, ist ein eigener Parser obsolet → Evaluations-Pflicht vor Bau.
5.4 BL-Priorisierung
Nach CSV-Export /api/auswertungen/export.csv und Wichtigkeit:
- NRW — größter Output, v5 vorhanden
- BUND — XML statt PDF, einfachste Implementierung, hoher Impact
- HE, BY, BW — große Flächenländer, hohe Drucksachen-Zahl
- BE, HH, HB — Stadtstaaten, geringer Protokoll-Umfang, guter Cross-Check für den Hybrid
- Rest nach Output-Ranking aus Dashboard
6. Sample-Beschaffungs-Plan
Da im Repo keine Plenarprotokolle liegen, für den POC zuerst manuell ziehen. Pro BL 1 aktuelles Protokoll:
| BL | Quelle | Format |
|---|---|---|
| NRW | landtag.nrw.de/portal/WWW/dokumentenarchiv/Dokument/MMP{WP}-{N}.pdf |
|
| BUND | bundestag.de/services/opendata (XML-Feed) |
XML |
| BY | bayern.landtag.de/parlament/dokumente/plenarprotokolle |
|
| BW | landtag-bw.de/home/dokumente/plenarprotokolle.html |
|
| BE | parlament-berlin.de/dokumente/open-data (XML verfügbar) |
XML/PDF |
| BB | parlamentsdokumentation.brandenburg.de (portala) |
|
| HB | paris.bremische-buergerschaft.de/starweb/paris |
|
| HH | buergerschaft-hh.de/parldok |
|
| HE | starweb.hessen.de/portal |
|
| MV | dokumentation.landtag-mv.de (ParlDok) |
|
| NI | nilas.niedersachsen.de (HAR-Capture nötig, siehe MEMORY) |
|
| RP | opal.rlp.de (portala) |
|
| SL | landtag-saar.de (Umbraco) |
|
| SN | edas.landtag.sachsen.de (XML-Export, siehe project_sn_xml_export.md) |
XML |
| LSA | padoka.landtag.sachsen-anhalt.de (PARDOK) |
|
| SH | lissh.lvn.parlanet.de/cgi-bin/starfinder/0 |
|
| TH | parldok.thueringer-landtag.de |
Speicherung unter webapp/tests/fixtures/protokolle/<bl>/ (git-lfs
oder außerhalb des Repos, je nach Dateigröße). Pro Protokoll eine
<protokoll>.expected.json mit Ground-Truth-Abstimmungen als
Fixture.
Minimum für POC: NRW (MMP18-119 als Regression), BUND (XML als Gegenprobe), HE (StarWeb als Cross-BL). Drei Protokolle × ~20-30 Abstimmungen = ~60-90 Ground-Truth-Items für Precision/Recall.
7. Offene Fragen für Tobias
- Bedarfs-Validierung: Wer konsumiert das Abstimmungsverhalten konkret im Dashboard? Gibt es eine UI-Spec dafür, oder ist das gedachte Feature? Issue #106 nennt keine konkreten Konsumenten.
- LLM-Kosten: Ist ~1500 € Budget für die 17-BL-Erstbefüllung einer WP vertretbar? Oder muss der Rollout auf eine WP pro Jahr gedeckelt werden?
- Alternative Datenquelle: Wurde
abgeordnetenwatch.de/apigeprüft? Die liefern auf Bundesebene strukturierte Abstimmungsdaten und haben teilweise Landesabdeckung. Wenn ja, ist v6 für diese BL obsolet. - v5-POC-Code: Existiert der Code noch auf der Festplatte (in einem früheren Session-Worktree)? Ein Fundstück würde 1-2 Tage Re-Implementations-Arbeit sparen.
8. Nächster Schritt (empfohlen)
- Vor jeder Zeile Code: Beantworte Fragen 1 und 3 oben. Das entscheidet, ob v6 überhaupt startet.
- Wenn ja: POC auf NRW + BUND + HE mit Option C, Sample-Zug manuell. 7 Tage Timebox.
- Wenn POC erfolgreich (Recall ≥ 80 %): ADR für v6 schreiben (analog 0001/0002), Rollout-Plan.
- Wenn POC scheitert: Zurück zu Option A nur für NRW, v5-Recall-Lücken schließen, #106 auf NRW-only beschränken und #126 schließen als „nicht wirtschaftlich".
9. Verifikations-Checkliste
- v5-Code lokal gesucht → nicht im Repo
- MEMORY-Referenz gelesen (95 Zeilen) — v5-Pattern dokumentiert
- Sample-Inventar: null Protokoll-Samples im Repo
- BL-Adapter-Architektur (ADR 0002) verstanden —
ParlamentAdapterhat aktuell keineget_plenarprotokoll-Methode - Fraktions-Normalisierung existiert (
parteien.py:187 normalize_partei) - Kosten-Schätzung für Option B/C kalkuliert
- Drei Optionen bewertet, Matrix + Mermaid-Diagramm
- Abbruch-Kriterien formuliert
- Sample-Beschaffungs-Plan für alle 17 BL
Nicht in diesem Dokument: Konkreter Code, Prompt-Vorlagen, ADR-Text für v6. Alles nächste Phase nach Go/No-Go-Entscheidung von Tobias.