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>
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>
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>
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.
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.
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.
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).
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).
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.
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.
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.
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
Abstimmungsergebnisse 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).
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).
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.
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.
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.
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.
- 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.
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.
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.
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.
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.
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.
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).
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).