Initialisiere Explor.Code.Law mit RII-Pipeline und Strato-Deploy.
Some checks failed
Deploy explore / deploy (push) Failing after 18s
Some checks failed
Deploy explore / deploy (push) Failing after 18s
Entscheidungen als Markdown, Normen-Index und schlankes Git-Sync nach /opt/explore (Gitea Actions, ohne Docker). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
245
index_normen.py
Normal file
245
index_normen.py
Normal file
@@ -0,0 +1,245 @@
|
||||
#!/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/<slug>/§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/<slug>/§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())
|
||||
Reference in New Issue
Block a user