diff --git a/app/drucksache_typen.py b/app/drucksache_typen.py index dc9f3fe..f2ec87d 100644 --- a/app/drucksache_typen.py +++ b/app/drucksache_typen.py @@ -128,3 +128,25 @@ def likely_kleine_anfrage_titel(title: str) -> bool: if t.rstrip().endswith("?"): return True return False + + +# Bundesrats-Drucksachen-Format erkennen. +# Bundestag: "21/9954" (Wahlperiode/Nummer, 2 Komponenten). +# Bundesrat: "186/3/26", "186/2/26" (Nummer/Sub/Jahr, 3 Komponenten). +import re as _re + +_BUNDESRAT_PATTERN = _re.compile(r"^\d+/\d+/\d+$") + + +def is_bundesrat_drucksache(drucksache: str) -> bool: + """Erkennt Bundesrats-Drucksachen am 3-Komponenten-Format ``N/M/JJ``. + + Diese Drucksachen finden sich über die DIP-API (Bundestags-Backend + indexiert sie mit), haben aber **keine Fraktionen** als Antragsteller — + sondern Bundesländer. Der GWÖ-Analyzer erwartet Fraktionen und schlägt + bei Bundesrats-Drucksachen fehl. Folge: separate Behandlung im Such- + + Analyse-Pfad. + """ + if not drucksache: + return False + return bool(_BUNDESRAT_PATTERN.match(drucksache.strip())) diff --git a/app/main.py b/app/main.py index ca4fb7a..394f8d0 100644 --- a/app/main.py +++ b/app/main.py @@ -1416,6 +1416,8 @@ async def search_landtag( "bundesland": bundesland, "typ": doc.typ, "typ_normiert": doc.typ_normiert, + "is_bundesrat": doc.is_bundesrat, + "urheber_bundeslaender": doc.urheber_bundeslaender, "gwoeScore": None, "status": "unchecked", }) @@ -1572,6 +1574,20 @@ async def analyze_drucksache( # Get document metadata doc = await adapter.get_document(drucksache) + # Bundesrats-Drucksachen ablehnen — Antragsteller sind Bundesländer, + # GWÖ-Wahlprogramm-Bewertung greift nicht (kein Partei-Bezug). + if doc and getattr(doc, "is_bundesrat", False): + bls = ", ".join(doc.urheber_bundeslaender or []) or "—" + raise HTTPException( + status_code=400, + detail=( + f"Bundesrats-Drucksache (Antragsteller: {bls}) wird derzeit " + f"nicht unterstützt. Die GWÖ-Bewertung erwartet eine " + f"antragstellende Fraktion, Bundesländer-Anträge aus dem " + f"Bundesrat haben aber keine Wahlprogramm-Verbindung." + ), + ) + # #127: Typ-Check — nur abstimmbare Drucksachen analysieren. # Falls der Adapter den Typ nicht richtig setzt (NRW: "Drucksache"), # versuche den Typ aus dem Dokument-Text zu erkennen. diff --git a/app/parlamente.py b/app/parlamente.py index 2f92544..e6c4535 100644 --- a/app/parlamente.py +++ b/app/parlamente.py @@ -23,11 +23,18 @@ class Drucksache: bundesland: str typ: str = "Antrag" # Original-Typ vom Landtag (z.B. "Kleine Anfrage", "Gesetzentwurf") typ_normiert: str = "" # Normierter Typ (wird automatisch gesetzt) + # Bundesrats-Drucksachen (DIP herausgeber=BR): Antragsteller sind + # Bundesländer (z.B. ['SN','HE']) statt Fraktionen. fraktionen bleibt + # leer, damit Stimmverhalten-Aggregate sich nicht verwirren. + is_bundesrat: bool = False + urheber_bundeslaender: list[str] = None # type: ignore[assignment] def __post_init__(self): from .drucksache_typen import normalize_typ if not self.typ_normiert: self.typ_normiert = normalize_typ(self.typ) + if self.urheber_bundeslaender is None: + self.urheber_bundeslaender = [] class ParlamentAdapter(ABC): @@ -2928,15 +2935,30 @@ class BundestagAdapter(ParlamentAdapter): if not pdf_url: return None - # Fraktionen aus urheber-Liste extrahieren. DIP listet sie als - # "Fraktion der AfD" o.ä. — extract_fraktionen kennt das Pattern - # bereits aus den Landtags-Adaptern. + # Bundesrats-Drucksachen erkennen am DIP-Feld `herausgeber: 'BR'`. + # Antragsteller sind dort Bundesländer (z.B. Sachsen, Hessen), + # nicht Bundestags-Fraktionen. + is_bundesrat = (doc.get("herausgeber") or "").upper() == "BR" + urheber_strs: list[str] = [] + urheber_bundeslaender: list[str] = [] for u in (doc.get("urheber") or []): if isinstance(u, dict): urheber_strs.append(u.get("titel") or u.get("bezeichnung") or "") - urheber_combined = ", ".join(filter(None, urheber_strs)) - fraktionen = extract_fraktionen(urheber_combined, bundesland=self.bundesland) + # Bei Bundesrat: bezeichnung ist der BL-Code (SN, HE, NW, ...) + if is_bundesrat: + code = (u.get("bezeichnung") or "").strip().upper() + if code and len(code) <= 4: + urheber_bundeslaender.append(code) + + if is_bundesrat: + # Fraktionen leer lassen — Bundesländer-Antragsteller sind + # in urheber_bundeslaender. Stimmverhalten-Aggregate ignorieren + # diese Drucksachen automatisch (kein fraktionen-Match). + fraktionen: list[str] = [] + else: + urheber_combined = ", ".join(filter(None, urheber_strs)) + fraktionen = extract_fraktionen(urheber_combined, bundesland=self.bundesland) return Drucksache( drucksache=nummer, @@ -2946,6 +2968,8 @@ class BundestagAdapter(ParlamentAdapter): link=pdf_url, bundesland=self.bundesland, typ=doc.get("drucksachetyp", "Antrag"), + is_bundesrat=is_bundesrat, + urheber_bundeslaender=urheber_bundeslaender, ) async def _fetch_page( diff --git a/app/templates/v2/screens/landtag_suche.html b/app/templates/v2/screens/landtag_suche.html index bb39666..994d5ed 100644 --- a/app/templates/v2/screens/landtag_suche.html +++ b/app/templates/v2/screens/landtag_suche.html @@ -273,29 +273,41 @@ function renderRow(item, bl) { var url = item.url || item.link || ''; var done = lsCheckedIds.has(ds); var fraktionen = Array.isArray(item.fraktionen) ? item.fraktionen : []; + var isBundesrat = !!item.is_bundesrat; + var bundeslaender = Array.isArray(item.urheber_bundeslaender) ? item.urheber_bundeslaender : []; var titleHtml = url ? '' + title + '' : title; - var fraktionenHtml = fraktionen.length - ? '