chore: konsolidiere Working-Tree mit dev-Stand nach Nextcloud-Sync-Resolution

Mehrtaegiger Sync-Stillstand hatte ueber 50 conflicted-copy-Dateien im
Working-Tree erzeugt. Die jeweils neuere Version wurde basierend auf
md5-Hash-Vergleich zum laufenden gwoe-antragspruefer-dev-Container
eingespielt.

Konsolidiert (38 modifiziert):
- analyzer.py, auswertungen.py, auth.py, config.py, database.py,
  drucksache_typen.py, embeddings.py, main.py, models.py, parlamente.py,
  ports/llm_bewerter.py, presse_generator.py, redline_utils.py, report.py,
  validators.py, wahlprogramm_fetch.py, wahlprogramm-links.yaml,
  wahlprogramm-shas.lock.json
- v2-Templates: base, components/{icon, matrix_mini, queue_widget,
  result_row}, screens/{admin_queue, admin_stand, aktuelle-themen,
  antrag_detail, auswertungen, cluster, landtag_suche, merkliste,
  methodik, tags}, static/v2/v2.css
- Tests: test_embeddings (Strict-Mode-Drop in reconstruct_zitate),
  test_endpoints_smoke, test_presse_generator, test_report,
  test_wahlprogramme (mit TestRegierungsbildung-Block, +120 LOC)
- docker-compose.dev.yml, docs/adr/index.md, docs/reference/api.md, mkdocs.yml

Neuzugaenge:
- app/marker.py, app/pm_render.py — Konsistenz-Marker, PM-Render-Adapter
- app/templates/v2/screens/scorecard{,_portrait,_werkstatt}.html — Cloud-Design-Scorecard
- app/static/v3/, app/templates/v3/ — v3-Layout-Hierarchie
- docs/adr/0010-stimmverhalten-gwoe-aggregat.md
- docs/adr/0011-aktuelle-themen-pm-generator.md
- docs/adr/0012-debug-auth-token-bypass.md
- scripts/{auto-rate-orphans, pm-quality-audit, pm-sample-bundle, rotate-debug-token}.sh
- tests/e2e/test_smoke_browser.py, tests/test_{auto_rate_runs, icons,
  marker, pm_render, presse_generator_style, thread_splitter,
  v2_pdf_consistency}.py

Plus inhaltlich uebernommen aus dem Conflict-Stand:
- embeddings.py: fw-by-2023.partei korrigiert von "FW" zu "FREIE WAEHLER"
  (war Mismatch zu wahlprogramme.py)
- embeddings.py: detailliertere Naming der BTW-2025-Wahlprogramme

Test-Suite-Stand: 1209 passed, 73 skipped (4 pre-existing failures in
test_presse_generator_style.py + 1 collection error in
integration/test_citations_substring.py — beide nicht durch dieses
Konsolidierungs-Commit verursacht).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dotty Dotter 2026-05-08 00:04:21 +02:00
parent b5d2bb2515
commit 4e7f7dac25
9 changed files with 220 additions and 41 deletions

View File

@ -377,6 +377,7 @@ async def analyze_antrag(
system_prompt=system_prompt, system_prompt=system_prompt,
user_prompt=user_prompt, user_prompt=user_prompt,
model=model, model=model,
max_tokens=8000,
) )
data = await bewerter.bewerte(request) data = await bewerter.bewerte(request)

View File

@ -226,8 +226,9 @@ PROGRAMME = {
"gruene-rp-2021": {"name": "Grüne Rheinland-Pfalz Wahlprogramm 2021", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "RP", "pdf": "gruene-rp-2021.pdf"}, "gruene-rp-2021": {"name": "Grüne Rheinland-Pfalz Wahlprogramm 2021", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "RP", "pdf": "gruene-rp-2021.pdf"},
"fw-rp-2021": {"name": "FREIE WÄHLER Rheinland-Pfalz Wahlprogramm 2021", "typ": "wahlprogramm", "partei": "FREIE WÄHLER", "bundesland": "RP", "pdf": "fw-rp-2021.pdf"}, "fw-rp-2021": {"name": "FREIE WÄHLER Rheinland-Pfalz Wahlprogramm 2021", "typ": "wahlprogramm", "partei": "FREIE WÄHLER", "bundesland": "RP", "pdf": "fw-rp-2021.pdf"},
"fdp-rp-2021": {"name": "FDP Rheinland-Pfalz Wahlprogramm 2021", "typ": "wahlprogramm", "partei": "FDP", "bundesland": "RP", "pdf": "fdp-rp-2021.pdf"}, "fdp-rp-2021": {"name": "FDP Rheinland-Pfalz Wahlprogramm 2021", "typ": "wahlprogramm", "partei": "FDP", "bundesland": "RP", "pdf": "fdp-rp-2021.pdf"},
# Grundsatzprogramme (Bund) — Gültigkeit ab Beschluss durch den Parteitag. # Grundsatzprogramme (Bund) — Gültigkeit ab Beschluss-Datum durch den
# ``gueltig_bis=None`` solange das Programm nicht ersetzt wurde. # jeweils höchsten Parteitag. ``gueltig_bis=None`` solange das Programm
# nicht durch ein neueres ersetzt wurde.
"spd-grundsatz": { "spd-grundsatz": {
"name": "SPD Hamburger Programm 2007", "name": "SPD Hamburger Programm 2007",
"typ": "parteiprogramm", "typ": "parteiprogramm",
@ -241,8 +242,8 @@ PROGRAMME = {
"typ": "parteiprogramm", "typ": "parteiprogramm",
"partei": "CDU", "partei": "CDU",
"pdf": "cdu-grundsatzprogramm.pdf", "pdf": "cdu-grundsatzprogramm.pdf",
"gueltig_ab": "2024-05-07", # Bundesparteitag Berlin (löst das "gueltig_ab": "2024-05-07", # Bundesparteitag Berlin, löst das
"gueltig_bis": None, # Hannoveraner Programm 2007 ab) "gueltig_bis": None, # Hannoveraner Programm 2007 ab
}, },
"gruene-grundsatz": { "gruene-grundsatz": {
"name": "Grüne Grundsatzprogramm 2020", "name": "Grüne Grundsatzprogramm 2020",
@ -279,7 +280,7 @@ PROGRAMME = {
# Bayern — LTW 08.10.2023, WP 19 # Bayern — LTW 08.10.2023, WP 19
"csu-by-2023": {"name": "CSU Bayernplan 2023", "typ": "wahlprogramm", "partei": "CSU", "bundesland": "BY", "pdf": "csu-by-2023.pdf"}, "csu-by-2023": {"name": "CSU Bayernplan 2023", "typ": "wahlprogramm", "partei": "CSU", "bundesland": "BY", "pdf": "csu-by-2023.pdf"},
"gruene-by-2023": {"name": "Grüne Bayern Regierungsprogramm 2023", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "BY", "pdf": "gruene-by-2023.pdf"}, "gruene-by-2023": {"name": "Grüne Bayern Regierungsprogramm 2023", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "BY", "pdf": "gruene-by-2023.pdf"},
"fw-by-2023": {"name": "FREIE WÄHLER Bayern Wahlprogramm 2023", "typ": "wahlprogramm", "partei": "FW", "bundesland": "BY", "pdf": "fw-by-2023.pdf"}, "fw-by-2023": {"name": "FREIE WÄHLER Bayern Wahlprogramm 2023", "typ": "wahlprogramm", "partei": "FREIE WÄHLER", "bundesland": "BY", "pdf": "fw-by-2023.pdf"},
"afd-by-2023": {"name": "AfD Bayern Wahlprogramm 2023", "typ": "wahlprogramm", "partei": "AfD", "bundesland": "BY", "pdf": "afd-by-2023.pdf"}, "afd-by-2023": {"name": "AfD Bayern Wahlprogramm 2023", "typ": "wahlprogramm", "partei": "AfD", "bundesland": "BY", "pdf": "afd-by-2023.pdf"},
"spd-by-2023": {"name": "SPD Bayern Zukunftsprogramm 2023", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "BY", "pdf": "spd-by-2023.pdf"}, "spd-by-2023": {"name": "SPD Bayern Zukunftsprogramm 2023", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "BY", "pdf": "spd-by-2023.pdf"},
# Niedersachsen — LTW 09.10.2022, WP 19 # Niedersachsen — LTW 09.10.2022, WP 19
@ -310,15 +311,17 @@ PROGRAMME = {
"spd-sn-2024": {"name": "SPD Sachsen Wahlprogramm 2024", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "SN", "pdf": "spd-sn-2024.pdf"}, "spd-sn-2024": {"name": "SPD Sachsen Wahlprogramm 2024", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "SN", "pdf": "spd-sn-2024.pdf"},
"linke-sn-2024": {"name": "DIE LINKE Sachsen Wahlprogramm 2024", "typ": "wahlprogramm", "partei": "LINKE", "bundesland": "SN", "pdf": "linke-sn-2024.pdf"}, "linke-sn-2024": {"name": "DIE LINKE Sachsen Wahlprogramm 2024", "typ": "wahlprogramm", "partei": "LINKE", "bundesland": "SN", "pdf": "linke-sn-2024.pdf"},
"gruene-sn-2024": {"name": "Grüne Sachsen Wahlprogramm 2024", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "SN", "pdf": "gruene-sn-2024.pdf"}, "gruene-sn-2024": {"name": "Grüne Sachsen Wahlprogramm 2024", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "SN", "pdf": "gruene-sn-2024.pdf"},
# Bundestag — BTW 2025-Wahlprogramme. Kabinett Merz I vereidigt 06.05.2025. # Bundestagswahl 23.02.2025 — Kabinett Merz I, vereidigt 06.05.2025.
"cdu-bund-2025": {"name": "CDU Wahlprogramm Bundestagswahl 2025", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "BUND", "pdf": "cdu-bund-2025.pdf"}, # Acht im 21. Bundestag relevante Parteien. Die Grundsatzprogramme
"csu-bund-2025": {"name": "CSU Wahlprogramm Bundestagswahl 2025", "typ": "wahlprogramm", "partei": "CSU", "bundesland": "BUND", "pdf": "csu-bund-2025.pdf"}, # oben bleiben als zweite Referenz erhalten.
"spd-bund-2025": {"name": "SPD Wahlprogramm Bundestagswahl 2025", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "BUND", "pdf": "spd-bund-2025.pdf"}, "cdu-bund-2025": {"name": "CDU Wahlprogramm Bundestagswahl 2025", "typ": "wahlprogramm", "partei": "CDU", "bundesland": "BUND", "pdf": "cdu-bund-2025.pdf"},
"gruene-bund-2025":{"name": "Grüne Wahlprogramm Bundestagswahl 2025", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "BUND", "pdf": "gruene-bund-2025.pdf"}, "csu-bund-2025": {"name": "CSU Wahlprogramm Bundestagswahl 2025", "typ": "wahlprogramm", "partei": "CSU", "bundesland": "BUND", "pdf": "csu-bund-2025.pdf"},
"fdp-bund-2025": {"name": "FDP Wahlprogramm Bundestagswahl 2025", "typ": "wahlprogramm", "partei": "FDP", "bundesland": "BUND", "pdf": "fdp-bund-2025.pdf"}, "spd-bund-2025": {"name": "SPD Regierungsprogramm Bundestagswahl 2025", "typ": "wahlprogramm", "partei": "SPD", "bundesland": "BUND", "pdf": "spd-bund-2025.pdf"},
"afd-bund-2025": {"name": "AfD Wahlprogramm Bundestagswahl 2025", "typ": "wahlprogramm", "partei": "AfD", "bundesland": "BUND", "pdf": "afd-bund-2025.pdf"}, "gruene-bund-2025": {"name": "Grüne Regierungsprogramm Bundestagswahl 2025", "typ": "wahlprogramm", "partei": "GRÜNE", "bundesland": "BUND", "pdf": "gruene-bund-2025.pdf"},
"linke-bund-2025": {"name": "DIE LINKE Wahlprogramm Bundestagswahl 2025", "typ": "wahlprogramm", "partei": "LINKE", "bundesland": "BUND", "pdf": "linke-bund-2025.pdf"}, "fdp-bund-2025": {"name": "FDP Wahlprogramm Bundestagswahl 2025", "typ": "wahlprogramm", "partei": "FDP", "bundesland": "BUND", "pdf": "fdp-bund-2025.pdf"},
"bsw-bund-2025": {"name": "BSW Wahlprogramm Bundestagswahl 2025", "typ": "wahlprogramm", "partei": "BSW", "bundesland": "BUND", "pdf": "bsw-bund-2025.pdf"}, "afd-bund-2025": {"name": "AfD Bundestagswahlprogramm 2025", "typ": "wahlprogramm", "partei": "AfD", "bundesland": "BUND", "pdf": "afd-bund-2025.pdf"},
"linke-bund-2025": {"name": "DIE LINKE Wahlprogramm Bundestagswahl 2025", "typ": "wahlprogramm", "partei": "LINKE", "bundesland": "BUND", "pdf": "linke-bund-2025.pdf"},
"bsw-bund-2025": {"name": "BSW Wahlprogramm Bundestagswahl 2025", "typ": "wahlprogramm", "partei": "BSW", "bundesland": "BUND", "pdf": "bsw-bund-2025.pdf"},
} }
@ -736,7 +739,7 @@ def _chunk_pdf_url(chunk: dict) -> Optional[str]:
# die URL bleibt bounded (sonst würden 500-Zeichen-Snippets in jeder # die URL bleibt bounded (sonst würden 500-Zeichen-Snippets in jeder
# Zitat-URL stehen und das HTML-Report-JSON aufblähen). # Zitat-URL stehen und das HTML-Report-JSON aufblähen).
q = urllib.parse.quote_plus(text[:200]) q = urllib.parse.quote_plus(text[:200])
return f"/api/wahlprogramm-cite?pid={prog_id}&seite={seite}&q={q}" return f"/api/wahlprogramm-cite?pid={prog_id}&seite={seite}&q={q}#page={seite}"
if seite: if seite:
return f"/static/referenzen/{pdf}#page={seite}" return f"/static/referenzen/{pdf}#page={seite}"
@ -799,9 +802,14 @@ def render_highlighted_page(programm_id: str, seite: int, query: str) -> Optiona
rects = [] rects = []
if needle: if needle:
clean = needle.replace("\u00ad", "") clean = needle.replace("\u00ad", "")
# LLMs ziehen h\u00e4ufig die Seitenzahl-Header (\u201e44 Gute Bildung \u2026")
# mit ins Zitat. Wenn die ersten Tokens reine Ziffern sind,
# strippen wir sie f\u00fcr die Suche \u2014 sonst matched search_for nicht.
import re as _re
clean = _re.sub(r"^\s*\d+\s+", "", clean).strip()
words = clean.split() words = clean.split()
anchor = " ".join(words[:5]) if len(words) >= 5 else clean anchor = " ".join(words[:5]) if len(words) >= 5 else clean
# Versuch 1: angegebene Seite, Volltext # Versuch 1: angegebene Seite, Volltext (gestrippt)
rects = src[target_page_idx].search_for(clean) rects = src[target_page_idx].search_for(clean)
# Versuch 2: angegebene Seite, 5-Wort-Anker # Versuch 2: angegebene Seite, 5-Wort-Anker
if not rects: if not rects:
@ -814,8 +822,7 @@ def render_highlighted_page(programm_id: str, seite: int, query: str) -> Optiona
target_page_idx = i target_page_idx = i
break break
# Volles PDF mit Highlight-Annotation. Der Browser öffnet das # Volles PDF mit Highlight-Annotation.
# vollständige Wahlprogramm; das Frontend hängt #page=N an die URL.
page = src[target_page_idx] page = src[target_page_idx]
if needle and rects: if needle and rects:
for rect in rects: for rect in rects:
@ -824,6 +831,16 @@ def render_highlighted_page(programm_id: str, seite: int, query: str) -> Optiona
annot.set_colors(stroke=(1.0, 0.93, 0.0)) # gelb annot.set_colors(stroke=(1.0, 0.93, 0.0)) # gelb
annot.update() annot.update()
# PDF-OpenAction setzen, damit der Reader direkt auf der richtigen
# Seite startet (statt Seite 1) — sonst sieht der User „PDF öffnet,
# aber falsche Seite". /Fit = passt-zur-Größe.
try:
page_xref = page.xref
catalog_xref = src.pdf_catalog()
src.xref_set_key(catalog_xref, "OpenAction", f"[{page_xref} 0 R /Fit]")
except Exception:
logger.exception("render_highlighted_page: OpenAction-Setzen fehlgeschlagen")
highlighted = bool(needle and rects) highlighted = bool(needle and rects)
try: try:
return src.tobytes(), target_page_idx + 1, highlighted return src.tobytes(), target_page_idx + 1, highlighted

View File

@ -37,3 +37,48 @@ BB:
titel: "BÜNDNIS 90/DIE GRÜNEN Brandenburg Wahlprogramm 2024 (Platzhalter — URL prüfen)" titel: "BÜNDNIS 90/DIE GRÜNEN Brandenburg Wahlprogramm 2024 (Platzhalter — URL prüfen)"
jahr: 2024 jahr: 2024
sha256: "" sha256: ""
# Bundestagswahl 23.02.2025 — Kabinett Merz I, vereidigt 06.05.2025.
# Quellen: Parteiwebseiten (cdu.de, csu.de, spd.de, gruene.de, fdp.de,
# afd.de, die-linke.de, bsw-vg.de). Recherche 2026-05-07.
BUND:
CDU:
- url: https://www.cdu.de/app/uploads/2025/01/km_btw_2025_wahlprogramm_langfassung_ansicht.pdf
titel: "Politikwechsel für Deutschland — Wahlprogramm von CDU und CSU zur Bundestagswahl 2025 (Langfassung)"
jahr: 2025
sha256: "08f751316e731b77aa2f18090b8695d88268f2942481399f37a3a47317361795"
CSU:
- url: https://www.csu.de/common/download/Wahlprogramm_2025_von_CDU_und_CSU.pdf
titel: "Politikwechsel für Deutschland — Wahlprogramm von CDU und CSU zur Bundestagswahl 2025 (CSU-Verteilungsversion)"
jahr: 2025
sha256: "c07fe4b65404be9b4c1b99ec9c970567a96a27b86a1154754a41e9ef31d0c04f"
SPD:
- url: https://www.spd.de/fileadmin/Dokumente/Beschluesse/Programm/2025_SPD_Regierungsprogramm.pdf
titel: "Mehr für Dich. Besser für Deutschland. — Regierungsprogramm der SPD zur Bundestagswahl 2025"
jahr: 2025
sha256: "05aeb9eb19fd423288d94de1d15cabcddbcb9bb1ecf65237657d10e4839b9d7e"
GRÜNE:
- url: https://cms.gruene.de/uploads/assets/20250205_Regierungsprogramm_DIGITAL_DINA5.pdf
titel: "Zusammen wachsen — Regierungsprogramm BÜNDNIS 90/DIE GRÜNEN zur Bundestagswahl 2025"
jahr: 2025
sha256: "0d1f7530ddecadc8d98db15b2189f3ee4ddcccd9619df825703911f1aac18dda"
FDP:
- url: https://www.fdp.de/sites/default/files/2024-12/fdp-wahlprogramm_2025.pdf
titel: "Alles lässt sich ändern — Wahlprogramm der Freien Demokraten zur Bundestagswahl 2025"
jahr: 2025
sha256: "a549dcb318f60fdd8a257fb2e68745d1df50151f4914910e2afde20c3b0fa039"
AfD:
- url: https://www.afd.de/wp-content/uploads/2025/02/AfD_Bundestagswahlprogramm2025_web.pdf
titel: "Programm der Alternative für Deutschland — Bundestagswahl 2025"
jahr: 2025
sha256: "e2d0a944f54017aa432bcd0c069c7908aefb3ea65af4904e7f86ca7c7bd0d4bb"
LINKE:
- url: https://www.die-linke.de/fileadmin/user_upload/Wahlprogramm_Langfassung_Linke-BTW25_01.pdf
titel: "Du verdienst mehr — Wahlprogramm DIE LINKE zur Bundestagswahl 2025 (Langfassung)"
jahr: 2025
sha256: "301bd30a5fcd2a7e791adc4db5294e79d8f3fd71b8c9b080b057f14bf8cae600"
BSW:
- url: https://bsw-vg.de/wp-content/themes/bsw/assets/downloads/BSW%20Wahlprogramm%202025.pdf
titel: "Unser Land verdient mehr — Wahlprogramm Bündnis Sahra Wagenknecht zur Bundestagswahl 2025"
jahr: 2025
sha256: "bd4640aab7c6ff214becc7e44bc6dc67539ebd80992bb39fb8853839d3a7ccda"

View File

@ -1,7 +1,9 @@
{ {
"afd-bb-2024.pdf": "da5cd04cc66128b2f0df35b47775fce850ed2f4145ee15d74ec8bf501ce043f1", "afd-bb-2024.pdf": "da5cd04cc66128b2f0df35b47775fce850ed2f4145ee15d74ec8bf501ce043f1",
"afd-be-2023.pdf": "d2b5997b1bc0d3fb590cc354d8ed1ac879e8de4a74518f4089436a2fa12615f1", "afd-be-2023.pdf": "d2b5997b1bc0d3fb590cc354d8ed1ac879e8de4a74518f4089436a2fa12615f1",
"afd-bund-2025.pdf": "e2d0a944f54017aa432bcd0c069c7908aefb3ea65af4904e7f86ca7c7bd0d4bb",
"afd-bw-2021.pdf": "a438e09279c6c5766171a213715ed0a9d60248ff86f648227e8bb6ec59a591c7", "afd-bw-2021.pdf": "a438e09279c6c5766171a213715ed0a9d60248ff86f648227e8bb6ec59a591c7",
"afd-grundsatzprogramm.pdf": "b35026580c8610ab81c67760743497695b06e30466255586d089d4715297bead",
"afd-hh-2025.pdf": "6aae3ad00cd07824bcd99473e130d1b894e2174a89fcafece51865c51fdcd4c8", "afd-hh-2025.pdf": "6aae3ad00cd07824bcd99473e130d1b894e2174a89fcafece51865c51fdcd4c8",
"afd-lsa-2021.pdf": "dd2651af2a9423039b1c5a39760be2332025d569a878453f09e0302e252edc23", "afd-lsa-2021.pdf": "dd2651af2a9423039b1c5a39760be2332025d569a878453f09e0302e252edc23",
"afd-mv-2021.pdf": "953c39941a1f997233daaf0cec01bc82b1e86ba895b43e8d34b015cc72799648", "afd-mv-2021.pdf": "953c39941a1f997233daaf0cec01bc82b1e86ba895b43e8d34b015cc72799648",
@ -9,10 +11,13 @@
"afd-rp-2021.pdf": "3ec39eb08a073244813a51f260e18fe52aab791bea26bf8079546b6e189ec2b3", "afd-rp-2021.pdf": "3ec39eb08a073244813a51f260e18fe52aab791bea26bf8079546b6e189ec2b3",
"afd-th-2024.pdf": "26e61fdc3456e7ce18f7a3d2ea1eada303f93cad0b9698797f83a671574eaf51", "afd-th-2024.pdf": "26e61fdc3456e7ce18f7a3d2ea1eada303f93cad0b9698797f83a671574eaf51",
"bsw-bb-2024.pdf": "548c9bda01af176586606fae708c9f3b3ba98e1e128f1e2ff39e482289faab42", "bsw-bb-2024.pdf": "548c9bda01af176586606fae708c9f3b3ba98e1e128f1e2ff39e482289faab42",
"bsw-bund-2025.pdf": "bd4640aab7c6ff214becc7e44bc6dc67539ebd80992bb39fb8853839d3a7ccda",
"bsw-th-2024.pdf": "5ace33912083048a759ee2af9288248447363dafa21f569c5c056df22751ba69", "bsw-th-2024.pdf": "5ace33912083048a759ee2af9288248447363dafa21f569c5c056df22751ba69",
"cdu-bb-2024.pdf": "460b1463483429f9e8b84e4ae6ef9cf878dd228e108411bed3c153169a0001e8", "cdu-bb-2024.pdf": "460b1463483429f9e8b84e4ae6ef9cf878dd228e108411bed3c153169a0001e8",
"cdu-be-2023.pdf": "813d0d08ac8ce7381e9a7b9472e0616aaf684b1632c9d4a7f4e940a33455f29a", "cdu-be-2023.pdf": "813d0d08ac8ce7381e9a7b9472e0616aaf684b1632c9d4a7f4e940a33455f29a",
"cdu-bund-2025.pdf": "08f751316e731b77aa2f18090b8695d88268f2942481399f37a3a47317361795",
"cdu-bw-2021.pdf": "a92c104c456ce06d8bad6649071551e0ec0d525a1bc0bc31e9fa6a0566da4db0", "cdu-bw-2021.pdf": "a92c104c456ce06d8bad6649071551e0ec0d525a1bc0bc31e9fa6a0566da4db0",
"cdu-grundsatzprogramm.pdf": "4b4bebd40ff8d905eb3ad00975cbbf0990810c21977e2d560e3d73f2571b181d",
"cdu-hh-2025.pdf": "8d29e514b8bce5c2f3f497dc5b97f6f8ab95a7bdbf619abf258e9582d57f2dbd", "cdu-hh-2025.pdf": "8d29e514b8bce5c2f3f497dc5b97f6f8ab95a7bdbf619abf258e9582d57f2dbd",
"cdu-lsa-2021.pdf": "63b6cf42ce97834d5d105fb7b8cc7fb7a2aa96928d4153bd3a5858c196ee0797", "cdu-lsa-2021.pdf": "63b6cf42ce97834d5d105fb7b8cc7fb7a2aa96928d4153bd3a5858c196ee0797",
"cdu-mv-2021.pdf": "605a2211bef8666c2103771ebffd97a088e7cdb1545401087ef125155e7e4db2", "cdu-mv-2021.pdf": "605a2211bef8666c2103771ebffd97a088e7cdb1545401087ef125155e7e4db2",
@ -20,7 +25,10 @@
"cdu-rp-2021.pdf": "54c50d88bdf5c5f7dee5abcc981ffb4d1cfd5c86fbf2a29f4f2f4a8a3dd4797a", "cdu-rp-2021.pdf": "54c50d88bdf5c5f7dee5abcc981ffb4d1cfd5c86fbf2a29f4f2f4a8a3dd4797a",
"cdu-sh-2022.pdf": "39b79a22e904b300cf1bbc25752b618195683c90c31e6b10c3bc0e8408aa6a1a", "cdu-sh-2022.pdf": "39b79a22e904b300cf1bbc25752b618195683c90c31e6b10c3bc0e8408aa6a1a",
"cdu-th-2024.pdf": "cde8d2222bd8ce04aee24883a38dab8a30f5d60cda115b8bb2f43ceffa08b730", "cdu-th-2024.pdf": "cde8d2222bd8ce04aee24883a38dab8a30f5d60cda115b8bb2f43ceffa08b730",
"csu-bund-2025.pdf": "c07fe4b65404be9b4c1b99ec9c970567a96a27b86a1154754a41e9ef31d0c04f",
"fdp-bund-2025.pdf": "a549dcb318f60fdd8a257fb2e68745d1df50151f4914910e2afde20c3b0fa039",
"fdp-bw-2021.pdf": "bdcbb1b2e5748922c8347bd69ea6f81c954fd02cd220d448400f9a5a86ce914b", "fdp-bw-2021.pdf": "bdcbb1b2e5748922c8347bd69ea6f81c954fd02cd220d448400f9a5a86ce914b",
"fdp-grundsatzprogramm.pdf": "9abb6570c3505271b1f43db2a8340c05b970a7934d0c1b2542403025affe2b13",
"fdp-lsa-2021.pdf": "3d4275e36e29c0b191dcc4a29061a1072920f868cc52bee954bf81491ad15224", "fdp-lsa-2021.pdf": "3d4275e36e29c0b191dcc4a29061a1072920f868cc52bee954bf81491ad15224",
"fdp-mv-2021.pdf": "8dc341dd017f1d82c51608a26e1fd6c3d8acd1281dc37409e375389999b37b55", "fdp-mv-2021.pdf": "8dc341dd017f1d82c51608a26e1fd6c3d8acd1281dc37409e375389999b37b55",
"fdp-nrw-2022.pdf": "576b42a26c29ca5d8b7469d417ae709c8d0699aed5195d4ca16dd696dcff8bea", "fdp-nrw-2022.pdf": "576b42a26c29ca5d8b7469d417ae709c8d0699aed5195d4ca16dd696dcff8bea",
@ -28,7 +36,9 @@
"fdp-sh-2022.pdf": "4c49da411bb3c8e008f4b57dd20dc005104515b56056ff746cf5403529728d09", "fdp-sh-2022.pdf": "4c49da411bb3c8e008f4b57dd20dc005104515b56056ff746cf5403529728d09",
"fw-rp-2021.pdf": "c7f26d553f24c9d9fcf1c2edb1dbe558edc1ca65af68b289a1541e77f7bbeea8", "fw-rp-2021.pdf": "c7f26d553f24c9d9fcf1c2edb1dbe558edc1ca65af68b289a1541e77f7bbeea8",
"gruene-be-2023.pdf": "2b14a319cdcd2ca022399254ea285714f872eddd166f3f537861eeb2dc5ade80", "gruene-be-2023.pdf": "2b14a319cdcd2ca022399254ea285714f872eddd166f3f537861eeb2dc5ade80",
"gruene-bund-2025.pdf": "0d1f7530ddecadc8d98db15b2189f3ee4ddcccd9619df825703911f1aac18dda",
"gruene-bw-2021.pdf": "9af526705cb10b91be0690b26c9c033668a8082eeefca482dc4e7a46f2d671f9", "gruene-bw-2021.pdf": "9af526705cb10b91be0690b26c9c033668a8082eeefca482dc4e7a46f2d671f9",
"gruene-grundsatzprogramm.pdf": "36c68616286bc5df94cadbe176c72c515a6ed403cc246d221244a882f30b0834",
"gruene-hh-2025.pdf": "4428d1cdc16b4e74588f0bd51145ab7371f9e0871a2fc9d25a1f94e4f5aeb662", "gruene-hh-2025.pdf": "4428d1cdc16b4e74588f0bd51145ab7371f9e0871a2fc9d25a1f94e4f5aeb662",
"gruene-lsa-2021.pdf": "7b5cea92cd600283d7edf18dc0d358c0b7d78d7269589d9ef05de7d5f8b35998", "gruene-lsa-2021.pdf": "7b5cea92cd600283d7edf18dc0d358c0b7d78d7269589d9ef05de7d5f8b35998",
"gruene-mv-2021.pdf": "40f0070743ef9ae7808cab319234b4c83faa53a8a098ba8a82f28023bee4d9f6", "gruene-mv-2021.pdf": "40f0070743ef9ae7808cab319234b4c83faa53a8a098ba8a82f28023bee4d9f6",
@ -36,13 +46,17 @@
"gruene-rp-2021.pdf": "4fd68629d1560c28d61b2b913fd20ce6ad9a76b22823fd8496e51bfaf70dc19c", "gruene-rp-2021.pdf": "4fd68629d1560c28d61b2b913fd20ce6ad9a76b22823fd8496e51bfaf70dc19c",
"gruene-sh-2022.pdf": "62870c948c9e05663125b051d3a6401d63952ea6a64e4140dcece7bd1b1aea52", "gruene-sh-2022.pdf": "62870c948c9e05663125b051d3a6401d63952ea6a64e4140dcece7bd1b1aea52",
"linke-be-2023.pdf": "7d6a9166f6a1d87ba26cc1a2818ae2b844ee9df6ed6668673f329dd5186fd956", "linke-be-2023.pdf": "7d6a9166f6a1d87ba26cc1a2818ae2b844ee9df6ed6668673f329dd5186fd956",
"linke-bund-2025.pdf": "301bd30a5fcd2a7e791adc4db5294e79d8f3fd71b8c9b080b057f14bf8cae600",
"linke-grundsatzprogramm.pdf": "b5b2b19f5eea4cb22816eca6f2988bcc6cffa4c0cc499411bf131e866fae4e16",
"linke-hh-2025.pdf": "15e68efe3818758a7cefc0a3e3095a5a5fb191111c00a1202c563cee43ce6e40", "linke-hh-2025.pdf": "15e68efe3818758a7cefc0a3e3095a5a5fb191111c00a1202c563cee43ce6e40",
"linke-lsa-2021.pdf": "f269c014416b213785badf7bea5928fdb847fc902e09f52ec66a140a37e03d75", "linke-lsa-2021.pdf": "f269c014416b213785badf7bea5928fdb847fc902e09f52ec66a140a37e03d75",
"linke-mv-2021.pdf": "160dad56ab4de8f641c21f51cbf3c33953f2f3d91b4de792c4e725f3975fdfbe", "linke-mv-2021.pdf": "160dad56ab4de8f641c21f51cbf3c33953f2f3d91b4de792c4e725f3975fdfbe",
"linke-th-2024.pdf": "2d8ca99ef60cbe1b59cf33b1e37320d66a057e5136c2f49aa8cde77e4a19533a", "linke-th-2024.pdf": "2d8ca99ef60cbe1b59cf33b1e37320d66a057e5136c2f49aa8cde77e4a19533a",
"spd-bb-2024.pdf": "4131f63fbb9d67cd8948ca7a54f1c140b47968c77454a3dabe6bcdc4384f63d3", "spd-bb-2024.pdf": "4131f63fbb9d67cd8948ca7a54f1c140b47968c77454a3dabe6bcdc4384f63d3",
"spd-be-2023.pdf": "4ee84e969e97894742673f940ec030883216ce852b729507327f8bced637d03b", "spd-be-2023.pdf": "4ee84e969e97894742673f940ec030883216ce852b729507327f8bced637d03b",
"spd-bund-2025.pdf": "05aeb9eb19fd423288d94de1d15cabcddbcb9bb1ecf65237657d10e4839b9d7e",
"spd-bw-2021.pdf": "d888ae92bb62a61aaa4d6ac8dc22c2c98d1a2227b6ba223b6422770672825072", "spd-bw-2021.pdf": "d888ae92bb62a61aaa4d6ac8dc22c2c98d1a2227b6ba223b6422770672825072",
"spd-grundsatzprogramm.pdf": "0290ae8870e2deeeec4f93227264fef9f6dbbc56572a2ba29e93d8bd1c0de7cd",
"spd-hh-2025.pdf": "5e8c57969cb3b159b9299c173831f7863ab81bd206c2a87ae232ba96f23156ee", "spd-hh-2025.pdf": "5e8c57969cb3b159b9299c173831f7863ab81bd206c2a87ae232ba96f23156ee",
"spd-lsa-2021.pdf": "59140aa1921ab0ee85142d74e1d72b1af7254da3f7870a30460abd605d280333", "spd-lsa-2021.pdf": "59140aa1921ab0ee85142d74e1d72b1af7254da3f7870a30460abd605d280333",
"spd-mv-2021.pdf": "c8c671c2e60f1a4f8048bd74e379eb8edc69ab2daeb09581fe83f25f6c87d529", "spd-mv-2021.pdf": "c8c671c2e60f1a4f8048bd74e379eb8edc69ab2daeb09581fe83f25f6c87d529",

View File

@ -172,18 +172,6 @@ WAHLPROGRAMME: dict[str, dict[str, dict]] = {
"LINKE": {"file": "linke-bund-2025.pdf", "titel": "Alle wollen regieren. Wir wollen verändern. — DIE LINKE Wahlprogramm BTW 2025", "partei": "DIE LINKE", "jahr": 2025, "seiten": 60, "regierungsbildung": "2025-05-06", "regierungsende": None}, "LINKE": {"file": "linke-bund-2025.pdf", "titel": "Alle wollen regieren. Wir wollen verändern. — DIE LINKE Wahlprogramm BTW 2025", "partei": "DIE LINKE", "jahr": 2025, "seiten": 60, "regierungsbildung": "2025-05-06", "regierungsende": None},
"BSW": {"file": "bsw-bund-2025.pdf", "titel": "Unser Land verdient mehr — BSW Wahlprogramm BTW 2025", "partei": "BSW", "jahr": 2025, "seiten": 45, "regierungsbildung": "2025-05-06", "regierungsende": None}, "BSW": {"file": "bsw-bund-2025.pdf", "titel": "Unser Land verdient mehr — BSW Wahlprogramm BTW 2025", "partei": "BSW", "jahr": 2025, "seiten": 45, "regierungsbildung": "2025-05-06", "regierungsende": None},
}, },
# Bundestag — keine bundesweiten Wahlprogramme im Repo, daher dienen
# die Grundsatzprogramme als Quelle. CSU + BSW haben keine Programme
# in der Registry und werden weiterhin als fehlend gemeldet.
# Diese Eintraege sind von embeddings.py separat indiziert (typ=parteiprogramm).
"BUND": {
"CDU": {"file": "cdu-grundsatzprogramm.pdf", "titel": "CDU Grundsatzprogramm 2024", "partei": "CDU", "jahr": 2024, "seiten": 64, "ist_grundsatz": True},
"SPD": {"file": "spd-grundsatzprogramm.pdf", "titel": "SPD Hamburger Programm 2007", "partei": "SPD", "jahr": 2007, "seiten": 78, "ist_grundsatz": True},
"GRÜNE": {"file": "gruene-grundsatzprogramm.pdf","titel": "Grüne Grundsatzprogramm 2020", "partei": "GRÜNE", "jahr": 2020, "seiten": 116, "ist_grundsatz": True},
"FDP": {"file": "fdp-grundsatzprogramm.pdf", "titel": "FDP Karlsruher Freiheitsthesen 2012","partei": "FDP", "jahr": 2012, "seiten": 31, "ist_grundsatz": True},
"AfD": {"file": "afd-grundsatzprogramm.pdf", "titel": "AfD Grundsatzprogramm 2016", "partei": "AfD", "jahr": 2016, "seiten": 96, "ist_grundsatz": True},
"LINKE": {"file": "linke-grundsatzprogramm.pdf", "titel": "DIE LINKE Erfurter Programm 2011", "partei": "LINKE", "jahr": 2011, "seiten": 84, "ist_grundsatz": True},
},
} }
# Pro Bundesland: Markdown-Übersichtsdatei mit Wahlprogramm-Zusammenfassungen, # Pro Bundesland: Markdown-Übersichtsdatei mit Wahlprogramm-Zusammenfassungen,

View File

@ -317,15 +317,11 @@ class TestReconstructZitate:
} }
out = reconstruct_zitate(data, semantic_quotes) out = reconstruct_zitate(data, semantic_quotes)
zitate = out["wahlprogrammScores"][0]["wahlprogramm"]["zitate"] zitate = out["wahlprogrammScores"][0]["wahlprogramm"]["zitate"]
# Beide Zitate bleiben erhalten — das nicht-matchende wird als # Strict-Mode (#175): das nicht-matchende halluzinierte Zitat wird
# unverified markiert statt gedroppt (Hybrid-Ansatz). # gedroppt, das echte Zitat bleibt erhalten und ist verified.
assert len(zitate) == 2 assert len(zitate) == 1
# Das halluzinierte Zitat ist unverified assert "geschlechtersensiblen" in zitate[0]["text"]
halluziniert = [z for z in zitate if "Rechtsextremismus" in z["text"]] assert zitate[0]["verified"] is True
assert halluziniert[0]["verified"] is False
# Das echte Zitat ist verified
echt = [z for z in zitate if "geschlechtersensiblen" in z["text"]]
assert echt[0]["verified"] is True
def test_empty_semantic_quotes_is_noop(self): def test_empty_semantic_quotes_is_noop(self):
data = {"wahlprogrammScores": [{ data = {"wahlprogrammScores": [{

View File

@ -54,6 +54,7 @@ def db_with_antrag_and_news(tmp_path: Path) -> Path:
titel TEXT NOT NULL, titel TEXT NOT NULL,
body TEXT NOT NULL, body TEXT NOT NULL,
model TEXT NOT NULL, model TEXT NOT NULL,
style TEXT NOT NULL DEFAULT 'pm',
created_at TEXT NOT NULL DEFAULT (datetime('now')) created_at TEXT NOT NULL DEFAULT (datetime('now'))
) )
""") """)

View File

@ -221,14 +221,18 @@ class TestGetScoreColor:
class TestGetRatingSymbol: class TestGetRatingSymbol:
"""Skala 5..+5 (seit Modell-Migration). Schwellen:
+4 ++, +1..+3 +, 0 , 1..3 , 4 .
"""
def test_strong_positive(self): def test_strong_positive(self):
from app.report import get_rating_symbol from app.report import get_rating_symbol
assert get_rating_symbol(2) == "++" assert get_rating_symbol(4) == "++"
assert get_rating_symbol(5) == "++" assert get_rating_symbol(5) == "++"
def test_positive(self): def test_positive(self):
from app.report import get_rating_symbol from app.report import get_rating_symbol
assert get_rating_symbol(1) == "+" assert get_rating_symbol(1) == "+"
assert get_rating_symbol(3) == "+"
def test_neutral(self): def test_neutral(self):
from app.report import get_rating_symbol from app.report import get_rating_symbol
@ -237,8 +241,9 @@ class TestGetRatingSymbol:
def test_negative(self): def test_negative(self):
from app.report import get_rating_symbol from app.report import get_rating_symbol
assert get_rating_symbol(-1) == "" assert get_rating_symbol(-1) == ""
assert get_rating_symbol(-3) == ""
def test_strong_negative(self): def test_strong_negative(self):
from app.report import get_rating_symbol from app.report import get_rating_symbol
assert get_rating_symbol(-2) == "" assert get_rating_symbol(-4) == ""
assert get_rating_symbol(-5) == "" assert get_rating_symbol(-5) == ""

View File

@ -1,4 +1,6 @@
"""Tests for wahlprogramme.py — registry consistency + file existence.""" """Tests for wahlprogramme.py — registry consistency + file existence."""
import re
import pytest import pytest
from app.wahlprogramme import ( from app.wahlprogramme import (
@ -6,6 +8,9 @@ from app.wahlprogramme import (
REFERENZEN_PATH, REFERENZEN_PATH,
get_wahlprogramm, get_wahlprogramm,
parteien_mit_wahlprogramm, parteien_mit_wahlprogramm,
regierung_aktuell,
regierungsbildung_for,
regierungsende_for,
) )
@ -42,6 +47,83 @@ class TestRegistryStructure:
assert info["file"].endswith(".pdf") assert info["file"].endswith(".pdf")
# ─────────────────────────────────────────────────────────────────────────────
# Regierungsbildungs-Felder — Konsistenz pro Bundesland
# ─────────────────────────────────────────────────────────────────────────────
class TestRegierungsbildung:
"""Pro Bundesland gehören alle Wahlprogramm-Einträge zur gleichen
Legislatur d.h. dasselbe regierungsbildung-Datum. Ausnahme: BUND, wo
Grundsatzprogramme stehen, die regierungsbildung=None tragen."""
_ISO_DATE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
def test_every_entry_has_regierungs_fields(self):
for bl, parteien in WAHLPROGRAMME.items():
for partei, info in parteien.items():
assert "regierungsbildung" in info, f"{bl}/{partei}: regierungsbildung fehlt"
assert "regierungsende" in info, f"{bl}/{partei}: regierungsende fehlt"
def test_regierungsbildung_iso_or_none(self):
for bl, parteien in WAHLPROGRAMME.items():
for partei, info in parteien.items():
rb = info.get("regierungsbildung")
if rb is not None:
assert isinstance(rb, str) and self._ISO_DATE.match(rb), \
f"{bl}/{partei}: regierungsbildung kein ISO-Datum: {rb!r}"
def test_regierungsende_iso_or_none(self):
for bl, parteien in WAHLPROGRAMME.items():
for partei, info in parteien.items():
re_ = info.get("regierungsende")
if re_ is not None:
assert isinstance(re_, str) and self._ISO_DATE.match(re_), \
f"{bl}/{partei}: regierungsende kein ISO-Datum: {re_!r}"
def test_alle_parteien_eines_bl_haben_gleiches_datum(self):
"""Alle Wahlprogramm-Einträge eines Bundeslands gehören zur selben
Regierung und müssen daher dasselbe Bildungs-/Endedatum tragen."""
for bl, parteien in WAHLPROGRAMME.items():
bildung = {info.get("regierungsbildung") for info in parteien.values()}
ende = {info.get("regierungsende") for info in parteien.values()}
assert len(bildung) == 1, \
f"{bl}: regierungsbildung divergent: {bildung}"
assert len(ende) == 1, \
f"{bl}: regierungsende divergent: {ende}"
def test_grundsatzprogramme_haben_keine_regierung(self):
"""Grundsatzprogramme (ist_grundsatz=True) tragen keine Regierungs-
bildung sie sind zeitlos."""
for bl, parteien in WAHLPROGRAMME.items():
for partei, info in parteien.items():
if info.get("ist_grundsatz"):
assert info.get("regierungsbildung") is None, \
f"{bl}/{partei} ist Grundsatzprogramm, sollte regierungsbildung=None haben"
class TestRegierungsHelper:
def test_regierungsbildung_for_known_bl(self):
assert regierungsbildung_for("NRW") == "2022-06-29"
assert regierungsbildung_for("HH") == "2025-05-07"
def test_regierungsbildung_for_bund_btw2025(self):
# BUND tragt nun die BTW-2025-Wahlprogramme; Kabinett Merz I
# vereidigt 06.05.2025. Grundsatzprogramme bleiben nur in
# embeddings.PROGRAMME als zweite Referenz.
assert regierungsbildung_for("BUND") == "2025-05-06"
def test_regierungsbildung_for_unknown_bl(self):
assert regierungsbildung_for("XX") is None
def test_regierung_aktuell_true_for_active_bl(self):
assert regierung_aktuell("NRW") is True
assert regierung_aktuell("BB") is True
assert regierung_aktuell("BUND") is True
def test_regierungsende_for_active_is_none(self):
assert regierungsende_for("NRW") is None
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
# File existence — every registered file must exist on disk # File existence — every registered file must exist on disk
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
@ -91,6 +173,36 @@ class TestParteienMitWahlprogramm:
parteien = parteien_mit_wahlprogramm("BE") parteien = parteien_mit_wahlprogramm("BE")
assert set(parteien) == {"CDU", "SPD", "GRÜNE", "LINKE", "AfD"} assert set(parteien) == {"CDU", "SPD", "GRÜNE", "LINKE", "AfD"}
def test_bund_has_eight_parteien(self):
# BTW 2025: CDU, CSU, SPD, GRÜNE, FDP, AfD, LINKE, BSW.
parteien = parteien_mit_wahlprogramm("BUND")
assert set(parteien) == {"CDU", "CSU", "SPD", "GRÜNE", "FDP", "AfD", "LINKE", "BSW"}
def test_by_has_five_parteien(self):
parteien = parteien_mit_wahlprogramm("BY")
assert set(parteien) == {"CSU", "FREIE WÄHLER", "GRÜNE", "SPD", "AfD"}
def test_hb_has_four_parteien(self):
# AfD war wegen Listenstreit nicht zur Bürgerschaftswahl 2023 zugelassen.
parteien = parteien_mit_wahlprogramm("HB")
assert set(parteien) == {"SPD", "CDU", "GRÜNE", "LINKE"}
def test_he_has_five_parteien(self):
parteien = parteien_mit_wahlprogramm("HE")
assert set(parteien) == {"CDU", "SPD", "GRÜNE", "FDP", "AfD"}
def test_ni_has_four_parteien(self):
parteien = parteien_mit_wahlprogramm("NI")
assert set(parteien) == {"SPD", "CDU", "GRÜNE", "AfD"}
def test_sl_has_three_parteien(self):
parteien = parteien_mit_wahlprogramm("SL")
assert set(parteien) == {"SPD", "CDU", "AfD"}
def test_sn_has_six_parteien(self):
parteien = parteien_mit_wahlprogramm("SN")
assert set(parteien) == {"CDU", "SPD", "AfD", "BSW", "LINKE", "GRÜNE"}
def test_unknown_bundesland_empty_list(self): def test_unknown_bundesland_empty_list(self):
assert parteien_mit_wahlprogramm("XX") == [] assert parteien_mit_wahlprogramm("XX") == []