gwoe-antragspruefer/docs/adr/0009-protokoll-parser-registry.md
Dotty Dotter 7de4df1fef feat(#126): protokoll_parsers/-Sub-Package + Registry-Pattern + ADR 0009
Architektur-Refactor zur Vorbereitung BL-uebergreifender Parser:

- app/protokoll_parser_nrw.py → app/protokoll_parsers/nrw.py
- app/ingest_votes_nrw.py → app/ingest_votes.py (BL-uebergreifend)
- Neue app/protokoll_parsers/__init__.py mit:
  - PROTOKOLL_PARSERS-Dict (BL-Code → Parser-Funktion, derzeit nur NRW)
  - parse_protocol(bundesland, pdf_path) als BL-uebergreifender Einstieg
  - supported_bundeslaender()-Helper
  - NotImplementedError mit hilfreicher Message bei unbekanntem BL

CLI bekommt --supported-Flag fuer BL-Discovery:
  python -m app.ingest_votes --supported  → 'NRW'

ADR 0009 dokumentiert das Muster (Sub-Package + Funktions-Registry,
analog zu ADR 0002 fuer ParlamentAdapter). Folge-BL bekommen je
eine eigene Datei und einen Eintrag in PROTOKOLL_PARSERS — kein
Refactoring der Bestands-Logik.

Tests:
- 7 neue Tests in test_protokoll_parsers.py fuer Registry und Dispatch
- Bestehende NRW-Tests umbenannt zu test_protokoll_parsers_nrw.py,
  Imports angepasst — keine Verhaltens-Aenderung
- Bestehende Ingest-Tests umbenannt zu test_ingest_votes.py

642 Tests gruen, kein Verhaltens-Drift.
2026-04-28 08:37:31 +02:00

4.7 KiB

0009 — Plenarprotokoll-Parser-Registry pro Bundesland

Status accepted
Datum 2026-04-28
Refs #106, #126, ADR 0002 (Adapter-Pattern)

Kontext

Der NRW-Plenarprotokoll-Parser (#106) ist deterministisch, anchor-basiert und erreicht 19/19 auf der MMP18-119-Fixture. Damit war die Architektur-Frage gelöst — aber nur fuer NRW. Andere Bundeslaender publizieren ihre Plenarprotokolle in fundamental anderen Formaten:

  • Hessen: HTML mit semantischen Tags pro Beschluss
  • Brandenburg: PDF mit Tabellen-Layout fuer Vote-Counts
  • Mecklenburg-Vorpommern: ParLDok-XML-Export
  • Berlin: PDF mit eigenem Formularkasten-Schema
  • ...

Ein einziger Parser fuer alle BL ist nicht baubar. Die Reverse-Engineering- Arbeit pro Landtag ist substantiell und passiert phasenweise: zuerst NRW wegen der hohen Antragsdichte, danach BL fuer BL nach Bedarf.

Das Adapter-Pattern aus ADR 0002 (ParlamentAdapter) hat dieses Problem fuer die Antrags-Suche bereits geloest. Plenarprotokoll-Parser ist die naechste Familie mit derselben Form: pro BL eine eigene Implementierung, ein gemeinsamer Aufruf-Vertrag, ein Registry-Lookup.

Optionen

Option A — Eine grosse Datei mit If-Else-Dispatch

Eine einzige app/protokoll_parser.py-Datei mit einem parse_protocol(bl, pdf), das je nach BL andere Funktionen ruft. Vorteile: flach, einfach. Nachteile: waechst zur 2000-LOC-Datei, BL-spezifische Reverse-Engineering- Notizen und Helper-Functions vermischen sich, schlechte Test-Isolation.

Option B — OOP-Hierarchie mit ProtokollParserBase als ABC

Abstrakte Basisklasse mit parse(pdf_path) -> list[VoteResult], konkrete Subklassen pro BL. Vorteile: typisierter Vertrag. Nachteile: Boilerplate fuer Klassen-Definitionen ohne Mehrwert, weil der NRW-Parser keine Instanz-State hat (alles def-Funktionen, keine self.x).

Option C — Sub-Package mit Funktions-Registry (gewaehlt)

app/protokoll_parsers/ als Sub-Package, pro BL eine eigene Datei (nrw.py, mv.py, he.py, ...) die mindestens parse_protocol(pdf_path: str) -> list[dict] exportiert. Eine PROTOKOLL_PARSERS-Dict in __init__.py mappt BL-Code → Funktion. Das BL-uebergreifende parse_protocol(bl, pdf_path) macht den Lookup.

Vorteile:

  • Konsistent mit dem ADAPTERS-Dict in parlamente.py (ADR 0002)
  • BL-Code lebt in eigener Datei mit eigenen Helpern und Notizen
  • Neue BL = neue Datei + ein Eintrag in __init__.py, kein Refactoring
  • Tests pro BL in eigener Test-Datei (tests/test_protokoll_parsers_<bl>.py)
  • Parser-Funktionen bleiben simpel, kein OOP-Overhead

Nachteile:

  • Vertrag ist nur per Convention dokumentiert (nicht via Type-System erzwingbar) — dafuer ein Schema-Test in test_protokoll_parsers.py als Sicherheitsnetz.

Entscheidung

Option C. Konkret:

app/protokoll_parsers/
├── __init__.py     # Registry + parse_protocol(bl, pdf) + supported_bundeslaender()
├── nrw.py          # NRW v5 (vorher app/protokoll_parser_nrw.py)
└── <bl>.py         # je BL eine Datei, sobald implementiert

Vertrag fuer jeden Parser (verbindlich):

def parse_protocol(pdf_path: str) -> list[dict]:
    """Returns: [
        {
            "drucksache": str | None,
            "ergebnis": str,         # angenommen/abgelehnt/ueberwiesen/...
            "einstimmig": bool,
            "kind": str,             # parser-intern, fuer Debug
            "votes": {
                "ja": list[str],     # Fraktions-Codes (CDU, SPD, GRUENE, ...)
                "nein": list[str],
                "enthaltung": list[str],
            },
        },
        ...
    ]"""

Naming: Datei-Stem = lowercase BL-Code (nrw.py, mv.py, ...). Registry-Key = uppercase BL-Code ("NRW", "MV").

Konsumenten rufen parse_protocol(bundesland, pdf_path) aus dem Sub-Package, nicht direkt eine BL-Datei.

Konsequenzen

Positiv

  • Folge-BL-Implementierungen ohne Refactoring der Bestands-Logik.
  • Reverse-Engineering-Notizen leben pro BL in einer Datei statt verteilt ueber eine Mega-Datei.
  • Der supported_bundeslaender()-Helper macht in CLI und UI sofort sichtbar, wo Daten verfuegbar sind und wo nicht.
  • Neue Adapter-Test-Files folgen demselben Schema (test_protokoll_parsers_<bl>.py).

Negativ

  • Schema-Vertrag nur per Convention (kein TypedDict). Dafuer ein Smoke-Test in tests/test_protokoll_parsers.py, der pro registriertem Parser die Result-Keys pruefen wird, sobald >1 Implementation existiert.

Folgen fuer andere ADRs

  • ADR 0002 (Adapter-Pattern) bleibt gueltig; dieses ADR ueberbruckt es nicht, sondern wendet das gleiche Muster auf eine zweite Adapter-Familie an.
  • Folge-Issues (HE/BB/MV/BE/...) sind reine Implementation-Tickets ohne Architektur-Diskussion — der Vertrag ist hier festgelegt.