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>
248 lines
9.7 KiB
Markdown
248 lines
9.7 KiB
Markdown
# 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.
|