# 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 18–21) ### 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.