Commit Graph

80 Commits

Author SHA1 Message Date
Dotty Dotter
f56c2af5cd Fix: Auth-Callback setzt Cookie via HTML-Response statt RedirectResponse 2026-04-10 21:27:32 +02:00
Dotty Dotter
0d0c06106a Auth-UI: Logout-Button + Re-Analyze-Feedback + Uhrzeit beim Bewertungsdatum 2026-04-10 21:24:07 +02:00
Dotty Dotter
9195d976bc Fix: httpx import in auth callback 2026-04-10 21:19:31 +02:00
Dotty Dotter
c3bcf1501d Auth: OIDC Code→Token Exchange Callback + Cookie-basiertes Login 2026-04-10 21:18:10 +02:00
Dotty Dotter
4c8b180383 Fix: Keycloak redirect_uri http→https (Traefik TLS-Termination) 2026-04-10 21:16:15 +02:00
Dotty Dotter
f728388286 #97 Neu bewerten: manueller Re-Analyse-Button + Bewertungsdatum
Fußzeile unter jedem Assessment-Detail jetzt mit:
- Bewertungsdatum ("Bewertet am DD.MM.YYYY") aus updated_at
- Quelle + Modell (batch-reanalyze / webapp, qwen-plus)
- "Neu bewerten"-Button (Auth-pflichtig, ausgegraut ohne Login)

Flow: Klick → DELETE /api/assessment/delete → POST /api/analyze-drucksache
→ Queue → pollAnalysis → Detail neu laden

Neuer DELETE-Endpoint /api/assessment/delete mit require_auth.

API-Response erweitert um updatedAt, source, model für beide
Endpoints (list + single assessment).

Tests: 206 passed.

Refs: #97
2026-04-10 21:10:33 +02:00
Dotty Dotter
790fe1a121 CDU Grundsatzprogramm: korruptes 2007er ersetzt durch echtes 2024er (82 Seiten) 2026-04-10 20:25:56 +02:00
Dotty Dotter
660498e8e3 LINKE Bremen (78p via Wayback) + CDU Hessen Langfassung (164p) + AfD SL registriert 2026-04-10 20:22:50 +02:00
Dotty Dotter
78f3e4e9f0 Wahlprogramme HB/HE/SN + AfD SL: 15 neue Programme registriert
Bremen WP 21 (2023): SPD, CDU, GRÜNE — 3 PDFs
  (AfD Bremen + LINKE Bremen nicht als PDF downloadbar)
Hessen WP 21 (2023): CDU, AfD, SPD, GRÜNE, FDP — 5 PDFs
Sachsen WP 8 (2024): CDU, AfD, BSW, SPD, LINKE, GRÜNE — 6 PDFs
Saarland: AfD SL 2022 ("Heimat ist wählbar") — aus real3d-flipbook
  extrahiert (pdfUrl in data-flipbook-options). 102 Seiten.

Total: 84 Programme registriert. Indexierung erfolgt nach Deploy.
2026-04-10 20:14:22 +02:00
Dotty Dotter
3b6ecacc1e Tuning: min_similarity 0.45→0.35 + Anker 5→4 Wörter — mehr Chunks + weniger Drops 2026-04-10 20:06:35 +02:00
Dotty Dotter
14140571d8 Fix: CDU-PDF AssertionError Fallback + Kopfzeile vereinheitlicht + Fehler-Debug 2026-04-10 20:05:28 +02:00
Dotty Dotter
916b0ca643 Debug: JS-Fehler anzeigen + docker-compose version entfernt 2026-04-10 19:55:08 +02:00
Dotty Dotter
d75e9441a3 Quellen-Seite: Programme nach Bundesland gruppiert statt einer langen Liste 2026-04-10 19:10:18 +02:00
Dotty Dotter
ee08cb0c29 Quellen-Seite: PDF-Thumbnails der ersten Seite + Thumbnail-API-Endpoint 2026-04-10 18:40:13 +02:00
Dotty Dotter
11e4da0bf3 Wahlprogramme BY/NI/SL: 11 PDFs registriert + Linke-Grundsatzprogramm
Bayern WP 19 (2023): CSU, GRÜNE, FW, AfD, SPD — 5 PDFs
Niedersachsen WP 19 (2022): SPD, CDU, GRÜNE, AfD — 4 PDFs
Saarland WP 17 (2022): SPD, CDU — 2 PDFs (AfD SL nicht auffindbar)

Plus: DIE LINKE Erfurter Programm 2011 (111 Chunks indexiert)
Plus: AfD Grundsatzprogramm 2016 (128 Chunks, vorheriger Commit)

Alle PDFs verifiziert: korrekte Seitenzahlen, keine HTML-Wrapper,
Parteiname und Wahljahr im Titel korrekt. Quellen: offizielle
Partei-Websites, Wayback Machine, originalsozial.de.

Indexierung erfolgt nach Deploy im Container.
2026-04-10 18:27:38 +02:00
Dotty Dotter
1f53ca5a25 #63: Linke Erfurter Programm 2011 + AfD registriert — alle 6 Grundsatzprogramme komplett 2026-04-10 18:23:20 +02:00
Dotty Dotter
b6160cc6cb #31/#34/#35: BY, NI, SL auf aktiv=True — alle 17 Parlamente jetzt im UI 2026-04-10 17:43:32 +02:00
Dotty Dotter
521d940611 #22 NI: Deduplizierung (Server liefert manche Treffer doppelt) 2026-04-10 17:40:46 +02:00
Dotty Dotter
edcb4e9c76 #22 NI-Adapter: PortalaAdapter mit JSON-in-Comment-Parsing
Niedersachsen (NILAS) nutzt denselben portala/eUI-Stack wie LSA/BE/BB/RP,
aber mit einem dritten Hit-Format: JSON-Objekte in HTML-Kommentaren
(statt Perl-Dumps oder HTML-Card-Elements). Reverse-engineered aus
HAR-Capture www.nilas.niedersachsen.de.har.

Neuer dritter Parsing-Pfad in PortalaAdapter._parse_hit_list_html:
Auto-Detection via "<!-- {" + "WEV" im HTML → _parse_hit_list_json_comments.

Feld-Mapping (NI JSON-in-Comment):
- WEV01[0].main → Titel
- WEV03[0].main → Typ
- WEV05[0].main → Metadata (Urheber + DD.MM.YYYY + "Drucksache XX/YYYY")
- WEV05[0].1 oder WEV08[0].1 → PDF-URL

ADAPTERS-Eintrag:
- bundesland="NI", db_id="lns.lissh", wahlperiode=19,
  portala_path="/portala", document_type="Antrag"

Tests: 201 passed.

Refs: #22, #34 (UI-Aktivierung folgt separat)
2026-04-10 17:39:18 +02:00
Dotty Dotter
4565a5cf0c #63 teilweise: AfD-Grundsatzprogramm 2016 registriert + PDF (96 Seiten, via Wayback Machine) 2026-04-10 17:30:28 +02:00
Dotty Dotter
6a433e9217 #44 Batch-Analyse: POST /api/batch-analyze
Neuer Endpoint der die neuesten ungeprüften Drucksachen eines BL
automatisch sucht, herunterlädt und in die Queue (#95) einreiht:

POST /api/batch-analyze
  bundesland=NRW  (Pflicht)
  limit=10        (1-100, default 10)

Flow:
1. adapter.search("", limit=limit*3) holt neueste Drucksachen
2. Pro Drucksache: check ob schon bewertet → skip
3. download_text → enqueue(run_drucksache_analysis)
4. Queue verarbeitet seriell mit 10s Pause (DashScope-freundlich)

Response:
{
  "status": "batch_enqueued",
  "enqueued": 7,
  "skipped_existing": 3,
  "jobs": [{"drucksache": "18/...", "title": "...", "queue_position": 1}, ...]
}

Rate-limited auf 3/min. Erfordert Auth (#43).
Bei voller Queue: enqueued nur soweit Platz, kein Error.

Tests: 201 passed.

Refs: #44, #95 (Queue-Basis)
2026-04-10 17:26:05 +02:00
Dotty Dotter
289d37a84b #95 Job-Queue: SQLite-backed asyncio Worker mit Backpressure
FIFO-Queue für Analyse-Jobs — ersetzt FastAPI BackgroundTasks:

app/queue.py:
- asyncio.Queue mit MAX_QUEUE_SIZE=50
- Einzelner Worker-Coroutine (Concurrency=1, DashScope-freundlich)
- MIN_PAUSE_SECONDS=10 zwischen Jobs
- Exponentielles Backoff bei Serien-Fehlern (15s → 5min)
- get_queue_status() für den Status-Endpoint
- QueueFullError → HTTP 429 + Retry-After Header
- start_worker() als FastAPI-Startup-Task
- re_enqueue_pending() markiert Crash-Überlebende als 'stale'

main.py:
- POST /api/analyze-drucksache nutzt queue.enqueue() statt
  background_tasks.add_task()
- Response enthält queue_position
- GET /api/queue/status zeigt pending, max_size, processed,
  estimated_wait_seconds, worker_running
- Worker wird bei app.startup() gestartet

Tests: 201 passed, 5 skipped.

Refs: #95, #44 (Batch baut auf Queue auf)
2026-04-10 17:24:34 +02:00
Dotty Dotter
1a82f8294c #57 Security: print() → logger.exception für alle Module
Befund #4 aus dem Security-Audit (PII/LLM-Content im Container-Log):
Die letzten 10 print()-Aufrufe in app/{report,embeddings,parlamente}.py
durch strukturiertes Logging (logger.warning/exception/info) ersetzt.

Betroffen:
- report.py: 2× print in _append_original_antrag → logger.exception
- embeddings.py: 3× print in index_programm → logger.warning/info/exception
- parlamente.py: 5× print in NRWAdapter → logger.error/exception

logger.exception statt print+traceback: Stack-Trace wird automatisch
angehängt, ohne den LLM-Content oder Antrags-Details als Volltext zu
leaken (nur die Drucksache-ID als Kontext-Parameter).

Audit-Status nach diesem Commit: alle 7 adressierbaren Befunde aus #57
sind gefixt (1 Rate-Limit, 2/6 XSS/XXE, 3 Path-Traversal, 4 PII-Log,
5 CSRF via Auth, 7 Search-DoS). Befund 8 (Secrets als ENV) ist
akzeptiertes Risiko für Single-Server-Docker.

Tests: 201 passed, 5 skipped.
2026-04-10 17:05:12 +02:00
Dotty Dotter
0870e8a910 #96: Methodik-Seite um konkretes Bewertungsbeispiel ergänzt 2026-04-10 16:34:44 +02:00
Dotty Dotter
07507de24a #96 Methodik-/Transparenz-Seite unter /methodik
Neue Seite für Endnutzer-Transparenz über die Bewertungsmethodik:

- GWÖ-Matrix 2.0 Erklärung mit interaktivem 5×5-Grid
- Analyse-Pipeline als 5-Schritt-Visualisierung (Download → Embedding
  → LLM → Verifikation → Darstellung)
- Wahlprogramm-Vergleich: Erklärung des Retrieval + Top-K + Verifikation
- Qualitätssicherung: Sub-D Property-Tests, server-seitige Quellen-
  Rekonstruktion, automatische Neu-Analyse
- Einschränkungen: KI-Bias, keine juristische Bewertung, nur indexierte
  Programme, kein Abstimmungsverhalten
- Datenquellen: dynamische Tabelle aller angebundenen Parlamente aus
  ADAPTERS + bundeslaender.py
- Technische Details aufklappbar (details/summary) für Interessierte,
  Haupttext verständlich für Nicht-Techniker
- Links zu Quellen-Seite, Adapter-Matrix, ADRs

In Hauptnavigation verlinkt (neben Quellen + Auswertungen).
Template-Variablen: adapter_count, model_name, programme_count,
chunk_count, bundeslaender — alles dynamisch aus dem Backend.

Tests: 194/194 grün.

Refs: #96
2026-04-10 16:14:38 +02:00
Dotty Dotter
5ea507b771 Fix: PFLICHT-FRAKTIONEN = alle Landtagsfraktionen der WP, nicht nur Antragsteller+Regierung 2026-04-10 16:08:04 +02:00
Dotty Dotter
038ebd6447 Fix: NRW-Titel + Regierungsfraktionen-Pflicht im LLM-Prompt
Bug 1 — NRW-Titel "Drucksache XX/YYYYY":
NRW's get_document machte nur HEAD-Request auf die PDF-URL und gab
title="Drucksache 18/18085" zurück — keinen echten Titel. Fix: nutzt
jetzt search(drucksache) um den echten Eintrag von OPAL zu holen.
Fallback: leerer Titel statt generischer, damit der LLM-Titel nicht
überschrieben wird. Plus _pick_best_title Helper: doc.title nur
übernehmen wenn es ein echter Titel ist (nicht "Drucksache XX").

Bug 2 — Nur Antragsteller im Passungsprofil, keine Regierungsfraktionen:
Der LLM ignorierte die "UND Regierungsfraktionen"-Anweisung im Prompt.
Fix: explizite PFLICHT-FRAKTIONEN-Zeile im User-Prompt:
"Du MUSST folgende Fraktionen in wahlprogrammScores bewerten: SPD, CDU, GRÜNE"
(dedupliziert aus fraktionen + regierungsfraktionen).

Tests: 194/194 grün.
Batch-Re-Analyse muss nochmal laufen mit den Fixes (21 bereits fertig,
15 noch offen — werden alle erneut benötigt weil die Titel/Fraktionen
in den neuen Assessments falsch sind).
2026-04-10 16:05:57 +02:00
Dotty Dotter
303b30f6dd Fix SyntaxError: user=Depends nach Form-Params (Python positional-after-default) 2026-04-10 14:30:54 +02:00
Dotty Dotter
7159240f49 #43 Keycloak SSO: JWT-Middleware + UI-Guiding
Auth-Schicht vorbereitet — Dev-Modus (KEYCLOAK_URL leer) lässt alles
durch, Prod-Modus (ENV gesetzt) validiert JWT gegen Keycloak-JWKS.

Backend (app/auth.py):
- JWKS-Cache mit 1h TTL (async httpx fetch)
- get_current_user: Optional, gibt User-Dict oder None
- require_auth: Pflicht, gibt User-Dict oder HTTP 401
- keycloak_login_url: Baut die OIDC-Login-URL
- _is_auth_enabled: prüft ob alle 3 ENV-Vars gesetzt sind

Abgesicherte POST-Endpoints:
- POST /analyze → Depends(require_auth)
- POST /api/analyze-drucksache → Depends(require_auth)
- POST /api/programme/index → Depends(require_auth)

Neue Endpoints:
- GET /api/auth/me → {authenticated, sub, email, name, roles} oder {authenticated: false}
- GET /api/auth/login-url → {enabled, url} für Keycloak-Redirect

Frontend (index.html):
- initAuth() beim DOMContentLoaded → prüft /api/auth/me
- "Anmelden"-Button im Header (neben "Quellen")
- "Jetzt prüfen"-Button: disabled + Tooltip "Nur nach Anmeldung
  verfügbar" wenn nicht eingeloggt; aktiv wenn eingeloggt
- currentUser-State steuert Button-Zustände

Dev-Modus: Solange KEYCLOAK_URL nicht gesetzt ist (lokale Dev, aktueller
Prod-Stand), sind alle Endpoints offen wie bisher. Kein Breaking Change.

Dependency: python-jose[cryptography]>=3.3.0 in requirements.txt.

Tests: 194/194 grün (auth.py hat keine Seiteneffekte im Import).

Refs: #43
2026-04-10 14:28:57 +02:00
Dotty Dotter
a821c19202 #47: Auto-Re-Analyse bei nicht-verifizierbaren Zitaten
Statt eine Nachricht "Textstelle nicht auffindbar" zu zeigen (was User
zurecht als Quatsch bezeichnet hat), erkennt der Cite-Endpoint jetzt
halluzinierte Zitate und triggert automatisch eine Re-Analyse:

Flow:
1. User klickt auf Zitat-Link
2. render_highlighted_page gibt (pdf, page, highlighted=False) zurück
3. Endpoint prüft: ds+bl Parameter vorhanden? Assessment in DB?
4. → Löscht altes Assessment, startet Re-Analyse als Background-Task
5. → Zeigt HTML-Warte-Seite mit Spinner und "Wird neu analysiert..."
6. → Auto-Redirect nach 15s zurück zum Assessment

Das neue Assessment hat durch reconstruct_zitate verifizierte Zitate,
die dann beim nächsten Klick korrekt gehighlighted werden.

Änderungen:
- embeddings.render_highlighted_page: Return-Typ (bytes, int, bool) —
  drittes Element ist True wenn Highlight gesetzt wurde
- database.delete_assessment: neue Funktion für die Re-Analyse
- main.py cite-Endpoint: akzeptiert ds= und bl= als optionale Params,
  triggert Re-Analyse bei highlighted=False + ds vorhanden
- Frontend: makeCiteUrl reicht ds+bl aus dem Assessment-Kontext mit
  durch in die Cite-URL
- Cache-Control auf 1h reduziert (war 24h, zu aggressiv für
  Assessments die sich durch Re-Analyse ändern)

Tests: 194/194 grün.

Refs: #47, #60
2026-04-10 10:35:01 +02:00
Dotty Dotter
8c27c302f7 #47: Fallback-Notiz bei nicht-auffindbarem Zitat + Year-Suffix-Fix
Wenn search_for den Zitat-Text in keiner Seite findet (Pre-#60
halluzinierte Snippets die nie im PDF standen), wird jetzt statt
stilles Nicht-Highlighting eine sichtbare FreeText-Annotation am
Seitenkopf platziert: "Textstelle nicht im Dokument auffindbar —
das Zitat wurde möglicherweise vom LLM paraphrasiert."

Damit versteht der User sofort warum kein Gelb-Highlighting da ist.
Die echte Lösung ist Re-Analyse mit der neuen Pipeline (reconstruct_
zitate erzeugt verifizierte Zitate), aber bis dahin ist die Notiz
der ehrliche UX-Fallback.

Tests: 194/194 grün.

Refs: #47
2026-04-10 10:22:36 +02:00
Dotty Dotter
6f35efe4d7 #47: Volles PDF mit Highlight statt 1-Seiten-Extract
User-Feedback: "Kontext geht verloren wenn nur 1 Seite kommt".

Änderung: render_highlighted_page liefert jetzt das GESAMTE Wahlprogramm-
PDF mit gelber Highlight-Annotation auf der Fundstelle, statt eines
1-Seiten-Auszugs. Der Browser öffnet das vollständige Programm.

Frontend hängt #page=N an die URL → Browser scrollt direkt zur
Fundstelle. found_page wird als X-Found-Page Header mitgeliefert,
falls der Text auf einer anderen Seite als angefordert gefunden wurde
(Pre-#60 halluzinierte Seitennummern).

Return-Typ geändert: (bytes, int) statt bytes — zweiter Wert ist die
1-indexed Seitennummer wo der Treffer tatsächlich liegt.

Tests angepasst: Tuple-Unpacking, Size-Check entfernt (volles PDF ist
größer als 1-Seiten-Extract, der alte Vergleich war obsolet).

Refs: #47
2026-04-10 10:16:00 +02:00
Dotty Dotter
5a035be20b #47 Fix: Highlighting für falsche Seitenzahlen + Year-Suffix-Matching
Zwei Bugs aus User-Test:

1. "Unbekanntes Wahlprogramm" bei Klick auf Grünes Grundsatzprogramm:
   Pre-#60 Assessments haben halluzinierte Dateinamen wie
   "gruene-grundsatzprogramm-2020.pdf" statt "gruene-grundsatzprogramm.pdf".
   Fix: Year-Suffix-Stripping im Reverse-Lookup (X-YYYY.pdf → X.pdf).

2. "Eine Seite, aber kein Highlighting": Pre-#60 Assessments haben oft
   falsche Seitennummern. search_for findet nichts auf der falschen Seite.
   Fix: wenn die angegebene Seite leer ist, ALLE Seiten durchsuchen und
   die erste mit einem Treffer nehmen. So funktioniert Highlighting auch
   bei halluzinierten Seitenzahlen retroaktiv. Performance: ~50ms pro PDF
   (Grundsatzprogramme haben ~100-160 Seiten), akzeptabel für on-demand.

Tests: 194/194 grün.

Refs: #47
2026-04-10 10:08:02 +02:00
Dotty Dotter
47897e13cd #47 Fix: Highlighting retroaktiv für alle bestehenden Assessments
Problem: Alle Assessments in der Prod-DB haben Pre-#47-URLs
(/static/referenzen/X.pdf#page=N). Die _chunk_pdf_url-Änderung wirkt
nur auf NEUE Analysen, die noch nicht stattgefunden haben.

Fix (zwei Seiten):

1. Endpoint /api/wahlprogramm-cite akzeptiert jetzt auch pdf=<filename>
   als Alternative zu pid=<programm_id>. Reverse-Lookup über PROGRAMME-
   Registry: pdf-Filename → programm_id. Damit können die statischen
   URLs aus Pre-#47-Assessments trotzdem an den Cite-Endpoint geleitet
   werden.

2. Frontend: neue JS-Funktion makeCiteUrl(z) die JEDE Zitat-URL on-the-
   fly umschreibt:
   - /static/referenzen/X.pdf#page=N + z.text
     → /api/wahlprogramm-cite?pdf=X.pdf&seite=N&q=<urlencoded text>
   - /api/wahlprogramm-cite?... → durchreichen (schon Cite-URL)
   - Fallback: URL unverändert

   Funktioniert retroaktiv für ALLE ~31 Assessments in der DB, ohne
   Re-Analyse. Sobald ein User auf ein Zitat klickt, wird die Seite
   des Wahlprogramms mit gelber Markierung gerendert.

Tests: 194/194 grün.

Refs: #47
2026-04-10 09:57:58 +02:00
Dotty Dotter
92dcd25f73 #63 B+C: Force-Honesty + UI-Warning bei Score ohne Zitate
Problem: BUND 21/3660 zeigt Score 10/10 für Linke und Grüne, aber null
Zitate — der Report sieht aus als sei die Bewertung fundiert, obwohl das
LLM mangels indexierter Quellen (linke-grundsatz fehlt) aus
Trainingswissen geraten hat. User-Feedback: "Da muss stehen warum."

Fix C — Force-Honesty im Prompt:

- format_quotes_for_prompt akzeptiert neuen Parameter searched_parties.
  Parteien, für die kein Chunk retrievt wurde, werden explizit als
  "KEINE QUELLEN VORHANDEN" markiert, mit der Anweisung "score: 0,
  zitate: [], Begründung: keine Quellen im Index".
- Neue ZITATEREGEL Punkt 5: "Wenn KEINE QUELLEN VORHANDEN → score 0."
  Das ist die strukturelle Lösung — das LLM darf nicht mehr raten.
- analyzer.py: fraktionen-Liste wird an format_quotes_for_prompt als
  searched_parties durchgereicht.

Fix B — UI-Transparenz:

- index.html: gelbe Warn-Box (amber, border-left #ffc107) wenn
  wp.wahlprogramm.score > 0 aber wp.wahlprogramm.zitate.length === 0:
  "Keine belegbaren Quellen im Index gefunden — Score basiert auf
  LLM-Einschätzung, nicht auf verifizierten Programm-Stellen."
- Wird für bestehende Assessments sofort sichtbar (JS-seitig berechnet),
  keine DB-Migration nötig. Neue Assessments nach Force-Honesty sollten
  idealerweise Score=0 haben, aber die Warning ist ein Fallback für
  den Fall dass das LLM die Prompt-Regel nicht immer 100% befolgt.

Fix A (Linke/AfD-Grundsatzprogramme) folgt als separater Commit —
sind öffentlich downloadbar, brauchen manuellen Sichtbarkeitscheck.

Tests: 194/194 grün (keine Schema-Änderung, nur Prompt + Template).

Refs: #63, ADR 0001
2026-04-10 09:32:31 +02:00
Dotty Dotter
4ec6190416 #47 PDF Zitat-Highlighting via PyMuPDF Single-Page-Render
Klick auf eine Zitat-Quelle im Report öffnet jetzt eine 1-Seiten-PDF-
Variante des Wahlprogramms mit gelb markiertem Snippet, statt nur zum
Page-Anchor zu springen und den Leser selbst suchen zu lassen.

Implementation:

embeddings.render_highlighted_page(programm_id, seite, query)
- Validiert programm_id gegen PROGRAMME (Path-Traversal-Schutz)
- Lädt das volle Wahlprogramm-PDF, extrahiert via insert_pdf nur die
  angeforderte Seite in einen neuen Document → kleinere Response
- search_for(query[:200]) → Bounding-Boxes aller Treffer
- Fallback: 5-Wort-Anker wenn Volltext-Match leer (LLM-Truncation,
  identisch zu find_chunk_for_text/Sub-D-Logik)
- add_highlight_annot mit gelber stroke-Color (1.0, 0.93, 0.0)
- Returns serialisierte PDF-Bytes oder None

embeddings._chunk_pdf_url
- Wenn chunk["text"] vorhanden: emittiert /api/wahlprogramm-cite-URL
  mit pid=, seite=, q=urlencoded(text[:200])
- Sonst: alter statischer /static/referenzen/X.pdf#page=N (Pre-#47
  rückwärts-kompatibel)
- text wird auf 200 Zeichen abgeschnitten, sonst blasen
  500-Zeichen-Snippets jedes Assessment-JSON auf

main.py /api/wahlprogramm-cite Endpoint
- Validiert pid gegen PROGRAMME registry
- seite: 1 ≤ n ≤ 2000
- Response: application/pdf, Cache-Control max-age=86400
- 404 bei unknown pid oder fehlendem PDF, 400 bei seite out of range

Reconstruct-Pipeline (Issue #60 Option B) zieht das automatisch durch:
reconstruct_zitate ruft _chunk_pdf_url(matched_chunk) auf, der jetzt
bevorzugt die Cite-URL emittiert. Keine Änderung an reconstruct_zitate
selbst nötig.

Tests: 194/194 grün (185 + 9 neue):

- TestChunkPdfUrl: 4 Cases (cite vs static, unknown prog, 200-char-truncate)
- TestRenderHighlightedPage: 5 Cases (unknown pid, invalid seite, valid
  render, empty query, query-not-found-falls-back-zu-leerem-Highlight)
- Plus Bridge im Test-Stub: pymupdf-as-fitz Shim falls eine
  third-party "fitz" das Pkg shadowt (kommt auf älteren Dev-Setups vor)

Refs: #47
2026-04-10 01:09:45 +02:00
Dotty Dotter
27ae82a758 #23 BayernAdapter — TYPO3-Solr HTML scraping (Anträge in WP19)
Stub durch echten Adapter ersetzt. Recon + Implementierung in einem
Wurf, weil das Backend deutlich freundlicher ist als bei SL/NI:

- Server-side rendered HTML, keine SPA, keine Auth, keine Cookies
- TYPO3 mit ext-solr unter /parlament/dokumente/drucksachen
- Filter direkt als URL-Query-Params (q, dokumentenart, wahlperiodeid[],
  sort, anzahl_treffer, page)
- 17.598 Drucksachen in WP19, davon ~10-15% Anträge — wir holen pro
  Page 100 Hits, paginieren bis 3 Pages und filtern client-seitig auf
  <p>Antrag …</p> (analog zu SL/HE)

Pattern-Extraktion über drei Regexen aus dem stabilen result-block:

  <div class="row result">
    <h4><a href="…pdf">Drucksache Nr. 19/<NR> vom DD.MM.YYYY</a></h4>
    <p>Antrag <FRAKTION>[, <FRAKTION2>]</p>
    <h5><strong>TITLE</strong></h5>
  </div>

Drucksachen-Lookup: q=<drucksache> matched die Nummer im Volltext und
liefert sie als einzigen Hit — wie bei SL und HB, kein dedizierter
GetById-Endpoint nötig.

Smoke-Test im Container:

  search("Schule", 5) → 5 Anträge in WP19 (SPD/FW-BAYERN+CSU/GRÜNE/AfD/AfD)
  get_document(19/11388) → match
  download_text(19/11388) → 4694 chars echter Antrags-Volltext
  search("", 5) → 5 newest Anträge mit korrektem date-DESC sort

Free-Voters-Disambiguation funktioniert über den #55 Parteinamen-Mapper:
"FREIE WÄHLER" auf Bayerns Liste wird zu "FW-BAYERN" canonicalized
(separat von "FREIE WÄHLER" in RP und "BVB-FW" in BB).

Tests: 185/185 grün.

UI-Aktivierung erfolgt separat in #35 (blockiert auf diesem Commit
und auf den BY-WP19-Wahlprogrammen — CSU, GRÜNE, AfD, SPD, FDP, FW).

Refs: #23, #49 (Roadmap Phase 3)
2026-04-10 01:00:47 +02:00
Dotty Dotter
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)
2026-04-10 00:46:02 +02:00
Dotty Dotter
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)
2026-04-09 22:52:17 +02:00
Dotty Dotter
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)
2026-04-09 22:21:39 +02:00
Dotty Dotter
ed64399dbb Fix #60: NameError in get_relevant_quotes_for_antrag (Phase B refactor leftover)
Root cause: der #55-Refactor (eb045d0) hat in get_relevant_quotes_for_antrag
``partei_upper`` zu ``partei_lookup`` umbenannt — aber die Dict-Write-Zeile
``results[partei_upper] = ...`` wurde übersehen. Bei jedem Aufruf knallt seither
ein NameError, der in analyzer.py vom breiten ``except Exception`` verschluckt
und still auf die Keyword-Fallback-Suche umgeleitet wird. Konsequenz: 100% der
Assessments seit eb045d0 (inkl. autonomer Roadmap-Run #59) liefen ohne
Embedding-Retrieval — daher die LLM-Halluzinationen aus #60.

Fix:
- embeddings.py:528: partei_upper → partei_lookup
- analyzer.py:249: NameError/AttributeError/TypeError/KeyError nicht mehr
  schlucken. Programmierfehler im Embedding-Pfad sollen hart fehlschlagen,
  damit die nächste Refactor-Regression nicht wieder 24h still degradiert
  läuft. Echte Network-/API-Exceptions fallen weiterhin auf den
  Keyword-Pfad zurück.
- tests/test_embeddings.py: Regression-Test, der get_relevant_quotes_for_antrag
  mit gemockten chunks aufruft und sicherstellt, dass die Funktion nicht
  crasht und ein populiertes Result liefert. Hätte den Bug bei eb045d0
  sofort gefangen.

Refs: #60, #55, #59
2026-04-09 21:57:56 +02:00
Dotty Dotter
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>
2026-04-09 14:39:03 +02:00
Dotty Dotter
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>
2026-04-09 14:21:49 +02:00
Dotty Dotter
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>
2026-04-09 14:15:35 +02:00
Dotty Dotter
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>
2026-04-09 14:04:11 +02:00
Dotty Dotter
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>
2026-04-09 12:23:35 +02:00
Dotty Dotter
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>
2026-04-09 12:11:20 +02:00
Dotty Dotter
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>
2026-04-09 11:56:20 +02:00
Dotty Dotter
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>
2026-04-09 11:53:16 +02:00
Dotty Dotter
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>
2026-04-09 11:30:10 +02:00