db3ada9328
17 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
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> |
||
|
|
15b9af8795 |
Sub-B: NRW Sample 10/5376 — F.D.P.+CDU CO2-Minderungsprogramm 1990
Letzter offener Sub-B-Sample-Slot. NRW liefert ein historisches WP10-
Sample (28.03.1990, F.D.P.+CDU-Entschließung zum NRW-CO2-Minderungs-
programm) — interessant für die GWÖ-Bilanzierung als Beleg, dass
Klimaschutz seit 35 Jahren auf dem Tisch liegt.
NRWAdapter.get_document() konstruiert die PDF-URL deterministisch über
das MMD{wp}-{nummer}.pdf-Schema, das auch für historische Wahlperioden
funktioniert (HEAD 200 verifiziert). Die Title/Fraktionen/Datum-Felder
bleiben für historische WPs leer, weil der Adapter sie aus der OPAL-
Suche nicht extrahiert (die nur die aktuelle WP18 indexiert). Der
Sample-Eintrag prüft daher nur existence + URL-Schema, beides wird vom
Sub-B-Test honoriert (leere Felder werden geskipped).
Sub-B im Container: 10/10 grün (vorher 9/9 mit NRW als skip).
Refs: #52, #59 (Sub-B Live-Verifikation)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|
|
6ac330241a |
Sub-B: BE Sample auf 19/2606 (Menstruation, GRÜNE) — zuverlässig im Top-Result-Window
19/2650 ist eine echte BE-Drucksache (GRÜNE A100-Antrag) aber außerhalb des Top-Result-Windows von BE PARDOK — der Server-side ETYPF-Filter ist bei BE deaktiviert (document_type=None) und der client-side Filter verwirft die meisten Schriftlichen Anfragen, sodass die Pagination der verbleibenden Anträge nicht zuverlässig zu 19/2650 reicht. 19/2606 ist die Top-3-Antrag-Drucksache aus aktueller search() — als GRÜNE-Antrag mit Title 'Menstruation enttabuisieren' deutlich identifizierbar und im Window stabil. Refs: #61 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
a3a9052dec |
Sub-B Ground-Truth: TH und BE auf neuere Drucksachen umgestellt (#61)
TH 8/1594 wurde durch den TH-Adapter-Patch in #61 ausgefiltert (kein PDF freigegeben). Sample auf 8/3133 (Notfallversorgung, datum 2026-03-18, AfD) aktualisiert — die hat einen freigegebenen PDF-Link. BE 19/3107 ist außerhalb des 200-result-Windows von PortalaAdapter.get_document gewandert. Sample auf 19/2650 (A100, datum 2025-09-09, GRÜNE) aktualisiert. Refs: #61 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
6ebd7aac7a |
Sub-B Ground-Truth: BW URL-Encoding + RP URL-Schema-Drift
Live-Run von Sub-Issue B im Container hat zwei Test-False-Positives in ground_truth.py aufgedeckt, die nichts mit Adapter-Bugs zu tun haben: - BW: PDF-URL kodiert den Underscore als %5F (`17%5F10323.pdf`), nicht als nacktes `_`. pdf_url_substring auf `17%5f10323` aktualisiert. - RP: PDFs werden von `dokumente.landtag.rlp.de` ausgeliefert (nicht von `opal.rlp.de` — das ist nur das Suchfrontend). Substring auf die Drucksachen-Nummer im Pfad (`11250-18`) umgestellt — robust gegen weiteren URL-Schema-Drift. 176 Unit-Tests bleiben grün. Refs: #52, #59 (Sub-B Live-Verifikation) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
b76c08d92e |
Sub-D Citation-Test: PDF-Bindestrich + Token-Resolver + Anker-Match
Erster Live-Run von Sub-Issue D gegen die Prod-DB im Container hat 15 von
39 Citation-Tests fehlschlagen lassen. Detail-Analyse: 12 davon waren
Test-False-Positives (zwei Schichten von Brittleness im Test selbst), 3
sind echte LLM-Halluzinationen.
Drei Härtungen am Test-Resolver, damit er nur noch echte Halluzinationen
fängt:
1. **PDF-Bindestrich-Bridging in `_normalize`**:
PyMuPDF zerlegt Wörter über Zeilenumbrüche mit `-\n`. Nach unserer
Whitespace-Normalisierung wird daraus `- `, sodass aus
"Investitionsoffensive" im LLM-Snippet das PDF "investiti- onsoffensive"
gegenübersteht. Neue Regex `_RE_HYPHEN_BREAK` bridged das in einem
Konvergenz-Loop, damit auch mehrere aufeinanderfolgende Wort-Wraps
sauber verschmelzen.
2. **Token-Coverage-Resolver in `_resolve_quelle_to_programm_id`**:
Zwei-stufig — erst die alte strict-substring-Strategie (deckt
Adapter-konformes LLM-Output), dann ein Token-Coverage-Fallback. Der
zerlegt jeden PROGRAMME-Namen in (Partei + Bundesland + Jahr) mit
Aliasen (GRÜNE/Bündnis 90, LSA/Sachsen-Anhalt, …) und akzeptiert
eine Quelle, wenn alle drei Tokens in irgendeiner Reihenfolge in der
Quelle vorkommen. Fängt damit z.B. "Landtagswahlprogramm 2021 BÜNDNIS
90/DIE GRÜNEN Sachsen-Anhalt" → `gruene-lsa-2021`, ohne dass die LLM
den exakten Adapter-Label-Wortlaut treffen muss.
3. **Anker-Match-Fallback in `_is_substring`**:
Ein 200-Zeichen-Snippet, das nur in einem Wort kürzt, scheitert sonst
am Volltext-Substring-Check. Neuer Anker-Match zerlegt den Snippet
in 5-Wort-Sequenzen und akzeptiert, wenn mindestens eine wortwörtlich
im Seitentext steht. Erfundene Snippets haben keine 5-Wort-Sequenz,
die wortwörtlich im PDF steht — die false-negative-Rate für echte
Halluzinationen bleibt damit bei 0.
Live-Run nach dem Patch: **15 → 3 Failures** (39 Cases, 24 → 36 grüne).
Die verbleibenden 3 sind echte LLM-Bugs:
- 18/9605 NRW GRÜNE S.58 ('Wahlalter auf 16/14 absenken') — Snippet
und PDF-Seite zeigen komplett andere Themen, das LLM hat die Seite
oder den Snippet erfunden
- 18/18100 NRW B90/Grüne S.36 (Grundsatzprogramm 2020, Plattform-
Regulierung)
- 8/6645 LSA SPD S.37 ('Wir Sozialdemokratinnen ächten ...') — PDF
S.37 enthält dort Zweitstudiengebühren-Text
Diese drei werden als separates LLM-Bug-Issue erfasst.
13 Helper-Unit-Tests bleiben grün.
Refs: #54, #59 (Sub-D Live-Verifikation)
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> |
||
|
|
73a7f76472 |
Add E2E functional acceptance test suite (#50, #51, #52, #53, #54)
Vier Sub-Issues unter Umbrella #50 — opt-in via 'pytest -m integration', Default-Suite (77 Unit-Tests) bleibt unberührt. - Sub-Issue A (#51): test_adapters_live.py — pro aktivem BL Reachability, Drucksache-ID-Format, Type-Filter, Datum-/Fraktion-Plausibilität, PDF-Link-HEAD-Probe (slow). NI als xfail (Login-Wall). - Sub-Issue B (#52): test_frontend_xref.py + ground_truth.py — pro BL ein manuell kuratiertes Frontend-Sample (Drucksache + Title-Substring + Fraktionen + Datum + PDF-URL), gegen das adapter.get_document() gespiegelt wird. Fängt Bug-Klasse 14 (Cross-Bundesland-Match). - Sub-Issue C (#53): test_wahlprogramme_indexed.py — Indexing-Status pro aktivem BL aus embeddings.db, PDF-Inhalts-Plausibilität (14 Marker + Wahlperioden-Horizont), expliziter Anti-Marker für Bug-Klasse 8 (CDU-BE 2021 vs 2026 PDF-Tausch durch abgeordnetenwatch). - Sub-Issue D (#54): test_citations_substring.py — Property-Verification: jedes vom LLM zitierte Snippet muss als (whitespace-normalisierter) Substring auf der angegebenen PDF-Seite vorhanden sein. Strict-Match mit Truncation-Marker-Toleranz, kein Fuzzy. Liest reale Assessments aus gwoe-antraege.db. Fängt Bug-Klassen 7/10/17 (Halluzination). Architektur: separates tests/integration/ Verzeichnis mit eigenem conftest.py, das die Stubs der Unit-Suite (fitz/bs4/openai/pydantic_settings) gezielt entfernt und auf echte Module umstellt — mit Fallback-Skip via pytest.require_module wenn lokale Dev-Maschine die Prod-Deps nicht hat. 206 neue Integration-Tests, 13 Helper-Unit-Tests. 77 Unit-Tests bleiben grün. 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
|