d24949740b
93 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6dfcd69979 |
#19 SaarlandAdapter — Umbraco JSON-API mit Iframe-Unwrap
Reverse-Engineering aus HAR-Capture (User-Browser, /suche?searchValue=Schule):
- Endpoint: POST /umbraco/aawSearchSurfaceController/SearchSurface/GetSearchResults/
- Content-Type: application/x-www-form-urlencoded; charset=UTF-8 mit rohem
JSON im Body (Kendo-Konvention von $.ajax ohne expliziten contentType)
- Body MUSS Sections={} und Sort={} als leere Dicts haben — sobald
Sections.Print/etc. gesetzt sind, antwortet der Server mit HTTP 500
(eigene Stunden in der Sackgasse, bis HAR den minimalen Body zeigte)
- Body-Schema: {Filter:{Periods:[17]}, Pageination:{Skip,Take}, Sections:{},
Sort:{}, OnlyTitle:false, Value:<query>, CurrentSearchTab:0}
Response-Mapping (FilteredResult[*]):
- DocumentNumber → drucksache (e.g. "17/11")
- Title → title
- DocumentType → typ; client-side gefiltert auf "Antrag" (Print-Section
enthält Anfragen + Anträge + Gesetzentwürfe gemischt, ~30-50% sind Anträge)
- Publisher (kollektive Anträge: "CDU"/"SPD") + DocumentAuthor
(individuelle MdL: "Name, Vorname (CDU);…") via parteien.extract_fraktionen
- PublicDate (ISO mit T-Suffix) → datum (auf 10 Zeichen abgeschnitten)
- FilePath: ``/file.ashx?FileId=…&FileName=…`` ist ein HTML-Iframe-Wrapper
(455 Bytes), nicht das PDF! Echter Binär-Endpoint ist
``/Downloadfile.ashx`` (Großbuchstabe!) mit denselben Query-Parametern.
Der Wrapper hat mich beim ersten Smoke-Test mit "no objects found"
angeschmissen, der Iframe-Hint im HTML hat den Trick verraten.
Drucksachen-Lookup nutzt ``Value=<drucksache>``: der Server matcht die
Nummer im Volltext und liefert sie zuverlässig als ersten Hit. Kein
dedizierter GetById-Endpoint vorhanden.
Smoke-Test gegen prod (im Container):
- search("Schule", limit=5) → 2 Anträge in WP17 (140 Print-Hits gesamt,
Antrag-Filter auf 2/140 — der Rest sind Anfragen/Gesetzentwürfe):
17/11 [CDU] "Schule als Lern- und Bildungsort weiter stärken …"
17/419 [AfD] "Eine gute Bildungspolitik als wesentlicher Bestandteil …"
- get_document("17/11") → match
- download_text("17/11") → 3520 chars echter Antrags-Volltext (Header,
Fraktion, Resolutionstext)
Tests: 185/185 grün (keine Regression).
UI-Aktivierung erfolgt separat in #31 (blockiert auf diesem Commit).
Refs: #19, #49 (Roadmap Phase 3)
|
||
|
|
6ced7ae018 |
#60 Reopen — Option B: server-side reconstruct of zitat quelle/url
Sub-D Live-Run gegen Prod-DB nach dem db3ada9-Deploy hat einen neuen Halluzinations-Case gezeigt, den A+C nicht gefangen hat: BB 8/673 BSW: text aus bsw-bb-2024 S.27 (verifiziert via Volltext-Suche im PDF), aber LLM hat im quelle-Feld "S. 4" angegeben — die Seite des Top-2-Chunks im selben Retrieval-Window. Klassischer Cross-Mix zwischen Q-IDs. Strukturelle Diagnose: Das [Qn]-Tag aus A ist nur ein weicher Anker im Prompt. Das LLM darf Text aus Chunk Qn kopieren und trotzdem die quelle aus Chunk Qm zusammenbauen. Die ZITATEREGEL kann das nicht verhindern, solange wir der LLM-Selbstauskunft vertrauen. Fix (Option B aus dem ursprünglichen Plan): `embeddings.reconstruct_zitate(data, semantic_quotes)` läuft im analyzer **nach** json.loads aber **vor** Pydantic-Validation: 1. Flachen die retrievten Chunks aller Parteien zu einer einzigen Liste. 2. Pro Zitat: text via Substring oder 5-Wort-Anker gegen alle Chunks matchen (Helpers `find_chunk_for_text` + `_normalize_for_match`, identische Logik wie Sub-D Test). 3. Match → quelle/url server-seitig durch _chunk_source_label und _chunk_pdf_url des matchenden Chunks ÜBERSCHREIBEN. 4. Kein Match → Zitat verworfen (statt mit erfundener quelle persistiert). Damit kann der LLM nur noch sauber zitieren oder gar nicht — es gibt keinen Pfad mehr zu "echter Text, falsche quelle". Tests: - TestReconstructZitate (5 cases): BB 8/673 Re-Mapping, Drop bei hallucinated, no-op bei leeren chunks, anchor-match-Fallback, short-needle und soft-hyphen Edge-Cases - 185/185 grün (179 + 6 neu) Refs: #60, #54 (Sub-D) |
||
|
|
db3ada9328 |
#60 Fix A+C: ENUM-basiertes Zitieren + top_k 2→5
Strukturelle Lösung für die LLM-Halluzinations-Cases aus #60: A — ENUM-Anker - format_quotes_for_prompt nummeriert jeden retrievten Chunk als [Q1], [Q2], … - Neue ZITATEREGEL im Prompt erzwingt vier Bedingungen: 1. Jedes Zitat MUSS auf genau einen [Qn]-Chunk verweisen 2. Der text-String MUSS eine wörtliche, zusammenhängende Passage von min. 5 Wörtern aus genau diesem Chunk sein 3. Die quelle MUSS exakt das Source-Label des gewählten Chunks sein 4. Wenn kein Chunk passt: leeres zitate-Array — lieber 0 als erfunden - analyzer.py:get_system_prompt: Wichtige-Regeln-Block zieht den selben Mechanismus nach, damit das LLM den [Qn]-Anker auch im System-Prompt sieht und nicht nur im User-Prompt. C — Recall-Boost - analyzer.py:run_analysis: top_k_per_partei 2 → 5. In den drei Cases aus #60 lagen die "richtigen" Seiten (S.36, S.37) bisher außerhalb des Top-3-Windows; mit Top-5 erhöht sich die Wahrscheinlichkeit, dass sie überhaupt im Kontext landen. Hintergrund — die Halluzinationen waren KEIN Embedding-Bug: Die retrievten Chunks für Case 1 enthielten S.58 (richtige Seite, falscher Snippet) — das LLM hat den Snippet aus seinem Trainingswissen über GRÜNE-Wahlprogramme rekonstruiert statt aus dem retrievten Chunk-Text zu zitieren. Cases 2/3 hatten die zitierten Seiten gar nicht im Top-3-Window — das LLM hat sowohl Seite als auch Snippet halluziniert. ENUM-Anker verhindert beides strukturell, weil ein nicht-existenter [Qn] sofort als Cheating sichtbar wäre. Tests: - test_chunks_get_enum_ids - test_zitateregel_mentions_enum_anchor - 179/179 grün Refs: #60, #54 (Sub-D), #50 (Umbrella E2E) |
||
|
|
ed64399dbb |
Fix #60: NameError in get_relevant_quotes_for_antrag (Phase B refactor leftover)
Root cause: der #55-Refactor ( |
||
|
|
19e5fe4691 |
Phase J: SN EDAS-XML-Adapter (#26/#38) — Sachsen aktiv via XML-Export
Reaktiviert die in Phase J vertagte Adapter-Implementation: statt
ASP.NET-Postbacks zu simulieren (blockt durch __VIEWSTATE-Komplexität
plus robots.txt: Disallow: /), liest die neue ``SNEdasXmlAdapter``-
Klasse einen wöchentlich manuell aus EDAS exportierten XML-Dump.
Workflow:
1. User exportiert in der EDAS-Suchmaske mit Filter "Dokumententyp =
Antr" einen XML-Dump (bis zu 2500 Treffer/Export, sortiert
newest-first nach Datum)
2. Datei wird unter ``data/sn-edas-export.xml`` abgelegt (ins
persistent volume des prod-containers)
3. ``search()``/``get_document()`` lesen die XML-Datei lokal — keine
Server-Calls gegen edas.landtag.sachsen.de
4. ``download_text()`` resolved die echte PDF-URL on-demand über einen
einzelnen GET gegen ``viewer_navigation.aspx`` (single GET, kein
Postback) und holt dann das PDF von ``ws.landtag.sachsen.de/images``
XML-Schema (ISO-8859-1):
- ``<ID>`` interne EDAS-Doc-ID
- ``<Wahlperiode>``, ``<Dokumentenart>``, ``<Dokumentennummer>``
- ``<Fundstelle>`` z.B. ``"Antr CDU, BSW, SPD 01.10.2024 Drs 8/2"`` —
enthält Typ, Urheber und Datum, parsen via Regex
- ``<Titel>`` Volltext-Titel
PDF-URL-Schema (extrahiert aus dem viewer_navigation.aspx onLoad-
Handler): ``ws.landtag.sachsen.de/images/{wp}_Drs_{nr}_{...}.pdf``
mit variablen Suffix-Komponenten — wir machen die Resolution lazy.
Mapper-Erweiterung:
- ``parteien.PARTEIEN``-Tabelle um ``BÜNDNISGRÜNE``/``Bündnisgrüne``
ergänzt — der Sachsen-spezifische zusammengeschriebene Eigenname der
GRÜNEN-Fraktion (sonst wären 8/2100 etc. mit leerer Fraktionen-Liste
rausgekommen)
BL-Eintrag:
- ``SN.aktiv = True``
- ``doku_system="EDAS-XML-Export"`` (klare Klassifikation, dass es
KEIN normaler Webcrawler ist)
- Test ``test_sn_is_eigensystem_not_parldok`` umbenannt in
``test_sn_uses_xml_export_not_parldok``
Live-Probe lokal:
```
search('Klima', limit=5):
8/2100 2025-03-17 | [GRÜNE] | Fahrradoffensive Sachsen ...
7/192 2019-10-11 | [LINKE] | Erste Schritte zur Klimager...
7/2067 2020-03-19 | [CDU, SPD, GRÜNE] | Sächsische Waldbesitzer ...
```
176 Unit-Tests grün. Container braucht beim Deploy einen XML-Upload
ins data/-Volume — separater scp-Schritt.
Refs: #26, #38, #59 (Phase J revived)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|
|
278d74ff97 |
Phase I: HB PARiSHBAdapter (#21/#33) — Bremen aktiv
Schließt #21 (HB-Scraper) und #33 (UI-Aktivierung). Eigenständige ``PARiSHBAdapter``-Klasse für paris.bremische-buergerschaft.de. Backend (HAR-Trace TEMP/paris.bremische-buergerschaft.de.har): - Single-POST gegen ``/starweb/paris/servlet.starweb`` mit form-urlencoded Body - ``path=paris/LISSHFL.web``, ``format=LISSH_BrowseVorgang_Report`` - ``01_LISSHFL_Themen=<query>`` (Volltext-Thesaurus) - ``02_LISSHFL_PARL=S OR L`` (Stadt + Landtag in einem Rutsch) - ``03_LISSHFL_WP=21`` (aktuelle Wahlperiode; Multi-WP-Range timeout-t den Server bei 60s) - Wildcards (``*``) timeout-en ebenfalls — bei leerer Query verwenden wir das hochfrequente Stoppwort ``"der"`` als Catch-all Hit-Format aus dem Single-Page-HTML: - ``<tbody name="RecordRepeater"><tr name="Repeat_TYP">`` - Title in ``<h2><a>`` - ``Drs <b>21/730 S</b>`` mit S/L-Suffix für Stadtbürgerschaft vs Landtag — Drucksachen-IDs werden als ``21/730S`` (ohne Space) gespeichert - ``Änderungsantrag vom 23.02.2026`` (Typ + Datum) - Fraktionen-Liste nach ``<br/>`` - PDF-Link mit ``target="new"`` auf bremische-buergerschaft.de Pipeline: - ``search()`` mit client-side ``"antrag"``-Filter (analog #61), fängt ``"Antrag"``, ``"Änderungsantrag"`` etc. - ``get_document()`` linearer Lookup - ``download_text()`` PDF-via-fitz BL-Eintrag in ``bundeslaender.py``: - ``HB.aktiv = True`` - ``doku_system="PARiS"`` (statt der alten Klassifikation "StarWeb" — PARiS ist eine deutlich abweichende Servlet-Variante, kein eUI) - ``drucksache_format="21/1234S"`` - Test ``test_hb_is_starweb_not_paris`` umbenannt in ``test_hb_is_paris_starweb_variant``, prüft jetzt auf "PARiS" Live-Probe: ``` 21/730S 2026-02-23 | [SPD,GRÜNE,LINKE] | Änderungsantrag | Haushaltsgesetze ... 21/1449 2025-11-05 | [SPD,GRÜNE,LINKE] | Antrag | Finanzierung der Bremischen Häfen 21/555S 2025-06-17 | [CDU] | Antrag | Clima-Campus zügig beantworten ``` 176 Unit-Tests grün, Live-Verifikation Sub-A im Container nach Deploy. Refs: #21, #33, #59 (Phase I) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
4a8986e009 |
Phase H: HE StarWebHEAdapter (#24/#30) — Hessen aktiv
Schließt #24 (HE Card-Parser) und #36 (UI-Aktivierung). Eigenständige ``StarWebHEAdapter``-Klasse für starweb.hessen.de. Backend-Discovery aus HAR-Trace (TEMP/starweb.hessen.de.har): - starweb.hessen.de läuft auf einem eUI-Backend mit synchronem 2-Step- Flow (kein Polling wie BW PARLIS): POST ``browse.tt.json`` → ``report_id`` direkt in der Response → GET ``report.tt.html? report_id=...&start=0&chunksize=1500`` - Source: ``hlt.lis`` - Server verlangt ZWINGEND einen ``search.json``-Term-Tree, ``parsed``/ ``sref`` allein reichen nicht. Top-NOT mit zwei Operanden: ``not(WP-Filter, NOWEB=X)`` - Hit-Format: Cards (``efxRecordRepeater``) mit Daten in HTML-Kommentar- Perl-Dumps ``<!--<pre class="dump">$VAR1 = ...</pre>-->`` - Field-Mapping: WEV01=Title, WEV02=Datum, WEV03=Typ, WEV07=PDF-URL, WEV08=Drucksachen-Nummer, WEV12=Urheber Pipeline: - ``search()`` synchron 2-Step, client-side ``"antrag"``-Filter (analog #61 für portala) — fängt "Dringlicher Berichtsantrag" und ähnliche Subtypen - ``get_document()`` linearer Lookup über die ersten 200 Hits - ``download_text()`` PDF-via-fitz (HE-PDF-URLs werden auf https upgegradet) BL-Eintrag in ``bundeslaender.py``: - ``HE.aktiv = True`` - ``doku_system="portala"`` (statt "StarWeb" — die /starweb/LIS-Pfade sind nur Legacy, das echte Backend ist /portal) - ``doku_base_url="https://starweb.hessen.de/portal"`` ADAPTERS-Registrierung an Position vor NRW. Live-Probe: ``` 21/4157 2026-04-07 | [GRÜNE] | Dringlicher Berichtsantrag | Vorstellung, Kosten... 21/4156 2026-04-02 | [GRÜNE] | Berichtsantrag | Schulische Prävention... 21/4136 2026-03-30 | [GRÜNE] | Dringlicher Berichtsantrag | Streichung des Schulfachs... ``` 176 Unit-Tests grün, Sub-A im Container nach Deploy zu verifizieren. Refs: #24, #30, #36, #59 (Phase H) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
0f7d35f20e |
Phase G: BundestagAdapter via DIP-API (#56)
Schließt #56 (Bundespolitik überprüfbar machen). Neuer ``BundestagAdapter`` in ``app/parlamente.py``, neuer ``BUND``-Eintrag in ``app/bundeslaender.py`` als 17. Parlament-Slot. API: - DIP-Search-API auf ``search.dip.bundestag.de/api/v1/drucksache`` - API-Key aus ``dip-config.js`` gescraped (öffentlich, klartext) - Auth via URL-Param ``?apikey=...`` plus ``Origin: https://dip.bundestag.de``- Header (Origin-Locking, server-to-server-tauglich) - Pagination via ``cursor``-Parameter, 100 Hits pro Page - ``f.drucksachetyp=Antrag`` und ``f.wahlperiode=21`` als Server-Filter Mapping: - ``dokumentnummer`` → ``Drucksache.drucksache`` - ``titel`` → ``title`` - ``urheber[*].titel`` → durch ``parteien.extract_fraktionen`` zu ``["AfD"]``/``["GRÜNE"]``/etc. — die ``"Fraktion der AfD"``- Schreibweise wird vom zentralen Mapper aus #55 bereits korrekt geparst, kein Adapter-spezifisches Pattern nötig - ``fundstelle.pdf_url`` → ``link`` - ``datum`` → bereits ISO ``YYYY-MM-DD`` ``get_document(drucksache)`` nutzt ``f.dokumentnummer`` als direkter Server-Filter, kein linearer Pagination-Scan. BUND-Eintrag in ``bundeslaender.py``: - ``code="BUND"``, ``parlament_name="Deutscher Bundestag"``, ``wahlperiode=21``, ``wahlperiode_start="2025-03-25"`` (Konstituierung 21. WP nach BTW 2025), ``regierungsfraktionen=["CDU", "CSU", "SPD"]`` (Kabinett Merz) - ``aktiv=True`` — taucht automatisch in ``alle_bundeslaender()`` und ``aktive_bundeslaender()`` auf, damit die UI- und Auswertungs-Pipelines BUND ohne zusätzliche Sonderpfade kennen - 17 Einträge in ``BUNDESLAENDER`` statt 16 — Tests entsprechend aktualisiert (``test_sixteen_bundeslaender_plus_bund``, ``test_alle_bundeslaender_returns_all``, ``test_all_wahlperioden_lists_each_bl_twice``) Live-Probe direkt im Repo: ``` adapter: Deutscher Bundestag (DIP), wahlperiode=21 search returned 5 docs 21/5136 2026-03-31 | ['AfD'] | Transparenz, Wirtschaftlichkeit ... 21/5064 2026-03-27 | ['GRÜNE'] | Ausverkauf der Energieinfrastruktur ... 21/5059 2026-03-27 | ['AfD'] | Berufsfreiheit für Selbstständige ... get_document('21/5136') -> drucksache=21/5136 ``` 176 Unit-Tests grün, Live-Verifikation Sub-A im Container nach Deploy. Refs: #56, #59 (Phase G) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
015b134bc2 |
PortalaAdapter: chunksize-Floor auf 1500 (#61 Bug 5 follow-up)
Berlin-PARDOK ist von Schriftlichen Anfragen dominiert und liefert ohne server-side ETYPF-Filter (BE: document_type=None) bei chunksize=100 nur 1-2 Anträge zurück. Damit reicht das Window selbst für limit=20 nicht aus, um z.B. die A100-Antrag-Drucksache 19/2650 zu finden — und get_document() liefert None. Floor bewusst hoch auf 1500 angehoben (vorher 100/500). Bei einem typischen Verhältnis 1:30 Antrag/Anfrage in BE liefert das ~50 Anträge, genug für robuste Lookups in den letzten 24 Monaten. 176 Unit-Tests grün. Refs: #61 Bug 5 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
060a33ea5f |
PortalaAdapter: client-side Antrag-Filter immer aktiv (#61 Bug 5)
BE-Adapter hat document_type=None (eigene ETYPF-Werte werden vom Berliner PARDOK nicht akzeptiert), wodurch der Server alle Doku-Typen zurückliefert. Das 200-Result-Window war damit vollständig von 'Schriftliche Anfrage'-Hits aushungernd, sodass Anträge wie 19/2650 nie ans Frontend kamen — und get_document() für genau diese Drucksachen None lieferte. Patch: client-side 'antrag'-Substring-Filter läuft jetzt unabhängig vom Server-Filter (vorher nur wenn document_type gesetzt war). BB/RP und alle PortalaAdapter-Instanzen profitieren mit. 176 Unit-Tests grün, Live-Verifikation Sub-B im Container nach Deploy. Refs: #61 Bug 5 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
e72dd3ec21 |
Adapter-Bugs aus #61: BB Datum + BB/RP Type-Filter
Drei aus #61 identifizierte Production-Bugs gefixt: - **Bug 4 (BB Datum)**: BB.wahlperiode_start vom 2024-10-23 (Konstituie- rende Sitzung) auf 2024-09-22 (Wahltag) zurückgesetzt. Damit fällt die Geschäftsordnungs-Drucksache 8/2 vom 2024-10-17 in den Plausibilitäts-Check. Ist auch semantisch sauberer — die WP fängt mit der Wahl an, nicht mit der formalen Konstituierung. - **Bug 2/3 (BB/RP Type-Filter leakt Kleine Anfrage / Beschluss- empfehlung)**: Server-side ETYPF/DTYPF-Filter ist best-effort über die portala-Instanzen — BB+RP lassen die nicht-Antrag-Typen durch. Client-side strict-filter im PortalaAdapter.search() nach Aufruf von _parse_hit_list_html: nur Hits, deren typ-String das Substring "antrag" enthält, kommen weiter. Substring-Match (nicht exact), damit "Antrag gemäß § 79 GO" und ähnliche Subtypen passieren. 176 Unit-Tests grün, Live-Verifikation via Sub-A im Container nach Deploy. Refs: #61 (Bug 2, 3, 4) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
4bc583e490 |
ParLDokAdapter: Skip Hits mit leerem PDF-Link (#61 Bug 1, TH-Pipeline-Blocker)
Live-Verifikation in der Container-DB hat aufgedeckt, dass TH ParlDok für sehr frische Vorlagen (z.B. 8/1594, datum 2026-03-31, allowed=false) ``link``/``prelink`` als leeren String liefert — das PDF ist noch nicht zur Veröffentlichung freigegeben. Bisheriges Verhalten: Adapter konstruierte einen Drucksache-Eintrag mit ``link=''``, der dann durch die Pipeline rutschte und im Frontend als unklickbarer Eintrag erschien. ``download_text()`` würde später an ``not doc.link`` scheitern, was die Analyse blockt. Sauberer Skip an der Quelle: ``_hit_to_drucksache`` returnt None, wenn weder ``link`` noch ``prelink`` einen Pfad liefern. Das ist konsistent mit den anderen None-Returns für unbrauchbare Hits (kein lp, kein number). Lokal verifiziert: 176 Unit-Tests grün. Live-Verifikation gegen Production folgt nach Deploy via Sub-A-Test im Container. Refs: #61 (Bug 1: TH leerer Link) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
7cf073122f |
Phase E (substituted): Auswertungen-Drilldown-Modal (#59)
Sachsen-Adapter (#26/#38) ist Eigensystem mit ASP.NET-Webforms-Postbacks (__VIEWSTATE/__CALLBACKID, siehe bundeslaender.py:343-348) und braucht HAR-Aufnahme → Blocker für autonome Bearbeitung. Phase E entsprechend substituiert mit der Frontend-Erweiterung der Auswertungen. - Matrix-Zellen sind jetzt klickbar (`cell-with-data`-Klasse + hover-outline mit Blue-Border) - Klick öffnet ein Modal, das `/api/auswertungen/zeitreihe? bundesland=...&partei=...` aufruft und die Score-Entwicklung dieser (BL, Partei)-Kombination über alle bekannten WPs als Tabelle rendert - ESC-Taste oder Backdrop-Klick schließt das Modal - Schließt damit den Frontend-Loop für die in Phase C gebauten Backend-Endpoints (CLAUDE.md-Sync separat — die Datei liegt im Projekt-Root außerhalb des Webapp-Git-Repos.) Refs: #59 (Phase E substituted) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
26f13bd29d |
Phase D: zwei getrennte Suchfelder + Auswertungen-Link (#16, #59)
Schließt #16 (UI: zwei klar getrennte Suchen) und ergänzt den Header um den Link auf das neue Auswertungen-Dashboard aus Phase C. - Search-Row in `index.html` aufgespalten in zwei untereinanderliegende Inputs: oben "Suche in geprüften Anträgen (DB)" mit Live-Debouncing (wie bisher), unten "Im Landtag suchen (live)" mit Enter-Trigger und expliziter Such-Button. Beide Felder schreiben in dieselbe Liste, sind aber visuell und semantisch klar getrennt. - `searchLandtag()` zieht jetzt aus `landtag-search-input` statt aus dem DB-Suchfeld - `changeBundesland()` resettet zusätzlich das Landtag-Feld - Header: neuer `📈 Auswertungen`-Link neben `📚 Quellen` Refs: #16, #59 (Phase D) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
3631e5418c |
Phase C: Auswertungen-Dashboard #58 + CSV-Export #45 (Roadmap #59)
Drei-dimensionale Aggregations-Sicht über Bundesland × Partei × Wahlperiode mit minimalem Frontend. Backend (`app/auswertungen.py`): - `aggregate_matrix(filter_wp=None)` — 2D-Matrix Bundesland × Partei mit (n, Ø-Score) pro Zelle, optional gefiltert nach Wahlperiode - `aggregate_zeitreihe(bundesland, partei)` — Score-Verlauf einer (BL, Partei)-Kombination über alle bekannten WPs - `export_long_format()` — Long-Format-CSV-Export für externe Tools (deckt #45 vollständig ab) - Partei-Auflösung läuft strikt durch `normalize_partei()` aus #55 — damit wird BB-`FREIE WÄHLER` korrekt als `BVB-FW` aggregiert und NICHT mit dem RP-FW zusammengezählt Wahlperioden-Helper (`app/wahlperioden.py`): - `wahlperiode_for(datum, bundesland)` mappt ein ISO-Datum + BL auf eine Kennung wie `"NRW-WP18"` oder `"MV-WP7"` (Vorgänger-WP). Single Source of Truth ist `BUNDESLAENDER[bl].wahlperiode_start` - `all_wahlperioden()` für UI-Filter-Dropdowns Endpoints in `app/main.py`: - `GET /auswertungen` — HTML-Seite (neues Template) - `GET /api/auswertungen/matrix?wahlperiode=NRW-WP18` — JSON-Matrix - `GET /api/auswertungen/zeitreihe?bundesland=MV&partei=CDU` — JSON-Verlauf - `GET /api/auswertungen/export.csv` — CSV-Download Frontend (`app/templates/auswertungen.html`): - Statisches Template mit Vanilla-JS, kein Build-Step - Wahlperioden-Dropdown + Reload-Button + CSV-Export-Button - Matrix-Tabelle mit Score-Color-Coding (rot ≤ 3, gelb 3-6, grün > 6) - Sticky-Bundesland-Spalte für horizontales Scrolling Tests (`tests/test_auswertungen.py`): - 19 Cases mit in-memory SQLite-Fixture - Verifiziert WP-Mapping, Matrix-Aggregation, Koalitions-Counting, WP-Filter-Korrektheit, BVB-FW-Disambiguierung in der Matrix, CSV-Long-Format - 176 Unit-Tests grün (157 alt + 19 neu) Refs: #58, #45, #59 (Phase C) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
eb045d0ed3 |
Phase B: Parteinamen-Mapper #55 (Roadmap #59)
Zentrale `app/parteien.py` als Single Source of Truth für die Partei- Auflösung: - `PARTEIEN`-Tabelle mit kanonischem Key, langem Display-Namen, allen bekannten Aliasen, optionalem `bundesland_scope` und Government- Marker. 14 Einträge (CDU, CSU, SPD, GRÜNE, FDP, LINKE, AfD, BSW, SSW, BiW + die Freie-Wähler-Familie BVB-FW, FW-BAYERN, FW-SL und der generische FREIE WÄHLER-Eintrag). - `normalize_partei(raw, *, bundesland=None)` für Single-String-Lookups mit Government-Vorrang und FW-Familien-Disambiguierung - `extract_fraktionen(text, *, bundesland=None)` als Funnel für die vier alten Adapter-Helper. Kommagetrennte Listen, MdL-mit-Klammer- partei, HTML-Reste — alles fließt durch eine Stelle, mit BL-Scope- Filter (SSW nur in SH, BVB-FW nur in BB, etc.). - `display_name(canonical, *, long=False)` für UI/PDF — kurze Form bleibt der kanonische Key, lange Form ist "BÜNDNIS 90/DIE GRÜNEN" statt "GRÜNE" etc. Adapter-Migration in `app/parlamente.py`: - Vier nahezu identische `_normalize_fraktion()`-Methoden in PortalaAdapter, ParLDokAdapter, StarFinderCGIAdapter, PARLISAdapter durch einen einzeiligen Shim ersetzt, der `extract_fraktionen` mit `self.bundesland` aufruft. ~120 Zeilen Duplikation entfernt. - `@staticmethod` aufgehoben, weil wir jetzt `self.bundesland` brauchen für die FW-Disambiguierung — alle Aufrufer waren bereits `self._...`, also keine Call-Site-Änderung nötig. `app/embeddings.py:496` Workaround-Hack entfernt: - `partei.upper() if partei != "GRÜNE" else "GRÜNE"` durch zentralen `normalize_partei()`-Aufruf ersetzt — der Hack war ein Kommentarzeichen dafür, dass die Partei-Schreibweise irgendwo zwischen Adapter und Embedding-Lookup driften konnte. Mit dem Mapper ist die Schreibweise überall garantiert kanonisch. Tests: - Neue `tests/test_parteien.py` mit 52 Cases — Single-Lookup, FW- Disambiguierung (BVB/Bayern/Saarland/RP), Volltext-Extraktion, Government-Marker, Tabellen-Konsistenz - `tests/test_parlamente.py` Test-Klasse umgeschrieben: statt der 6 statischen `PortalaAdapter._normalize_fraktion(...)`-Tests jetzt 4 Roundtrip-Tests über echte Adapter-Instanzen, inkl. expliziter BB→BVB-FW vs. RP→FREIE WÄHLER-Verifikation 157 Unit-Tests grün (105 alt + 52 neu). Backwards-kompatibel — die kanonischen Keys sind exakt die in der DB stehenden Strings, kein Migrations-Schritt nötig. Refs: #55, #59 (Phase B) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
9c70b463ac |
Phase A: Audit-Restbefunde #57.3/4/7 (Roadmap #59)
Drei verbleibende Audit-Befunde aus #57 in einem Patch: - **#57.3 MEDIUM** Drucksache-Regex-Validation: neue app/validators.py mit validate_drucksache() als gemeinsamer Validation-Funnel. Pattern ^\d{1,3}/\d{1,7}([-(].{1,20})?$ deckt alle 10 aktiven Bundesländer (8/6390, 18/12345, 8/6390(neu), 23/3700-A) ab und blockt Path-Traversal (../, /etc/passwd) plus Standard-Injection (;, <, &). Drei Endpoints durchgeschleust: /api/assessment, /api/assessment/pdf, /api/analyze-drucksache. - **#57.4 MEDIUM** print() → logging.getLogger(__name__): main.py und analyzer.py auf strukturiertes Logging umgestellt. LLM-Inhalte werden NICHT mehr als Volltext geloggt — neue Helper _content_fingerprint() liefert nur "len=N sha1=XXXX", reicht zur Forensik ohne Antrag-Inhalte ins Container-Log zu leaken. basicConfig() mit ISO-Format setzt strukturiertes Logging früh, damit logger.exception() auch beim Boot greift. - **#57.7 LOW-MED** Search-Query-Limit: validate_search_query() mit MAX_SEARCH_QUERY_LEN=200 schützt /api/search und /api/search-landtag vor 10-MB-Query-DoS. database._parse_search_query() loggt jetzt shlex.ValueError-Fallback statt ihn zu verschlucken (deckt Memory- Regel "stille excepts in Adaptern" ab). Tests: neue tests/test_main_validators.py mit 22 Cases — Drucksache- Whitelist-Roundtrip + Path-Traversal-Reject, Search-Query Längen- Edge-Cases. 107 Unit-Tests grün (85 alt + 22 neu). Validators in eigenem Modul (app/validators.py), damit Tests sie ohne slowapi-Dependency direkt importieren können. Refs: #57, #59 (Phase A) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
64cbff5286 |
Security hotfixes #1, #2, #6 from audit (#57)
Drei akute Befunde aus dem Live-System-Audit (Issue #57): - **#1 HIGH** — Resource Exhaustion via öffentlichem POST: slowapi Limiter (in-memory, IP-key) auf /analyze (10/min), /api/analyze-drucksache (10/min) und /api/programme/index (3/min). Verhindert, dass ein unauthentifizierter Client mit einer Schleife die DashScope-Quota oder die CPU des Containers leerziehen kann. Default-Storage reicht solange wir auf einem einzigen Worker laufen. - **#2 MEDIUM** + **#6 MEDIUM** (selber Root-Cause) — XXE/Local-File-Read via WeasyPrint und Stored XSS via Browser-Rendering: alle LLM-getragenen Felder in app/report.py laufen jetzt durch html.escape() bevor sie in die HTML-Template interpoliert werden. format_redline_html escape-first und ersetzt dann die Markdown-Marker durch von uns kontrollierte <span>-Tags. build_matrix_html escaped das aspect-Attribut, sodass ein nacktes " den title="..."-Wert nicht mehr beenden und einen Event- Handler injizieren kann. Toter jinja2-Import in report.py entfernt (war never used, blockierte nur den lokalen Test). - **Tests** — neue tests/test_report.py mit 8 Cases, die direkt die Bug-Klasse verifizieren: <script>, file://-img, "-attribut-breakout in Title und ein End-to-End-Render mit XSS-Payloads in jedem LLM-Feld. Die Marker-Funktionalität (** und ~~) wird mit-getestet, damit der Escape-First-Ansatz das nicht versehentlich kaputt macht. 77 alte Unit-Tests + 8 neue → 85 grün. Rate-Limit-Verifikation per TestClient ist Integration-Scope und folgt in tests/integration/test_main_security.py als separates Folge-Item. Refs: #57 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
a4af79688a |
Add 30 Wahlprogramme für TH/BB/HH/SH/BW/RP (#37, #39, #40, #32, #41, #42)
Sechs der zehn aktiven Bundesländer hatten bisher keine Wahlprogramme
indexiert (alle sechs heute neu aktiviert: BW/HH/TH in Phase 1, SH/BB/RP
in Phase 2). Antrag-Analysen für diese BL fielen damit auf föderale
Grundsatzprogramme als Fallback zurück.
Beschafft via abgeordnetenwatch.de für die jeweils laufende WP:
- TH WP8 (LTW 01.09.2024): CDU, AfD, LINKE, BSW, SPD — 5 PDFs
- BB WP8 (LTW 22.09.2024): SPD, AfD, CDU, BSW — 4 PDFs
- HH WP23 (Bürgerschaftswahl 02.03.2025): SPD, CDU, GRÜNE, LINKE, AfD — 5 PDFs
- SH WP20 (LTW 08.05.2022): CDU, SPD, GRÜNE, FDP, SSW — 5 PDFs
- BW WP17 (LTW 14.03.2021): GRÜNE, CDU, AfD, SPD, FDP — 5 PDFs
- RP WP18 (LTW 14.03.2021): SPD, CDU, AfD, GRÜNE, FREIE WÄHLER, FDP — 6 PDFs
Insgesamt 30 PDFs in app/static/referenzen/, plus 30 Einträge in
WAHLPROGRAMME[bl][partei] und embeddings.PROGRAMME.
Naming-Schema wie etabliert: <partei>-<bl>-<jahr>.pdf, also
spd-th-2024.pdf, fw-rp-2021.pdf etc.
Wichtig zu Memory feedback_legislaturprogramme: alle BL nutzen das
Programm der LAUFENDEN Wahlperiode, NICHT Programme aus späteren
Wahlen. BW und RP wählen am 08.03.2026 / 22.03.2026 neu — der
18./19. Landtag konstituiert sich erst, daher sind die 17./18. WP
mit den 2021er Programmen weiterhin laufend bis zur Konstituierung.
Indexierung im prod-Container ist NICHT Teil dieses Commits — muss
separat ausgeführt werden:
ssh vserver 'docker exec gwoe-antragspruefer python -c "
from app.embeddings import index_programm
from pathlib import Path
d = Path(\"/app/app/static/referenzen\")
for pid in [
\"cdu-th-2024\",\"afd-th-2024\",\"linke-th-2024\",\"bsw-th-2024\",\"spd-th-2024\",
\"spd-bb-2024\",\"afd-bb-2024\",\"cdu-bb-2024\",\"bsw-bb-2024\",
\"spd-hh-2025\",\"cdu-hh-2025\",\"gruene-hh-2025\",\"linke-hh-2025\",\"afd-hh-2025\",
\"cdu-sh-2022\",\"spd-sh-2022\",\"gruene-sh-2022\",\"fdp-sh-2022\",\"ssw-sh-2022\",
\"gruene-bw-2021\",\"cdu-bw-2021\",\"afd-bw-2021\",\"spd-bw-2021\",\"fdp-bw-2021\",
\"spd-rp-2021\",\"cdu-rp-2021\",\"afd-rp-2021\",\"gruene-rp-2021\",\"fw-rp-2021\",\"fdp-rp-2021\",
]:
index_programm(pid, d)
"'
77 pytest tests passing — der File-Existenz-Check in test_wahlprogramme.py
hätte einen Tippfehler im PDF-Namen sofort gefangen.
Erledigt UI-Aktivierungs-Issues #37 (TH), #39 (BB), #40 (HH), #32 (SH),
#41 (BW), #42 (RP).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|
|
02ff1423a7 |
Activate Brandenburg + Rheinland-Pfalz via PortalaAdapter reuse (#27, #30, Phase 2)
Riesige Überraschung aus dem BB-HAR-Trace: Brandenburg ist NICHT StarWeb wie in dokukratie und bundeslaender.py klassifiziert, sondern läuft auch auf dem portala/eUI-Backend. Endpoint /portal/browse.tt.json mit db_id=lbb.lissh. Das alte /starweb/LBB/ELVIS/-Frontend ist nur Legacy. Folgeprobing offenbarte: RP/opal.rlp.de läuft ebenfalls portala (db_id=rlp.lissh, 46759 hits in WP18), ebenso NI/HE/BB. Damit ist Phase 2 großteils KEIN StarWeb-Adapter-Bau, sondern PortalaAdapter- Wiederverwendung mit konfigurierbaren Parametern. Activated via Registry-Einträge: - "BB" → PortalaAdapter(base_url=parlamentsdokumentation.brandenburg.de, db_id=lbb.lissh, wahlperiode=8). Nutzt die BE-Card-Variante des Hit-Parsers (efxRecordRepeater). - "RP" → PortalaAdapter(base_url=opal.rlp.de, db_id=rlp.lissh, wahlperiode=18). NICHT mit dem NRW OPAL verwechseln — anderer Markenname, andere Engine. PortalaAdapter erweitert um zwei neue Konstruktor-Parameter mit backward-kompatiblen Defaults: - typ_filter: Optional[str] = "DOKDBE" Wenn None, wird die TYP=<value>-Klausel weggelassen. Manche Instanzen (HE/hlt.lis) lehnen DOKDBE ab. - omit_date_filter: bool = False Wenn True, wird der DAT/DDAT/SDAT-Term weggelassen. HE und ähnliche Instanzen haben andere Date-Field-Namen. Plus _parse_hit_list_cards Date-Regex erweitert: zusätzlich zum "vom DD.MM.YYYY"-Pattern (BE) jetzt auch "DD.MM.YYYY"-plain (BB schreibt Datum vor Drucksachen-Nummer ohne "vom"-Marker). Smoke-Test (lokal): BB q="": 5 hits in 5.9s BB q="Schule": 5 hits (Pflegeschulen, Genderverbot, Hochschulen) RP q="": 5 hits in 4.1s (Entlastung, Bildungschancen) RP q="Schule": 5 hits (Hochschulbau, G9-Gymnasien, Leistungsgerechtigkeit) bundeslaender.py: BB.doku_system "StarWeb"→"portala", RP analog, beide aktiv=True. Anmerkungen mit dem portala-Verweis und der Klarstellung "OPAL/RLP ≠ NRW OPAL" erweitert. NICHT in diesem Commit: - HE: portala-Backend (hlt.lis) ist erreichbar, aber das HE-Card- Layout ist anders (Title direkt im <h3> statt <h3><span>, kein <span class="h6"> für Meta) — eigener Parser-Pfad nötig, deferred. - NI: nilas.niedersachsen.de/portal/ ist eine Login-Page, das öffentliche Backend ist nicht zugänglich — deferred. - HB: kein /portal/-Endpoint, bleibt das alte StarWeb-Servlet — braucht eigenen HAR-Trace, deferred. - BB als StarWeb-Template (#27) ist hinfällig, weil BB portala ist. Phase 2 (3/6) aus Roadmap-Issue #49. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
f82c60e40d |
Activate Schleswig-Holstein via StarFinderCGIAdapter (#20, Phase 2)
SH läuft auf der ältesten der vier Backend-Familien: Starfinder-CGI auf lissh.lvn.parlanet.de. URL-basiert (nicht stateful wie das moderne StarWeb-Servlet von BB/HE/NI/RP/HB), Latin-1-encoding, flat HTML-Tabelle als Hit-Format. Eigener Adapter weil das Schema fundamental anders ist als alles andere. Endpoint: http://lissh.lvn.parlanet.de/cgi-bin/starfinder/0 ?path=lisshfl.txt&id=FASTLINK&pass=&search=WP=20+AND+dtyp=antrag &format=WEBKURZFL Hit-Format pro <tr class="tabcol*">: <b>{TITLE}</b><br> Antrag {URHEBER} {DD.MM.YYYY} Drucksache <a href="{PDF}">{N/M}</a> Quelle: dokukratie/sh.yml + Live-Probing. Encoding: Server liefert iso-8859-1 ohne korrektes Content-Type- Header. Adapter dekodiert resp.content explizit als latin-1. SSW-Detection im _normalize_fraktion: SH ist das einzige BL mit SSW-Fraktion (von der 5%-Hürde befreit), pattern ist \\bSSW\\b analog zu \\bAfD\\b. Free-Text-Suche client-seitig (siehe #18) — server-side query- syntax mit (term) im starfinder-search-Param wird vom Server nicht als Volltext interpretiert, einheitlich mit allen anderen aktiven Adaptern. Smoke-Test (lokal): SH q="": 8 hits in 14.4s SH q="Schule": 8 hits in 14.8s (Schulentwicklung Westküste, Hochschulen, queere Vielfalt an Schule etc.) SH q="Klima": 8 hits (klimafreundlich, Klimafolgen, Strategischer Aktionsplan) SH q="Bildung": 8 hits (berufliche Bildung, Holocaust-Wissen) bundeslaender.py::SH.aktiv = True. doku_base_url auf lissh.lvn.parlanet.de korrigiert (ehemaliger landtag.ltsh.de- Eintrag passte nicht zum echten Endpoint). Damit ist Phase 2 (1/6) angefangen — als Nebenpfad, weil das StarWeb-Servlet (#27 BB als Template für 5 weitere) ohne HAR- Trace nicht sauber reverse-engineerbar war. Phase 2 (1/6) aus Roadmap-Issue #49. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
dc0bb07c12 |
Activate Thüringen via ParLDokAdapter reuse + filter widening (#25, Phase 1)
Thüringen läuft auf parldok.thueringer-landtag.de mit ParlDok 8.3.5
(J3S GmbH) — exakt dieselbe Version wie MV. Aber TH packt seine
Anträge unter zusammengesetzten type-Strings ("Antrag gemäß § 79 GO",
"Antrag gemäß § 74 (2) GO") und kind="Vorlage" statt der MV-Variante
kind="Drucksache"/type="Antrag". Strict-Match auf "Antrag" hat 0
Treffer geliefert.
Lösung: ParLDokAdapter um zwei Konstruktor-Parameter erweitert:
- document_typ_substring=True → Substring-Match auf type-Feld
("Antrag" matched "Antrag gemäß § 79 GO", "Alternativantrag" usw.)
- kinds=["Drucksache", "Vorlage"] → erweiterte kind-Liste
Defaults sind backward-kompatibel (Substring-Match aus, kinds nur
Drucksache), sodass MV und HH unverändert weiterlaufen.
_hit_matches_filters() als zentraler Filter-Helper extrahiert,
search() und get_document() nutzen ihn — get_document() überspringt
ihn allerdings, weil dort beliebige Drucksachen aufrufbar sein müssen,
unabhängig vom search-Time-Filter.
Hostname-Korrektur: parldok.thueringen.de redirected per 303 auf
parldok.thueringer-landtag.de. doku_base_url in bundeslaender.py
auf den neuen Host umgestellt.
Smoke-Test (lokal):
TH q="": 8 hits in 3.3s
TH q="Schule": 2 hits in 25.7s (Lernmittelbeschaffung, Modernisierung
Bund-Länder-Vereinbarung — beide Schul-bezogen)
TH q="Klima": 0 hits (keine in den letzten 1000 Drucksachen)
Damit ist Phase 1 (3/3) komplett. Nächstes Phase-2 Issue: #27 BB als
StarWebAdapter-Template.
Phase 1 (3/3) aus Roadmap-Issue #49.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|
|
916c5d84d7 |
Activate Hamburg via ParLDokAdapter reuse (#28, Phase 1)
Hamburg's parldok runs ParlDok 8.3.1 (J3S GmbH) — kompatibel mit der MV-Variante (8.3.5). Selber /parldok/Fulltext/Search-Endpoint, selbe Body-Schema, selbes Hit-Format. Dadurch ist der existierende ParLDokAdapter aus #4 ohne Code-Änderungen wiederverwendbar. Eingetragen wurde nur: - ADAPTERS["HH"] = ParLDokAdapter(base_url=buergerschaft-hh.de, wahlperiode=23, prefix=/parldok, document_typ="Antrag") - bundeslaender.py::HH.aktiv = True Smoke-Test (lokal): HH q="": 8 hits in 1.5s, jüngste WP23-Anträge sortiert newest-first HH q="Schule": 1 hit in 13.2s (HH ist klein, WP23 erst seit März 2025, HH nutzt eher "Kita"/"Bildung"/"Lehrkräfte" im Titel) HH q="Klima": 2 hits Verifikation HH ist 8.x: curl https://www.buergerschaft-hh.de/parldok/ | grep generator → "ParlDok 8.3.1, entwickelt von der J3S GmbH" Dies ist der zweite Phase-1-Win — ein nahezu kostenloser Adapter- Reuse weil das Backend identisch ist. Anders als BW (#29), das eine eigene PARLISAdapter-Klasse brauchte, braucht HH gar keinen neuen Code. Phase 1 (2/3) aus Roadmap-Issue #49. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
db5a875d7c |
Activate Baden-Württemberg via PARLISAdapter (#29, Phase 1)
PARLIS auf parlis.landtag-bw.de läuft technisch auf demselben
eUI-Backend wie LSA-PADOKA und BE-PARDOK, hat aber drei wichtige
Unterschiede, die eine eigene Klasse statt einer PortalaAdapter-
Subklasse rechtfertigen:
1. Body-Schema: minimales lines mit l1/l2/l3/l4 (statt LSA/BE
2/3/4/10/11/20.x/90.x), serverrecordname=vorgang,
format=suchergebnis-vorgang-full, sort=SORT01/D SORT02/D SORT03,
keine parsed/json-Felder. Quelle: dokukratie/scrapers/portala.query.bw.json
plus HAR-Verifikation gegen die Live-Instanz.
2. Async polling: die initiale SearchAndDisplay-Antwort liefert nur
search_id mit status=running, KEINE report_id. Erst eine zweite
SearchAndDisplay-Anfrage mit id=<search_id> (ohne search-Component)
bekommt nach 1-3 Sekunden die report_id zurück. Reverse-engineered
aus esearch-ui.main.js requestReportOK() Z. ~1268.
3. Hit-Format: report.tt.html liefert Records als JSON-in-HTML-Comments
<!--{"WMV33":[...],"EWBV22":[...],...}-->. Komplett anderes Format
als LSA Perl-Dump oder BE HTML-Cards. Felder:
- EWBV22: "Drucksache 17/10323"
- EWBD05: direkter PDF-URL
- WMV33: Schlagworte (joined by ;)
- WMV30: Urheber-Kurzform
- EWBV23: "Antrag <Urheber> <DD.MM.YYYY>"
Smoke-Test (lokal):
BW q='': 8 hits in 17s, jüngste WP17-Anträge mit Datum + Fraktion
BW q='Schule': 8 hits, alle wirklich Schul-bezogen (Hochschule, Grundschule,
Schwimmunterricht, Lehrerbedarf etc.)
BW q='Klima': 8 hits, Klimaschutz/CO2/Energieberatung
get_document(17/10323): roundtrip funktioniert
bundeslaender.py: aktiv=True für BW; Anmerkung erweitert mit
PARLISAdapter-Verweis und drei-Unterschiede-Hinweis für künftige
Wartung. Test test_four_active_bundeslaender umbenannt zu
test_active_bundeslaender_include_phase_1_set, prüft jetzt nur
Subset-Bedingung statt exakter Count, damit Phase-1/2-Erweiterungen
keine Test-Updates brauchen.
Phase 1 (1/3) aus Roadmap-Issue #49.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|
|
f98e64c734 |
Add pytest suite + fix two regex bugs uncovered by it (#46)
Erste Tests für die Codebase. 77 Tests, 0.08s Laufzeit, decken die
drei Bug-Klassen aus der April-2026-Adapter-Session ab plus haben
schon zwei weitere Bugs in Production-Code aufgedeckt.
## Setup
- requirements-dev.txt mit pytest + pytest-asyncio
- pytest.ini mit asyncio_mode=auto
- tests/conftest.py stubbt fitz/bs4/openai/pydantic_settings, damit
die Suite ohne den vollen prod-requirements-Satz läuft (pure unit
tests, kein PDF-Parsing, kein HTTP)
## Tests
- tests/test_parlamente.py (33 Tests)
* PortalaAdapter._parse_hit_list_cards: doctype/doctype_full
NameError-Regression aus
|
||
|
|
5a30ce8bab |
bundeslaender.py: doku_system-Klassifikation für TH, HB, SN korrigiert (#48)
Beim Auswerten der dokukratie/*.yml-Configs (Annotationen in Issues #19-#30) sind drei Falsch-Einträge aufgefallen, die zu unnötigem Reverse-Engineering bei den Adapter-Implementierungen geführt hätten: - TH "StarWeb" → "ParlDok" dokukratie/th.yml zeigt parldok.thueringen.de/ParlDok/formalkriterien/ mit ParlDok-typischen Form-Feldern (DokumententypId, LegislaturpNum). Live-System ggf. ParlDok 8.x SPA wie MV — dann ParLDokAdapter direkt wiederverwendbar (Issue #25 Annotation). - HB "PARiS" → "StarWeb" PARiS ist nur eine StarWeb-Skin auf paris.bremische-buergerschaft.de /starweb/paris/servlet.starweb?path=paris/LISSH.web. Wiederverwendbar mit dem generischen StarWebAdapter aus Issue #27 (Template). - SN "ParlDok" → "Eigensystem" EDAS auf edas.landtag.sachsen.de basiert auf ASP.NET-Webforms mit __VIEWSTATE/__CALLBACKID-Postbacks (siehe dokukratie/sn.yml). Nicht ParlDok-kompatibel mit MV. Eigener Adapter notwendig. Anmerkungs-Texte erweitert mit Adapter-Wiederverwendungs-Hinweis und Verifikations-Schritt für Live-System-Versionen. Phase 0 aus Roadmap-Issue #49. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
b5ae8894d4 |
ParLDokAdapter: Volltext (#12) deaktivieren — einheitlich Title-Filter (#18)
Server-side facet_fulltext-Suche aus #12 war asymmetrisch zu BE/LSA (beide nur Title-Filter über 730d-Window aus #13). User-Entscheidung 2026-04-08: einheitliches Verhalten ist wichtiger als das beste Verhalten in 2 von 4 Adaptern. Konkrete Änderungen: - _build_search_body() schickt query nicht mehr server-side. Der query-Parameter bleibt in der Signatur als unused-mit-del, weil die Wieder-Aktivierung später ein Drop-in sein soll wenn die PortalaAdapter-Variante reverse-engineered wurde. - _initial_search() und _paginated_hits() ohne query-Parameter. - search() macht clientseitigen Title+Urheber-Filter wie der PortalaAdapter — same Codepfad, einheitliches Verhalten. - get_document() nutzt die unveränderte Pagination. - FACET_FULLTEXT-Konstante und _fulltext_id-Helper bleiben im Code als Dokumentation für die spätere Re-Aktivierung. Im Docstring ist die Tag-Form festgehalten. Folgen: - MV "Schule" ist von 20 (mit Volltext) auf 3 zurück (Title-Filter über die letzten 1000 Drucksachen). Gleiches Niveau wie BE/LSA pre-#13. - Browse-Mode (no query) ist unverändert: ~10 hits in ~25s, MAX_PAGES=10. - Wenn das später nicht reicht: #16 (UI-Split DB vs. Landtag) und ein optionaler "echter Volltext"-Toggle (#17 closed-as-deferred) bleiben als Folge-Optionen. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
9eda6f9f36 |
PortalaAdapter: quick-win bigger window + chunksize for BE/LSA (#13)
Real server-side fulltext search through the eUI sf-Index requires reverse-engineering the LSA/BE-specific search field (the obvious candidates VOLL, VOLL.main, WEV62 and bare-term-without-sf all return zero hits when probed). Without browser DevTools to capture a real fulltext request that's a multi-hour project — split out to remain in #13 as a follow-up. This commit ships the pragmatic interim fix from #11: - BE date_window_days: 180 → 730 Berlin had a tight default window because PARDOK has ~10x more documents than PADOKA. With the bigger window the client-side title/Urheber filter reaches back across most of WP19 instead of just the last six months. - chunksize logic in PortalaAdapter.search() inverted from "small when query, big when no query" to the opposite. The query-filtered path now pulls up to max(limit*10, 500) records per page so the title-filter has enough material; the unfiltered browse path stays at max(limit*2, 100). - httpx timeout 30s → 60s. LSA's report.tt.html occasionally takes 30+s on cold start; warm requests are <10s. Smoke test (local): BE Schule: 15 hits (was 0) LSA Schule: 14 hits (was N/A; same path) Live verification follows after deploy. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
6184bf8a88 |
ParLDokAdapter: server-side fulltext search via facet_fulltext (#12)
Replaces the client-side title/Urheber substring filter with a
real server-side full-text search through ParlDok's facet_fulltext
tag (type=0). The tag schema is reverse-engineered from
pd.addInput in the live bundle.js:
{"type": 0,
"id": <getFulltextId(term)>, # non-alphanum → "-"
"fulltext": <raw term>,
"label": <raw term>,
"field": "Alle"} # search all indexed fields
The Resultpage queryid inherits the fulltext filter, so
pagination works without re-sending the tag.
Smoke test (local):
Schule → 10 hits (was 3)
Klima → 10 hits across multiple parties + dates
Wohnen → 10 hits including older 2025 Anträge
The 10-page (1000-doc) safety bound still applies on top of the
fulltext-filtered result set, but since the server now narrows
to ~2k Schule-related docs WP-wide instead of the 8k+ raw WP
total, the bound is no longer the limiting factor for typical
queries.
Closes #12. BE/LSA equivalent (#13) is independent — eUI
sf-index names still need DevTools tracing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|
|
bc7f4a67cb |
Analyzer prompt: strict citation rule against LLM hallucination
Even after format_quotes_for_prompt was fixed to expose the fully-qualified programme name + page in each chunk, Qwen continued to hallucinate familiar source labels (typically "CDU Wahlprogramm NRW 2022") for parties whose actual MV/BE chunks were thematically off-topic for the Antrag at hand. The model preferred its training prior over the prompt context. Smoke test: MV Drucksache 8/6390 (CDU "Krisenmechanismus Kraftstoffpreise"). The CDU MV chunks the embedder retrieved were about Senioren and Aussenwirtschaft — not about energy prices — so qwen pulled what it knew about CDU NRW transport policy and wrote that as the source, even though the prompt listed only "CDU Mecklenburg-Vorpommern Wahlprogramm 2021" chunks. The new explicit ZITATEREGEL block in the user prompt forbids cross-Bundesland citations and instructs the model to leave zitate empty rather than fabricate when no listed chunk fits. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
1b5fd96e16 |
Embeddings prompt: include programme name in chunk citations
format_quotes_for_prompt previously rendered each retrieved chunk as just "S. X: text", giving the LLM no way to know which Bundesland or Wahlprogramm the passage came from. Result: even when the embedding search correctly returned MV-only chunks, the LLM hallucinated familiar source labels from its training set (typically "FDP NRW Wahlprogramm 2022, S. 75") because that was its strongest prior for budget/transparency policy citations. Fix: prepend the fully-qualified PROGRAMME[programm_id]["name"] to each quote and explicitly instruct the model to use these labels verbatim. Discovered while smoke-testing MV after indexing the new MV+BE programmes — embedding retrieval was clean (sim ~0.6 chunks all from fdp-mv-2021), only the prompt serialisation was lossy. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
8992cffc64 |
Add MV+BE Wahlprogramme zur jeweils laufenden Legislatur (#4, #10)
11 PDFs in app/static/referenzen/ + Einträge in WAHLPROGRAMME und embeddings.PROGRAMME für die beiden bisher nur per föderalem Grundsatzprogramm-Fallback abgedeckten Landtage: - **MV** (WP 8, seit 26.10.2021): CDU, SPD, GRÜNE, FDP, AfD, LINKE Wahlprogramme zur LTW 26.09.2021. Issue #4. - **BE** (WP 19, konstituiert nach Wiederholungswahl 12.02.2023): CDU, SPD, GRÜNE, LINKE, AfD Programme zur AGH-Wahl 26.09.2021. Die Wiederholungswahl 2023 nutzte dieselben Programme wie die Originalwahl, daher die "be-2023.pdf"-Benennung mit Programm- jahr 2021. Issue #10. Quellen: abgeordnetenwatch.de Mirror für 9 PDFs, library.fes.de für SPD MV, cdu-mv.de direkt für CDU MV, fdp-mv.de direkt für FDP MV. Alle PDFs verifiziert via pdftotext gegen das im Programm genannte Wahldatum, um zu vermeiden, dass aktuellere Wahlkampf-Entwürfe (z.B. das CDU "Berlin-Plan 2026") als Legislatur-Programm fehlinterpretiert werden. Indexierung in die embeddings-DB ist NICHT Teil dieses Commits — sie muss separat im prod-Container ausgeführt werden: docker exec gwoe-antragspruefer python -c " from app.embeddings import index_programm from pathlib import Path d = Path('/app/static/referenzen') for pid in ['cdu-mv-2021','spd-mv-2021','gruene-mv-2021', 'fdp-mv-2021','afd-mv-2021','linke-mv-2021', 'cdu-be-2023','spd-be-2023','gruene-be-2023', 'linke-be-2023','afd-be-2023']: index_programm(pid, d) " Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
2b9c0b2908 |
Activate Mecklenburg-Vorpommern (ParlDok) — search-only MVP (#4)
Adds a new ParLDokAdapter for ParlDok 8.x parliament documentation
systems by J3S GmbH. MV becomes the fourth supported state alongside
NRW, LSA and BE.
Notable details:
- ParlDok 8.x is a single-page app whose backend is a JSON API rooted
at {base}/parldok/Fulltext/{Search,Resultpage}. The legacy ParLDok
5.x HTML POST form (parldok/formalkriterien) used by dokukratie's
mv.yml has been deprecated by the LandtagMV upgrade to 8.3.5 and
is no longer reachable via the old form fields — hence a new
adapter rather than reusing the dokukratie scraper.
- Two-stage pagination: Fulltext/Search returns the first 100 hits
+ a queryid; further pages come from Fulltext/Resultpage with
{queryid, limit:{Start,Length}}. The Search endpoint silently
ignores any non-zero Start, so single-stage offset pagination is
not an option.
- Server-side filter via facet_lp (type=10) on the configured WP;
type=Antrag is filtered client-side because the facet_type value
IDs are instance-specific and would require an extra
Fulltext/Filter discovery call. ParlDok also returns the same
Drucksache multiple times when it appears in several
Vorgänge/Beratungen, so search() dedupes by lp/number.
- Wahlprogramme zur LTW 26.09.2021 are not yet indexed (follow-up
in #4) — analyses run with the federal Grundsatzprogramm fallback,
same as Berlin until #10 lands.
Drive-by cleanup of PortalaAdapter print() statements: switched to
the module-level logger so adapter parser bugs no longer disappear
into stdout.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|
|
1cb030aab7 |
Fix NameError in PortalaAdapter card parser
_parse_hit_list_cards referenced an undefined `doctype` instead of `doctype_full` on the query-filter path. The surrounding try/except in search() swallowed the exception, so Berlin queries silently returned 0 hits whenever a search term was given. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
9e0f11f7c9 |
Activate Berlin (PARDOK) — search-only MVP (#3)
PortalaAdapter is now parameterizable and serves both LSA and Berlin
from a single class. Berlin is activated as the third live bundesland
(after NRW + LSA), with the deliberate caveat that the LTW 2023
Wahlprogramme are not yet indexed.
PortalaAdapter refactor
- Class attributes (bundesland, name, base_url, db_id, wahlperiode)
moved into the constructor. New optional parameters:
- portala_path: "/portal" for LSA, "/portala" for Berlin
- document_type: "Antrag" for LSA, None for Berlin (BE's ETYPF
index uses different value strings; the document_type subtree
is dropped from the action.search.json tree)
- pdf_url_prefix: "/files/" by default; absolute URLs in the hit
list are passed through unchanged (Berlin embeds full
starweb/adis/citat/... links)
- date_window_days: 730 for LSA, 180 for BE (BE has ~10x more
documents per WP, narrower window keeps payloads bounded)
- _build_search_body builds the JSON tree dynamically: when
document_type is None, the entire ETYPF/DTYPF/DART subtree is
omitted, mirrored in the parsed/sref display strings as well.
- _parse_hit_list_html now auto-detects between two formats:
1. LSA-style: <pre>$VAR1 = …</pre> Perl Data::Dumper records
(existing parser, untouched).
2. Berlin-style: production HTML cards with efxRecordRepeater
divs, h3 titles, h6 metadata lines containing the document
type, drucksachen-id and date, plus a direct <a href="…pdf">
to the PDF on the same host.
- Berlin extracts originator parties from the h6 line ("Antrag CDU,
SPD" → ["CDU","SPD"], typ "Antrag") via the new word-boundary
_normalize_fraktion regex.
- _normalize_fraktion rewritten with regex word boundaries, fixing a
long-standing bug where comma-separated fraction lists like
"CDU, SPD" failed to match CDU. Also picks up BSW for the
Brombeer/SPD-BSW landtage and "Senat von Berlin" as Landesregierung.
bundeslaender.py
- BE flipped to aktiv=True. anmerkung documents the Wahlprogramm-
Lücke and the auto-detected hit-list format.
Live verified against pardok.parlament-berlin.de:
- WP 19 with 180-day date window returns 2962 hits, page 1 contains
5 records all with title, drucksache, date, PDF URL.
- 19/3107 ("Kleingewässerprogramm") correctly extracted as Antrag of
CDU+SPD; 19/3104-3106 as Vorlagen zur Beschlussfassung; 19/3108 as
Vorlage zur Kenntnisnahme.
- LSA still returns the same 5 current Anträge of März 2026 — no
regression from the refactor.
Known limitation (will be tracked as a follow-up issue)
- Berlin Wahlprogramme zur LTW 2023 are not yet indexed in the
embeddings DB. The 2023 PDFs are no longer linked from the live
party websites (which currently feature 2026 draft programmes), and
Wayback has no snapshots. The analyzer therefore falls back to
bundesländer-übergreifende Grundsatzprogramme for BE Anträge until
the 2023 PDFs are sourced manually.
Refs #3.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
||
|
|
80e16df288 |
Append original Antrag-PDF to GWÖ-Report (#9)
Extends generate_pdf_report() with a best-effort second stage that
appends the original Antrag PDF to the freshly rendered GWÖ-Report so
the analysis and its source document live in the same file.
Pipeline
1. WeasyPrint renders the report PDF as before.
2. _append_original_antrag() then:
- Skips silently if assessment.link is empty or non-HTTP (manual
uploads / pasted text leave nothing to fetch).
- Downloads the original PDF via httpx (30s timeout, follow redirects,
custom user agent).
- Validates the response is actually a PDF (Content-Length not relied
on; the magic bytes %PDF- are checked).
- Adds a single A4 separator page that says "Original-Antrag",
repeats the Drucksachen-ID and title, and either confirms the
append or shows the failure reason (HTTP code, network error,
parse error) plus the source URL.
- Appends the downloaded PDF via PyMuPDF doc.insert_pdf().
- Saves to a sibling .tmp file and atomically replaces the original
(PyMuPDF refuses non-incremental save into the same file).
Edge cases handled
- No link / pasted-text upload → no append, no divider, original report
unchanged.
- Download error / 404 / non-PDF response → divider page with explicit
error message and source URL, report still ships.
- PDF parse error → divider page without appended content, error logged.
- Hard failure during save → fall back to the original WeasyPrint PDF.
Verified live in production container against drucksache 8/6645
(Untrending Frauenhass, BÜNDNIS 90/DIE GRÜNEN LSA):
- Report 4 pages + 1 divider + 3 pages original = 8 pages total
- Divider correctly placed at index 4
- Page 5 starts with "(Ausgegeben am 24.02.2026) … Drucksache 8/6645 …
Antrag — Fraktion BÜNDNIS 90/DIE GRÜNEN — Untrending Frauenhass …"
- Negative test with a synthetic 404 link: 5 pages total, divider at
index 4 with "Original-PDF konnte nicht angehängt werden. Grund: HTTP
404".
Resolves #9.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
||
|
|
f1867d463c |
Bundesland filter & transparency: stringent split + visible source (#8)
Brings the Bundesland-Dropdown from a cosmetic header widget to a real filter that propagates through every layer (Listing, internal search, statistics, party/tag filters, upload mode), and at the same time makes the source parliament visible in every place where assessments from multiple bundesländer can be mixed. Backend - database.get_all_assessments(bundesland=None) — new optional filter, "ALL" treated as None. - database.search_assessments — bug fix: previous `if bundesland:` branch incorrectly added a `WHERE bundesland='ALL'` clause; now guarded with `bundesland and bundesland != "ALL"`. - main.list_assessments — accepts ?bundesland= query param, includes the bundesland field in the response so the frontend can render badges. - main.get_single_assessment — also includes bundesland in the response so the detail header can show the source parlament. - main.search_landtag — early HTTP 400 when bundesland is missing or "ALL"; the live Landtag adapter cannot serve a synthetic Bundesweit request. - main.index() and main.list_bundeslaender — synthetic "🌍 Bundesweit" entry prepended to the bundesländer list (kept out of bundeslaender.py on purpose — ALL is not a real state). Both endpoints additionally expose a parlament_names map so the frontend can render the source parliament without an extra round-trip. Report (PDF + HTML) - generate_html_report / generate_pdf_report — new optional bundesland parameter. When set, the report header carries the parliament name ("Landtag von Sachsen-Anhalt", "Landtag Nordrhein-Westfalen", …) beside the title. Three call sites updated: run_analysis, run_drucksache_analysis, download_assessment_pdf. Frontend (templates/index.html) - Header dropdown gets the synthetic ALL entry as first option; initial currentBundesland is now 'ALL' (was 'NRW'). - localStorage persistence: changeBundesland writes, DOMContentLoaded reads and validates against the visible options. - changeBundesland resets the score / party / tag filter state, syncs the upload-mode bundesland select, disables the Landtag-Suche button + tooltip when ALL, and toggles a data-mode attribute on .list-content (used by CSS to show/hide the per-item bundesland badge). - loadAssessments now sends ?bundesland=… so the API does the actual filtering. updateStats renders an additional per-bundesland average block (Ø NRW: x · Ø LSA: y) when in ALL mode and the loaded list spans more than one bundesland. - renderList prepends a small "bl-badge" beside the Drucksachen-Nummer. Hidden in single-bundesland mode via CSS selector to avoid clutter. - showDetail header now shows the parliament name as its own line (.detail-parlament). - searchLandtag has an early-out alert if currentBundesland === 'ALL', saving a network round-trip. - Upload-Mode bundesland select now starts with a "— Bundesland wählen —" placeholder (no auto-default), and startAnalysis validates that a concrete bundesland was chosen. CSS - .bl-badge plus the .list-content[data-mode="single"] hide rule. - .detail-parlament for the detail header line. - .header-parlament for the PDF report header line. Resolves #8. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
87874a7a14 |
Activate LSA: Wahlprogramme + ingest + frontend (#2)
Brings Sachsen-Anhalt online as the second supported Bundesland after
NRW. Closes the gap that issue #2 left open: with the PortalaAdapter
already in place from
|
||
|
|
c7242f8413 |
Add PortalaAdapter for PADOKA / Sachsen-Anhalt (#2)
Adds a clean-room PortalaAdapter that talks to the eUI/portala framework behind PADOKA (Landtag Sachsen-Anhalt). Same engine powers Berlin's PARDOK; the same adapter will serve issue #3 once activated for BE. Reverse-engineering notes - The "PADOKA = StarWeb" assumption from issue #1 / dokukratie's st.yml is outdated. The Sachsen-Anhalt portal was migrated to the same eUI/portala SPA framework Berlin uses. The legacy starweb URL returns 503; the new entry point is /portal/browse.tt.html. - Search workflow is two-stage: 1. POST /portal/browse.tt.json with a JSON action body containing an Elasticsearch-style query tree under search.json. Returns a report_id plus hit count. 2. POST /portal/report.tt.html with {report_id, start, chunksize} returns the HTML hit list. Each record carries a Perl Data::Dumper block in a <pre> tag with the canonical metadata. - The query schema (sources, search.lines, search.json tree, report block) is taken from dokukratie/scrapers/portala.query.json (GPL-3.0) — only structure/selectors are reused, no Python code is ported. - DB id is "lsa.lissh"; the server validates this and rejects unknown interfaces with an explicit errormsg. - PDFs live under /files/drs/wp{N}/drs/d{nr}{xxx}.pdf and are served directly without any session cookie. What the adapter does - search() builds a date-window query (last ~24 months) for "Antrag" document type and returns the most recent hits. The user's free-text query is applied as a client-side title/Urheber filter (no fulltext search server-side yet — see "Limitations" below). - Hits are parsed from the Perl record dumps in the report HTML: - WEV06.main → title (Perl \x{xx} hex escapes decoded) - WEV32.5 → relative PDF path - WEV32.main → "Antrag <Urheber> <DD.MM.YYYY> Drucksache <b>X/YYYY</b>" - Fraktion strings are normalised to canonical codes (CDU, SPD, GRÜNE, FDP, AfD, LINKE, Landesregierung). - get_document() looks up a single Drucksache by re-running the search. - download_text() fetches the PDF and extracts text via PyMuPDF. - bundeslaender.py: LSA's doku_system corrected from "StarWeb" to "PARDOK", anmerkung updated with the migration story. Limitations (deliberate, MVP) - No server-side full-text search. The portala framework's sf index names for LSA full-text content are not yet known; tree mutations with sf=alAB return 0 hits. Client-side filter is "good enough" for the next ~24 months of Anträge (≈few hundred per WP). - LSA is still aktiv=False in bundeslaender.py — the adapter is dormant in production until issue #2's wahlprogramm ingest and frontend activation land. Verified live against padoka.landtag.sachsen-anhalt.de: - search(query="", limit=5) returned 5 current Anträge from März 2026 (LINKE + GRÜNE) with correct dates, fractions, titles and PDF URLs. - download_text("8/6790") returned 5051 chars of real Antragstext ("ICE-Halt für Salzwedel dauerhaft erhalten"). Refs #2. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
ee0218b5af |
Refactor wahlprogramme/embeddings/analyzer for multi-state (#5)
Atomic refactor of the three modules that previously hardcoded NRW
behaviour. After this commit, every analysis path consults the central
BUNDESLAENDER registry for governing fractions, parliament name, and
state metadata.
wahlprogramme.py
- WAHLPROGRAMME is now nested {bundesland: {partei: meta}}; NRW data
hoisted unchanged under the "NRW" key.
- New WAHLPROGRAMM_KONTEXT_FILES dict maps a state to its overview
markdown file (currently only NRW).
- find_relevant_quotes(text, fraktionen, bundesland) — bundesland is
now a required positional. Governing fractions for the requested
state are merged with the submitting fractions before lookup.
- Helpers get_wahlprogramm() and parteien_mit_wahlprogramm() expose
the new shape to other modules.
- ValueError on unknown bundesland (no silent fallback).
embeddings.py
- Schema migration in init_embeddings_db: adds a `bundesland` column
to the chunks table when missing, plus an index, and backfills
existing rows from the PROGRAMME registry. Grundsatzprogramme
(federal level) keep bundesland NULL by design.
- find_relevant_chunks accepts a bundesland filter that matches state
rows OR NULL — so federal Grundsatzprogramme remain visible to every
analysis.
- get_relevant_quotes_for_antrag(text, fraktionen, bundesland, …) —
bundesland required, governing fractions read from BUNDESLAENDER
instead of hardcoded ["CDU","GRÜNE"]. Order-preserving dedup
replaces the previous set-based merge.
- index_programm now writes the bundesland column on insert.
- Dropped the hardcoded "Wahlprogramm NRW 2022" label in
format_quotes_for_prompt — bundesland context is implicit in the
surrounding prompt block.
analyzer.py
- get_bundesland_context reads parlament_name, regierungsfraktionen,
landtagsfraktionen and the optional WAHLPROGRAMM_KONTEXT_FILES entry
from the central registry. Throws ValueError on unknown OR inactive
bundesland — kills the silent NRW fallback that previously masked
configuration gaps.
- The Antragsteller-detection heuristic now iterates
BUNDESLAENDER[bundesland].landtagsfraktionen instead of
WAHLPROGRAMME.keys(), so we recognise parties for which we don't
yet have a Wahlprogramm PDF.
- Both quote lookups (semantic + keyword fallback) now receive the
bundesland.
Resolves issue #5. Foundation for #2 (LSA), #3 (Berlin), #4 (MV).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
||
|
|
ac18743ff2 |
Add central bundeslaender.py module with all 16 states (#7)
Introduces app/bundeslaender.py as the single source of truth for all bundesland-specific data (parliament name, current legislative period, upcoming elections, governing coalition, doku system, base URLs, drucksache format, dokukratie scraper code, active flag, optional remarks). Data reflects April 2026 state. main.py::index() and /api/bundeslaender now derive their lists from this module instead of hardcoding. Frontend dropdown now shows all 16 bundesländer (15 disabled with "(bald)" suffix); previously the landing template showed only 4. NRW remains the only "aktiv" entry. API behaviour change worth noting: the /api/bundeslaender endpoint previously emitted code "ST" for Sachsen-Anhalt; it now emits "LSA" to match the politically dominant abbreviation. No functional impact because non-NRW bundesländer were inactive in both versions. Foundation for #5 and #2; deliberately a no-op for NRW so it can ship and rollback independently. Resolves issue #7. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
bcd532be89 |
Fix responsive layout for mobile viewports (#6)
Body becomes a flex column so the header takes its natural height and the main container fills the rest via flex:1 — replaces the brittle calc(100vh - 70px) that assumed a 70px header and broke as soon as the header wrapped on mobile. Adds 100dvh fallback for iOS Safari address bar quirks. Mobile breakpoint (≤900px) reworked: list scrolls internally via list-content max-height:50vh, detail-panel uses overflow:visible so the whole document scrolls naturally instead of nesting scrollers. Tapping an item auto-scrolls to the detail panel and a new "← Zur Liste" button (mobile-only) jumps back. Adds a tighter ≤600px breakpoint that hides the subtitle, collapses the matrix grid to one column and shrinks the matrix table for phone screens. Resolves issue #6. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
63de3ca20d |
Initial commit: GWÖ-Antragsprüfer v1.0
Features: - GWÖ-Matrix 2.0 Analyse für NRW-Landtagsanträge - Verbesserungsvorschläge im Redline-Format (Original/Vorschlag/Begründung) - Wahlprogramm- und Parteiprogrammtreue-Bewertung - Landtag-Suche via OPAL-API - Tag-Wolke mit Multi-Select Filter - Partei-Filter mit Durchschnittswerten - PDF-Report-Generierung - Security Headers (CSP, X-Frame-Options, etc.) - Persistente SQLite-DB via Docker Volumes Tech Stack: - FastAPI + Jinja2 - Qwen LLM via DashScope API - SQLite + aiosqlite - WeasyPrint für PDF - Docker Compose mit Traefik |