Commit Graph

14 Commits

Author SHA1 Message Date
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
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
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>
2026-04-09 00:59:28 +02:00
Dotty Dotter
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>
2026-04-09 00:34:06 +02:00
Dotty Dotter
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>
2026-04-08 23:48:02 +02:00
Dotty Dotter
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>
2026-04-08 23:41:23 +02:00
Dotty Dotter
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>
2026-04-08 23:38:04 +02:00
Dotty Dotter
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>
2026-04-08 23:19:41 +02:00
Dotty Dotter
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>
2026-04-08 08:19:48 +02:00
Dotty Dotter
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>
2026-04-07 23:33:16 +02:00
Dotty Dotter
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 c7242f8, this commit adds the reference data and
flips the activation switch.

Wahlprogramme (LTW Sachsen-Anhalt 06.06.2021)
- Six PDFs added under app/static/referenzen/{cdu,spd,gruene,fdp,afd,
  linke}-lsa-2021.pdf, plus paged plain-text extractions under
  app/kontext/*.txt for the keyword fallback search.
- Sources verified by hand:
  - CDU "Unsere Heimat. Unsere Verantwortung." (cdulsa.de, 82 pages)
  - SPD "Zusammenhalt und neue Chancen" (FES library, 77 pages)
  - GRÜNE "Verlässlich für Sachsen-Anhalt" (gruene-lsa.de, 164 pages)
  - FDP "Wahlprogramm zur Landtagswahl 2021" (Naumann-Stiftung, 76 pages)
  - AfD "Alles für unsere Heimat!" (klimawahlen.de mirror, 64 pages)
  - LINKE "Wahlprogramm zur Landtagswahl 2021" (dielinke-sachsen-anhalt.de,
    88 pages)
- The CDU PDF was the trickiest: KAS blocks bot downloads via
  Cloudflare; the cdulsa.de copy was located by an autonomous web
  search and verified to be byte-identical with the official document.

Embeddings indexed (in production container, OpenAI-compatible
DashScope embeddings via the existing index_programm pipeline):
- CDU 134, SPD 145, GRÜNE 183, FDP 100, AfD 64, LINKE 143 chunks
- Total LSA: 769 new chunks alongside the existing 775 NRW chunks
  and 335 federal Grundsatzprogramm chunks.

wahlprogramme.py
- WAHLPROGRAMME["LSA"] populated with all six parties (canonical fraction
  codes, original titles, page counts).

embeddings.py
- PROGRAMME extended with the six new "<partei>-lsa-2021" entries that
  the indexer pipeline expects.

bundeslaender.py
- LSA flipped to aktiv=True. The frontend dropdown will now offer
  Sachsen-Anhalt as a selectable bundesland and analyzer.get_bundesland_
  context() will produce a real LSA prompt block (CDU/SPD/FDP as
  governing fractions, all six landtagsfraktionen).

End-to-end smoke test (live in production container before commit)
- Adapter: PortalaAdapter.search() returned current Anträge of März 2026
  (LINKE + GRÜNE) with correct titles and PDF URLs.
- Semantic search for an LSA "ÖPNV in der Altmark" sample antrag
  matched LINKE S.53, SPD S.68, FDP S.52 — all three with similarity
  > 0.6 and topical hits (Regionalisierungsmittel, ÖPNV-Förderprogramm,
  Wasserstoffnetz).

Resolves issue #2.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 22:12:32 +02:00
Dotty Dotter
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>
2026-04-07 21:50:23 +02:00
Dotty Dotter
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>
2026-04-07 14:17:54 +02:00