Commit Graph

211 Commits

Author SHA1 Message Date
Dotty Dotter
d9ae0b0db8 feat(#153): HB-Parser produktiv — Bremer Beschlussprotokolle (Status-Only)
Bremen publiziert wie Hessen nur Beschlussprotokolle (TOPs + Status-Saetze),
KEINE Wortprotokolle mit Vote-Block. Daher minimaler Parser:
- Drucksache + Status (angenommen/abgelehnt/ueberwiesen)
- Vote-Listen bleiben leer (HB hat keine Fraktions-Detail)

Anchor-Regex: "Die Buergerschaft (Landtag|Stadtbuergerschaft) <verb> <rest> <terminator>"
Verb-Mapping:
- "lehnt ... ab" → abgelehnt
- "stimmt ... zu" → angenommen
- "beschliesst ..." → angenommen
- "verabschiedet ..." → angenommen
- "verweist|ueberweist|leitet" → ueberwiesen
- "nimmt ... Kenntnis" → uebersprungen (kein Vote)

Drucksachen-Aufloesung: erst Inline-Form "(21/N)", dann Block-Form
"Drucksache 21/N" rueckwaerts vom Anchor.

URL-Pattern (verifiziert WP21 Sitzung 33 Land):
https://www.bremische-buergerschaft.de/dokumente/wp21/land/protokoll/b21l{n4}.pdf

Cron unterstuetzt jetzt {n4}-Platzhalter (4-stellig). HB Land WP21
ingestiert via direktes URL-Probing (b21l0001.pdf … b21l9999.pdf).
Stadtbuergerschaft (b21s*) als Folge-Issue.

Tests: 21 HB-Tests, Verifikation S33 → 20 Beschluesse extrahiert.
Stand: 8 produktive Parser (NRW, BUND, BE, HH, TH, HE, SH, HB).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 01:41:40 +02:00
Dotty Dotter
329c6e25e5 fix: docker exec -i fuer Heredoc-Stdin in Auto-Ingest
Bug: Index-Scrape-Bloecke fuer HH/HE/SH produzierten 0 Protokolle, weil
\`docker exec ... python <<EOF\` ohne -i den Heredoc-Stdin nicht an
den Container weiterleitet.

Symptom in /tmp/aip.log:
  --- HH WP23 (Index-Scrape) ---
  --- HE WP21 (Index-Scrape) ---
  --- SH WP20 (Index-Scrape) ---
  (keine Output-Zeilen, exit 0)

Fix: docker exec -i an allen 3 Stellen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 01:37:42 +02:00
Dotty Dotter
7ebdc78331 feat(#160): SH-Parser produktiv — Schleswig-Holsteiner Plenarprotokolle
Verifiziert auf WP20 Sitzungen 115 + 116. Format ist TH-aehnlich:

Result-Anchor: "Damit ist [Subjekt] (mehrheitlich|einstimmig)? (angenommen|abgelehnt|überwiesen|so beschlossen)"
Vote-Block (Q+A im Reden-Stil):
  - JA: "Wer dem zustimmen will ... Das sind die Fraktionen von X"
  - NEIN: "Wer stimmt dagegen? ... Das sind die Fraktionen von Y"
  - ENTH: "Wer enthaelt sich? ... Z"
Drucksachen-Lookup: rueckwaerts vom Anchor

Besonderheiten:
- SSW (5%-Huerden-befreit) als feste Fraktion
- "Damit ist die Ausschussueberweisung einstimmig so beschlossen" → ergebnis="ueberwiesen"
- "Das sind alle anderen Fraktionen" → NEIN als Komplement von JA inferiert
- Soft-Hyphen-Reparatur (PDF-Zeilenumbruch "zustim- men" → "zustimmen")
- _last_match-Helper, weil 1500-char-Window mehrere Vote-Bloecke enthalten kann
  (TH-Limitierung gefixed)

URL-Pattern (verifiziert):
https://www.landtag.ltsh.de/export/sites/ltsh/infothek/wahl20/plenum/plenprot/{YYYY}/20-{n:03}_{MM-YY}.pdf

Datum-Anteile (YYYY-Pfad + MM-YY-Suffix) machen URL-Vorhersage unmoeglich
→ Auto-Ingest-Cron via Index-Scrape (analog HH/HE):
https://www.landtag.ltsh.de/infothek/wahl20/plenum/plenprot_seite/

Tests: 23 SH-Tests + Stub-Registry-Test angepasst.
Stand: 7 produktive Parser (NRW, BUND, BE, HH, TH, HE, SH).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 01:29:06 +02:00
Dotty Dotter
06918c71eb feat(#154): HE-Index-Scrape im Auto-Ingest-Cron
URL enthaelt Datum (DD-MM-YYYY), keine Vorhersage moeglich. Daher
analog HH: starweb-Index scrapen, neue PDFs einzeln ingesten.

Index-URL: https://starweb.hessen.de/starweb/LIS/Pd_Eingang.htm
PDF-Pattern: cache/hessen/landtag/Plenum/{wp}/Beschlussprotokoll_PL_{n}_{datum}.pdf
Protokoll-ID: PlPr{wp}-{n} (z.B. PlPr21-62)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 01:19:58 +02:00
Dotty Dotter
8125dbb731 feat(#154): HE-Parser produktiv — Hessen Beschlussprotokoll (Status-Only)
Hessen publiziert nur Beschlussprotokolle (Tagesordnung + Status), KEINE
Wortprotokolle mit Vote-Block. Daher minimaler Parser:
- Drucksache + Status (angenommen/abgelehnt/ueberwiesen)
- Vote-Listen bleiben leer (HE hat keine Fraktions-Detail)

URL-Pattern (verifiziert WP21 Sitzungen 61-63):
http://starweb.hessen.de/cache/hessen/landtag/Plenum/{wp}/Beschlussprotokoll_PL_{n}_{datum}.pdf

Datum-Teil DD-MM-YYYY → URL-Vorhersage unmoeglich, Auto-Ingest braucht
Index-Scrape via starweb.hessen.de/starweb/LIS/Pd_Eingang.htm (analog HH).

Status-Mapping:
- "angenommen" → ergebnis="angenommen"
- "Abgelehnt" → ergebnis="abgelehnt"
- "Nach (Aussprache|Lesung) an [Ausschuss]" → ergebnis="ueberwiesen"
- "Entgegengenommen", "Abgehalten", "Zur Kenntnis genommen" → uebersprungen

Tests: PROTOKOLL_PARSERS-Set jetzt {NRW, BUND, BE, HH, TH, HE}. STUB_BL_CODES
auf 11 BL reduziert (BB, BW, BY, HB, LSA, MV, NI, RP, SH, SL, SN bleiben).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 01:19:02 +02:00
Dotty Dotter
399dbc2639 feat(#163): TH-Parser produktiv — Thueringer Plenarprotokolle
Fuenfter produktiver Parser nach NRW + BUND + BE + HH.

URL-Pattern verifiziert (WP8 Sitzungen 1, 10, 20, 30, 40, 42):
  https://www.thueringer-landtag.de/uploads/tx_tltcalendar/protocols/Arbeitsfassung{n}.pdf

Anchor-Sprache (BE-aehnlich):
  Wer dem zustimmt, ... Das sind die Stimmen aus den Fraktionen der
  CDU, BSW, SPD und Die Linke. Wer stimmt gegen ...? Das sind die
  Stimmen aus der Fraktion der AfD. Damit ist [...] mehrheitlich
  angenommen.

Pattern:
- Result-Anchor: Damit ist [Subjekt] (mehrheitlich|einstimmig)?
  (angenommen|abgelehnt)
- Vote-Block: Wer dem zustimmt / Wer stimmt gegen / Wer enthaelt sich
- Drucksachen-Lookup: 'Drucksache 8/N' rueckwaerts

Fraktions-Mapping WP8 (ab Mai 2024): CDU, AfD, BSW, Linke, SPD
(WP7-Faktionen GRUENE/FDP fuer Backfill ebenfalls im Mapping).

Cron-PROTO_TARGETS um TH-WP8 erweitert. Stub-Test angepasst.
2026-04-29 01:11:58 +02:00
Dotty Dotter
c71f4bf9f8 ops: Cron-PROTO_TARGETS um BUND-WP19 + BE-WP18 erweitert
Vor-WP-Coverage fuer beide neuen produktiven Parser:
- BUND WP19 (2017-2021, 239 Sitzungen)
- BE WP18 (Berlin, ~85 Sitzungen)

Cron probiert kontinuierlich nach: bei jedem Lauf werden 50 weitere
Sitzungen probiert ab letztem ingestetem Stand. Bei 3 aufeinander-
folgenden 404 → Ende fuer dieses BL/WP.
2026-04-29 01:09:07 +02:00
Dotty Dotter
edbce27c49 test(#155): 19 Tests fuer HH-Parser
- TestNormalizeFraktionenHh: SPD, GRUENEN-Form, Linken-Form, kombinierte
  Phrasen, Doppelzaehl-Schutz
- TestParseVoteBlockHh: voller Vote-Block, ja+nein ohne enth, leerer Block,
  nur ja
- TestResolveDrucksacheHh: 'Drucksache 23/N', bare '23/N', closest-match,
  None bei keinem Treffer
- TestResultAnchorRegex: einstimmig vs. mehrheitlich, angenommen/abgelehnt
- Konstanten-Sanity: alle 5 HH-Fraktionen im Mapping abgedeckt

919 Tests gruen (+19).
2026-04-29 01:05:33 +02:00
Dotty Dotter
d39f9ef0e9 docs: Roadmap aktualisiert — 4 produktive Parser
NRW + BUND + BE + HH jetzt produktiv. 12 verbleibende BL als Stubs.
Tabelle reflektiert aktuelle Stats.
2026-04-29 01:04:32 +02:00
Dotty Dotter
67092d05b5 feat(#155): HH-Index-Scrape im Auto-Ingest-Cron
Hamburg hat keine vorhersagbare URL-Pattern (Blob-IDs + Hashes pro PDF).
Stattdessen: HH-Branch im Cron scraped die Protokoll-Liste auf
hamburgische-buergerschaft.de und ingestet jedes gefundene PDF, das
noch nicht in plenum_vote_results steht (idempotent).

Cron-Lauf morgens 06:30 zieht damit auch HH-Sitzungen automatisch nach,
sobald die Buergerschaft sie veroeffentlicht (typisch Tag nach der
Sitzung).

URL-Discovery-Pattern fuer Phase-2-BL mit aehnlich nicht-vorhersagbaren
URLs (z.B. SN, ggf. NI) — kann diese Index-Scrape-Logik wiederverwenden.
2026-04-29 01:01:52 +02:00
Dotty Dotter
5f97ae9fc3 feat(#155): HH-Parser produktiv — Hamburg Beschlussprotokolle
Vierter produktiver Plenarprotokoll-Parser nach NRW + BUND + BE.

Hamburg publiziert kompakte Beschlussprotokolle (Tabellen-Form mit
Vote-Block pro Beschluss):
  ... mehrheitlich mit den Stimmen der SPD und GRUENEN gegen die
  Stimmen der CDU und AfD bei Enthaltung der Linken angenommen

Pattern:
- einstimmig (angenommen|abgelehnt) — alle Fraktionen
- mehrheitlich mit den Stimmen X gegen die Stimmen Y bei Enthaltung Z
  (angenommen|abgelehnt)

Fraktions-Mapping WP23: SPD, GRUENE, CDU, AfD, Linke

URL-Discovery laeuft ueber die Protokoll-Liste der Buergerschaft
(Blob-IDs via Index-Page-Scrape). Cron-Eintrag erst sobald
URL-Discovery-Skript hier integriert ist.

Stub-Test angepasst (HH raus aus STUB_BL_CODES).
2026-04-29 00:57:58 +02:00
Dotty Dotter
c7d6ac7f5f feat(#150): BE-Parser produktiv — Berliner Abgeordnetenhaus-Plenarprotokolle
Dritter vollwertiger Plenarprotokoll-Parser nach NRW + BUND.

URL-Pattern verifiziert (WP19 Sitzungen 1, 10, 50, 80, 100):
  https://www.parlament-berlin.de/ados/{wp}/IIIPlen/protokoll/plen{wp}-{n:03}-pp.pdf

Anchor-Sprache (NRW-aehnlich, mit Berliner-Eigenheit 'pro forma'):
  Wer den Antrag auf Drucksache 19/X annehmen moechte, ... – Das sind
    die Fraktionen Buendnis 90/Die Gruenen und Die Linke.
  Wer stimmt dagegen? – Das sind die Fraktionen der CDU, SPD und AfD.
  Wer enthaelt sich, pro forma? – Das ist niemand.
  Damit ist der Antrag abgelehnt.

Pattern:
- Result-Anchor: Damit ist [Antrag/Aenderungsantrag/Gesetzentwurf/...]
  (angenommen|abgelehnt)
- Vote-Block: 3 Q+A-Paare im Reden-Stil (annehmen moechte / dagegen /
  enthaelt sich)
- Drucksachen-Lookup: 'Drucksache 19/N(-suffix)' rueckwaerts (1500-char Fenster)

Fraktions-Mapping WP19:
- Buendnis 90/Die Gruenen → GRÜNE
- Die Linke → LINKE
- CDU, SPD, AfD, FDP

21 Tests in test_protokoll_parsers_be.py.
Cron-PROTO_TARGETS erweitert um BE WP19 (~80 Sitzungen).
Stub-Test angepasst.

905 Tests gruen (889 → 905, +16 fuer BE).
2026-04-29 00:37:47 +02:00
Dotty Dotter
473637a842 docs: Roadmap-Tabelle um Status-Spalte erweitert
NRW + BUND als 'produktiv' markiert mit aktuellen Stats
(NRW: 2700/217, BUND: 112/39). 14 verbleibende BL als '📋 Stub' bzw
BW als '⚠ Stub (Datenmodell-Inkompatibilitaet)' wegen per-Artikel-
Voting in BW-Plenarprotokollen statt per-Drucksache.
2026-04-28 23:31:07 +02:00
Dotty Dotter
a83c770b93 docs(#151): BW-Datenmodell-Inkompatibilitaet vermerkt
Vertiefte Probe (WP17 Sitzung 50): BW stimmt 'pro Artikel'
('Damit ist Artikel 1 einstimmig zugestimmt'), nicht pro Drucksache.

Das ist andere Datenmodellierung als NRW (Drucksache→Vote) und BUND
(Beschlussempfehlung→Vote). Ein BW-Parser braucht entweder:
- Aggregations-Heuristik: alle Artikel angenommen → DS angenommen
- Schema-Erweiterung um 'artikel'-Spalte fuer per-Artikel-Records

Implementer muss vor Start mit Maintainer abstimmen, welcher Weg
gegangen wird. BW bleibt Stub bis Designwahl getroffen ist.
2026-04-28 23:29:31 +02:00
Dotty Dotter
22a2b63c35 feat(#148): BUND-Parser produktiv — Bundestags-XML-Plenarprotokolle
Erster vollwertiger Plenarprotokoll-Parser nach NRW. Quelle:
https://dserver.bundestag.de/btp/{wp}/{wp}{n:03}.xml

Anchor-Sprache (verifiziert WP20 Sitzungen 30 + 100):
  'Die Beschlussempfehlung ist mit den Stimmen der Koalitionsfraktionen
   gegen die Stimmen der CDU/CSU-Fraktion bei Enthaltung der AfD-Fraktion
   angenommen.'

Pattern:
- Subjekt: Beschlussempfehlung | Ueberweisungsvorschlag | Antrag | Gesetzentwurf
- Vote-Block: 'mit den Stimmen X / gegen die Stimmen Y / bei Enthaltung Z'
- Ergebnis: 'angenommen' oder 'abgelehnt'
- Drucksache rueckwaerts vom Anchor (1500 chars Window)
- Kind 'ueberweisung' invertiert ergebnis zu 'ueberwiesen'

Fraktions-Mapping (WP20 = Ampel):
- 'Koalitionsfraktionen' → SPD + GRÜNE + FDP
- 'Oppositionsfraktionen' → CDU/CSU + AfD + LINKE
- 'CDU/CSU-Fraktion', 'Fraktion Bündnis 90/Die Grünen', etc.

WP21 (ab 2025) braucht eigenes Mapping-Update.

26 Tests in test_protokoll_parsers_bund.py (Vote-Block-Parsing, Anchor-
Erkennung, Drucksachen-Lookup, End-to-End mit Mock-XML).

Cron + Ingest-CLI:
- PROTO_TARGETS-Format erweitert um PROTOKOLL_ID_PREFIX und {n3}-
  Placeholder fuer 3-stellig zero-gepaddetes BT-Schema (BTP20-N)
- ingest_votes URL-Suffix dynamisch (PDF vs XML) statt hardcoded .pdf
- Eintrag in PROTOKOLL_PARSERS (NRW + BUND)
- Stub-Test angepasst: BUND raus aus STUB_BL_CODES

889 Tests gruen (787 → 889, +102 fuer Phase-2 Stubs+Tests+BUND).
2026-04-28 23:21:39 +02:00
Dotty Dotter
7cfbd9f210 docs(#148): BUND-Stub um echte Anchor-Sprache aus WP20-Sitzung 30
Erste Probe (Sitzung 184) war Aussprache, daher 0 Beschluss-Anchors.
Sitzung 30 (572k chars, 5 angenommen-Anchors) zeigt die echte
BT-Vote-Sprache:

  'Die Beschlussempfehlung ist mit den Stimmen der Koalitions-
   fraktionen und der Fraktion Die Linke gegen die Stimmen der
   CDU/CSU-Fraktion bei Enthaltung der AfD-Fraktion angenommen.'

Pattern-Erkennung:
- Anchor-Verb 'angenommen' oder 'abgelehnt' am Satzende
- Vote-Block: 'mit den Stimmen [...] gegen die Stimmen [...]
  bei Enthaltung [...]'
- Fraktions-Phrasen: 'Fraktion X', 'X-Fraktion', 'Koalitionsfraktionen'
- Drucksachen rueckwaerts vom Anchor (oft 100+ Zeichen vorher)

Wichtig: BT-Anchor-Sprache ist viel laenger als NRW — Regex-Begrenzung
muss 200+ Zeichen tolerieren.

Sample-Sitzungen mit Beschluessen: WP20 30, 100, 150.
2026-04-28 23:15:36 +02:00
Dotty Dotter
171a05ed4d docs(#151): BW-Stub um konkrete Recherche-Befunde erweitert
Heutige Probe von WP17 Sitzung 50 (618 KB PDF) ergab:

URL-Pattern bestaetigt:
  https://www.landtag-bw.de/.../WP{wp}/Plp/{wp}_{n:04}.pdf
  4-stellige Sitzungs-Nr mit Padding (anders als NRW unkpaddet)

Anchor-Phrasen-Stichprobe:
  'einstimmig zugestimmt' x5 — Haupt-Anchor (NRW: 'angenommen')
  'Damit ist [...] einstimmig' x2 — NRW-aehnliche Struktur
  'angenommen' x1 — nur in einer Rede, KEIN Beschluss-Anchor!
  'Drucksache 17/N' x35 — DS-Pattern wie NRW
  'zugestimmt' x19 — dominierende Vote-Phrase

Fraktions-Auflistung pro Vote in BW deutlich weniger detailliert als
NRW — Parser wird oft nur 'einstimmig' / 'mit Mehrheit' extrahieren
koennen, kein ja/nein/enthaltung-Breakdown pro Fraktion.

Fuer den naechsten Implementer (BW-Session) wertvolle Vorarbeit.
2026-04-28 23:14:17 +02:00
Dotty Dotter
30d51da5f7 feat(#106 Folge): Datenquellen-Tabelle auf /methodik um Plenum-Vote-Spalte erweitert
Pro BL zeigt die Tabelle nun:
- Doku-System (wie bisher)
- Drucksachen: alle aktiv (Adapter laufen)
- Plenum-Votes: 'aktiv' wenn Parser registriert (NRW), sonst 'Stub'

Plus Erklär-Hinweis: 'Plenum-Votes = fraktions-aggregierte
Abstimmungs­ergebnisse aus den Plenarprotokollen (#106). Stubs sind
Tracking-Stellen fuer kuenftige Implementierungen (Issues #148-#163).'

main.py reicht supported_bundeslaender() aus protokoll_parsers an die
Template-Context durch (plenum_vote_parsers-Set).
2026-04-28 23:12:58 +02:00
Dotty Dotter
62fd25fbcb test(#106 Folge): Safety-Net fuer 16 Stub-Parser
81 Tests pruefen pro Stub:
- Modul ist importierbar
- Docstring enthaelt Recherche-Findings + Issue-Link
- parse_protocol() raised NotImplementedError mit informativer Message
- Stub ist NICHT in PROTOKOLL_PARSERS-Registry (sonst wuerde Cron crashen)
- Wenn parse_protocol kein NotImplementedError mehr wirft (also echt
  implementiert), MUSS es in PROTOKOLL_PARSERS sein — sonst Test rot

Damit ist sichergestellt: sobald ein Stub durch echten Parser ersetzt
wird, kann der Implementer nicht vergessen, gleichzeitig den Eintrag
in der Registry zu setzen.

868 Tests gruen, 787 → 868 (+81).
2026-04-28 23:11:38 +02:00
Dotty Dotter
c0692b3078 feat(#106 Folge): 16 Stub-Parser + Tracking-Issues fuer alle Bundeslaender
Pro BL (BUND + 15 Laender) ein Modul app/protokoll_parsers/<bl>.py mit:
- Recherche-Findings im Docstring (Doku-System, Base-URL, Format,
  URL-Discovery-Status, Familie, Aufwand-Schaetzung)
- parse_protocol() raised NotImplementedError mit Hinweis auf Issue-Tracker
- *Nicht* in PROTOKOLL_PARSERS-Registry → Auto-Ingest-Cron uebersieht sie

Tracking-Issues #148-#163 auf Gitea, jeweils mit den Recherche-Findings
und einer Checkliste fuer die Implementer-Session.

Roadmap-Doc (docs/protokoll-parser-roadmap.md) aktualisiert mit
Stub→Issue-Mapping-Tabelle.

Wenn der Implementer pro BL fertig ist:
1. NotImplementedError durch echten Parser ersetzen
2. Eintrag in app/protokoll_parsers/__init__.py::PROTOKOLL_PARSERS
3. PROTO_TARGETS in scripts/auto-ingest-protocols.sh ergaenzen

787 Tests gruen, NRW unveraendert.
2026-04-28 23:09:07 +02:00
Dotty Dotter
64a317f45f fix(#106): auto-ingest-protocols nutzt python statt sqlite3-CLI
Container hat kein sqlite3-CLI. docker exec sqlite3 schlug 'OCI runtime
exec failed' und last_n wurde zur Fehlermeldung statt einer Zahl,
woraufhin set -u im naechsten Arithmetic-Schritt knallte.

Fix: python -c mit sqlite3-Modul (Standard-Bibliothek, immer da). Plus
Numeric-Sanity-Check als Belt-and-Suspenders.
2026-04-28 22:29:36 +02:00
Dotty Dotter
fc9155de58 docs(#106,#126): Plenarprotokoll-Parser-Roadmap pro Bundesland
Format-Hints, URL-Patterns und Aufwand-Schaetzung pro BL fuer kuenftige
Phase-2-Implementierungen. Dokumentiert was pro Landtag zu tun ist:

- NRW: produktiv (38 Tests, Fixture-Garantie 19/19)
- BUND: XML-Endpoint fuer namentliche Abstimmungen empfohlen statt PDF
- MV/TH: ParlDok-Plattform, Synergien
- BB/BY/BE/BW/HB/HE/HH/LSA/NI/RP/SH/SL/SN: je 1-3 Tage Reverse-Engineering
- BUND-XML, MV/TH-Synergie und HE-HTML als naechste empfohlene Picks

Cron-Erweiterung pro neuem BL: ein PROTO_TARGETS-Eintrag in
scripts/auto-ingest-protocols.sh, kein Cron-Edit noetig.
2026-04-28 22:28:31 +02:00
Dotty Dotter
05b6b45e1b ops(#106): Backfill- + Auto-Ingest-Skripte fuer Plenum-Votes
scripts/backfill-nrw-protocols.sh:
  Probiert MMP{wp}-1.pdf bis MMP{wp}-200.pdf durch, ingestet alle 200er.
  Bei 3 aufeinanderfolgenden 404 Abbruch.
  Usage: backfill-nrw-protocols.sh [WP=18] [CONTAINER=gwoe-antragspruefer-dev]
  Idempotent ueber plenum_vote_results-Compound-PK.

scripts/auto-ingest-protocols.sh:
  BL-uebergreifend, Cron-tauglich. Liest fuer jeden konfigurierten
  BL/WP das letzte ingestete Protokoll aus der DB, probiert die
  naechste Sitzungsnummer, ingestet bis zur naechsten Luecke.
  Aktuell konfiguriert: NRW WP18, NRW WP17 (Pattern leicht erweiterbar).

Beide rein deterministisch — keine LLM-Calls, keine Embedding-Calls,
keine Kosten. Reines PDF-Download + Regex-Parsing + SQLite-Insert.
2026-04-28 22:23:51 +02:00
Dotty Dotter
1769c9f349 build(#134): Coverage-Schwelle 45% → 50% nach Test-Backfill-Welle
Aktuelle Total-Coverage 53.4% (vorher 46.2%). Mit der Phase-2-Welle:
- 787 Tests (von 580) — +207 Tests
- Coverage-Sprung +7 Punkte
- 11 Module auf >=99% gebracht (mail, og_card, redline_utils, drucksache_typen,
  wahlperioden, wahlprogramme, abonnement_repository, bewertung_repository,
  monitoring, clustering, ingest_votes)
- 3 Module auf >=85% (auth, protokoll_parsers/nrw, qwen_bewerter)
- Verbleibend integration-territory: main.py (Endpoints), parlamente.py
  (Live-Adapters), queue.py worker-Loop, report.py WeasyPrint,
  embeddings.py OpenAI-Calls
2026-04-28 11:14:09 +02:00
Dotty Dotter
16ecd31e50 test(#134): report.py Coverage 44.3% → 52.7%
- TestGetScoreColor: alle 5 Branches (>=7 blue, >=4 green, >=2 yellow,
  >=1 orange, sonst red)
- TestGetRatingSymbol: alle 5 Symbole (++, +, ○, −, −−)

Verbleibend (Lines 487-641): WeasyPrint-PDF-Render-Pfade — brauchen
echtes WeasyPrint-Setup, gehoeren in tests/integration/.

Total: 53.2% → 53.4%, 777 → 787 Tests.
2026-04-28 11:13:20 +02:00
Dotty Dotter
ccff2e3e8e test(#134): NRW Protokoll-Parser Coverage 51.7% → 85.1%
parse_protocol mit fitz-Mock (FakeDoc/FakePage):
- simple_angenommen mit ja/nein-Block
- einstimmig direct_broad → ja-Liste fallback
- ueber + so beschlossen → einstimmig-Fallback fuellt ja-Liste mit
  ALLE_FRAKTIONEN_NRW
- skips_anchor_without_drucksache: kein vorheriges 'Drucksache' → skip

compare_to_fixture:
- perfect_match → 1/1
- not_found → 0/1 mit 'NOT FOUND'-Error
- nicht_gesondert_abgestimmt: korrekt nicht-gefunden zaehlt als match
- wrong_ergebnis → error 'ergebnis X != Y'

Total Coverage: 52.1% → 53.2%, 769 → 777 Tests.
2026-04-28 11:11:52 +02:00
Dotty Dotter
58bfc84c41 test(#134): auth.py Coverage 47.1% → 86%
Security-kritisch — jetzt mit umfassender Test-Abdeckung:

- TestKeycloakUrls: issuer + jwks-URL-Konstruktion
- TestGetJwks: Cache-Hit (frisch), Fetch bei leerem Cache, Stale-Cache
  bei HTTP-Fehler (statt komplettem Crash)
- TestValidateToken: kein JWKS → None
- TestGetCurrentUser: Auth-disabled → None, kein Token → None
- TestRequireAuth: Dev-Modus, 401 ohne Token, 401 ungueltig, 200 mit
  validem Token
- TestRequireAdmin: Dev-admin, admin-Rolle, gwoe-admin-Rolle, 403 ohne
  Admin-Rolle
- TestKeycloakAdminToken: keine Credentials → 500, Erfolg → access_token,
  Keycloak-Fehler → 500

Verbleibend: kid-not-found-Pfad, ExpiredSignature/JWTError/ImportError-
Branches im _validate_token-Inneren — wuerden voll gemockten jose-Stack
brauchen.

Total Coverage: 51.2% → 52.1%, 750 → 769 Tests.
2026-04-28 11:10:08 +02:00
Dotty Dotter
3edb1e7501 test(#134): queue Coverage 26.6% → 43.4%
- TestStartWorker: erzeugt CONCURRENCY Tasks, ersetzt aktive nicht
- TestGracefulShutdown:
  - leerer Status → sofortiger Return
  - 'processing'-Job laesst shutdown warten bis er fertig ist
  - Timeout loggt ERROR
- TestEnqueueShuttingDown: enqueue blockiert mit QueueFullError waehrend
  Shutdown

Verbleibend: _worker-Hauptloop (while True, hart zu testen) und
re_enqueue_pending (DB+Adapter-I/O, eigenes Setup noetig).

Total Coverage: 50.8% → 51.2%, 744 → 750 Tests.
2026-04-28 11:08:04 +02:00
Dotty Dotter
8e6f435b94 test(#134): analyzer Coverage 70.1% → 83.1%
- TestContentFingerprint: empty/non-empty cases (Lines 45-48)
- TestGetDefaultBewerter: lazy-Import liefert QwenBewerter (Lines 58-60)
- TestLoadContextFile: existierende + fehlende Datei (Line 71)
- TestGetUserPromptTemplate: alle 4 Platzhalter im Template
- TestGetBundeslandContext:
  - unbekanntes BL → ValueError 'Unbekanntes Bundesland' (Line 263)
  - inaktives BL → ValueError 'nicht aktiv' (Line 265)

Verbleibend (alles im analyze_text LLM-Pfad): Embeddings-Fallback,
reconstruct_zitate-Branch, missing-Programme-Logging — wuerde End-to-End
Mock-Setup brauchen, Aufwand vs. Nutzen unguenstig.

Total: 50.6% → 50.8%, 736 → 744 Tests.
2026-04-28 11:06:24 +02:00
Dotty Dotter
98f7e610b4 test(#134): drucksache_typen Coverage 72.5% → 100%
likely_kleine_anfrage_titel-Heuristik (#149-Folge):
- empty/None Titel false
- 'Welche', 'Warum', 'Was' und andere Frage-Praefixe true
- Frage am Ende mit '?' true
- Nummern-Praefix (NRW '1Welche...', '12. Wie viele...') wird weg-gestrippt
- pure Digits-only Titel: nach Strippen leer → false
- case-insensitive Praefix-Match
- normaler Antrag-Titel ohne Frage → false

Coverage 50.4% → 50.6%, 724 → 736 Tests.
2026-04-28 11:04:31 +02:00
Dotty Dotter
581d1591b8 test(#134): clustering.py Coverage 82.3% → 99.3%
- TestUnionFindRankSwap: rank-Asymmetrie-Branch (Line 69)
- TestLoadAssessmentItems: tmp-DB mit korrekten + kaputten Embeddings,
  bundesland-Filter, vollstaendiges Item-Schema
- TestBuildHierarchySubclusters:
  - max_cluster_size=3 zwingt grossen Cluster zu sub-clustern
  - kleiner Cluster bekommt subclusters=None

Total Coverage: 49.9% → 50.4% (50%-Marke ueberschritten),
718 → 724 Tests.
2026-04-28 11:02:58 +02:00
Dotty Dotter
999926b5f3 test(#134): monitoring.py Coverage 83.2% → 99.3%
- TestSearchAdapterFallbackLogging: erster Query-Versuch failt mit
  Debug-Log, dritter klappt
- TestDailyScanDbUpsertFailure: erster upsert_monitoring_scan crasht,
  zweiter klappt → der Rest des Protokolls wird nicht blockiert,
  ERROR-Log ist da
- TestSendMonitoringDigest:
  - mail_sent=True bei erfolgreichem send_mail
  - mail_sent=False bei SMTP-Fehler, aber kein Crash

Verbleibend: Line 122 (return [] nach drei Fallback-Misses ohne
Exception — schwer ohne Adapter-Mock zu provozieren).

Total Coverage: 49.5% → 49.9%, 714 → 718 Tests.
2026-04-28 11:01:19 +02:00
Dotty Dotter
e69ca1c29d test(#134): mail.py Coverage 88.2% → 100%
- TestSendSync.test_raises_when_smtp_not_configured: leerer host/user
  fuehrt zu RuntimeError
- TestSendSync.test_calls_smtp_ssl_with_settings: smtplib.SMTP_SSL wird
  mit host/port instanziiert, login + send_message aufgerufen
- TestSendMailAsync.test_runs_send_sync_in_executor: send_mail()
  delegiert per loop.run_in_executor an _send_sync
2026-04-28 10:58:03 +02:00
Dotty Dotter
9af74b1a05 test(#134): qwen_bewerter Coverage 86% → 94%
- TestContentFingerprint: leerer/None content → 'len=0', sha1-Praefix
- TestStripMarkdownJsonFences: explizite ```json-Sprache-Erkennung
- TestLazyClientInstantiation:
  - injected client umgeht Lazy-Import
  - kein injected client triggert openai.AsyncOpenAI-Aufruf
    (sys.modules-Stub fuer Lazy-Import-Branch)

Verbleibend uncovered: Line 46 (json-Fence ohne Newline, defensiv aber
unerreichbar weil split('\n', 1) vorher crashen wuerde) und 110-111
(assert/raise-Pfad, im Code als 'unreachable' markiert).
2026-04-28 10:56:56 +02:00
Dotty Dotter
698562b1f5 test(#134): Coverage-Backfill auswertungen + Repositories
- app/auswertungen.py 87.4% → 97.9%
  - TestLoadAssessmentsRobustness: ungueltiges JSON in fraktionen-Spalte
    fallback to []
  - TestAggregateMatrixSkipsBlanks: bundesland-NULL-Eintrag wird ignoriert
  - TestGetWahlperioden: sortierte Liste

- app/repositories/abonnement_repository.py 85.2% → 100%
- app/repositories/antrag_repository.py 87.0% → 98.1%
- app/repositories/bewertung_repository.py 90% → 100%

Pattern fuer Sqlite-Repos: AsyncMock auf database.X-Funktion, dann
pruefen dass die Methode korrekt delegiert (Argumente, Return-Wert).
Trivial wrappers, aber jetzt auditierbar.

Total: 48.7% → 49.2%, 686 → 705 Tests.
2026-04-28 10:54:28 +02:00
Dotty Dotter
b13b46a444 test(#134): Coverage-Backfill drei Module
- app/ingest_votes.py 39.2% → 100%
  - TestDownloadPdf: schreibt Bytes, propagiert HTTP-Fehler
  - TestCli: --supported, kein-arg-error, fehlender PDF-Pfad,
    pdf-Pfad-Run, --url-Download-Pfad, exit-Code 2 bei null Resultaten,
    Errors-Liste im Output
  - DB-Error-Collection in ingest_pdf

- app/wahlprogramme.py 90.7% → 100%
  - TestLoadWahlprogrammText: paged-Datei, Normal-Datei-Fallback,
    fehlende Datei
  - TestSearchWahlprogramm: leere Returns
  - TestFindRelevantQuotes: ValueError bei unbekanntem BL
  - TestFormatQuoteForPrompt: leeres Dict

- app/abgeordnetenwatch.py 95.2% → 97.6%
  - test_rp_pattern_nr_wp_swap: '/538-18.pdf' → '18/538'
  - test_sn_pattern_dok_nr_leg_per_swap: 'dok_nr=2150&leg_per=8' → '8/2150'

Total: 47.59% → 48.69%, 666 → 686 Tests, 0 Failures.
2026-04-28 10:50:26 +02:00
Dotty Dotter
145ad1e8d4 docs(methodik): klarstellen wie System- und User-Prompt zusammenwirken
User-Frage zur Transparenz-Seite: 'Welcher Prompt wird ausgefuehrt?
Der System-Prompt ist deutlich umfangreicher.' Antwort: keiner allein —
beide werden in einem API-Call zusammen gesendet und gemeinsam
ausgewertet.

Auf /methodik#prompts neu vor den details-Bloecken:
- Erklaerung 'in einem einzigen API-Call', beide ins Kontextfenster
- 2-Spalten-Tabelle 'System (Wer/wie)' vs. 'User (Was)'
- Begruendung der Trennung (Caching, Compliance, Wartbarkeit)
- Code-Referenz zu qwen_bewerter.py:83-85 mit messages-Aufbau

Reine UI-Aenderung, keine Code-Logik betroffen.
2026-04-28 09:14:22 +02:00
Dotty Dotter
eb0669d6ac feat(#147): Hover-Tooltips fuer Abkuerzungen auf Antrag-Detail
User-Feedback: '(A)' hinter Partei, 'WP', 'PP' brauchen Erklaerung
fuer Erstleser:innen. Loesung: ausfuehrliche title-Tooltips plus
visuelle Affordanz (cursor:help).

Geaendert:
- v2-badge-antragsteller / -regierung: cursor:help
- v2-score-chip[title]: cursor:help
- (A) → 'A — Antragstellende Fraktion: hat den Antrag eingereicht.'
- (R) → 'R — Regierungsfraktion: traegt die aktuelle Mehrheit im Landtag.'
- WP-Chip: 'WP — Wahlprogramm-Treue (0–10): wie gut passt der Antrag
  zum aktuellen Wahlprogramm? + Begruendung'
- PP-Chip: analog fuer Parteiprogramm-Treue
- Score-Hero: Tooltip mit GWÖ-Score-Definition + Methodik-Verweis
- 'Enth.:' im Abstimmungs-Block: dotted underline + Tooltip 'Enth. —
  Enthaltung: weder Zustimmung noch Ablehnung'

Closes #147
2026-04-28 08:46:27 +02:00
Dotty Dotter
722b073bbd test(#134): wahlprogramm_fetch Coverage 42.8% → 54.4%
8 zusaetzliche Tests:
- TestLockFileRobustness: kaputtes JSON, fehlende Datei, _save_lock-Roundtrip
- TestLoadLinks: missing yaml + empty yaml (gestubbed)
- TestGetMissingProgrammes: leere/gefuellte Eintraege, Bundesland-Filter

yaml ist im Unit-Setup gestubbed; Tests patchen _load_links direkt
statt echte YAML-Parsing zu erzwingen — die echte Datei-Validierung
gehoert in die integration-Suite gegen die produktive links.yaml.
2026-04-28 08:42:29 +02:00
Dotty Dotter
8f3a811a83 test(#134): app/og_card.py Coverage 44% → 100%
10 Tests in test_og_card.py:
- TestCacheKey: deterministisch, aenderungs-empfindlich, 16 Zeichen lang
- TestGetCached: Pfad-Lookup mit/ohne Datei
- TestRenderOgCard: Cache-Hit vs Cache-Miss, URL-Encoding der DS,
  Playwright-Exception → None, cache_dir wird angelegt

Playwright wird ueber sys.modules-Stub eingehaengt, sync_playwright()
liefert einen ContextManager mit gemocktem Browser/Page-Stack — keine
echte Chromium-Installation noetig fuer den lokalen Run.

cache_key/get_cached-Tests waren bisher in test_wahlprogramm_fetch.py
verstreut; bleiben dort als Smoke, das eigentliche Modul-Test-File ist
jetzt test_og_card.py.
2026-04-28 08:40:20 +02:00
Dotty Dotter
50442f203a test(#134): build_pdf_href Coverage 50% → 100%
6 neue Tests in TestBuildPdfHref:
- explizite url wird unveraendert durchgereicht
- ohne url: WAHLPROGRAMME-Lookup ueber quelle-Feld
- ohne Seitenzahl in quelle → leerer href
- Quelle ohne WAHLPROGRAMME-Match → leerer href
- Query nutzt nur die ersten 5 Worte des Zitats
- Komma-Separator 'Titel, S. 17' parst genauso wie ' · S. 17'

app/redline_utils.py jetzt bei 100% Branch-Coverage.
2026-04-28 08:39:05 +02:00
Dotty Dotter
7de4df1fef feat(#126): protokoll_parsers/-Sub-Package + Registry-Pattern + ADR 0009
Architektur-Refactor zur Vorbereitung BL-uebergreifender Parser:

- app/protokoll_parser_nrw.py → app/protokoll_parsers/nrw.py
- app/ingest_votes_nrw.py → app/ingest_votes.py (BL-uebergreifend)
- Neue app/protokoll_parsers/__init__.py mit:
  - PROTOKOLL_PARSERS-Dict (BL-Code → Parser-Funktion, derzeit nur NRW)
  - parse_protocol(bundesland, pdf_path) als BL-uebergreifender Einstieg
  - supported_bundeslaender()-Helper
  - NotImplementedError mit hilfreicher Message bei unbekanntem BL

CLI bekommt --supported-Flag fuer BL-Discovery:
  python -m app.ingest_votes --supported  → 'NRW'

ADR 0009 dokumentiert das Muster (Sub-Package + Funktions-Registry,
analog zu ADR 0002 fuer ParlamentAdapter). Folge-BL bekommen je
eine eigene Datei und einen Eintrag in PROTOKOLL_PARSERS — kein
Refactoring der Bestands-Logik.

Tests:
- 7 neue Tests in test_protokoll_parsers.py fuer Registry und Dispatch
- Bestehende NRW-Tests umbenannt zu test_protokoll_parsers_nrw.py,
  Imports angepasst — keine Verhaltens-Aenderung
- Bestehende Ingest-Tests umbenannt zu test_ingest_votes.py

642 Tests gruen, kein Verhaltens-Drift.
2026-04-28 08:37:31 +02:00
Dotty Dotter
a9f0b61c75 build(#134): Coverage-Schwelle auf realistische Baseline 45%
Vorheriger Wert 60 unerreichbar mit reinen Unit-Tests, weil drei
grosse Bereiche bewusst nicht in der Default-Suite getestet werden:

- app/main.py — FastAPI-Endpoints, lokal via TestClient nur skipped;
  echte Smoke-Tests laufen in Docker-Suite oder integration/.
- app/parlamente.py — 16 Adapter, ~3400 LOC HTTP-Code; tests/integration/
  deckt das via Live-Calls.
- app/queue.py, app/report.py — Async-Worker und PDF-Renderer.

45% spiegelt das tatsaechliche Default-Suite-Coverage wider (46.21% am
2026-04-28), Schwelle steigt mit ergaenzenden Tests automatisch.
2026-04-28 08:07:53 +02:00
Dotty Dotter
7e0f0117e6 feat(#106): UI-Block 'Abstimmungsergebnis' auf Antrag-Detail
Antrag-Detail-Endpoint liest plenum_votes via get_plenum_votes() und
reicht sie an antrag_detail.html durch.

Block rendert pro Plenum-Abstimmung eine Karte:
- Ergebnis (angenommen/abgelehnt/...) farb-kodiert
- 'einstimmig'-Annotation falls gesetzt
- Quelle (Protokoll-ID, mit URL als Tooltip)
- Fraktions-Chips fuer Ja/Nein/Enthaltung

Mehrfach-Abstimmungen einer Drucksache (Ueberweisung + finale
Beschlussfassung) erzeugen mehrere Karten — chronologisch via
parsed_at DESC im Repository sortiert.

Block erscheint nur, wenn Eintraege existieren (kein leerer Header).
2026-04-28 08:04:32 +02:00
Dotty Dotter
e26607854f feat(#106): Ingest-CLI fuer NRW-Plenarprotokolle
app/ingest_votes_nrw.py: Pipeline PDF → protokoll_parser_nrw → DB.

CLI:
  python -m app.ingest_votes_nrw --pdf /pfad/MMP18-119.pdf
  python -m app.ingest_votes_nrw --url https://landtag.nrw.de/.../MMP18-119.pdf
  python -m app.ingest_votes_nrw --pdf x.pdf --protokoll-id MMP18-119 --bundesland NRW

Protokoll-ID wird default aus Datei-Stem abgeleitet (MMP18-119.pdf →
MMP18-119), URL-Mode parst sie aus dem letzten Pfadsegment.

ingest_pdf() ist die programmatische API (auch fuer Folge-Cron, falls
spaeter automatisch Plenarprotokoll-Sammelinges nachgeruestet wird).
Statistik-Dict: parsed/written/skipped_no_drucksache/errors.

6 Tests: Roundtrip, skip-bei-fehlender-Drucksache, default + override
fuer Protokoll-ID, BL-Override (fuer #126-Folge), idempotenter Re-Ingest.
2026-04-28 08:03:18 +02:00
Dotty Dotter
ae3f48be41 feat(#106): plenum_vote_results-Tabelle + Repository
DB-Schema fuer fraktions-aggregierte Plenum-Abstimmungsergebnisse:
- bundesland, drucksache, quelle_protokoll als Compound-PK
  (eine Drucksache kann mehrfach abgestimmt werden — Ausschuss-Empfehlung
  und finale Beschlussfassung leben nebeneinander)
- ergebnis (angenommen/abgelehnt/ueberwiesen/...), einstimmig-Flag
- fraktionen_ja/_nein/_enthaltung als JSON-Arrays
- quelle_protokoll (z.B. 'MMP18-119') + optional quelle_url
- Index auf (bundesland, drucksache) fuer Lookup-Path

Repository-API:
- upsert_plenum_vote(...) idempotent ueber Compound-PK
- get_plenum_votes(bl, drucksache) → Liste, neueste zuerst

7 Tests fuer Roundtrip, einstimmig-Flag, Idempotenz, Multi-Protokoll-Erhalt,
leere Queries, Unicode-Handling von 'GRÜNE'.

Refs #106 — naechster Schritt: Ingest-CLI gegen NRW-PDFs.
2026-04-28 08:01:26 +02:00
Dotty Dotter
d640734641 feat(#106,#134): NRW-Protokoll-Parser v5 ins Repo migriert
Vorher als parser_v5_iteration15.py nur auf Prod-Server, nicht
versionskontrolliert. Jetzt unter app/protokoll_parser_nrw.py
mit klarem Naming-Schema (BL-Suffix, damit Folge-Adapter analog
heissen koennen, vgl. ADR 0002).

Aenderungen am Code:
- from __future__ import annotations (Py3.9-kompatibel fuer 'str | None')
- fitz-Import optional (try/except), damit pure-string-Funktionen
  auch im Stub-conftest funktionieren

30 Tests in test_protokoll_parser_nrw.py (#134 Phase 2):
- normalize_fraktionen: F.D.P., GRÜNE-Aliase, Landesregierung
- _is_empty_phrase: Niemand/Keine/nicht-Mustern
- _parse_vote_block: ja/nein-Extraktion plus Negationen
- find_results: angenommen/abgelehnt, einstimmig (nur ueber-Kind!),
  (neu)-Suffix in Drucksachen-Nrn, Sortierung, Dedup
- resolve_drucksache_for_ueber: Backward-Search mit closest-match

Refs #106 (Abstimmungsverhalten verknuepfen — Vorbereitung fuer DB-Schema)
Refs #126 (BL-uebergreifender Parser — NRW als Referenz-Implementierung)
Refs #134 (Test-Suite Audit — Phase 2)
2026-04-28 02:08:03 +02:00
Dotty Dotter
3262f17458 build(#134): Coverage-Baseline (.coveragerc) + pytest-cov in dev-deps
Phase 3 von #134 / ADR 0007: 60%-Mindestschwelle pro Default-Lauf, mit
show_missing fuer schnelle Lueckenanalyse.

Konfiguration:
- source = app, omits Hilfs-Skripte (reindex_embeddings, sync_abgeordnetenwatch)
- exclude_lines: __repr__, NotImplementedError, __main__-Block,
  TYPE_CHECKING, Ellipsis-Stubs
- htmlcov-Ordner via .gitignore ausgeschlossen

Aufruf:
  pytest --cov=app --cov-report=term-missing
  pytest --cov=app --cov-report=html  # detaillierte HTML-Ansicht in htmlcov/

ADR 0007 (Test-Taxonomie) erklaert das Gesamtschema.
2026-04-28 02:05:39 +02:00
Dotty Dotter
7e20f910fe docs(#134): ADR 0007 — Test-Taxonomie
Phase 3 von #134: Klassifizierung Unit / Integration / E2E / Property / Smoke
mit Markern, Latenz-Budgets, Verzeichnis-Konventionen und Lauf-Befehlen.

Index aktualisiert (0007 zwischen 0006 und 0008 eingefuegt — ADRs sind
chronologisch, nicht numerisch sortiert).
2026-04-28 02:04:24 +02:00
Dotty Dotter
3a8c03db6c test(#134): test_wahlperioden.py — Datum→WP-Mapping
12 Tests fuer app/wahlperioden.py:
- aktuelle WP fuer Datum >= wahlperiode_start
- Vorgaenger-WP fuer Datum davor
- None bei unbekanntem BL
- Empty/None Datum → aktuelle WP (Default)
- Boundary-Tag (= start) gehoert zur neuen WP
- ISO-lexikographische Vergleichsannahme stimmt fuer alle BL
- all_wahlperioden() enthaelt aktuelle + Vorgaenger pro BL, keine Duplikate
2026-04-28 02:02:40 +02:00