#!/usr/bin/env python3 """ Baut data/normen_index.json: Gesetz/Paragraph → Liste von Entscheidungen (doknr). Liest YAML-Köpfe aus ge_md/, mappt Abkürzungen auf Lawgit-Slugs und prüft, ob laws_md//§N.md existiert. """ from __future__ import annotations import argparse import json import os import re import sys from collections import defaultdict from pathlib import Path # Häufige RII-Abkürzungen → Ordnername unter laws_md/ GESETZ_ALIASE: dict[str, str] = { "bgb": "bgb", "gg": "gg", "stgb": "stgb", "stpo": "stpo", "zpo": "zpo", "inso": "inso", "hgb": "hgb", "gmbhg": "gmbhg", "aktg": "aktg", "sgb": "sgb", # oft mit Bandnummer — unmapped oft "bverfgg": "bverfgg", "vwgo": "vwgo", "fgo": "fgo", "sgg": "sgg", "arbgg": "arbgg", "gvg": "gvg", "egbgb": "egbgb", "agg": "agg", "urhg": "urhg", "tmg": "tmg", "bdsg": "bdsg", "ao": "ao", "ustg": "ustg", "estg": "estg", "vvg": "vvg", "vob": "vob", "baugb": "baugb", "whg": "whg", "bnatschg": "bnatschg", } def map_gesetz_slug(abbrev: str) -> str: key = (abbrev or "").strip().lower() key = ( key.replace("ä", "ae") .replace("ö", "oe") .replace("ü", "ue") .replace("ß", "ss") .replace(".", "") ) return GESETZ_ALIASE.get(key, key) def lawgit_path_for( lawgit_root: Path, gesetz_slug: str, paragraph: str ) -> tuple[Path | None, bool]: """Pfad zu laws_md//§N.md (oder Art. N.md) und ob die Datei existiert.""" if not gesetz_slug: return None, False slug = map_gesetz_slug(gesetz_slug) base = lawgit_root / "laws_md" / slug if not paragraph: readme = base / "README.md" return readme, readme.is_file() candidates = [ base / f"§{paragraph}.md", base / f"§{paragraph.lower()}.md", base / f"Art. {paragraph}.md", base / f"Art. {paragraph.lower()}.md", base / f"Art {paragraph}.md", base / f"Artikel {paragraph}.md", ] for path in candidates: if path.is_file(): return path, True return candidates[0], False def parse_frontmatter(md: str) -> dict: """Einfacher YAML-Kopf-Parser für unsere festgelegten Felder.""" if not md.startswith("---"): return {} end = md.find("\n---", 3) if end < 0: return {} block = md[3:end].strip() meta: dict = {"normen": [], "vorinstanz": []} current_list: str | None = None for line in block.splitlines(): if line.startswith("normen:"): current_list = "normen" if line.strip() == "normen: []": current_list = None continue if line.startswith("vorinstanz:"): current_list = "vorinstanz" if line.strip() == "vorinstanz: []": current_list = None continue if line.startswith(" - ") and current_list == "normen": # { gesetz: "...", paragraph: "...", roh: "..." } m = re.search( r'gesetz:\s*"([^"]*)".*?paragraph:\s*"([^"]*)".*?roh:\s*"([^"]*)"', line, ) if m: meta["normen"].append( {"gesetz": m.group(1), "paragraph": m.group(2), "roh": m.group(3)} ) continue if line.startswith(" - ") and current_list == "vorinstanz": continue if re.match(r"^[a-z_]+:", line) and not line.startswith(" "): current_list = None key, _, val = line.partition(":") val = val.strip() if val.startswith('"') and val.endswith('"'): val = json.loads(val) meta[key.strip()] = val return meta def parse_frontmatter_normen(md: str) -> list[dict]: return parse_frontmatter(md).get("normen") or [] def build_index( ge_md: Path, lawgit_root: Path, ) -> dict: by_norm: dict[str, list[dict]] = defaultdict(list) unmapped: list[dict] = [] decisions = 0 for path in sorted(ge_md.rglob("*.md")): try: text = path.read_text(encoding="utf-8", errors="replace") except OSError: continue meta = parse_frontmatter(text) if not meta.get("doknr"): continue decisions += 1 entry_base = { "doknr": meta.get("doknr"), "gericht": meta.get("gericht"), "ebene": meta.get("ebene"), "aktenzeichen": meta.get("aktenzeichen"), "datum": meta.get("datum"), "path": str(path.relative_to(ge_md)), "doktyp": meta.get("doktyp"), "hat_leitsatz": bool( re.search(r"^## Leitsatz\s*$", text, re.M) and re.search( r"^## Leitsatz\s*\n\n.+\n", text, re.M | re.S ) ), } norms = meta.get("normen") or [] if not norms: unmapped.append({**entry_base, "grund": "keine_normen"}) continue for n in norms: slug = map_gesetz_slug(n.get("gesetz", "")) para = n.get("paragraph", "") key = f"{slug}/§{para}" if para else slug lg_path, ok = lawgit_path_for(lawgit_root, slug, para) item = { **entry_base, "roh": n.get("roh"), "gesetz": slug, "paragraph": para, "lawgit": str(lg_path.relative_to(lawgit_root)) if lg_path and ok else None, "mapped": ok, } if ok: by_norm[key].append(item) else: unmapped.append({**item, "grund": "kein_lawgit_pfad"}) return { "lawgit_root": str(lawgit_root.resolve()), "ge_md": str(ge_md.resolve()), "entscheidungen": decisions, "normen": {k: v for k, v in sorted(by_norm.items())}, "unmapped": unmapped, } def main() -> int: parser = argparse.ArgumentParser(description="Normen-Index aus ge_md/ bauen") parser.add_argument( "--lawgit", default=os.environ.get("LAWGIT_ROOT", "../lawgit"), help="Pfad zu lawgit (Default: ../lawgit oder LAWGIT_ROOT)", ) parser.add_argument("--ge-md", default="ge_md", help="Verzeichnis der Entscheidungen") parser.add_argument( "--out", default="data/normen_index.json", help="Ausgabedatei", ) args = parser.parse_args() lawgit = Path(args.lawgit).resolve() ge_md = Path(args.ge_md).resolve() if not ge_md.is_dir(): print(f"ge_md fehlt: {ge_md}", file=sys.stderr) return 1 if not (lawgit / "laws_md").is_dir(): print( f"Warnung: {lawgit}/laws_md fehlt — Mapping wird überwiegend unmapped.", file=sys.stderr, ) index = build_index(ge_md, lawgit) out = Path(args.out) out.parent.mkdir(parents=True, exist_ok=True) out.write_text( json.dumps(index, indent=2, ensure_ascii=False) + "\n", encoding="utf-8", ) mapped_norms = len(index["normen"]) print( f"Index: {index['entscheidungen']} Entscheidungen, " f"{mapped_norms} Norm-Schlüssel, " f"{len(index['unmapped'])} unmapped → {out}" ) return 0 if __name__ == "__main__": raise SystemExit(main())