gwoe-antragspruefer/docs/adr/0013-programme-legislaturen-zeitpunktige-bewertung.md
Dotty Dotter 89b82b1627 docs(adr): 0013 — Programme + Legislaturen mit zeitpunktiger Bewertung
Architektur-Doku zum Schema-Refactor der letzten Session:
- Begruendung fuer programme.py + legislaturen.py
- Optionen-Vergleich (zentrale Registry vs. Liste-im-Schema vs. Status quo)
- Migrationsweg via _migrate_from_legacy() und Lazy-Init
- Datenstand (86 Wahlprogramme + 12 Grundsatzprogramme + 56 Legislaturen
  + 70 Regierungen)
- Offene Folgearbeiten: historische Wahlprogramme indizieren,
  analyzer.py-Migration, wahlprogramme.py-Compat-Shim

Index aktualisiert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 01:04:04 +02:00

248 lines
9.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 0013 — Programme + Legislaturen mit zeitpunktiger Bewertung
| | |
|---|---|
| **Status** | accepted |
| **Datum** | 2026-05-08 |
| **Refs** | Commits 991d1eb, b5d2bb2, 4e7f7da, a80ac17, c7861cf, d16cacc |
## Kontext
Bis zur 21. Wahlperiode des Bundestags (Mai 2025) wurde im
GWÖ-Antragsprüfer ein flaches Schema gefahren:
- `wahlprogramme.WAHLPROGRAMME[bundesland][partei]` → genau **ein**
Wahlprogramm pro Partei pro BL, ohne explizites Geltungsdatum
- `embeddings.PROGRAMME` → flacher Indexer-Lookup, ohne Datierung
- Antrags-Bewertung: implizit gegen das jeweils im Schema hinterlegte
Programm — egal welches Datum der Antrag trägt
Drei Probleme wurden über die Zeit sichtbar:
1. **Keine historische Korrektheit:** Anträge aus z.B. 2018 (NRW WP17,
Kabinett Laschet I) wurden gegen das aktuelle WP18-Programm
(Kabinett Wüst II) bewertet, weil das alte Programm gar nicht im
Schema stand.
2. **Sukzessionen verschluckt:** RP-Übergang Dreyer III (2021-05-18) →
Schweitzer I (2024-07-10) ohne Wahl — beide in derselben WP18 — ließ
sich nicht modellieren.
3. **Zwei parallele Datenstrukturen:** `WAHLPROGRAMME` und
`embeddings.PROGRAMME` enthielten teils dieselben Daten in
leicht unterschiedlicher Form. Inkonsistenzen waren ein
wiederkehrendes Bug-Muster (#175 Citation-Cross-Attribution war ein
Symptom davon).
Hinzu kam die User-Anforderung im Mai 2026 (BTW 2025): die 19
BUND-Anträge sollten gegen die **BTW-2025-Wahlprogramme** der 8 im 21.
Bundestag vertretenen Parteien bewertet werden, nicht gegen die
6 Bundesgrundsatzprogramme. Außerdem sollen historische Vorgänger und
Landesgrundsatzprogramme (CSU 2023, CDU NRW 2015, CDU SN 2023, CDU LSA
2023, SSW SH 2016, FW Bund) im System verfügbar sein.
## Optionen
### Option A — Eine zentrale Programm-Registry mit Geltungsdaten
Alle politischen Programm-Dokumente (Wahlprogramme, Bundes- und
Landesgrundsatzprogramme) leben in einem einzigen Dict mit explizitem
``gueltig_ab`` / ``gueltig_bis``. Helper-Funktionen liefern „welches
Programm galt zum Zeitpunkt X". Legislaturen + Regierungen sind ein
zweites, ergänzendes Modul für die zeitliche Einordnung.
**Vorteile:**
- Einzige Source of Truth, keine doppelten Datenbestände.
- Historische Bewertung möglich, sobald historische Programme indiziert
sind.
- Sukzessionen wie Dreyer III → Schweitzer I sauber modellierbar.
- API-Symmetrie: `wahlprogramm_zum_zeitpunkt(bl, partei, datum)`,
`regierung_zum_zeitpunkt(bl, datum)`, `legislatur_zum_zeitpunkt(...)`.
**Nachteile:**
- Eingriff in zwei bestehende Module
(`wahlprogramme.WAHLPROGRAMME` und `embeddings.PROGRAMME`) bzw.
Migrationspfad nötig.
- Tests gegen die alten Konstanten (39 Tests in
`test_wahlprogramme.py`) müssen ggf. angepasst werden.
### Option B — `WAHLPROGRAMME` als Liste statt Single-Dict
Pro `(bundesland, partei)` eine Liste aller Versionen, älteste zuerst.
Helper liest die Liste und sucht das passende Element.
**Vorteile:**
- Minimal-invasive Erweiterung des bestehenden Schemas.
- Backward-Kompatibilität mit `WAHLPROGRAMME[bl][partei]`-Lookups durch
`[-1]`-Slicing möglich.
**Nachteile:**
- Landesgrundsatzprogramme passen nicht ins Schema (haben kein
klassisches Wahlprogramm-Profil mit Wahltag).
- `embeddings.PROGRAMME` bleibt zweite Datenstruktur — keine
Konsolidierung.
- Sukzessionen ohne Wahl (Dreyer III → Schweitzer I) sind ein
Sonderfall, der schlecht zu „Liste pro Wahl" passt.
### Option C — Status quo mit kleinen Erweiterungen
`WAHLPROGRAMME` bekommt ein `regierungsbildung`-Feld, sonst nichts. Keine
historischen Programme, keine Landesgrundsatzprogramme.
**Vorteile:**
- Kleinster Aufwand. Tests bleiben grün.
**Nachteile:**
- Adressiert keines der drei Probleme oben. Erste Option wenn jemand
später historische Bewertung will: nochmal rausreißen.
## Entscheidung
**Option A** wurde gewählt und vollständig umgesetzt:
### Schemata
```python
# app/programme.py
class Programm(TypedDict):
id: str # "cdu-nrw-2022", "csu-grundsatz", …
titel: str
name: str # voll-qualifiziert für Citation
typ: Literal[
"wahlprogramm",
"grundsatzprogramm-bund",
"grundsatzprogramm-land",
]
partei: str # normalisiert: CDU, GRÜNE, FREIE WÄHLER, …
bundesland: Optional[str] # None bei reinen Bundesprogrammen
beschluss: Optional[str] # ISO-Datum (Grundsatzprogramme)
wahl: Optional[str] # ISO-Datum (Wahlprogramme)
wp: Optional[int] # Legislatur-Nr (Wahlprogramme)
gueltig_ab: str # bei Wahl: regierungsbildung;
# bei Grundsatz: beschluss
gueltig_bis: Optional[str] # None = aktuell
pdf: str
seiten: int
hinweis: Optional[str] # freier Text (Sonderfälle, BSW)
PROGRAMME: dict[str, Programm] = {} # gefüllt via _migrate_from_legacy()
# Helper
get_programm(programm_id) -> Optional[Programm]
aktuelles_wahlprogramm(bl, partei) -> Optional[Programm]
wahlprogramm_zum_zeitpunkt(bl, partei, datum) -> Optional[Programm]
grundsatzprogramm_zum_zeitpunkt(partei, datum, bundesland=None) -> Optional[Programm]
parteien_mit_wahlprogramm(bl) -> list[str]
alle_versionen(bl, partei) -> list[Programm]
```
```python
# app/legislaturen.py
class Legislatur(TypedDict):
bundesland: str
wp: int
wahltermin: str
konstituierung: str
ende: Optional[str]
class Regierung(TypedDict):
bundesland: str
name: str # "Wüst II", "Schweitzer I", "Merz I", …
wp: int
parteien: list[str]
ministerpraesident: str
von: str # Vereidigung
bis: Optional[str] # Vereidigung der Nachfolge oder Ende WP
LEGISLATUREN: list[Legislatur] # 56 Einträge für 16 BL + Bund
REGIERUNGEN: list[Regierung] # 70 Einträge
# Helper
legislatur_zum_zeitpunkt(bl, datum) -> Optional[Legislatur]
regierung_zum_zeitpunkt(bl, datum) -> Optional[Regierung]
aktuelle_legislatur(bl), aktuelle_regierung(bl)
regierungen_einer_wp(bl, wp) -> list[Regierung]
```
### Migrationsweg
`programme.py` ist neue SoT. Das alte `WAHLPROGRAMME`-Dict wird **nicht**
gelöscht (würde 19 Konsumenten brechen — analyzer, find_relevant_quotes,
embeddings, etc.). Stattdessen pflegt die Lazy-Init-Funktion
`_migrate_from_legacy()` die `PROGRAMME`-Registry beim ersten Helper-Aufruf
aus zwei Quellen:
1. `wahlprogramme.WAHLPROGRAMME` — Wahlprogramme mit `regierungsbildung`
und `regierungsende` (wurden im selben Refactor um Geltungsdaten
erweitert)
2. `embeddings.PROGRAMME` — Grundsatzprogramme mit `gueltig_ab`,
`gueltig_bis`, plus optional `bundesland` (→
`grundsatzprogramm-land`)
`embeddings.PROGRAMME` bleibt als Indexer-Registry (Reindex-Skript liest
es direkt für DashScope-Embeddings). Compat-Shim für `WAHLPROGRAMME` als
View über `programme.PROGRAMME` ist offen — nicht dringend, weil keine
Datenduplikation entsteht (beide Strukturen sind heute kohärent).
### Daten-Stand
Im Repo bei diesem ADR (2026-05-08):
- 16 BL + Bund × bis zu 8 Parteien × Wahlprogramme = 86 indizierte
Wahlprogramme (alle aktuell)
- 12 Bundes- und Landes-Grundsatzprogramme indiziert (CDU, SPD, GRÜNE,
FDP, AfD, LINKE, CSU 2023, CDU NRW 2015, CDU Sachsen 2023, CDU
Sachsen-Anhalt 2023, SSW SH 2016, FREIE WÄHLER Bund)
- 56 Legislaturen + 70 Regierungen historisch erfasst (mind. 3
vergangene WP pro BL, plus Bund 1821)
### Was zu tun bleibt
1. **Historische Wahlprogramme indizieren** (~50 PDFs für 3 Vorperioden
pro BL). Erst dann liefert `wahlprogramm_zum_zeitpunkt(...)` für
Anträge aus älteren Legislaturen ein Ergebnis ungleich `None`.
2. **`analyzer.py` migrieren** auf
`wahlprogramm_zum_zeitpunkt(bl, partei, antrag.datum)` — heute
ohne Mehrwert, weil keine historischen Embeddings da. Wird mit (1)
gemeinsam scharfgeschaltet.
3. **`wahlprogramme.WAHLPROGRAMME` als Compat-Shim** umbauen — Code-
Hygiene. Nicht dringend, beide Strukturen koexistieren ohne Drift.
## Konsequenzen
### Positiv
- Antrag-Detail zeigt jetzt einen **Bewertungs-Kontext-Block**
(Wahlperiode, Regierung zur Antragszeit, Wahlprogramm pro
Antragsteller-Fraktion mit PDF-Link + Geltungsdatum + LLM-Modell +
Bewertungs-Snapshot-Datum). Macht transparent, gegen welchen Stand
bewertet wurde.
- BUND-Anträge der 21. WP werden gegen die BTW-2025-Wahlprogramme
bewertet — vorher mussten sie ein Bundes-Grundsatzprogramm
reflektieren, das mit dem Tagesgeschäft im Bundestag nur lose
zusammenhing.
- Sukzessionen wie RP Dreyer III → Schweitzer I, BUND Scholz I (Ampel) →
Scholz I (geschäftsführend) → Merz I, oder das Kemmerich-Kabinett
(TH 2020-02) sind im Schema ohne Sonderfall darstellbar.
- 88 neue/erweiterte Tests sichern Konsistenz: alle Bundesländer
haben genau eine aktuelle Regierung, alle Regierungs-Intervalle pro
BL sind ohne Lücke aneinandergereiht, alle Programm-IDs sind
eindeutig.
### Negativ
- Doppelter Daten-Bestand zwischen `WAHLPROGRAMME` und
`embeddings.PROGRAMME` ist nicht aufgelöst — wenn man eines ändert,
muss man das andere nachpflegen. Risk: stille Drift.
- `_migrate_from_legacy()` läuft beim ersten Helper-Aufruf. In Tests die
`programme.PROGRAMME` direkt inspizieren ohne vorherigen API-Call
müssen explizit `_ensure_initialized()` aufrufen.
### Folgen für andere ADRs
- ADR 0001 (Citation-Binding): `partei`-Field aus `programme.PROGRAMME`
liefert die Partei eindeutig — partei-skopierte Citation-Verification
arbeitet jetzt damit, statt aus dem Programm-Namen zu parsen.
- ADR 0002 (Adapter-Architektur): `programme.py` ist eine reine
Daten-Modul-Schicht ohne IO, hängt sich nicht an Adapter dran.
- Keine Auswirkung auf ADR 0006 (Embedding-Modell-Migration v3→v4)
oder ADR 0008 (DDD-Lightweight) — orthogonal.