# 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-Stimmer - `Wer stimmt dagegen?` / `Wer lehnt … ab?` → Nein-Stimmer - `Wer 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-PDFs - `webapp/data/` → nur `gwoe-antraege.db` (SQLite) - `antraege/` (Projekt-Root) → 80+ PDFs, aber alle Drucksachen (Anträge), keine MMP-Protokolle - `TEMP/` → 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_.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: ```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`, `Abstimmung` als 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 ```mermaid flowchart TD Start([Issue #106: Abstimmungsverhalten]) --> Samples{Plenarprotokoll-
Samples
beschaffbar?} Samples -->|nein| Stop1[STOP:
Sample-Beschaffung zuerst] Samples -->|ja, ≥3 BL| Scope{Scope
klar?} Scope -->|nur NRW, kurzfristig| OptA[Option A
v5 härten auf NRW] Scope -->|alle 17 BL| OptFamily{Kosten-Bereitschaft
für LLM-Calls?} OptFamily -->|nein, offline nötig| OptA_scaled[Option A:
Rule-Engine pro BL
34-51 Tage] OptFamily -->|ja, 1500 €/WP OK| OptC[Option C Hybrid:
5-7 Tage Grund
+ 0,5 Tag/BL] OptC --> POC[POC auf 2 BL:
NRW + HE
Ground-Truth-Set je 3 Prot.] POC --> Eval{Recall
≥ 80 % auf HE?} Eval -->|ja| Rollout[Rollout auf 17 BL] Eval -->|nein| Rethink[LLM-Prompt iterieren
oder auf B eskalieren] OptA_scaled --> NRW_first[NRW zuerst,
dann nach Output-Ranking] ``` ## 5. Empfehlung ### 5.1 Familie **Option C (Hybrid)** als Primärweg. Begründung: 1. Die bestehende Architektur passt: `analyzer.py` nutzt schon LLM, ADR 0001 liefert das Binding-Pattern, `parteien.py:normalize_partei` gibt den Verifier. 2. Option A skaliert nicht auf 17 BL ohne Personen-Aufwand, den das Projekt realistisch nicht hat (siehe Issue-Backlog). 3. Option B hat das Halluzinations-Risiko und Kosten ohne Gewinn gegenüber C. ### 5.2 Minimum-Viable-Scope 1. **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). 2. **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`). 3. **Stufe-2-Prompt** mit Zitat-Binding-Anforderung (jede Fraktion muss als Exakt-Substring im Kandidat-Block vorkommen). 4. **Stufe-3-Verifier** via `normalize_partei` und `BUNDESLAENDER[bl].landtagsfraktionen`. 5. **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: 1. **NRW** — größter Output, v5 vorhanden 2. **BUND** — XML statt PDF, einfachste Implementierung, hoher Impact 3. **HE, BY, BW** — große Flächenländer, hohe Drucksachen-Zahl 4. **BE, HH, HB** — Stadtstaaten, geringer Protokoll-Umfang, guter Cross-Check für den Hybrid 5. 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` | PDF | | BUND | `bundestag.de/services/opendata` (XML-Feed) | **XML** | | BY | `bayern.landtag.de/parlament/dokumente/plenarprotokolle` | PDF | | BW | `landtag-bw.de/home/dokumente/plenarprotokolle.html` | PDF | | BE | `parlament-berlin.de/dokumente/open-data` (XML verfügbar) | XML/PDF | | BB | `parlamentsdokumentation.brandenburg.de` (portala) | PDF | | HB | `paris.bremische-buergerschaft.de/starweb/paris` | PDF | | HH | `buergerschaft-hh.de/parldok` | PDF | | HE | `starweb.hessen.de/portal` | PDF | | MV | `dokumentation.landtag-mv.de` (ParlDok) | PDF | | NI | `nilas.niedersachsen.de` (HAR-Capture nötig, siehe MEMORY) | PDF | | RP | `opal.rlp.de` (portala) | PDF | | SL | `landtag-saar.de` (Umbraco) | PDF | | SN | `edas.landtag.sachsen.de` (XML-Export, siehe `project_sn_xml_export.md`) | **XML** | | LSA | `padoka.landtag.sachsen-anhalt.de` (PARDOK) | PDF | | SH | `lissh.lvn.parlanet.de/cgi-bin/starfinder/0` | PDF | | TH | `parldok.thueringer-landtag.de` | PDF | Speicherung unter `webapp/tests/fixtures/protokolle//` (git-lfs oder außerhalb des Repos, je nach Dateigröße). Pro Protokoll eine `.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 1. **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. 2. **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? 3. **Alternative Datenquelle:** Wurde `abgeordnetenwatch.de/api` geprüft? Die liefern auf Bundesebene strukturierte Abstimmungsdaten und haben teilweise Landesabdeckung. Wenn ja, ist v6 für diese BL obsolet. 4. **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) 1. **Vor jeder Zeile Code:** Beantworte Fragen 1 und 3 oben. Das entscheidet, ob v6 überhaupt startet. 2. **Wenn ja:** POC auf NRW + BUND + HE mit Option C, Sample-Zug manuell. 7 Tage Timebox. 3. **Wenn POC erfolgreich (Recall ≥ 80 %):** ADR für v6 schreiben (analog 0001/0002), Rollout-Plan. 4. **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 - [x] v5-Code lokal gesucht → nicht im Repo - [x] MEMORY-Referenz gelesen (95 Zeilen) — v5-Pattern dokumentiert - [x] Sample-Inventar: **null Protokoll-Samples im Repo** - [x] BL-Adapter-Architektur (ADR 0002) verstanden — `ParlamentAdapter` hat aktuell **keine** `get_plenarprotokoll`-Methode - [x] Fraktions-Normalisierung existiert (`parteien.py:187 normalize_partei`) - [x] Kosten-Schätzung für Option B/C kalkuliert - [x] Drei Optionen bewertet, Matrix + Mermaid-Diagramm - [x] Abbruch-Kriterien formuliert - [x] 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.