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.
128 lines
4.7 KiB
Markdown
128 lines
4.7 KiB
Markdown
# 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):
|
|
|
|
```python
|
|
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.
|