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>
This commit is contained in:
Dotty Dotter 2026-05-08 01:04:04 +02:00
parent d16cacc7fe
commit 89b82b1627
2 changed files with 248 additions and 0 deletions

View File

@ -0,0 +1,247 @@
# 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.

View File

@ -29,6 +29,7 @@ und Konsequenzen. Format inspiriert von [Michael Nygard](https://cognitect.com/b
| [0010](0010-stimmverhalten-gwoe-aggregat.md) | Stimmverhalten × GWÖ-Bewertung als JOIN-Aggregat (Heuchelei + Konsistenz) | accepted | 2026-05-06 | | [0010](0010-stimmverhalten-gwoe-aggregat.md) | Stimmverhalten × GWÖ-Bewertung als JOIN-Aggregat (Heuchelei + Konsistenz) | accepted | 2026-05-06 |
| [0011](0011-aktuelle-themen-pm-generator.md) | Aktuelle-Themen-Dashboard mit PM-Generator (Persona-Prompt + Versionierung) | accepted | 2026-05-06 | | [0011](0011-aktuelle-themen-pm-generator.md) | Aktuelle-Themen-Dashboard mit PM-Generator (Persona-Prompt + Versionierung) | accepted | 2026-05-06 |
| [0012](0012-debug-auth-token-bypass.md) | DEBUG_AUTH_TOKEN-Bypass für Diagnose-Sessions auf dev | accepted | 2026-05-06 | | [0012](0012-debug-auth-token-bypass.md) | DEBUG_AUTH_TOKEN-Bypass für Diagnose-Sessions auf dev | accepted | 2026-05-06 |
| [0013](0013-programme-legislaturen-zeitpunktige-bewertung.md) | Programme + Legislaturen mit zeitpunktiger Bewertung | accepted | 2026-05-08 |
## Wann ADR, wann nicht ## Wann ADR, wann nicht