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

9.7 KiB
Raw Blame History

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

# 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]
# 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.