commit bdaa21dc31e875b5d193e65cdd94709c9b11a6f1 Author: Michael Hermann Date: Mon Aug 17 12:03:40 2026 +0200 Initialisiere Explor.Code.Law mit RII-Pipeline und Strato-Deploy. Entscheidungen als Markdown, Normen-Index und schlankes Git-Sync nach /opt/explore (Gitea Actions, ohne Docker). Co-authored-by: Cursor diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..2681ed5 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,59 @@ +# Sync Explor.Code.Law nach /opt/explore +# Gitea Secret: SSH_PRIVATE_KEY +# +# Schlankes Daten-Deploy: nur Git-Checkout, kein Docker. +# Kein git clean / kein reset --hard — ge_xml/ und .env bleiben. + +name: Deploy explore + +on: + push: + branches: [main] + workflow_dispatch: + +concurrency: + group: explore-deploy + cancel-in-progress: false + +jobs: + deploy: + runs-on: ubuntu-latest + container: + image: python:3.13-bookworm + steps: + - name: Deploy to Server + uses: appleboy/ssh-action@v1.0.3 + with: + host: 82.165.92.92 + username: root + key: ${{ secrets.SSH_PRIVATE_KEY }} + command_timeout: 15m + script: | + set -euo pipefail + echo "Starting explore deployment..." + mkdir -p /opt/explore + cd /opt/explore + + if [ ! -d .git ]; then + git init + git remote add origin https://git.coded.law/michaelkole/explore.git || true + fi + git remote set-url origin https://git.coded.law/michaelkole/explore.git + git fetch origin main + # Tracked Dateien aktualisieren. Kein reset --hard, kein git clean: + # ge_xml/ und .env bleiben unangetastet. + git checkout origin/main -- . + + mkdir -p ge_md data ge_xml kommentar + chmod 600 .env 2>/dev/null || true + if [ ! -f .env ] && [ -f env.example ]; then + cp env.example .env + chmod 600 .env + fi + grep -q "^LAWGIT_ROOT=" .env 2>/dev/null || echo "LAWGIT_ROOT=/opt/lawgit" >> .env + grep -q "^DOMAIN=" .env 2>/dev/null || echo "DOMAIN=explore.coded.law" >> .env + + echo "ge_md files: $(find ge_md -name '*.md' 2>/dev/null | wc -l)" + echo "index: $([ -f data/normen_index.json ] && wc -c < data/normen_index.json || echo missing) bytes" + echo "ge_xml files (local disk, not from git): $(find ge_xml -type f 2>/dev/null | wc -l)" + echo "Deployment complete!" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..02e5706 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Roh-XML/ZIP von Rechtsprechung im Internet +ge_xml/ +*.zip + +# Python +__pycache__/ +*.py[cod] +*.egg-info/ +.venv/ +venv/ +.env +.Python + +# OS / IDE +.DS_Store +.idea/ +.vscode/ + +# Test / Cache +.pytest_cache/ +.coverage +htmlcov/ +*.log diff --git a/INFRASTRUCTURE.md b/INFRASTRUCTURE.md new file mode 100644 index 0000000..e74526f --- /dev/null +++ b/INFRASTRUCTURE.md @@ -0,0 +1,126 @@ +# Infrastruktur — Explor.Code.Law + +Gerichtsentscheidungen und Normen-Index auf dem Strato-VServer, analog zu Lawgit. +Aktuell **Daten + Pipeline** (kein UI). Domain und Port sind für später reserviert. + +## Architektur + +``` +Lokal / CI + → Gitea git.coded.law/michaelkole/explore + → Checkout /opt/explore + ├── ge_md/ # Markdown-Volltexte (im Git) + ├── data/ # normen_index.json (im Git) + ├── ge_xml/ # Roh-ZIP/XML (NICHT im Git, nur auf Disk) + ├── extract_rii.py … + └── LAWGIT_ROOT → /opt/lawgit +``` + +Gesetze bleiben in Lawgit (`/opt/lawgit/laws_md/`). Explore kopiert sie nicht. + +## Pfade + +| Was | Pfad | +|-----|------| +| Checkout | `/opt/explore/` | +| Entscheidungen (Markdown) | `/opt/explore/ge_md/` | +| Normen-Index | `/opt/explore/data/normen_index.json` | +| Rohdaten (gitignoriert) | `/opt/explore/ge_xml/` | +| Lawgit (Read-only Bezug) | `/opt/lawgit/` | +| Optionale Env | `/opt/explore/.env` (Mode 600) | + +**Wichtig:** Deploy darf `ge_xml/` und `.env` nie löschen (`git clean` / `reset --hard` vermeiden). + +## Domain / Port (vorbereitet) + +| Variable | Wert | Status | +|----------|------|--------| +| Domain | `explore.coded.law` | DNS + Certbot noch offen | +| Frontend-Port (später) | `127.0.0.1:3097` | frei (Stand Server-Check) | +| UI / Apache / Docker | — | bewusst noch nicht eingerichtet | + +## Gitea + +- Host: `https://git.coded.law` +- Repo (Ziel): `michaelkole/explore` +- Deploy-Workflow: `.gitea/workflows/deploy.yml` +- Secret: `SSH_PRIVATE_KEY` (siehe `setup-ssh-deploy.sh`) + +SSH zum Server: `ssh -i ~/.ssh/gitea_deploy root@82.165.92.92` + +## Sync & Deploy + +### Erstinstallation (einmalig) + +1. Gitea-Repo `michaelkole/explore` anlegen und lokalen Stand pushen. +2. Auf dem Server: + +```bash +git clone https://git.coded.law/michaelkole/explore.git /opt/explore +cd /opt/explore +cp env.example .env && chmod 600 .env # optional +# optional: Roh-XML von lokal nachziehen +# ./sync_to_server.sh --xml-only +``` + +Oder lokal: `./deploy.sh` (legt `/opt/explore` an und pullt). + +3. CI: `./setup-ssh-deploy.sh` (Gitea-API-Token mit repo-Rechten). + +### Laufendes Update (Git) + +Push auf `main` → Workflow aktualisiert tracked Dateien unter `/opt/explore` +(`ge_md/`, `data/`, Skripte). `ge_xml/` bleibt unberührt. + +Manuell: + +```bash +ssh -i ~/.ssh/gitea_deploy root@82.165.92.92 +cd /opt/explore +git fetch origin main +git checkout origin/main -- . +``` + +### Roh-XML (`ge_xml/`) + +Liegt nicht in Git. Nach lokaler Extraktion auf den Server spiegeln: + +```bash +./sync_to_server.sh --xml-only +# oder voller Bootstrap (ohne .venv): +./sync_to_server.sh +``` + +### Index auf dem Server neu bauen + +```bash +cd /opt/explore +python3 -m pip install -r requirements.txt --break-system-packages # oder venv +python3 index_normen.py --lawgit /opt/lawgit +``` + +## Env (`env.example` → `.env`) + +| Variable | Default | Zweck | +|----------|---------|--------| +| `LAWGIT_ROOT` | `/opt/lawgit` | Pfad zu `laws_md/` für `index_normen.py` | +| `DOMAIN` | `explore.coded.law` | reserviert für späteres UI | + +## Nützliche Befehle + +```bash +# Bestand prüfen +ssh -i ~/.ssh/gitea_deploy root@82.165.92.92 'du -sh /opt/explore/ge_md /opt/explore/ge_xml /opt/explore/data; find /opt/explore/ge_md -name "*.md" | wc -l' + +# Extraktion auf dem Server (braucht Netz + Platz) +cd /opt/explore && python3 extract_rii.py --limit 5 --court bgh,bverfg +``` + +## Abgrenzung zu Lawgit + +| | lawgit | explore | +|--|--------|---------| +| Checkout | `/opt/lawgit` | `/opt/explore` | +| Inhalt | `laws_md/`, UI, Postgres | `ge_md/`, Index, später Kommentar | +| Deploy | Docker Compose + Apache | Git-Sync (schlank) | +| Domain | lawgit.coded.law | explore.coded.law (geplant) | diff --git a/README.md b/README.md new file mode 100644 index 0000000..b4792f2 --- /dev/null +++ b/README.md @@ -0,0 +1,69 @@ +# Explor.Code.Law + +Kommentar- und Rechtsprechungsschicht auf Basis von [lawgit](../lawgit). + +Gesetze kommen aus Lawgit (`laws_md/`). Hier landen Gerichtsentscheidungen +als Markdown-Volltext und später Kommentare (Beck/Grüneberg-Stil), die sich +mit neuer Rechtsprechung fort schreiben. + +## Verhältnis zu Lawgit + +| Repo | Inhalt | +|------|--------| +| **lawgit** | geltender Wortlaut (`laws_md/`), Fassungsgeschichte, Normzeitachse | +| **explore** | Gerichtsentscheidungen (`ge_md/`), Normen-Index, Kommentare | + +Gesetze werden **nicht** kopiert. Explore liest sie über `LAWGIT_ROOT` +(Default: `../lawgit`). + +## Schnellstart + +```bash +cd /Users/michaelhermann/Source/law/coded/explore +python3 -m pip install -r requirements.txt + +# Rauchtest: fünf BGH- und BVerfG-Entscheidungen +python3 extract_rii.py --limit 5 --court bgh,bverfg + +# Normen an Lawgit-Paragraphen hängen +python3 index_normen.py --lawgit ../lawgit + +# Offline-Tests +python3 -m unittest discover -s tests -v +``` + +## Struktur + +``` +explore/ +├── ge_md/ # Entscheidungen als Markdown (Volltext) +├── ge_xml/ # Roh-ZIP/XML (gitignoriert) +├── kommentar/ # später: Kommentartexte pro Paragraph +├── data/ # normen_index.json +├── extract_rii.py # Download + Konvertierung +├── rii_to_markdown.py +├── index_normen.py +└── docs/ + ├── QUELLEN.md + └── EINARBEITUNG.md +``` + +## Server (Strato) + +Persistenz unter `/opt/explore` auf `82.165.92.92` (Domain geplant: +`explore.coded.law`). Details: [INFRASTRUCTURE.md](INFRASTRUCTURE.md). + +```bash +# Bootstrap / ge_xml spiegeln (SSH-Key gitea_deploy) +./sync_to_server.sh +./sync_to_server.sh --xml-only +``` + +## Quellen + +Primär: [rechtsprechung-im-internet.de](https://www.rechtsprechung-im-internet.de/) +(BVerfG und oberste Bundesgerichte ab 2010). Details und Lücken: +[docs/QUELLEN.md](docs/QUELLEN.md). + +Wie neue Entscheidungen den Kommentar treffen: +[docs/EINARBEITUNG.md](docs/EINARBEITUNG.md). diff --git a/data/.gitkeep b/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/data/normen_index.json b/data/normen_index.json new file mode 100644 index 0000000..5bd11ed --- /dev/null +++ b/data/normen_index.json @@ -0,0 +1,420 @@ +{ + "lawgit_root": "/Users/michaelhermann/Source/law/coded/lawgit", + "ge_md": "/Users/michaelhermann/Source/law/coded/explore/ge_md", + "entscheidungen": 9, + "normen": { + "bverfgg/§23": [ + { + "doknr": "KVRE000022442", + "gericht": "BVerfG", + "ebene": "verfg", + "aktenzeichen": "1 BvR 2231/23", + "datum": "2024-07-02", + "path": "bverfg/2024/1-bvr-2231-23.md", + "doktyp": "Nichtannahmebeschluss", + "hat_leitsatz": false, + "roh": "§ 23 Abs 1 S 2 BVerfGG", + "gesetz": "bverfgg", + "paragraph": "23", + "lawgit": "laws_md/bverfgg/§23.md", + "mapped": true + }, + { + "doknr": "KVRE000012442", + "gericht": "BVerfG", + "ebene": "verfg", + "aktenzeichen": "2 BvR 923/24", + "datum": "2024-07-12", + "path": "bverfg/2024/2-bvr-923-24.md", + "doktyp": "Ablehnung einstweilige Anordnung", + "hat_leitsatz": false, + "roh": "§ 23 Abs 1 S 2 BVerfGG", + "gesetz": "bverfgg", + "paragraph": "23", + "lawgit": "laws_md/bverfgg/§23.md", + "mapped": true + } + ], + "bverfgg/§32": [ + { + "doknr": "KVRE000012442", + "gericht": "BVerfG", + "ebene": "verfg", + "aktenzeichen": "2 BvR 923/24", + "datum": "2024-07-12", + "path": "bverfg/2024/2-bvr-923-24.md", + "doktyp": "Ablehnung einstweilige Anordnung", + "hat_leitsatz": false, + "roh": "§ 32 Abs 1 BVerfGG", + "gesetz": "bverfgg", + "paragraph": "32", + "lawgit": "laws_md/bverfgg/§32.md", + "mapped": true + } + ], + "bverfgg/§90": [ + { + "doknr": "KVRE000012542", + "gericht": "BVerfG", + "ebene": "verfg", + "aktenzeichen": "1 BvR 2268/23", + "datum": "2024-11-20", + "path": "bverfg/2024/1-bvr-2268-23.md", + "doktyp": "Kammerbeschluss", + "hat_leitsatz": false, + "roh": "§ 90 BVerfGG", + "gesetz": "bverfgg", + "paragraph": "90", + "lawgit": "laws_md/bverfgg/§90.md", + "mapped": true + }, + { + "doknr": "KVRE000032442", + "gericht": "BVerfG", + "ebene": "verfg", + "aktenzeichen": "1 BvR 875/23", + "datum": "2024-06-20", + "path": "bverfg/2024/1-bvr-875-23.md", + "doktyp": "Prozesskostenhilfebeschluss", + "hat_leitsatz": false, + "roh": "§ 90 BVerfGG", + "gesetz": "bverfgg", + "paragraph": "90", + "lawgit": "laws_md/bverfgg/§90.md", + "mapped": true + } + ], + "bverfgg/§92": [ + { + "doknr": "KVRE000022442", + "gericht": "BVerfG", + "ebene": "verfg", + "aktenzeichen": "1 BvR 2231/23", + "datum": "2024-07-02", + "path": "bverfg/2024/1-bvr-2231-23.md", + "doktyp": "Nichtannahmebeschluss", + "hat_leitsatz": false, + "roh": "§ 92 BVerfGG", + "gesetz": "bverfgg", + "paragraph": "92", + "lawgit": "laws_md/bverfgg/§92.md", + "mapped": true + }, + { + "doknr": "KVRE000012442", + "gericht": "BVerfG", + "ebene": "verfg", + "aktenzeichen": "2 BvR 923/24", + "datum": "2024-07-12", + "path": "bverfg/2024/2-bvr-923-24.md", + "doktyp": "Ablehnung einstweilige Anordnung", + "hat_leitsatz": false, + "roh": "§ 92 BVerfGG", + "gesetz": "bverfgg", + "paragraph": "92", + "lawgit": "laws_md/bverfgg/§92.md", + "mapped": true + } + ], + "inso/§315": [ + { + "doknr": "JURE100055035", + "gericht": "BGH", + "ebene": "bund", + "aktenzeichen": "IX ZB 76/09", + "datum": "2010-01-14", + "path": "bgh/2010/ix-zb-76-09.md", + "doktyp": "Beschluss", + "hat_leitsatz": false, + "roh": "§ 315 InsO", + "gesetz": "inso", + "paragraph": "315", + "lawgit": "laws_md/inso/§315.md", + "mapped": true + } + ], + "inso/§4": [ + { + "doknr": "JURE100055033", + "gericht": "BGH", + "ebene": "bund", + "aktenzeichen": "IX ZB 72/08", + "datum": "2010-01-14", + "path": "bgh/2010/ix-zb-72-08.md", + "doktyp": "Beschluss", + "hat_leitsatz": false, + "roh": "§ 4 InsO", + "gesetz": "inso", + "paragraph": "4", + "lawgit": "laws_md/inso/§4.md", + "mapped": true + } + ], + "jgg/§42": [ + { + "doknr": "JURE100055051", + "gericht": "BGH", + "ebene": "bund", + "aktenzeichen": "2 ARs 569/09", + "datum": "2010-01-13", + "path": "bgh/2010/2-ars-569-09.md", + "doktyp": "Beschluss", + "hat_leitsatz": false, + "roh": "§ 42 Abs 3 JGG", + "gesetz": "jgg", + "paragraph": "42", + "lawgit": "laws_md/jgg/§42.md", + "mapped": true + } + ], + "stgb/§20": [ + { + "doknr": "JURE100055074", + "gericht": "BGH", + "ebene": "bund", + "aktenzeichen": "2 StR 447/09", + "datum": "2010-01-13", + "path": "bgh/2010/2-str-447-09.md", + "doktyp": "Beschluss", + "hat_leitsatz": false, + "roh": "§ 20 StGB", + "gesetz": "stgb", + "paragraph": "20", + "lawgit": "laws_md/stgb/§20.md", + "mapped": true + } + ], + "stgb/§212": [ + { + "doknr": "JURE100055072", + "gericht": "BGH", + "ebene": "bund", + "aktenzeichen": "2 StR 428/09", + "datum": "2010-01-13", + "path": "bgh/2010/2-str-428-09.md", + "doktyp": "Urteil", + "hat_leitsatz": false, + "roh": "§ 212 StGB", + "gesetz": "stgb", + "paragraph": "212", + "lawgit": "laws_md/stgb/§212.md", + "mapped": true + } + ], + "stgb/§224": [ + { + "doknr": "JURE100055074", + "gericht": "BGH", + "ebene": "bund", + "aktenzeichen": "2 StR 447/09", + "datum": "2010-01-13", + "path": "bgh/2010/2-str-447-09.md", + "doktyp": "Beschluss", + "hat_leitsatz": false, + "roh": "§ 224 StGB", + "gesetz": "stgb", + "paragraph": "224", + "lawgit": "laws_md/stgb/§224.md", + "mapped": true + } + ], + "stgb/§227": [ + { + "doknr": "JURE100055072", + "gericht": "BGH", + "ebene": "bund", + "aktenzeichen": "2 StR 428/09", + "datum": "2010-01-13", + "path": "bgh/2010/2-str-428-09.md", + "doktyp": "Urteil", + "hat_leitsatz": false, + "roh": "§ 227 StGB", + "gesetz": "stgb", + "paragraph": "227", + "lawgit": "laws_md/stgb/§227.md", + "mapped": true + } + ], + "stgb/§241": [ + { + "doknr": "JURE100055074", + "gericht": "BGH", + "ebene": "bund", + "aktenzeichen": "2 StR 447/09", + "datum": "2010-01-13", + "path": "bgh/2010/2-str-447-09.md", + "doktyp": "Beschluss", + "hat_leitsatz": false, + "roh": "§ 241 StGB", + "gesetz": "stgb", + "paragraph": "241", + "lawgit": "laws_md/stgb/§241.md", + "mapped": true + } + ], + "zpo/§114": [ + { + "doknr": "KVRE000032442", + "gericht": "BVerfG", + "ebene": "verfg", + "aktenzeichen": "1 BvR 875/23", + "datum": "2024-06-20", + "path": "bverfg/2024/1-bvr-875-23.md", + "doktyp": "Prozesskostenhilfebeschluss", + "hat_leitsatz": false, + "roh": "§ 114 Abs 1 S 1 ZPO", + "gesetz": "zpo", + "paragraph": "114", + "lawgit": "laws_md/zpo/§114.md", + "mapped": true + } + ], + "zpo/§13": [ + { + "doknr": "JURE100055033", + "gericht": "BGH", + "ebene": "bund", + "aktenzeichen": "IX ZB 72/08", + "datum": "2010-01-14", + "path": "bgh/2010/ix-zb-72-08.md", + "doktyp": "Beschluss", + "hat_leitsatz": false, + "roh": "§ 13 ZPO", + "gesetz": "zpo", + "paragraph": "13", + "lawgit": "laws_md/zpo/§13.md", + "mapped": true + }, + { + "doknr": "JURE100055035", + "gericht": "BGH", + "ebene": "bund", + "aktenzeichen": "IX ZB 76/09", + "datum": "2010-01-14", + "path": "bgh/2010/ix-zb-76-09.md", + "doktyp": "Beschluss", + "hat_leitsatz": false, + "roh": "§ 13 ZPO", + "gesetz": "zpo", + "paragraph": "13", + "lawgit": "laws_md/zpo/§13.md", + "mapped": true + } + ], + "zpo/§16": [ + { + "doknr": "JURE100055035", + "gericht": "BGH", + "ebene": "bund", + "aktenzeichen": "IX ZB 76/09", + "datum": "2010-01-14", + "path": "bgh/2010/ix-zb-76-09.md", + "doktyp": "Beschluss", + "hat_leitsatz": false, + "roh": "§ 16 ZPO", + "gesetz": "zpo", + "paragraph": "16", + "lawgit": "laws_md/zpo/§16.md", + "mapped": true + } + ], + "zpo/§251": [ + { + "doknr": "JURE100055033", + "gericht": "BGH", + "ebene": "bund", + "aktenzeichen": "IX ZB 72/08", + "datum": "2010-01-14", + "path": "bgh/2010/ix-zb-72-08.md", + "doktyp": "Beschluss", + "hat_leitsatz": false, + "roh": "§ 251 ZPO", + "gesetz": "zpo", + "paragraph": "251", + "lawgit": "laws_md/zpo/§251.md", + "mapped": true + }, + { + "doknr": "KVRE000012542", + "gericht": "BVerfG", + "ebene": "verfg", + "aktenzeichen": "1 BvR 2268/23", + "datum": "2024-11-20", + "path": "bverfg/2024/1-bvr-2268-23.md", + "doktyp": "Kammerbeschluss", + "hat_leitsatz": false, + "roh": "§ 251 ZPO", + "gesetz": "zpo", + "paragraph": "251", + "lawgit": "laws_md/zpo/§251.md", + "mapped": true + } + ] + }, + "unmapped": [ + { + "doknr": "JURE100055035", + "gericht": "BGH", + "ebene": "bund", + "aktenzeichen": "IX ZB 76/09", + "datum": "2010-01-14", + "path": "bgh/2010/ix-zb-76-09.md", + "doktyp": "Beschluss", + "hat_leitsatz": false, + "roh": "§ 3 EGV 1346/2000", + "gesetz": "", + "paragraph": "", + "lawgit": null, + "mapped": false, + "grund": "kein_lawgit_pfad" + }, + { + "doknr": "KVRE000022442", + "gericht": "BVerfG", + "ebene": "verfg", + "aktenzeichen": "1 BvR 2231/23", + "datum": "2024-07-02", + "path": "bverfg/2024/1-bvr-2231-23.md", + "doktyp": "Nichtannahmebeschluss", + "hat_leitsatz": false, + "roh": "Art 4 Abs 1 GG", + "gesetz": "gg", + "paragraph": "4", + "lawgit": null, + "mapped": false, + "grund": "kein_lawgit_pfad" + }, + { + "doknr": "KVRE000022442", + "gericht": "BVerfG", + "ebene": "verfg", + "aktenzeichen": "1 BvR 2231/23", + "datum": "2024-07-02", + "path": "bverfg/2024/1-bvr-2231-23.md", + "doktyp": "Nichtannahmebeschluss", + "hat_leitsatz": false, + "roh": "Art 4 Abs 2 GG", + "gesetz": "gg", + "paragraph": "4", + "lawgit": null, + "mapped": false, + "grund": "kein_lawgit_pfad" + }, + { + "doknr": "KVRE000012542", + "gericht": "BVerfG", + "ebene": "verfg", + "aktenzeichen": "1 BvR 2268/23", + "datum": "2024-11-20", + "path": "bverfg/2024/1-bvr-2268-23.md", + "doktyp": "Kammerbeschluss", + "hat_leitsatz": false, + "roh": "§ 17 EnSiG", + "gesetz": "ensig", + "paragraph": "17", + "lawgit": null, + "mapped": false, + "grund": "kein_lawgit_pfad" + } + ] +} diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..b305f4c --- /dev/null +++ b/deploy.sh @@ -0,0 +1,60 @@ +#!/bin/bash +# Erstinstallation / Sync von Explor.Code.Law auf dem Strato-VServer. +# Kein Docker, kein Apache — nur Git-Checkout unter /opt/explore. +# ge_xml/ und .env werden nicht gelöscht. +set -euo pipefail + +echo "explore – Server-Sync" +echo "=====================" + +PROJECT_DIR="/opt/explore" +GITEA_URL="https://git.coded.law/michaelkole/explore.git" +REMOTE_BRANCH="main" + +if [ "$(id -u)" -ne 0 ] && [ ! -w "$(dirname "$PROJECT_DIR")" ]; then + echo "Hinweis: Skript typischerweise als root auf dem Server ausführen," + echo " oder lokal sync_to_server.sh für rsync nutzen." +fi + +mkdir -p "$PROJECT_DIR" +cd "$PROJECT_DIR" + +if [ ! -d .git ]; then + if git ls-remote "$GITEA_URL" HEAD &>/dev/null; then + echo "Klone $GITEA_URL → $PROJECT_DIR" + # Verzeichnis kann schon Dateien haben (Bootstrap-rsync) → init + fetch + git init + git remote add origin "$GITEA_URL" 2>/dev/null || git remote set-url origin "$GITEA_URL" + git fetch origin "$REMOTE_BRANCH" + git checkout -B "$REMOTE_BRANCH" "origin/$REMOTE_BRANCH" + else + echo "Gitea-Repo noch nicht erreichbar: $GITEA_URL" + echo "Repo anlegen, pushen, dann dieses Skript erneut ausführen." + echo "Bis dahin: lokal ./sync_to_server.sh nutzen." + exit 1 + fi +else + echo "Aktualisiere tracked Dateien (ohne git clean)…" + git remote set-url origin "$GITEA_URL" 2>/dev/null || true + git fetch origin "$REMOTE_BRANCH" + # Tracked Dateien aktualisieren. Kein reset --hard, kein git clean: + # ge_xml/ und .env bleiben unangetastet. + git checkout "origin/$REMOTE_BRANCH" -- . +fi + +mkdir -p ge_md data ge_xml kommentar + +if [ ! -f .env ] && [ -f env.example ]; then + cp env.example .env + chmod 600 .env + echo ".env aus env.example angelegt" +fi +chmod 600 .env 2>/dev/null || true + +echo "" +echo "Fertig: $PROJECT_DIR" +echo " ge_md: $(find ge_md -name '*.md' 2>/dev/null | wc -l | tr -d ' ') Dateien" +echo " ge_xml: $(find ge_xml -type f 2>/dev/null | wc -l | tr -d ' ') Dateien" +echo " Index: $([ -f data/normen_index.json ] && echo ja || echo fehlt)" +echo "" +echo "Optional Index: python3 index_normen.py --lawgit /opt/lawgit" diff --git a/docs/EINARBEITUNG.md b/docs/EINARBEITUNG.md new file mode 100644 index 0000000..daaa7d2 --- /dev/null +++ b/docs/EINARBEITUNG.md @@ -0,0 +1,75 @@ +# Einarbeitungsmodus + +Neue Informationen **ändern nicht das Gesetz** (das bleibt in Lawgit), +sondern den **Kommentar** zum Paragraphen. Gerichtsentscheidungen aus +`ge_md/` werden in drei Stufen verarbeitet. + +```mermaid +flowchart TD + GE[Neue Entscheidung ge_md] + Fund[1 Fundstelle Index] + Gewicht[2 Gewicht nach Ebene] + Edit[3 Kommentar-Edit] + GE --> Fund + Fund --> Gewicht + Gewicht -->|Revisionskandidat| Edit + Gewicht -->|nur Fundstelle| Stop[Index fertig] +``` + +## Stufe 1 — Fundstelle (immer) + +Aus dem Feld `norm` der RII-XML (im YAML-Kopf als `normen`) werden +zitierte Stellen gelesen und gegen `LAWGIT_ROOT/laws_md//§N.md` +gematcht. + +Werkzeug: `python3 index_normen.py --lawgit ../lawgit` + +Ergebnis: `data/normen_index.json` + +- Treffer → Eintrag unter Schlüssel `bgb/§242` usw. +- Kein Treffer → Liste `unmapped` (manuelle Aliase analog Lawgit + `norm_registry.py`) + +Jeder Index-Eintrag trägt `ebene`, `gericht`, `doknr`, `path`, `doktyp` +und ob ein Leitsatz im Markdown steht. + +## Stufe 2 — Gewicht nach Ebene + +Steuert, ob der Kommentar *bearbeitet* wird oder die Entscheidung nur +als Fundstelle hängt. + +| Ebene | Wert in YAML | Regel | +|-------|--------------|--------| +| **VerfG** | `verfg` | Immer Revisionskandidat. Bindungswirkung nach § 31 BVerfGG; kann die Lesart einer Norm kippen. | +| **Bund** | `bund` | Revisionskandidat, wenn Leitsatz vorhanden oder `doktyp` auf Leitentscheidung / Großen Senat hindeutet; sonst nur Fundstelle. | +| **OLG** | `olg` | Nur Fundstelle, außer **Divergenz** (abweichende OLG-Linien ohne BGH) oder Lücke auf Bundesebene. (Extraktion noch Stub.) | + +Ableitung der Ebene aus `gertyp`: + +- `BVerfG` → `verfg` +- `BGH`, `BVerwG`, `BFH`, `BAG`, `BSG`, `BPatG` → `bund` +- später `OLG …` → `olg` + +## Stufe 3 — Kommentar-Edit (noch nicht implementiert) + +Pro Paragraph eine Datei `kommentar//§N.md` mit Randnummern. + +Jede Einarbeitung soll ein Git-Commit mit Trailer werden: + +``` +RII-Doknr: KORE… +``` + +analog zu `BGBl-Id:` in Lawgit — damit nachvollziehbar bleibt, welche +Entscheidung den Kommentartext bewegt hat. + +Der eigentliche Text (LLM oder Hand) ist der nächste Projektschritt. +Dieser Modus dokumentiert nur die Regeln, damit der Lauf nicht neu +erfunden wird. + +## Praxis-Reihenfolge + +1. `extract_rii.py` — Entscheidungen holen +2. `index_normen.py` — Fundstellen bauen +3. Später: Revisionskandidaten aus dem Index filtern (`ebene=verfg` oder + Bund mit Leitsatz) und Kommentardateien fortschreiben diff --git a/docs/QUELLEN.md b/docs/QUELLEN.md new file mode 100644 index 0000000..c8c52c3 --- /dev/null +++ b/docs/QUELLEN.md @@ -0,0 +1,58 @@ +# Quellen + +## Primär: Rechtsprechung im Internet (RII) + +Portal: [https://www.rechtsprechung-im-internet.de/](https://www.rechtsprechung-im-internet.de/) + +| Ressource | URL | +|-----------|-----| +| Inhaltsverzeichnis | `https://www.rechtsprechung-im-internet.de/rii-toc.xml` | +| DTD | `https://www.rechtsprechung-im-internet.de/dtd/v1/rii-dok.dtd` | +| Dokument | ZIP mit einer XML-Datei pro Entscheidung | + +**Gerichte (amtlich, ausgewählt, anonymisiert, ab 2010):** + +- Bundesverfassungsgericht (BVerfG) +- Bundesgerichtshof (BGH) +- Bundesverwaltungsgericht (BVerwG) +- Bundesfinanzhof (BFH) +- Bundesarbeitsgericht (BAG) +- Bundessozialgericht (BSG) +- Bundespatentgericht (BPatG) + +Nutzung: amtliche Werke, gemeinfrei nach § 5 UrhG. Weiterverwendung der +bereitgestellten Formate ist ausdrücklich vorgesehen. + +Wichtige XML-Felder: `doknr`, `ecli`, `gertyp`, `spruchkoerper`, +`entsch-datum`, `aktenzeichen`, `doktyp`, `norm`, `leitsatz`, `tenor`, +`tatbestand`, `entscheidungsgruende` / `gruende`, `abwmeinung`, `vorinstanz`. + +## Gesetzesbasis: Lawgit + +Geltender Wortlaut und Paragraphenpfade: `LAWGIT_ROOT/laws_md/` +(Default `../lawgit`). Explore kopiert Gesetze nicht. + +## Bewusste Lücken + +### OLG / LG + +RII enthält **keine** systematische OLG-/LG-Sammlung. Das Datenmodell kennt +trotzdem `ebene: olg`. In Bundesentscheidungen speichern wir `vorinstanz` +(oft OLG-Aktenzeichen) schon mit. Ein eigener OLG-Extraktor folgt erst, wenn +eine konkrete Landesquelle feststeht. + +### BVerfG vor 2010 + +RII startet 2010. Ältere BVerfGE / Gerichtssite sind Phase 2. + +### NeuRIS + +Die Testphase unter `testphase.rechtsinformationen.bund.de` bietet eine +JSON-API, der Datensatz ist noch unvollständig. Nicht als Primärquelle; +höchstens später Abgleich zu RII. + +## Volumen und Höflichkeit + +Rund 70–80 000 Entscheidungen. `extract_rii.py` ist wiederaufsetzbar +(überspringt bekannte `doknr`), mit `--court`, `--since`, `--limit` und +Delay (Default 0,4 s). diff --git a/env.example b/env.example new file mode 100644 index 0000000..93f6c7f --- /dev/null +++ b/env.example @@ -0,0 +1,5 @@ +# Server-Env für /opt/explore/.env (nicht committen) +# cp env.example .env && chmod 600 .env + +LAWGIT_ROOT=/opt/lawgit +DOMAIN=explore.coded.law diff --git a/extract_rii.py b/extract_rii.py new file mode 100644 index 0000000..fb52bdc --- /dev/null +++ b/extract_rii.py @@ -0,0 +1,308 @@ +#!/usr/bin/env python3 +""" +Gerichtsentscheidungen von rechtsprechung-im-internet.de holen. + + python3 extract_rii.py --limit 5 --court bgh,bverfg + python3 extract_rii.py --court bgh --since 2020 + python3 extract_rii.py # voller Bestand (lang) + +Wiederaufsetzbar: vorhandene doknr in ge_md/ werden übersprungen. +Roh-ZIP/XML landen in ge_xml/ (gitignoriert). +""" + +from __future__ import annotations + +import argparse +import io +import re +import sys +import time +import zipfile +import xml.etree.ElementTree as ET +from pathlib import Path + +import requests + +from rii_to_markdown import ( + COURT_MAP, + convert_xml_to_markdown_file, + iso_date, + parse_rii_xml, + relative_path, +) + +TOC_URL = "https://www.rechtsprechung-im-internet.de/rii-toc.xml" +UA = "explor-code-law/0.1 (https://git.coded.law; RII Open Data)" +SESSION = requests.Session() +SESSION.headers.update({"User-Agent": UA}) + +# Kurzname → Anzeigewert in TOC +COURT_FILTER: dict[str, tuple[str, ...]] = { + "bverfg": ("BVerfG", "Bundesverfassungsgericht"), + "bgh": ("BGH", "Bundesgerichtshof"), + "bverwg": ("BVerwG", "Bundesverwaltungsgericht"), + "bfh": ("BFH", "Bundesfinanzhof"), + "bag": ("BAG", "Bundesarbeitsgericht"), + "bsg": ("BSG", "Bundessozialgericht"), + "bpatg": ("BPatG", "Bundespatentgericht"), +} + + +def load_toc(url: str = TOC_URL) -> list[dict]: + print(f"Lade Inhaltsverzeichnis: {url}") + resp = SESSION.get(url, timeout=120) + resp.raise_for_status() + root = ET.fromstring(resp.content) + items: list[dict] = [] + for item in root.findall("item"): + def t(tag: str) -> str: + el = item.find(tag) + return (el.text or "").strip() if el is not None else "" + + gericht = t("gericht") + link = t("link") + az = t("aktenzeichen") + datum = iso_date(t("entsch-datum")) + # doknr manchmal im Link / Titel + doknr = t("doknr") + if not doknr and link: + m = re.search(r"/([A-Z]{4}\d+)", link) + if m: + doknr = m.group(1) + if not link: + continue + items.append( + { + "gericht": gericht, + "aktenzeichen": az, + "entsch-datum": datum, + "link": link, + "doknr": doknr, + "title": t("title") or t("titel"), + } + ) + print(f" {len(items)} Einträge") + return items + + +def match_court(gericht: str, wanted: set[str]) -> bool: + if not wanted: + return True + g = gericht or "" + for key in wanted: + aliases = COURT_FILTER.get(key, (key,)) + for a in aliases: + if a.lower() in g.lower() or g.lower() in a.lower(): + return True + # exakter Typ + if g.strip().upper() == a.upper(): + return True + return False + + +def court_keys_from_arg(raw: str | None) -> set[str]: + if not raw: + return set() + keys = set() + for part in raw.split(","): + k = part.strip().lower() + if not k: + continue + if k in COURT_FILTER: + keys.add(k) + elif k.upper() in COURT_MAP: + keys.add(COURT_MAP[k.upper()][1]) + else: + keys.add(k) + return keys + + +def existing_doknrs(ge_md: Path) -> set[str]: + found: set[str] = set() + if not ge_md.is_dir(): + return found + for path in ge_md.rglob("*.md"): + try: + head = path.read_text(encoding="utf-8", errors="replace")[:1200] + except OSError: + continue + m = re.search(r'doknr:\s*"([^"]+)"', head) + if m: + found.add(m.group(1)) + continue + m = re.search(r"doknr:\s*(\S+)", head) + if m: + found.add(m.group(1).strip('"')) + return found + + +def main() -> int: + ap = argparse.ArgumentParser(description="RII-Entscheidungen extrahieren") + ap.add_argument("--toc", default=TOC_URL, help="URL des Inhaltsverzeichnisses") + ap.add_argument( + "--court", + default="", + help="Kommagetrennt: bgh,bverfg,bverwg,bfh,bag,bsg,bpatg", + ) + ap.add_argument("--since", type=int, default=0, help="Nur ab Entscheidungsjahr") + ap.add_argument("--limit", type=int, default=0, help="Max. neu geschriebene Dateien") + ap.add_argument("--delay", type=float, default=0.4, help="Pause zwischen Downloads (s)") + ap.add_argument("--ge-md", default="ge_md") + ap.add_argument("--ge-xml", default="ge_xml") + ap.add_argument("--dry-run", action="store_true") + args = ap.parse_args() + + ge_md = Path(args.ge_md) + ge_xml = Path(args.ge_xml) + wanted = court_keys_from_arg(args.court) + + items = load_toc(args.toc) + known = existing_doknrs(ge_md) + print(f" bereits in ge_md: {len(known)} doknr") + + work: list[dict] = [] + skipped_year = 0 + skipped_court = 0 + skipped_known = 0 + by_court: dict[str, list[dict]] = {k: [] for k in wanted} if wanted else {"_all": []} + + for it in items: + if not match_court(it["gericht"], wanted): + skipped_court += 1 + continue + year_s = (it["entsch-datum"] or "")[:4] + if args.since and year_s.isdigit() and int(year_s) < args.since: + skipped_year += 1 + continue + if it["doknr"] and it["doknr"] in known: + skipped_known += 1 + continue + if wanted: + bucket = next( + (k for k in wanted if match_court(it["gericht"], {k})), + None, + ) + if bucket is None: + continue + by_court[bucket].append(it) + else: + by_court["_all"].append(it) + + # Bei --limit: Gerichte round-robin mischen, damit z. B. bgh,bverfg beide vorkommen + if args.limit and wanted: + per = max(1, (args.limit + len(wanted) - 1) // len(wanted)) + queues = {k: v[: per * 2] for k, v in by_court.items()} + while any(queues.values()) and len(work) < args.limit * 2: + for k in list(queues.keys()): + if queues[k]: + work.append(queues[k].pop(0)) + if len(work) >= args.limit * 2: + break + else: + for v in by_court.values(): + work.extend(v) + + print( + f" Filter: Gericht übersprungen={skipped_court}, " + f"Jahr={skipped_year}, bekannt={skipped_known}, Kandidaten={len(work)}" + ) + if wanted: + print( + " je Gericht: " + + ", ".join(f"{k}={len(by_court[k])}" for k in sorted(by_court)) + ) + + if args.dry_run: + for it in work[: args.limit or 20]: + print(f" would fetch: {it['gericht']} {it['aktenzeichen']} {it['link']}") + return 0 + + written = 0 + failed = 0 + limit = args.limit or 10**9 + + for i, it in enumerate(work): + if written >= limit: + break + link = it["link"] + try: + resp = SESSION.get(link, timeout=60) + resp.raise_for_status() + blob = resp.content + except requests.RequestException as e: + print(f" Fehler {link}: {e}", file=sys.stderr) + failed += 1 + time.sleep(args.delay) + continue + + is_zip = link.endswith(".zip") or blob[:2] == b"PK" + if is_zip: + try: + with zipfile.ZipFile(io.BytesIO(blob)) as zf: + names = [n for n in zf.namelist() if n.endswith(".xml")] + if not names: + failed += 1 + time.sleep(args.delay) + continue + xml_bytes = zf.read(names[0]) + except zipfile.BadZipFile: + failed += 1 + time.sleep(args.delay) + continue + else: + xml_bytes = blob + + parsed = parse_rii_xml(xml_bytes) + if not parsed: + failed += 1 + time.sleep(args.delay) + continue + + doknr = parsed["meta"].get("doknr") or "" + if doknr and doknr in known: + skipped_known += 1 + time.sleep(args.delay) + continue + + # Zielpfad schon da? + rel = relative_path(parsed["meta"]) + target = ge_md / rel + if target.is_file(): + known.add(doknr) + time.sleep(args.delay) + continue + + out = convert_xml_to_markdown_file(xml_bytes, ge_md) + if out is None: + failed += 1 + time.sleep(args.delay) + continue + + # Roh speichern + try: + gertyp = parsed["meta"].get("gericht") or "unbekannt" + court = COURT_MAP.get(gertyp, ("bund", gertyp.lower()))[1] + dest_dir = ge_xml / court + dest_dir.mkdir(parents=True, exist_ok=True) + raw_name = doknr or out.stem + if is_zip: + (dest_dir / f"{raw_name}.zip").write_bytes(blob) + else: + (dest_dir / f"{raw_name}.xml").write_bytes(xml_bytes) + except OSError as e: + print(f" Warnung Rohspeicher: {e}", file=sys.stderr) + + if doknr: + known.add(doknr) + written += 1 + if written % 50 == 0 or written <= 5: + print(f" geschrieben {written}: {out}") + + time.sleep(args.delay) + + print(f"Fertig. neu={written}, Fehler={failed}") + return 0 if failed == 0 or written > 0 else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/ge_md/.gitkeep b/ge_md/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/ge_md/bgh/2010/2-ars-569-09.md b/ge_md/bgh/2010/2-ars-569-09.md new file mode 100644 index 0000000..253e2b0 --- /dev/null +++ b/ge_md/bgh/2010/2-ars-569-09.md @@ -0,0 +1,27 @@ +--- +ebene: "bund" +gericht: "BGH" +spruchkoerper: "2. Strafsenat" +datum: "2010-01-13" +aktenzeichen: "2 ARs 569/09" +doknr: "JURE100055051" +doktyp: "Beschluss" +quelle: "rii" +normen: + - { gesetz: "jgg", paragraph: "42", roh: "§ 42 Abs 3 JGG" } +vorinstanz: [] +--- + +# BGH Beschluss — 2 ARs 569/09 vom 13.01.2010 + +## Titelzeile + +Jugendgerichtsverfahren: Abgabe an das nach Wohnsitzwechsel des Angeklagten zuständige Gericht + +## Tenor + +Für die Untersuchung und Entscheidung der Sache ist das Amtsgericht - Jugendrichter - Nürnberg zuständig. + +## Gründe + +1Die Abgabe durch das Amtsgericht Darmstadt gemäß § 42 Abs. 3 JGG ist zulässig, weil der Angeklagte seinen Wohnsitz Anfang September 2009 und damit nach der Erhebung der Anklage nach Nürnberg verlegt hat (vgl. BGHSt 13, 209, 217). Sie ist auch im Hinblick auf die am Verfahren zu beteiligende Jugendgerichtshilfe des neuen Wohnortes zweckmäßig (vgl. BGH StraFo 2007, 162). Demgegenüber kommt hier dem Umstand, dass Zeugen in Hessen wohnhaft sind, nur eine untergeordnete Bedeutung zu (vgl. Senat, Beschluss vom 7. Februar 2007 -  2 ARs 547/06).Rissing-van Saan                               Fischer                             ApplCierniak                             Krehl diff --git a/ge_md/bgh/2010/2-str-428-09.md b/ge_md/bgh/2010/2-str-428-09.md new file mode 100644 index 0000000..9845a74 --- /dev/null +++ b/ge_md/bgh/2010/2-str-428-09.md @@ -0,0 +1,29 @@ +--- +ebene: "bund" +gericht: "BGH" +spruchkoerper: "2. Strafsenat" +datum: "2010-01-13" +aktenzeichen: "2 StR 428/09" +doknr: "JURE100055072" +doktyp: "Urteil" +quelle: "rii" +normen: + - { gesetz: "stgb", paragraph: "212", roh: "§ 212 StGB" } + - { gesetz: "stgb", paragraph: "227", roh: "§ 227 StGB" } +vorinstanz: + - "vorgehend LG Frankfurt, 10. Juni 2009, Az: 5/22 Ks 5/09 - 3590 Js 242390/08 Kap, Urteil" +--- + +# BGH Urteil — 2 StR 428/09 vom 13.01.2010 + +## Titelzeile + +Bedingter Tötungsvorsatz bei zum Tode führender Körperverletzung + +## Tenor + +Auf die Revision der Staatsanwaltschaft wird das Urteil des Landgerichts Frankfurt am Main vom 10. Juni 2009 mit den Feststellungen aufgehoben.Die Sache wird zu neuer Verhandlung und Entscheidung, auch über die Kosten des Rechtsmittels, an eine andere als Schwurgericht zuständige Strafkammer des Landgerichts zurückverwiesen.Von Rechts wegen + +## Gründe + +1Das Landgericht hat den Angeklagten wegen Körperverletzung mit Todesfolge zu einer Freiheitsstrafe von sechs Jahren verurteilt, zugleich seine Unterbringung in einer Entziehungsanstalt angeordnet und bestimmt, dass ein Jahr der Strafe vor der Maßregel zu vollziehen ist. Die zu Ungunsten des Angeklagten eingelegte und vom Generalbundesanwalt vertretene Revision der Staatsanwaltschaft, die sich vor allem gegen die Ablehnung eines (bedingten) Tötungsvorsatzes wendet, hat mit der Sachrüge Erfolg.I.21. Nach den Feststellungen der Schwurgerichtskammer hielt sich der Angeklagte, bei dem schon im Grundschulalter ein (nicht erfolgreich behandeltes) Aufmerksamkeitsdefizit- und Hyperaktivitäts-Syndrom festgestellt worden war, am 4. Oktober 2008 zwischen 1.00 Uhr und 1.30 Uhr im Bereich der P. schule in F. auf. Auf einem zum Teil unbeleuchteten Verbindungsweg, der zu Sportplätzen und einem Kleingartengelände führt, traf er auf das Tatopfer, einen ihm vom Sehen her bekannten Obdachlosen. Aus nicht näher zu klärenden Gründen gerieten dieser und der Angeklagte, der seiner Gewohnheit entsprechend im Laufe des Tages in größeren Mengen alkoholische Getränke zu sich genommen und mehrere Joints Marihuana geraucht hatte, in einen Streit, der in eine heftige Schlägerei überging. Im Zuge der gewalttätigen, etwa 30 Minuten dauernden Auseinandersetzung, deren Ablauf im Einzelnen nicht geklärt werden konnte, brachte der Angeklagte, der selbst unverletzt blieb, seinem Opfer - u.a. durch Faustschläge und Fußtritte - zahlreiche schwerwiegende Verletzungen bei. Anschließend zerrte er den Schwerverletzten - mit dem Rücken auf dem Boden schleifend - über den Weg, ließ ihn schließlich dort liegen und entfernte sich sodann vom Tatort. Er ging zum in der Nähe gelegenen Wohnhaus eines Freundes, berichtete diesem, er habe sich mit jemandem geschlagen, der "irgendwo da hinten" liege, und veranlasste ihn, mit ihm in Richtung Tatort zu laufen. Als es diesem aber mulmig wurde, weigerte er sich, weiter mit dem Angeklagten ins Dunkle zu gehen, und lief nach Hause zurück. Er ging dabei davon aus, dass der Angeklagte ihm ohnehin eine Lügengeschichte erzählt und niemanden geschlagen habe.3Das Tatopfer wurde am frühen Morgen des 4. Oktober 2008 tot auf dem Verbindungsweg aufgefunden. Es wies zahlreiche Verletzungen am gesamten Körper auf, insbesondere im Gesicht und im Bauchbereich, u.a. eine Mittelgesichtstrümmerfraktur und eine Einblutung in das große Netz mit Rippenserienbrüchen auf beiden Seiten. Ursache des binnen 30 Minuten eingetretenen Todes waren durch einen Jochbeinbruch eingetretene Verletzungen arterieller Gefäße und/oder venöser Geflechte, die zu starker Blutung und schneller Einatmung des Blutes geführt hatten.42. Das Landgericht hat den Angeklagten wegen Körperverletzung mit Todesfolge verurteilt, sich aber an einer Verurteilung wegen Totschlags, auch in Form eines Unterlassens, gehindert gesehen, weil ein (bedingter) Tötungsvorsatz dem Angeklagten, der sein Opfer erheblich verletzen wollte, nicht nachweisbar gewesen sei. Zwar könnten massive Verletzungen, wie der Angeklagte sie seinem Opfer zugefügt habe, den Schluss auf einen bedingten Tötungsvorsatz nahe legen. Angesichts der Besonderheiten in der Persönlichkeitsentwicklung des Angeklagten, seiner alkohol- und drogenbedingten Enthemmung zur Tatzeit und des Umstandes, dass die konkrete Ursache der letztlich zum Tode führenden Gesichtsverletzung nicht zu klären gewesen sei, könne nicht ausgeschlossen werden, dass dem Angeklagten das Ausmaß der Gefährlichkeit seines Handelns nicht bewusst gewesen sei und er den tödlichen Ausgang auch nicht gebilligt habe. Dies gelte auch, soweit der Angeklagte das Tatopfer liegen gelassen habe, ohne Hilfe zu holen. Ob ihm zu diesem Zeitpunkt bewusst gewesen sei, dass dieses unversorgt sterben könnte, und ob ihm dies gegebenenfalls gleichgültig gewesen sei, müsse offen bleiben. Angesichts der persönlichkeitsbedingten Unfähigkeit des Angeklagten, Verantwortung zu empfinden und Konsequenzen seines Handelns zu bedenken, könne nicht ausgeschlossen werden, dass er darauf vertraut habe, das Opfer werde die Verletzungen überleben (UA S. 18).II.5Die Verneinung bedingten Tötungsvorsatzes durch das Landgericht hält sachlich-rechtlicher Prüfung nicht stand.61. Spricht der Tatrichter einen Angeklagten frei oder sieht er von einer weiter reichenden Verurteilung ab, weil er Zweifel nicht zu überwinden vermag, so ist dies durch das Revisionsgericht in der Regel hinzunehmen. Dieses hat insoweit nur zu beurteilen, ob dem Tatrichter bei der Beweiswürdigung Rechtsfehler unterlaufen sind. Das ist dann der Fall, wenn die Beweiswürdigung widersprüchlich, unklar oder lückenhaft ist, gegen Denkgesetze oder gesicherte Erfahrungssätze verstößt oder wenn an die zur Verurteilung erforderliche Gewissheit überspannte Anforderungen gestellt worden sind (st. Rspr.; vgl. BGH NStZ-RR 2004, 238; 2005, 147).72. Die Beweiswürdigung, mit der das Landgericht einen Tötungsvorsatz hinsichtlich der aktiven Verletzungshandlung des Angeklagten verneint hat, ist rechtsfehlerhaft.8a) Das Landgericht hat bereits das Wissenselement eines bedingten Tötungsvorsatzes nicht feststellen können (UA S. 18). Die Beweiswürdigung hierzu ist lückenhaft. Keiner der drei zum Ausschluss des Wissenselementes vorgebrachten Umstände vermag dieses Ergebnis ohne nähere Erläuterung zu tragen. Die "Besonderheiten in der Persönlichkeitsentwicklung des Angeklagten" beschreiben Verhaltensauffälligkeiten und Eigenschaften, die im Laufe der Zeit - ausgehend von einer hyperkinetischen Störung (ICD-10 F90.1) - zu einer dissozialen Entwicklung des Angeklagten (F60.2) geführt haben (UA S. 3 f., 16). Anhaltspunkte dafür, dass sich das dem Angeklagten nach diesem Befund eigene Verhaltensmuster auf seine Erkenntnisfähigkeit ausgewirkt haben könnte, teilt das Urteil nicht mit. So lässt sich für das Revisionsgericht nicht nachvollziehen, ob der beschriebenen Persönlichkeitsentwicklung überhaupt Einfluss auf das Wissenselement beim Vorsatz zukommen kann.9Auch die "alkohol- und drogenbedingte Enthemmung des Angeklagten" zur Tatzeit ergibt nicht ohne Weiteres einen Anhalt dafür, dass ihm das Ausmaß der Gefährlichkeit seines Handelns nicht bewusst gewesen sein könnte. Nur in Fällen außergewöhnlich hoher Alkohol- und Drogenintoxikation liegt es auf der Hand, dass es neben der Beeinträchtigung des Hemmungsvermögens auch zu einer Einschränkung der Wahrnehmungsfähigkeit kommen kann. Insoweit wäre das Landgericht gehalten gewesen, näher darzulegen, dass beim Angeklagten - trotz des Umstands, dass man seinen übertriebenen Trinkmengenangaben keinen Glauben geschenkt hat (UA S. 16) - ein solcher auch die Erkenntnisfähigkeit beeinträchtigender Zustand vorgelegen haben könnte. Dabei hätte es sich auch damit auseinandersetzen müssen, dass nach der Rechtsprechung des Bundesgerichtshofs jedenfalls bei schweren und lang andauernden Gewalthandlungen die Erkenntnisfähigkeit trotz alkoholbedingter Bewusstseinstrübung kaum fraglich sein kann (vgl. BGH NStZ-RR 1997, 296).10Auch erlaubt der Umstand, dass das Landgericht eine konkrete Ursache für die zum Tode führenden Gesichtsverletzungen nicht feststellen konnte, einen Rückschluss darauf nicht, der Angeklagte habe die Gefährlichkeit der beim Tatopfer eingetretenen, zum Tode führenden Gesichtsverletzungen nicht wahrgenommen. Das Landgericht hat - sachverständig beraten - ausgeschlossen, dass das Opfer ohne Zutun des Angeklagten mehrmals hintereinander mit dem Gesicht auf das Geländer gestürzt sein und sich dadurch verletzt haben könnte. Es ist rechtsfehlerfrei davon ausgegangen, dass die Gesichtsverletzungen dem Opfer durch mehrfache Gewalteinwirkung seitens des Angeklagten beigebracht worden sind, wobei die konkrete Art der Verursachung offen geblieben ist (UA S. 15). Warum der Angeklagte trotz der Feststellung, dass die Verletzungen jedenfalls auf konkreten, jeweils für sich lebensgefährlichen Gewalttätigkeiten des Angeklagten beruhen, gleichwohl die Gefährlichkeit seines Handelns nicht erkannt haben soll, erschließt sich deshalb nicht.11Schließlich ergibt sich auch unter zusammenfassender Betrachtung aller Gesichtspunkte keine tragfähige Grundlage für die Ablehnung des kognitiven Vorsatzelements. Auch soweit das Landgericht letztlich lediglich nicht ausgeschlossen hat, dass dem Angeklagten das Ausmaß der Gefährlichkeit seines Handelns nicht bewusst gewesen sei, fehlt es an der erforderlichen umfassenden Würdigung aller subjektiven und objektiven Tatumstände (vgl. dazu Fischer, StGB, 57. Aufl., 2010, § 212, Rdn. 7 m.w.N.). Das Landgericht hätte zusätzlich etwa den Umstand erörtern müssen, dass der Angeklagte nach Verlassen des Tatortes einen Freund aufgesucht hat und mit diesem zu dem am Boden liegend zurückgelassenen Tatopfer zurückkehren wollte. Mit der sich danach aufdrängenden Überlegung, der Angeklagte habe erkannt oder zumindest mit der Möglichkeit gerechnet, dass sich das Opfer in einer hilfsbedürftigen, lebensbedrohlichen Lage befand, hat sich das Landgericht nicht auseinandergesetzt.12b) Das Schwurgericht hat aufgrund der genannten Umstände auch nicht ausschließen können, dass der Angeklagte den tatsächlichen Ausgang seines Handelns nicht gebilligt habe. Auch hinsichtlich dieser (wohl zusätzlich für den Fall des Vorliegens des Wissenselements angestellten) Erwägung fehlt es an der gebotenen umfassenden Abwägung sämtlicher die Tat und die Persönlichkeit des Angeklagten betreffender Umstände.133. Die fehlerhafte Beweiswürdigung zum bedingten Vorsatz hinsichtlich der gegen das Tatopfer gerichteten Gewalthandlung des Angeklagten führt zur Aufhebung und Zurückverweisung, ohne dass es noch auf die weiter erhobenen Einwände gegen die Ablehnung eines versuchten Totschlags durch Unterlassen und die unterlassene Prüfung einer Strafbarkeit nach § 221 StGB ankommt.Rissing-van Saan                                       Fischer                                         ApplCierniak                                          Krehl diff --git a/ge_md/bgh/2010/2-str-447-09.md b/ge_md/bgh/2010/2-str-447-09.md new file mode 100644 index 0000000..4f82ab8 --- /dev/null +++ b/ge_md/bgh/2010/2-str-447-09.md @@ -0,0 +1,30 @@ +--- +ebene: "bund" +gericht: "BGH" +spruchkoerper: "2. Strafsenat" +datum: "2010-01-13" +aktenzeichen: "2 StR 447/09" +doknr: "JURE100055074" +doktyp: "Beschluss" +quelle: "rii" +normen: + - { gesetz: "stgb", paragraph: "20", roh: "§ 20 StGB" } + - { gesetz: "stgb", paragraph: "224", roh: "§ 224 StGB" } + - { gesetz: "stgb", paragraph: "241", roh: "§ 241 StGB" } +vorinstanz: + - "vorgehend LG Mühlhausen, 24. Juni 2009, Az: 120 Js 58726/08 - 1 Ks, Urteil" +--- + +# BGH Beschluss — 2 StR 447/09 vom 13.01.2010 + +## Titelzeile + +Strafverfahren wegen Bedrohung und gefährlicher Körperverletzung: Prüfung der Schuldunfähigkeit bei hoher Blutalkoholkonzentration und nicht nachvollziehbarem Tatmotiv + +## Tenor + +Auf die Revision des Angeklagten wird das Urteil des Landgerichts Mühlhausen vom 24. Juni 2009 mit den Feststellungen aufgehoben.Die Sache wird zu neuer Verhandlung und Entscheidung, auch über die Kosten des Rechtsmittels, an eine andere Strafkammer des Landgerichts zurückverwiesen. + +## Gründe + +1Das Landgericht hat den Angeklagten wegen Bedrohung und gefährlicher Körperverletzung zu einer Gesamtfreiheitsstrafe von drei Jahren verurteilt. Die Revision des Angeklagten hat mit der allgemeinen Sachrüge Erfolg.21. Nach den Feststellungen spielten der Angeklagte, das spätere Tatopfer und ein Dritter am Tattag seit dem frühen Morgen im Haus des Geschädigten Karten und konsumierten dabei erhebliche Mengen Alkohol. Gegen 16.30 Uhr fütterte der Angeklagte eine fremde Katze aus der Nachbarschaft, worüber der Geschädigte seinen Unmut zum Ausdruck brachte. Daraufhin verließ der Angeklagte das Haus, holte aus dem Hof eine Axt und äußerte ins Wohnzimmer zurückkehrend gegenüber seinen Mitspielern, er werde sie totschlagen. Anschließend griff er, die Axt schwingend, den Geschädigten an, dem - von zwei Schlägen nur gestreift - leicht verletzt die Flucht zu einem Nachbarhaus gelang, von wo aus er die Polizei verständigte. Währenddessen setzte sich der Angeklagte wortlos zu dem dritten im Wohnzimmer verbliebenen Spieler und wartete auf das Eintreffen der Polizei. Eine ihm um 18.00 Uhr entnommene Blutprobe ergab eine BAK von 2,91 Promille.32. Das Landgericht ist sachverständig beraten davon ausgegangen, der Angeklagte sei infolge des zuvor genossenen Alkohols aufgrund eines mittelgradigen Rausches nur erheblich vermindert in der Lage gewesen, sein Verhalten zu steuern. Mit der Frage einer möglichen Schuldunfähigkeit gemäß § 20 StGB hat sich die Strafkammer nicht auseinandergesetzt. Dies begegnet durchgreifenden rechtlichen Bedenken. Rückgerechnet auf den Tatzeitpunkt wies der Angeklagte - was das Landgericht nicht bedacht hat - eine BAK von 3,41 Promille auf. Bei einer solch hohen Blutalkoholkonzentration ist regelmäßig die Prüfung einer Aufhebung der Schuldfähigkeit veranlasst (Fischer, StGB 57. Aufl. § 20 Rdn. 19 ff. m.w.N.). Zu einer solchen Prüfung bestand hier auch Veranlassung im Hinblick auf das kaum nachvollziehbare Tatmotiv, das ungewöhnliche Nachtatverhalten und angesichts des Umstandes, dass der nicht vorbestrafte Angeklagte weder generell zu Wut- und Aggressionsausbrüchen neigt noch sonst für aggressive Entgleisungen bekannt ist (UA 9).43. Die aufgezeigte Lückenhaftigkeit des Urteils führt zu dessen Aufhebung und Zurückverweisung. Der neue Tatrichter wird - sollte er erneut zu einer entsprechenden Verurteilung kommen - zu bedenken haben, dass bei Verhängung von einzelnen Geldstrafen, die in einer Gesamtstrafe aufgehen, neben der Anzahl der Tagessätze auch immer die Tagessatzhöhe festzusetzen ist (BGHSt 30, 93, 96; BGHR StGB § 54 Abs. 3 Tagessatzhöhe 1).Rissing-van Saan                                      Fischer                                       ApplCierniak                                        Krehl diff --git a/ge_md/bgh/2010/ix-zb-72-08.md b/ge_md/bgh/2010/ix-zb-72-08.md new file mode 100644 index 0000000..5ef17a0 --- /dev/null +++ b/ge_md/bgh/2010/ix-zb-72-08.md @@ -0,0 +1,30 @@ +--- +ebene: "bund" +gericht: "BGH" +spruchkoerper: "9. Zivilsenat" +datum: "2010-01-14" +aktenzeichen: "IX ZB 72/08" +doknr: "JURE100055033" +doktyp: "Beschluss" +quelle: "rii" +normen: + - { gesetz: "inso", paragraph: "4", roh: "§ 4 InsO" } + - { gesetz: "zpo", paragraph: "13", roh: "§ 13 ZPO" } + - { gesetz: "zpo", paragraph: "251", roh: "§ 251 ZPO" } +vorinstanz: + - "vorgehend LG Aachen, 17. März 2008, Az: 6 T 104/07, Beschlussvorgehend AG Aachen, 13. Februar 2007, Az: 92 IN 9/07" +--- + +# BGH Beschluss — IX ZB 72/08 vom 14.01.2010 + +## Titelzeile + +Insolvenzverfahren: Anwendbarkeit der Vorschrift über das Ruhen des Verfahrens; Beschwerderecht des Insolvenzverwalters im Zusammenhang mit der Verfahrenseröffnung + +## Tenor + +Der Antrag des weiteren Beteiligten zu 2 auf Bewilligung von Prozesskostenhilfe wird abgelehnt.Die Rechtsbeschwerde gegen den Beschluss der 6. Zivilkammer des Landgerichts Aachen vom 17. März 2008 wird auf Kosten der Schuldnerin als unzulässig verworfen.Der Gegenstandswert des Rechtsbeschwerdeverfahrens wird auf 50.000 € festgesetzt. + +## Gründe + +11. Einer Entscheidung steht nicht entgegen, dass die Schuldnerin das Ruhen des Verfahrens beantragt hat. Die Vorschrift über das Ruhen des Verfahrens (§ 251 ZPO) ist im grundsätzlich eilbedürftigen, auf eine rasche Befriedigung der Gläubiger angelegten Insolvenzverfahren und damit auch im Verfahren über eine insolvenzrechtliche Rechtsbeschwerde nicht anwendbar (MünchKomm-InsO/Ganter, 2. Aufl. § 4 Rn. 15; Jaeger/Gerhardt, InsO § 4 Rn. 57; HK-InsO/Kirchhof, 5. Aufl. § 4 Rn. 25; HmbKomm-InsO/Rüther, 3. Aufl. § 4 Rn. 57). Im Übrigen hat die weitere Beteiligte zu 1, die als Gläubigerin die Eröffnung des Insolvenzverfahrens beantragt hat, als Rechtsbeschwerdegegnerin dem Antrag nicht zugestimmt.22. Dem weiteren Beteiligten zu 2 kann nach § 4 InsO in Verbindung mit §§ 114 ff ZPO keine Prozesskostenhilfe gewährt werden, weil er im Rechtsbeschwerdeverfahren gegen die Eröffnung des Insolvenzverfahrens keine eigenen Rechte verfolgen kann. Im Ausgangspunkt kann Prozesskostenhilfe nur der "Partei" gewährt werden (§ 114 Abs. 1 Satz 1 ZPO). Dieser Begriff ist zwar weit auszulegen (vgl. Musielak/Fischer, ZPO 7. Aufl. § 114 Rn. 2); es ist deshalb anerkannt, dass die Vorschrift auch Antragsteller, Antragsgegner sowie die Streithelfer der Parteien erfasst. Der weitere Beteiligte zu 2 gehört im Streitfall als Insolvenzverwalter indes nicht zu diesem Personenkreis (vgl. MünchKomm-InsO/Ganter, aaO § 4 Rn. 21; Jaeger/Gerhardt, aaO § 4 Rn. 48). Er ist nicht berechtigt, die Eröffnung des Insolvenzverfahrens zu beantragen (§ 13 Abs. 1 InsO). Auch ein Beschwerderecht räumt ihm die Insolvenzordnung im Zusammenhang mit der Verfahrenseröffnung nicht ein (BGH, Beschl. v. 8. März 2007 - IX ZB 163/06, ZIP 2007, 792).33. Die Rechtsbeschwerde ist statthaft (§§ 7, 6, 34 Abs. 2 Satz 1 InsO, § 574 Abs. 1 Satz 1 Nr. 1 ZPO), aber unzulässig. Weder hat die Rechtssache grundsätzliche Bedeutung noch erfordert die Fortbildung des Rechts oder die Sicherung einer einheitlichen Rechtsprechung eine Entscheidung des Rechtsbeschwerdegerichts (§ 574 Abs. 2 ZPO).4a) Die von der Rechtsbeschwerde für grundsätzlich erachtete Frage, ob eine nach dem Ausscheiden ihres vorletzten Gesellschafters erloschene, aber noch im Handelsregister eingetragene Kommanditgesellschaft nach dem Rechtsgedanken des § 15 HGB weiterhin insolvenzfähig ist, bedarf im Streitfall keiner Entscheidung, weil das Beschwerdegericht den Austritt der Gesellschafter aus der KG ohne zulassungsrelevanten Rechtsfehler als unwirksam nach § 117 BGB beurteilt hat; seine Rechtsauffassung zu § 15 HGB war daher nicht entscheidungserheblich.5b) Auch die Ausführungen des Beschwerdegerichts zum rechtlichen Interesse der weiteren Beteiligten zu 1 an der Eröffnung des Insolvenzverfahrens (§ 14 Abs. 1 InsO) begründen nicht die Zulässigkeit der Rechtsbeschwerde. Dass die Vorinstanzen den zugrunde liegenden Sachverhalt insoweit anders subsumiert haben als der zuständige Abteilungsrichter im Insolvenzverfahren betreffend das Vermögen des L. N., berührt nicht die Einheitlichkeit der Rechtsprechung i.S.v. § 574 Abs. 2 Nr. 2 ZPO (BGH, Beschl. v. 16. September 2003 - XI ZR 238/02, NJW 2004, 1167).6c) Dahinstehen kann schließlich, ob das Beschwerdegericht die Forderung der U. GmbH gegen die Schuldnerin bei der Beurteilung des Eröffnungsgrundes der Zahlungsunfähigkeit trotz der Erklärung des "Generalbevollmächtigten" der U. GmbH betreffend eine angebliche Stundung dieser Forderung berücksichtigen durfte. Denn die Beurteilung des Beschwerdegerichts zur Zahlungsunfähigkeit der Schuldnerin wird bereits durch die unstreitig bestehende Umsatzsteuerforderung des Finanzamts G. getragen. Die von der Rechtsbeschwerde insoweit gerügte Verletzung des rechtlichen Gehörs (Art. 103 Abs. 1 GG) liegt nicht vor.Ganter                                Gehrlein                                    VillFischer                                    Grupp diff --git a/ge_md/bgh/2010/ix-zb-76-09.md b/ge_md/bgh/2010/ix-zb-76-09.md new file mode 100644 index 0000000..149ae46 --- /dev/null +++ b/ge_md/bgh/2010/ix-zb-76-09.md @@ -0,0 +1,31 @@ +--- +ebene: "bund" +gericht: "BGH" +spruchkoerper: "9. Zivilsenat" +datum: "2010-01-14" +aktenzeichen: "IX ZB 76/09" +doknr: "JURE100055035" +doktyp: "Beschluss" +quelle: "rii" +normen: + - { gesetz: "inso", paragraph: "315", roh: "§ 315 InsO" } + - { gesetz: "zpo", paragraph: "13", roh: "§ 13 ZPO" } + - { gesetz: "zpo", paragraph: "16", roh: "§ 16 ZPO" } + - { gesetz: "", paragraph: "", roh: "§ 3 EGV 1346/2000" } +vorinstanz: + - "vorgehend LG München I, 14. Februar 2009, Az: 14 T 352/09, Beschlussvorgehend AG München, 2. Oktober 2008, Az: 1542 IN 2678/08" +--- + +# BGH Beschluss — IX ZB 76/09 vom 14.01.2010 + +## Titelzeile + +Nachlassinsolvenzverfahren: Örtliche Zuständigkeit des Insolvenzgerichts; grenzüberschreitende Insolvenz; wohnsitzlose Person als Erblasser + +## Tenor + +Die Rechtsbeschwerde gegen den Beschluss der 14. Zivilkammer des Landgerichts München I vom 14. Februar 2009 wird auf Kosten des Antragstellers als unzulässig verworfen.Der Gegenstandswert des Rechtsbeschwerdeverfahrens wird auf 50.000 € festgesetzt. + +## Gründe + +1Die Rechtsbeschwerde ist statthaft (§ 574 Abs. 1 Satz 1 Nr. 1 ZPO in Verbindung mit §§ 6, 7, 34 Abs. 1 InsO), jedoch unzulässig. Die nach § 574 Abs. 2 ZPO geltend gemachten Zulässigkeitsgründe liegen nicht vor; weder hat die Rechtssache grundsätzliche Bedeutung noch erfordert die Sicherung einer einheitlichen Rechtsprechung eine Entscheidung des Rechtsbeschwerdegerichts.21. Die Frage der örtlichen Zuständigkeit des Amtsgerichts München für die Eröffnung des Nachlassinsolvenzverfahrens wäre schon nicht entscheidungserheblich, wenn die internationale Zuständigkeit der deutschen Gerichte nicht gegeben wäre. Wäre § 3 EuInsVO für das Nachlassinsolvenzverfahren anwendbar (nach wohl herrschender Meinung ist dies zu bejahen; vgl. HK-InsO/Marotzke, 5. Aufl. § 315 Rn. 8 m.w.N.), schiede die internationale Zuständigkeit der deutschen Gerichte aus, weil der Schuldner den Mittelpunkt seiner hauptsächlichen Interessen in Spanien hatte. Ein Zulässigkeitsgrund wird insoweit nicht dargelegt.32. Ist § 3 EuInsVO nicht anwendbar, wird für die internationale und örtliche Zuständigkeit § 315 InsO maßgebend. Ausschließlich zuständig ist danach das Insolvenzgericht, in dessen Bezirk der Erblasser zur Zeit seines Todes seinen allgemeinen Gerichtsstand hatte. Dies richtet sich gemäß § 13 ZPO nach dem Wohnsitz. Bei wohnsitzlosen Personen ist gemäß § 16 ZPO allgemeiner Gerichtsstand der Aufenthaltsort im Inland, wenn ein solcher nicht bekannt ist der Ort des letzten Wohnsitzes. Hat die Person einen Wohnsitz im Ausland, ist dagegen § 16 ZPO nicht anwendbar, und es besteht keine inländische Zuständigkeit (MünchKomm-InsO/Siegmann, 2. Aufl. § 315 Rn. 8; MünchKomm-InsO/Ganter, aaO § 3 Rn. 17). Das Beschwerdegericht hat einen Wohnsitz im Ausland bejaht.4Die Frage, wann jemand wohnsitzlos im Sinne des § 16 ZPO ist, hat keine grundsätzliche Bedeutung. Es ist geklärt, wann ein Wohnsitz vorliegt. Der Wohnsitz wird gemäß § 7 Abs. 1 BGB dort begründet, wo sich eine Person ständig niederlässt. Erforderlich ist eine tatsächliche Niederlassung mit dem Willen, den Ort zum ständigen Schwerpunkt der Lebensverhältnisse zu machen. Die Niederlassung erfordert eine eigene Unterkunft, ein Obdachloser hat keinen Wohnsitz. Es genügt jedoch eine behelfsmäßige Unterkunft. Der Domizilwille muss darauf gerichtet sein, den Ort zum ständigen Schwerpunkt der Lebensverhältnisse zu machen. Dies kann sich aus den Umständen ergeben. Beides war hier der Fall. Es ist nicht erkennbar, dass das Landgericht den Begriff verkannt hätte. Es hat auch nicht Obdachlosigkeit mit Wohnsitzlosigkeit verwechselt, sondern lediglich angedeutet, dass Obdachlose keinen Wohnsitz haben.5Aus der öffentlichen Zustellung des vom Antragsteller erwirkten Versäumnisurteils ergibt sich schon deshalb nichts anderes, weil völlig offen ist, ob damals die hierfür erforderlichen Voraussetzungen tatsächlich vorgelegen haben und sich zwischenzeitlich auch nichts geändert hat.63. Eine Verletzung des rechtlichen Gehörs des Antragstellers liegt nicht darin, dass das Beschwerdegericht kein Partikularinsolvenzverfahren eröffnet hat. Die Eröffnung eines solchen Verfahrens war nicht - auch nicht hilfsweise - beantragt worden. Dessen Voraussetzungen hatte der Gläubiger auch nicht dargelegt, sein Interesse hieran nicht glaubhaft gemacht. Das Vorliegen inländischen Vermögens wurde nicht substantiiert dargelegt. Die Grundstücke liegen in Spanien und stehen im Eigentum spanischer Gesellschaften. Ob auf die Gesellschaftsanteile des Schuldners zugegriffen werden kann und ob sie werthaltig sind, ist unklar. Lediglich eine geschäftsführend tätige Gesellschaft soll ihren Sitz in B. S. haben.7Eine örtliche Zuständigkeit des Amtsgerichts München besteht hierfür jedenfalls nicht (vgl. § 354 Abs. 3 InsO; Art. 102 § 1 Abs. 2 und 3 EGInsO). Der Gläubiger hatte auch keinen Verweisungsantrag gemäß § 281 ZPO, § 4 InsO gestellt.Ganter                                  Gehrlein                                  VillLohmann                                    Fischer diff --git a/ge_md/bverfg/2024/1-bvr-2231-23.md b/ge_md/bverfg/2024/1-bvr-2231-23.md new file mode 100644 index 0000000..390c6fd --- /dev/null +++ b/ge_md/bverfg/2024/1-bvr-2231-23.md @@ -0,0 +1,32 @@ +--- +ebene: "verfg" +gericht: "BVerfG" +spruchkoerper: "1. Senat 3. Kammer" +datum: "2024-07-02" +aktenzeichen: "1 BvR 2231/23" +ecli: "ECLI:DE:BVerfG:2024:rk20240702.1bvr223123" +doknr: "KVRE000022442" +doktyp: "Nichtannahmebeschluss" +quelle: "rii" +normen: + - { gesetz: "gg", paragraph: "4", roh: "Art 4 Abs 1 GG" } + - { gesetz: "gg", paragraph: "4", roh: "Art 4 Abs 2 GG" } + - { gesetz: "bverfgg", paragraph: "23", roh: "§ 23 Abs 1 S 2 BVerfGG" } + - { gesetz: "bverfgg", paragraph: "92", roh: "§ 92 BVerfGG" } +vorinstanz: + - "vorgehend BAG, 25. April 2023, Az: 9 AZR 254/22, Urteil" +--- + +# BVerfG Nichtannahmebeschluss — 1 BvR 2231/23 vom 02.07.2024 + +## Titelzeile + +Nichtannahmebeschluss: Erfolglose Verfassungsbeschwerde bzgl der Arbeitnehmereigenschaft von Sevaka-Mitgliedern eines Yoga- und Meditationszentrums - religiöse Prägung der Dienstleistung weder dargelegt noch ersichtlich + +## Tenor + +Die Verfassungsbeschwerde wird nicht zur Entscheidung angenommen, weil sie den gesetzlichen Darlegungsanforderungen nicht gerecht wird (§ 23 Abs. 1 Satz 2, § 92 BVerfGG) und es an einem Annahmegrund fehlt (§ 93a Abs. 1 BVerfGG, vgl. BVerfGE 111, 1 <4 f.>). Es kann dahinstehen, ob die Annahme des Bundesarbeitsgerichts, bei dem Beschwerdeführer, einem „(…)“-Verein, handele es sich nicht um eine Religions- oder Weltanschauungsgemeinschaft, mit Artikel 4 Absatz 1 und 2 Grundgesetz vereinbar ist. Denn es ist weder dargelegt noch sonst ersichtlich, dass die von der Klägerin des Ausgangsverfahrens geleisteten Dienste der Aufrechterhaltung des Beherbergungs- und Seminarbetriebs des Vereins und des Vertriebs von Yoga-Produkten, um deren arbeitsrechtliche Beurteilung es hier geht, für sich genommen religiös geprägt waren.Von einer weiteren Begründung der Nichtannahme der Verfassungsbeschwerde zur Entscheidung wird nach § 93d Abs. 1 Satz 3 BVerfGG abgesehen. + +## Gründe + +Diese Entscheidung ist unanfechtbar. diff --git a/ge_md/bverfg/2024/1-bvr-2268-23.md b/ge_md/bverfg/2024/1-bvr-2268-23.md new file mode 100644 index 0000000..4e4e58c --- /dev/null +++ b/ge_md/bverfg/2024/1-bvr-2268-23.md @@ -0,0 +1,31 @@ +--- +ebene: "verfg" +gericht: "BVerfG" +spruchkoerper: "1. Senat 2. Kammer" +datum: "2024-11-20" +aktenzeichen: "1 BvR 2268/23" +ecli: "ECLI:DE:BVerfG:2024:rk20241120.1bvr226823" +doknr: "KVRE000012542" +doktyp: "Kammerbeschluss" +quelle: "rii" +normen: + - { gesetz: "bverfgg", paragraph: "90", roh: "§ 90 BVerfGG" } + - { gesetz: "zpo", paragraph: "251", roh: "§ 251 ZPO" } + - { gesetz: "ensig", paragraph: "17", roh: "§ 17 EnSiG" } +vorinstanz: + - "vorgehend BVerwG, 2. November 2023, Az: 8 A 3/23, Beschlussvorgehend BVerwG, 14. März 2023, Az: 8 A 2/22, Urteil" +--- + +# BVerfG Kammerbeschluss — 1 BvR 2268/23 vom 20.11.2024 + +## Titelzeile + +Anordnung des Ruhens eines Verfassungsbeschwerdeverfahrens + +## Tenor + +Das Ruhen des Verfahrens wird angeordnet. + +## Gründe + +I.1Die Beschwerdeführerinnen haben sich mit ihrer Verfassungsbeschwerde gegen die Anordnung einer Treuhandverwaltung gemäß § 17 EnSiG und gegen die Entscheidungen des Bundesverwaltungsgerichts, in denen die Klage der Beschwerdeführerinnen gegen die Anordnung der Treuhandverwaltung sowie deren Anhörungsrüge zurückgewiesen wurden, gewandt.2Mittlerweile haben die Beschwerdeführerinnen das Ruhen des Verfahrens beantragt und Gründe vorgetragen, aus denen sich ergibt, dass sich die Bedeutung des vorliegenden Verfahrens für die Beschwerdeführerinnen in den kommenden Monaten grundlegend verändern könnte. Die Bundesregierung, vertreten durch das Bundesministerium für Wirtschaft und Klimaschutz, befürwortet die Anordnung des Ruhens des Verfahrens.II.3Das Ruhen des Verfahrens ist in entsprechender Anwendung von § 251 ZPO anzuordnen (vgl. - ohne Nennung von § 251 ZPO - BVerfGE 89, 327 <328>). Das von den Beschwerdeführerinnen beantragte Ruhen ist zweckmäßig; öffentliche Interessen an der Fortführung des Verfahrens stehen nicht entgegen. diff --git a/ge_md/bverfg/2024/1-bvr-875-23.md b/ge_md/bverfg/2024/1-bvr-875-23.md new file mode 100644 index 0000000..eba4e57 --- /dev/null +++ b/ge_md/bverfg/2024/1-bvr-875-23.md @@ -0,0 +1,30 @@ +--- +ebene: "verfg" +gericht: "BVerfG" +spruchkoerper: "1. Senat 3. Kammer" +datum: "2024-06-20" +aktenzeichen: "1 BvR 875/23" +ecli: "ECLI:DE:BVerfG:2024:rk20240620.1bvr087523" +doknr: "KVRE000032442" +doktyp: "Prozesskostenhilfebeschluss" +quelle: "rii" +normen: + - { gesetz: "bverfgg", paragraph: "90", roh: "§ 90 BVerfGG" } + - { gesetz: "zpo", paragraph: "114", roh: "§ 114 Abs 1 S 1 ZPO" } +vorinstanz: + - "vorgehend SG Münster, 21. März 2023, Az: S 11 SF 21/23 E RG, Beschlussvorgehend SG Münster, 12. Januar 2023, Az: S 11 SF 66/22 E, Beschlussvorgehend SG Münster, 22. September 2023, Az: S 11 AS 529/14, Kostenfestsetzungsbeschluss" +--- + +# BVerfG Prozesskostenhilfebeschluss — 1 BvR 875/23 vom 20.06.2024 + +## Titelzeile + +Ablehnung eines isolierten PKH-Antrags für eine noch zu erhebende Verfassungsbeschwerde - mangelnde Darlegungen zur Erforderlichkeit der PKH-Gewährung sowie zu den Erfolgsaussichten der Rechtsverfolgung + +## Tenor + +Der Antrag auf Bewilligung von Prozesskostenhilfe und Beiordnung eines Rechtsanwalts für die beabsichtigte Verfassungsbeschwerde gegen den Kostenfestsetzungsbeschluss des Sozialgerichts Münster vom 22. September 2022 - S 11 AS 529/14 -, den Beschluss des Sozialgerichts Münster vom 12. Januar 2023 - S 11 SF 66/22 E - und den Beschluss des Sozialgerichts Münster vom 21. März 2023 - S 11 SF 21/23 E RG - wird abgelehnt. + +## Gründe + +11. Nach der Rechtsprechung des Bundesverfassungsgerichts ist im Verfahren über eine Verfassungsbeschwerde die Bewilligung von Prozesskostenhilfe an den Beschwerdeführer entsprechend § 114 ff. ZPO möglich (vgl. BVerfGE 1, 109 <110 f.>; 1, 415 <416>; 79, 252 <253>; 92, 122 <123>; BVerfG, Beschluss der 2. Kammer des Ersten Senats vom 23. Februar 2022 - 1 BvR 2434/21 -, Rn. 1). Auch die isolierte Bewilligung von PKH für eine noch zu erhebende Verfassungsbeschwerde ist nicht ausgeschlossen. PKH wird aber nur unter strengen Voraussetzungen gewährt, weil das Verfahren kostenfrei ist und kein Anwaltszwang besteht. Sie wird daher nur gewährt, wenn dies unbedingt erforderlich erscheint, weil die betroffene Person nicht in der Lage ist, sich selbst zu vertreten, sie die Kosten der Prozessführung nach den persönlichen und wirtschaftlichen Verhältnissen nicht aufbringen kann und die beabsichtigte Rechtsverfolgung hinreichend Aussicht auf Erfolg bietet (vgl. BVerfGE 78, 7 <19 f.>; 92, 122 <123>; BVerfG, Beschluss der 3. Kammer des Zweiten Senats vom 9. Juli 2010 - 2 BvR 2258/09 -, juris, Rn. 6; Beschluss der 1. Kammer des Ersten Senats vom 2. Dezember 2016 - 1 BvR 2014/16 -, Rn. 2 m.w.N.; Beschluss der 2. Kammer des Zweiten Senats vom 2. März 2020 - 2 BvR 1819/19 -, Rn. 3).22. Diese Voraussetzungen liegen hier nicht vor.3Der Antragsteller legt nicht in einer den Anforderungen des § 23 Abs. 1 Satz 2 Halbsatz 1 BVerfGG entsprechenden Weise dar, dass er daran gehindert ist, seine Rechte selbst und ohne anwaltliche Hilfe angemessen wahrzunehmen.4Überdies bietet die beabsichtigte Verfassungsbeschwerde keine hinreichende Aussicht auf Erfolg, insbesondere weil weder eine Verletzung des Willkürverbots (Art. 3 Abs. 1 GG) noch eine Verletzung des Anspruchs auf rechtliches Gehör (Art. 103 Abs. 1 GG) erkennbar sind.5Diese Entscheidung ist unanfechtbar. diff --git a/ge_md/bverfg/2024/2-bvr-923-24.md b/ge_md/bverfg/2024/2-bvr-923-24.md new file mode 100644 index 0000000..a076efd --- /dev/null +++ b/ge_md/bverfg/2024/2-bvr-923-24.md @@ -0,0 +1,31 @@ +--- +ebene: "verfg" +gericht: "BVerfG" +spruchkoerper: "2. Senat 2. Kammer" +datum: "2024-07-12" +aktenzeichen: "2 BvR 923/24" +ecli: "ECLI:DE:BVerfG:2024:rk20240712.2bvr092324" +doknr: "KVRE000012442" +doktyp: "Ablehnung einstweilige Anordnung" +quelle: "rii" +normen: + - { gesetz: "bverfgg", paragraph: "23", roh: "§ 23 Abs 1 S 2 BVerfGG" } + - { gesetz: "bverfgg", paragraph: "32", roh: "§ 32 Abs 1 BVerfGG" } + - { gesetz: "bverfgg", paragraph: "92", roh: "§ 92 BVerfGG" } +vorinstanz: + - "vorgehend VG Potsdam, 10. Juli 2024, Az: VG 14 L 588/24.A, Beschluss" +--- + +# BVerfG Ablehnung einstweilige Anordnung — 2 BvR 923/24 vom 12.07.2024 + +## Titelzeile + +Erfolgloser Eilantrag im Verfassungsbeschwerdeverfahren bei derzeitiger offensichtlicher Unzulässigkeit der Verfassungsbeschwerde + +## Tenor + +Der Antrag auf Erlass einer einstweiligen Anordnung wird abgelehnt. Die Verfassungsbeschwerde erweist sich im gegenwärtigen Zeitpunkt als offensichtlich unzulässig. Für eine Folgenabwägung bleibt daher kein Raum. + +## Gründe + +1Der Antrag auf Erlass einer einstweiligen Anordnung wird abgelehnt, weil die Verfassungsbeschwerde sich nach derzeitiger Aktenlage als offensichtlich unzulässig erweist. Denn der bisherige Vortrag der Antragstellerinnen genügt den Begründungsanforderungen gemäß § 23 Absatz 1 Satz 2 Halbsatz 1, § 92 des Bundesverfassungsgerichtsgesetzes nicht, da eine mögliche Verletzung von Grundrechten nicht hinreichend dargelegt wurde. Für eine Folgenabwägung bleibt daher kein Raum.2Von einer weiteren Begründung wird nach § 93d Abs. 1 Satz 3 BVerfGG abgesehen.3Diese Entscheidung ist unanfechtbar. diff --git a/index_normen.py b/index_normen.py new file mode 100644 index 0000000..f9194dc --- /dev/null +++ b/index_normen.py @@ -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//§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()) diff --git a/kommentar/README.md b/kommentar/README.md new file mode 100644 index 0000000..3af4d61 --- /dev/null +++ b/kommentar/README.md @@ -0,0 +1,10 @@ +# Kommentar + +Hier entstehen später Kommentare im Stil von BeckOK / Grüneberg: +eine Datei pro Paragraph, z. B. `bgb/§242.md`, mit Randnummern. + +Der geltende Normtext bleibt in Lawgit (`laws_md/`). Neue +Gerichtsentscheidungen aus `ge_md/` werden nach den Regeln in +[docs/EINARBEITUNG.md](../docs/EINARBEITUNG.md) eingearbeitet. + +Noch keine Kommentartexte — nur der Anker für den nächsten Schritt. diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0eb8cae --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +requests>=2.31.0 diff --git a/rii_to_markdown.py b/rii_to_markdown.py new file mode 100644 index 0000000..3d3ab53 --- /dev/null +++ b/rii_to_markdown.py @@ -0,0 +1,457 @@ +#!/usr/bin/env python3 +""" +RII-XML (rechtsprechung-im-internet.de, rii-dok.dtd) → Markdown mit YAML-Kopf. + +Eine Entscheidung wird eine Datei unter ge_md///.md. +""" + +from __future__ import annotations + +import re +import xml.etree.ElementTree as ET +from pathlib import Path +from typing import Any + +# Gerichtstyp → Ebene und Ordnername +COURT_MAP: dict[str, tuple[str, str]] = { + "BVerfG": ("verfg", "bverfg"), + "BGH": ("bund", "bgh"), + "BVerwG": ("bund", "bverwg"), + "BFH": ("bund", "bfh"), + "BAG": ("bund", "bag"), + "BSG": ("bund", "bsg"), + "BPatG": ("bund", "bpatg"), +} + +BUND_COURTS = {"BGH", "BVerwG", "BFH", "BAG", "BSG", "BPatG"} + + +def local_tag(tag: str | None) -> str: + if not tag: + return "" + if "}" in tag: + return tag.rsplit("}", 1)[-1] + return tag + + +def elem_text(elem: ET.Element | None) -> str: + if elem is None: + return "" + return "".join(elem.itertext()).strip() + + +def first_child(parent: ET.Element, name: str) -> ET.Element | None: + for child in parent: + if local_tag(child.tag) == name: + return child + return None + + +def all_children(parent: ET.Element, name: str) -> list[ET.Element]: + return [c for c in parent if local_tag(c.tag) == name] + + +def find_deep(parent: ET.Element, name: str) -> ET.Element | None: + for elem in parent.iter(): + if local_tag(elem.tag) == name: + return elem + return None + + +def xml_fragment_to_markdown(elem: ET.Element | None) -> str: + """HTML/XML-Fragment (P, BR, DL, …) zu lesbarem Markdown.""" + if elem is None: + return "" + + result: list[str] = [] + + def process_list(dl_elem: ET.Element) -> None: + for child in dl_elem: + tag = local_tag(child.tag) + if tag == "DT": + if result and result[-1] not in ("\n", "\n\n"): + result.append("\n") + process_element(child) + elif tag == "DD": + process_element(child) + result.append("\n") + else: + process_element(child) + + def process_element(e: ET.Element) -> None: + if e.text and e.text.strip(): + result.append(e.text.strip()) + + for child in e: + tag = local_tag(child.tag) + if tag == "P": + if result and result[-1] != "\n\n": + result.append("\n\n") + process_element(child) + result.append("\n\n") + elif tag == "BR": + result.append("\n") + if child.text and child.text.strip(): + result.append(child.text.strip()) + elif tag == "DL": + process_list(child) + elif tag in ("DT", "DD"): + if tag == "DT" and result and result[-1] not in ("\n", "\n\n"): + result.append("\n") + process_element(child) + if tag == "DD": + result.append("\n") + elif tag == "LA": + process_element(child) + elif tag in ("table", "tgroup", "tbody", "thead", "row", "entry"): + if tag in ("table", "tgroup"): + result.append("\n[Tabelle]\n") + else: + process_element(child) + + if child.tail and child.tail.strip(): + result.append(child.tail.strip()) + + process_element(elem) + text = "".join(result) + text = ( + text.replace(""", '"') + .replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace(" ", " ") + .replace("§", "§") + ) + text = re.sub(r"\n{3,}", "\n\n", text) + return text.strip() + + +def iso_date(raw: str) -> str: + s = (raw or "").strip() + m = re.match(r"^(\d{4})-(\d{2})-(\d{2})", s) + if m: + return f"{m.group(1)}-{m.group(2)}-{m.group(3)}" + m = re.match(r"^(\d{2})\.(\d{2})\.(\d{4})", s) + if m: + return f"{m.group(3)}-{m.group(2)}-{m.group(1)}" + m = re.match(r"^(\d{4})(\d{2})(\d{2})$", s) + if m: + return f"{m.group(1)}-{m.group(2)}-{m.group(3)}" + return "" + + +def ebene_for(gertyp: str) -> str: + g = (gertyp or "").strip() + if g in COURT_MAP: + return COURT_MAP[g][0] + if g.upper().startswith("OLG") or g.upper().startswith("LG"): + return "olg" + if g in BUND_COURTS: + return "bund" + return "bund" + + +def court_dir(gertyp: str) -> str: + g = (gertyp or "").strip() + if g in COURT_MAP: + return COURT_MAP[g][1] + slug = re.sub(r"[^a-z0-9]+", "-", g.lower()).strip("-") + return slug or "unbekannt" + + +def az_slug(aktenzeichen: str, doknr: str = "") -> str: + s = ( + aktenzeichen.lower() + .replace("ä", "ae") + .replace("ö", "oe") + .replace("ü", "ue") + .replace("ß", "ss") + ) + s = re.sub(r"[\s/]+", "-", s) + s = re.sub(r"[^a-z0-9-]", "", s) + s = re.sub(r"-+", "-", s).strip("-") + if len(s) <= 80: + return s or (doknr.lower() if doknr else "ohne-az") + suf = "-" + re.sub(r"[^a-z0-9]", "", doknr.lower()) if doknr else "" + return s[: max(1, 80 - len(suf))].rstrip("-") + suf + + +def parse_norm_string(roh: str) -> dict[str, str] | None: + """ + Einzelnen Norm-String zu {gesetz, paragraph, roh} zerlegen. + + Beispiele: + § 242 BGB + §§ 133, 134 InsO + Art. 2 Abs. 1 GG + § 31 Abs. 1 BVerfGG + """ + roh = (roh or "").strip() + if not roh: + return None + + # Gesetz am Ende: Abkürzung (Buchstaben/Ziffern, mind. 2 Zeichen) + m = re.search( + r"^(?:§{1,2}|Art\.?)\s*(.+?)\s+([A-Za-zÄÖÜäöüß][A-Za-zÄÖÜäöüß0-9.\-]{1,})\s*$", + roh, + ) + if not m: + # nur Abkürzung ohne Paragraph + m2 = re.match(r"^([A-Za-zÄÖÜäöüß][A-Za-zÄÖÜäöüß0-9.\-]{1,})$", roh) + if m2: + return {"gesetz": m2.group(1).lower(), "paragraph": "", "roh": roh} + return {"gesetz": "", "paragraph": "", "roh": roh} + + middle, gesetz = m.group(1).strip(), m.group(2).strip() + # erste Paragraphen-/Artikelnummer + num = re.search(r"(\d+[a-z]?)", middle, re.I) + paragraph = num.group(1) if num else "" + return { + "gesetz": gesetz.lower().replace("ä", "ae").replace("ö", "oe").replace("ü", "ue"), + "paragraph": paragraph, + "roh": roh, + } + + +def collect_normen(root: ET.Element) -> list[dict[str, str]]: + raw_parts: list[str] = [] + for norm_el in root.iter(): + if local_tag(norm_el.tag) != "norm": + continue + text = elem_text(norm_el) + if not text: + continue + # Zeilen, Semikolon, und Komma vor neuem §/Art. trennen + chunks = re.split(r"[\n;]+|,\s*(?=§|Art\.?\s)", text) + for part in chunks: + part = part.strip().rstrip(",").strip() + if not part: + continue + if re.search(r"(§{1,2}|Art\.?)\s*\d", part, re.I): + raw_parts.append(part) + elif re.match(r"^[A-Za-zÄÖÜäöüß]", part): + raw_parts.append(part) + + seen: set[str] = set() + out: list[dict[str, str]] = [] + for roh in raw_parts: + if roh in seen: + continue + seen.add(roh) + parsed = parse_norm_string(roh) + if parsed: + out.append(parsed) + return out + + +def collect_vorinstanz(root: ET.Element) -> list[str]: + out: list[str] = [] + for el in root.iter(): + if local_tag(el.tag) != "vorinstanz": + continue + t = elem_text(el) + if t: + out.append(t) + else: + # strukturierte Vorinstanz: Gericht + AZ + bits = [] + for child in el: + ct = elem_text(child) + if ct: + bits.append(ct) + if bits: + out.append(" — ".join(bits)) + return out + + +def yaml_scalar(value: str) -> str: + """YAML-sicherer Skalar (JSON-String-Stil).""" + import json + + return json.dumps(value, ensure_ascii=False) + + +def build_frontmatter(meta: dict[str, Any]) -> str: + lines = ["---"] + for key in ( + "ebene", + "gericht", + "spruchkoerper", + "datum", + "aktenzeichen", + "ecli", + "doknr", + "doktyp", + "quelle", + ): + val = meta.get(key, "") + if val is None or val == "": + continue + lines.append(f"{key}: {yaml_scalar(str(val))}") + + normen = meta.get("normen") or [] + if normen: + lines.append("normen:") + for n in normen: + gesetz = yaml_scalar(n.get("gesetz", "")) + paragraph = yaml_scalar(n.get("paragraph", "")) + roh = yaml_scalar(n.get("roh", "")) + lines.append( + f" - {{ gesetz: {gesetz}, paragraph: {paragraph}, roh: {roh} }}" + ) + else: + lines.append("normen: []") + + vor = meta.get("vorinstanz") or [] + if vor: + lines.append("vorinstanz:") + for v in vor: + lines.append(f" - {yaml_scalar(v)}") + else: + lines.append("vorinstanz: []") + + lines.append("---") + return "\n".join(lines) + + +def section_markdown(root: ET.Element, tag_names: tuple[str, ...]) -> str: + for name in tag_names: + el = find_deep(root, name) + if el is not None: + # Wenn das Element Kinder mit Markup hat, fragmentarisch konvertieren + if list(el): + md = xml_fragment_to_markdown(el) + else: + md = elem_text(el) + if md: + return md + return "" + + +def parse_rii_xml(xml_content: str | bytes) -> dict[str, Any] | None: + """Parst ein RII-Dokument. Rückgabe: Meta + Body-Abschnitte oder None.""" + if isinstance(xml_content, bytes): + xml_content = xml_content.decode("utf-8", errors="replace") + + # Externe DTD nicht laden; DOCTYPE entfernen + xml_content = re.sub(r"]*>", "", xml_content, count=1, flags=re.I) + cleaned = re.sub(r"&(?!(amp|lt|gt|quot|apos|#\d+|#x[0-9a-fA-F]+);)", "&", xml_content) + try: + root = ET.fromstring(cleaned) + except ET.ParseError: + return None + + doknr = elem_text(find_deep(root, "doknr")) or root.get("doknr", "") + gertyp = elem_text(find_deep(root, "gertyp")) or "unbekannt" + az = elem_text(find_deep(root, "aktenzeichen")) + if not az and not doknr: + return None + + datum = iso_date(elem_text(find_deep(root, "entsch-datum"))) + ecli = elem_text(find_deep(root, "ecli")) + doktyp = elem_text(find_deep(root, "doktyp")) or "Entscheidung" + spruch = elem_text(find_deep(root, "spruchkoerper")) + + meta = { + "ebene": ebene_for(gertyp), + "gericht": gertyp, + "spruchkoerper": spruch, + "datum": datum, + "aktenzeichen": az, + "ecli": ecli, + "doknr": doknr, + "doktyp": doktyp, + "quelle": "rii", + "normen": collect_normen(root), + "vorinstanz": collect_vorinstanz(root), + } + + body = { + "leitsatz": section_markdown(root, ("leitsatz",)), + "tenor": section_markdown(root, ("tenor",)), + "tatbestand": section_markdown(root, ("tatbestand",)), + "gruende": section_markdown( + root, ("entscheidungsgruende", "gruende") + ), + "abwmeinung": section_markdown(root, ("abwmeinung",)), + "titelzeile": section_markdown(root, ("titelzeile",)), + } + return {"meta": meta, "body": body} + + +def to_markdown(parsed: dict[str, Any]) -> str: + meta = parsed["meta"] + body = parsed["body"] + parts = [build_frontmatter(meta), ""] + + title_bits = [meta.get("gericht") or "", meta.get("doktyp") or ""] + if meta.get("aktenzeichen"): + title_bits.append(f"— {meta['aktenzeichen']}") + if meta.get("datum"): + ddmmyyyy = "-".join(reversed(meta["datum"].split("-"))) if meta["datum"] else "" + # datum is YYYY-MM-DD → DD.MM.YYYY + if meta["datum"] and len(meta["datum"]) == 10: + y, m, d = meta["datum"].split("-") + ddmmyyyy = f"{d}.{m}.{y}" + title_bits.append(f"vom {ddmmyyyy}") + parts.append("# " + " ".join(b for b in title_bits if b).strip()) + parts.append("") + + if body.get("titelzeile"): + parts.extend(["## Titelzeile", "", body["titelzeile"], ""]) + if body.get("leitsatz"): + parts.extend(["## Leitsatz", "", body["leitsatz"], ""]) + if body.get("tenor"): + parts.extend(["## Tenor", "", body["tenor"], ""]) + if body.get("tatbestand"): + parts.extend(["## Tatbestand", "", body["tatbestand"], ""]) + if body.get("gruende"): + parts.extend(["## Gründe", "", body["gruende"], ""]) + if body.get("abwmeinung"): + parts.extend(["## Abweichende Meinung", "", body["abwmeinung"], ""]) + + text = "\n".join(parts).rstrip() + "\n" + text = re.sub(r"\n{3,}", "\n\n", text) + return text + + +def relative_path(meta: dict[str, Any]) -> Path: + """Relativer Pfad unter ge_md/: //.md""" + court = court_dir(meta.get("gericht", "")) + year = (meta.get("datum") or "")[:4] or "ohne-jahr" + slug = az_slug(meta.get("aktenzeichen", ""), meta.get("doknr", "")) + return Path(court) / year / f"{slug}.md" + + +def convert_xml_to_markdown_file( + xml_content: str | bytes, + output_base: Path | str = "ge_md", +) -> Path | None: + """Schreibt Markdown-Datei; Rückgabe: Pfad oder None.""" + parsed = parse_rii_xml(xml_content) + if not parsed: + return None + rel = relative_path(parsed["meta"]) + out = Path(output_base) / rel + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(to_markdown(parsed), encoding="utf-8") + return out + + +def doknr_already_exists(doknr: str, output_base: Path | str = "ge_md") -> Path | None: + """Sucht eine vorhandene Markdown-Datei mit dieser doknr im YAML-Kopf.""" + if not doknr: + return None + base = Path(output_base) + if not base.is_dir(): + return None + needle = f'doknr: "{doknr}"' + needle2 = f"doknr: {doknr}" + for path in base.rglob("*.md"): + try: + head = path.read_text(encoding="utf-8", errors="replace")[:800] + except OSError: + continue + if needle in head or f"doknr: {yaml_scalar(doknr)}" in head or needle2 in head: + return path + return None diff --git a/setup-ssh-deploy.sh b/setup-ssh-deploy.sh new file mode 100755 index 0000000..5c9f677 --- /dev/null +++ b/setup-ssh-deploy.sh @@ -0,0 +1,150 @@ +#!/bin/bash +# SSH Deploy Key Setup for explore +# +# Automatisiert: +# 1. Erzeugt ein SSH-Schlüsselpaar (nur für Deployment) +# 2. Hinterlegt den Public Key auf dem Server +# 3. Speichert den Private Key als Gitea Secret via API +# +# Voraussetzungen: +# - ssh-keygen, jq, curl +# - SSH-Zugang zum Server (z.B. ~/.ssh/gitea_deploy) +# - Gitea API-Token (mit repo-Rechten) +# - Repo michaelkole/explore muss bereits existieren +# +# Usage: ./setup-ssh-deploy.sh +set -euo pipefail + +PROJECT_NAME="explore" +SERVER_IP="82.165.92.92" +SERVER_USER="root" +GITEA_HOST="git.coded.law" +GITEA_OWNER="michaelkole" +GITEA_REPO="explore" +KEY_FILE="$HOME/.ssh/deploy_${PROJECT_NAME}" +SECRET_NAME="SSH_PRIVATE_KEY" + +echo "" +echo "SSH Deploy Key Setup für $PROJECT_NAME" +echo "========================================" +echo "" + +if [ -z "${GITEA_TOKEN:-}" ]; then + echo "Gitea API-Token wird benötigt (mit repo-Rechten)." + echo "Erstellen unter: https://$GITEA_HOST/user/settings/applications" + echo "" + read -rsp "Gitea API-Token: " GITEA_TOKEN + echo "" +fi + +if [ -z "$GITEA_TOKEN" ]; then + echo "Fehler: Kein Token angegeben." + exit 1 +fi + +echo "Prüfe Gitea-Zugang..." +HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \ + -H "Authorization: token $GITEA_TOKEN" \ + "https://$GITEA_HOST/api/v1/repos/$GITEA_OWNER/$GITEA_REPO") + +if [ "$HTTP_CODE" != "200" ]; then + echo "Fehler: Gitea API antwortet mit HTTP $HTTP_CODE." + echo "Prüfe Token und Repo-Pfad: $GITEA_OWNER/$GITEA_REPO" + echo "Repo ggf. zuerst anlegen unter https://$GITEA_HOST/repo/create" + exit 1 +fi +echo " Gitea OK" + +if [ -f "$KEY_FILE" ]; then + echo "" + echo "SSH Key existiert bereits: $KEY_FILE" + read -rp "Überschreiben? (j/N) " OVERWRITE + if [[ ! "$OVERWRITE" =~ ^[jJyY]$ ]]; then + echo "Verwende bestehenden Key." + else + rm -f "$KEY_FILE" "$KEY_FILE.pub" + fi +fi + +if [ ! -f "$KEY_FILE" ]; then + echo "Erzeuge SSH-Schlüsselpaar..." + ssh-keygen -t ed25519 -C "deploy@$PROJECT_NAME" -f "$KEY_FILE" -N "" + chmod 600 "$KEY_FILE" + echo " Key erzeugt: $KEY_FILE" +fi + +echo "" +echo "Hinterlege Public Key auf $SERVER_USER@$SERVER_IP..." + +SSH_OPTS="-o StrictHostKeyChecking=accept-new -o ConnectTimeout=5" +EXISTING_KEY="" + +if ! ssh $SSH_OPTS -o BatchMode=yes "$SERVER_USER@$SERVER_IP" "true" 2>/dev/null; then + echo " Direkter SSH-Zugang nicht möglich (nur Key-Auth?)." + echo " Suche bestehenden SSH-Key..." + for candidate in ~/.ssh/id_ed25519 ~/.ssh/id_rsa ~/.ssh/gitea_deploy; do + if [ -f "$candidate" ] && ssh -i "$candidate" $SSH_OPTS -o BatchMode=yes "$SERVER_USER@$SERVER_IP" "true" 2>/dev/null; then + EXISTING_KEY="$candidate" + echo " Gefunden: $candidate" + break + fi + done + if [ -z "$EXISTING_KEY" ]; then + echo "" + echo " Kein funktionierender SSH-Key gefunden." + echo " Bitte manuell:" + echo " cat $KEY_FILE.pub | ssh -i ~/.ssh/gitea_deploy $SERVER_USER@$SERVER_IP 'cat >> ~/.ssh/authorized_keys'" + exit 1 + fi +fi + +if [ -n "$EXISTING_KEY" ]; then + ssh -i "$EXISTING_KEY" "$SERVER_USER@$SERVER_IP" "mkdir -p ~/.ssh && chmod 700 ~/.ssh" + cat "$KEY_FILE.pub" | ssh -i "$EXISTING_KEY" "$SERVER_USER@$SERVER_IP" "cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys" +else + ssh "$SERVER_USER@$SERVER_IP" "mkdir -p ~/.ssh && chmod 700 ~/.ssh" + cat "$KEY_FILE.pub" | ssh "$SERVER_USER@$SERVER_IP" "cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys" +fi +echo " Public Key auf Server hinterlegt" + +echo "Teste SSH-Verbindung mit Deploy Key..." +if ssh -i "$KEY_FILE" $SSH_OPTS -o BatchMode=yes "$SERVER_USER@$SERVER_IP" "echo 'SSH OK'" 2>/dev/null; then + echo " SSH-Verbindung OK" +else + echo " WARNUNG: SSH-Test fehlgeschlagen. Prüfe authorized_keys auf dem Server." +fi + +if ! command -v jq &>/dev/null; then + echo "Fehler: jq fehlt (brew install jq)." + exit 1 +fi + +echo "" +echo "Speichere Private Key als Gitea Secret '$SECRET_NAME'..." +PRIVATE_KEY_CONTENT=$(cat "$KEY_FILE") + +HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \ + -X PUT \ + -H "Authorization: token $GITEA_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"data\": $(echo "$PRIVATE_KEY_CONTENT" | jq -Rs .)}" \ + "https://$GITEA_HOST/api/v1/repos/$GITEA_OWNER/$GITEA_REPO/actions/secrets/$SECRET_NAME") + +if [ "$HTTP_CODE" = "201" ] || [ "$HTTP_CODE" = "204" ]; then + echo " Secret '$SECRET_NAME' gespeichert" +else + echo " Fehler: Gitea API antwortet mit HTTP $HTTP_CODE" + echo " Manuell: https://$GITEA_HOST/$GITEA_OWNER/$GITEA_REPO/settings/actions/secrets" + exit 1 +fi + +echo "" +echo "========================================" +echo "SSH Deploy Key Setup abgeschlossen!" +echo "========================================" +echo "" +echo " Private Key: $KEY_FILE" +echo " Public Key: $KEY_FILE.pub" +echo " Server: $SERVER_USER@$SERVER_IP" +echo " Gitea Secret: $SECRET_NAME ($GITEA_OWNER/$GITEA_REPO)" +echo "" diff --git a/sync_to_server.sh b/sync_to_server.sh new file mode 100755 index 0000000..26b3d48 --- /dev/null +++ b/sync_to_server.sh @@ -0,0 +1,66 @@ +#!/bin/bash +# Spiegelt Explor.Code.Law auf den Strato-VServer (rsync). +# Nützlich bevor das Gitea-Repo existiert, und für ge_xml/ (gitignoriert). +# +# Usage: +# ./sync_to_server.sh # Code + ge_md + data + ge_xml +# ./sync_to_server.sh --xml-only +# ./sync_to_server.sh --dry-run +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")" && pwd)" +SERVER="root@82.165.92.92" +SSH_KEY="${SSH_KEY:-$HOME/.ssh/gitea_deploy}" +REMOTE_DIR="/opt/explore" +RSYNC_SSH="ssh -i $SSH_KEY -o BatchMode=yes -o StrictHostKeyChecking=accept-new" + +XML_ONLY=0 +DRY=() +for arg in "$@"; do + case "$arg" in + --xml-only) XML_ONLY=1 ;; + --dry-run) DRY=(--dry-run) ;; + -h|--help) + sed -n '2,12p' "$0" + exit 0 + ;; + *) + echo "Unbekanntes Argument: $arg" >&2 + exit 1 + ;; + esac +done + +if [ ! -f "$SSH_KEY" ]; then + echo "SSH-Key fehlt: $SSH_KEY" >&2 + exit 1 +fi + +ssh -i "$SSH_KEY" -o BatchMode=yes "$SERVER" "mkdir -p $REMOTE_DIR/ge_md $REMOTE_DIR/data $REMOTE_DIR/ge_xml $REMOTE_DIR/kommentar" + +if [ "$XML_ONLY" -eq 1 ]; then + echo "Sync ge_xml/ → $SERVER:$REMOTE_DIR/ge_xml/" + rsync -avz "${DRY[@]}" \ + -e "$RSYNC_SSH" \ + --exclude '.DS_Store' \ + "$ROOT/ge_xml/" "$SERVER:$REMOTE_DIR/ge_xml/" +else + echo "Sync Projekt → $SERVER:$REMOTE_DIR/" + rsync -avz "${DRY[@]}" \ + -e "$RSYNC_SSH" \ + --exclude '.git/' \ + --exclude '.venv/' \ + --exclude 'venv/' \ + --exclude '__pycache__/' \ + --exclude '.pytest_cache/' \ + --exclude '.DS_Store' \ + --exclude '.env' \ + "$ROOT/" "$SERVER:$REMOTE_DIR/" +fi + +echo "" +ssh -i "$SSH_KEY" -o BatchMode=yes "$SERVER" \ + "echo ge_md: \$(find $REMOTE_DIR/ge_md -name '*.md' 2>/dev/null | wc -l); \ + echo ge_xml: \$(find $REMOTE_DIR/ge_xml -type f 2>/dev/null | wc -l); \ + echo index: \$([ -f $REMOTE_DIR/data/normen_index.json ] && wc -c < $REMOTE_DIR/data/normen_index.json || echo fehlt) bytes" +echo "Fertig." diff --git a/tests/fixtures/bgh_ix_zr_72_20.xml b/tests/fixtures/bgh_ix_zr_72_20.xml new file mode 100644 index 0000000..2a874d3 --- /dev/null +++ b/tests/fixtures/bgh_ix_zr_72_20.xml @@ -0,0 +1,32 @@ + + + + KORE000000001 + ECLI:DE:BGH:2021:060521UIXZR72.20.0 + BGH + Karlsruhe + IX. Zivilsenat + 2021-05-06 + IX ZR 72/20 + Urteil + § 133 InsO +§ 242 BGB +

Vorsatzanfechtung

+ +

Die erkannte Zahlungsunfähigkeit allein trägt den Vorsatz nicht.

+
+ +

Auf die Revision wird das Urteil aufgehoben.

+
+ +

Die Klägerin verlangt Rückgewähr.

+
+ +

Die Revision ist begründet.

+

I. Das Berufungsgericht hat angenommen, der Anfechtungsgegner habe vorsätzlich gehandelt.

+
+ + OLG Hamm + I-27 U 12/19 + +
diff --git a/tests/fixtures/bverfg_1_bvr_1585_19.xml b/tests/fixtures/bverfg_1_bvr_1585_19.xml new file mode 100644 index 0000000..264f67d --- /dev/null +++ b/tests/fixtures/bverfg_1_bvr_1585_19.xml @@ -0,0 +1,25 @@ + + + + KVRE000000002 + ECLI:DE:BVerfG:2020:rs20200505.1bvr158519 + BVerfG + Karlsruhe + 1. Senat + 20200505 + 1 BvR 1585/19 + Beschluss + Art. 2 Abs. 1 GG +Art. 1 Abs. 1 GG +§ 31 BVerfGG +

Recht auf informationelle Selbstbestimmung

+ +

Das allgemeine Persönlichkeitsrecht schützt auch vor der unkontrollierten Weitergabe von Daten.

+
+ +

Die Verfassungsbeschwerde wird nicht zur Entscheidung angenommen.

+
+ +

Die Annahmevoraussetzungen des § 93a Abs. 2 BVerfGG liegen nicht vor.

+
+
diff --git a/tests/test_rii.py b/tests/test_rii.py new file mode 100644 index 0000000..57d2c1c --- /dev/null +++ b/tests/test_rii.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python3 +"""Offline-Tests für rii_to_markdown und index_normen.""" + +from __future__ import annotations + +import sys +import tempfile +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) + +from rii_to_markdown import ( # noqa: E402 + az_slug, + convert_xml_to_markdown_file, + ebene_for, + parse_norm_string, + parse_rii_xml, + relative_path, + to_markdown, +) +from index_normen import ( # noqa: E402 + lawgit_path_for, + map_gesetz_slug, + parse_frontmatter_normen, +) + +FIXTURES = Path(__file__).parent / "fixtures" + + +class TestEbene(unittest.TestCase): + def test_bverfg(self): + self.assertEqual(ebene_for("BVerfG"), "verfg") + + def test_bgh(self): + self.assertEqual(ebene_for("BGH"), "bund") + + def test_olg(self): + self.assertEqual(ebene_for("OLG München"), "olg") + + +class TestParseNorm(unittest.TestCase): + def test_paragraph_bgb(self): + n = parse_norm_string("§ 242 BGB") + self.assertEqual(n["gesetz"], "bgb") + self.assertEqual(n["paragraph"], "242") + self.assertEqual(n["roh"], "§ 242 BGB") + + def test_artikel_gg(self): + n = parse_norm_string("Art. 2 Abs. 1 GG") + self.assertEqual(n["gesetz"], "gg") + self.assertEqual(n["paragraph"], "2") + + def test_comma_separated_norms(self): + from rii_to_markdown import collect_normen + import xml.etree.ElementTree as ET + + root = ET.fromstring( + "§ 20 StGB, § 224 StGB, Art. 2 Abs. 1 GG" + ) + norms = collect_normen(root) + roh = {n["roh"] for n in norms} + self.assertIn("§ 20 StGB", roh) + self.assertIn("§ 224 StGB", roh) + self.assertIn("Art. 2 Abs. 1 GG", roh) + gg = next(n for n in norms if n["gesetz"] == "gg") + self.assertEqual(gg["paragraph"], "2") + + +class TestBghFixture(unittest.TestCase): + def setUp(self): + xml = (FIXTURES / "bgh_ix_zr_72_20.xml").read_text(encoding="utf-8") + self.parsed = parse_rii_xml(xml) + + def test_parses(self): + self.assertIsNotNone(self.parsed) + + def test_meta(self): + m = self.parsed["meta"] + self.assertEqual(m["ebene"], "bund") + self.assertEqual(m["gericht"], "BGH") + self.assertEqual(m["aktenzeichen"], "IX ZR 72/20") + self.assertEqual(m["datum"], "2021-05-06") + self.assertEqual(m["doknr"], "KORE000000001") + self.assertEqual(m["doktyp"], "Urteil") + self.assertEqual(m["quelle"], "rii") + + def test_normen(self): + roh = {n["roh"] for n in self.parsed["meta"]["normen"]} + self.assertIn("§ 133 InsO", roh) + self.assertIn("§ 242 BGB", roh) + bgb = next(n for n in self.parsed["meta"]["normen"] if n["gesetz"] == "bgb") + self.assertEqual(bgb["paragraph"], "242") + + def test_body(self): + b = self.parsed["body"] + self.assertIn("Zahlungsunfähigkeit", b["leitsatz"]) + self.assertIn("aufgehoben", b["tenor"]) + self.assertIn("Rückgewähr", b["tatbestand"]) + self.assertIn("begründet", b["gruende"]) + + def test_vorinstanz(self): + self.assertTrue(any("OLG Hamm" in v for v in self.parsed["meta"]["vorinstanz"])) + + def test_markdown_sections(self): + md = to_markdown(self.parsed) + self.assertIn("ebene: \"bund\"", md) + self.assertIn("## Leitsatz", md) + self.assertIn("## Tenor", md) + self.assertIn("## Tatbestand", md) + self.assertIn("## Gründe", md) + self.assertIn("gesetz: \"bgb\"", md) + + def test_path(self): + rel = relative_path(self.parsed["meta"]) + self.assertEqual(rel, Path("bgh/2021/ix-zr-72-20.md")) + + def test_write_file(self): + xml = (FIXTURES / "bgh_ix_zr_72_20.xml").read_bytes() + with tempfile.TemporaryDirectory() as tmp: + out = convert_xml_to_markdown_file(xml, tmp) + self.assertIsNotNone(out) + self.assertTrue(out.exists()) + self.assertEqual(out.name, "ix-zr-72-20.md") + + +class TestBverfgFixture(unittest.TestCase): + def setUp(self): + xml = (FIXTURES / "bverfg_1_bvr_1585_19.xml").read_text(encoding="utf-8") + self.parsed = parse_rii_xml(xml) + + def test_ebene_verfg(self): + self.assertEqual(self.parsed["meta"]["ebene"], "verfg") + self.assertEqual(self.parsed["meta"]["gericht"], "BVerfG") + + def test_datum_yyyymmdd(self): + self.assertEqual(self.parsed["meta"]["datum"], "2020-05-05") + + def test_gruende_from_gruende_tag(self): + self.assertIn("Annahmevoraussetzungen", self.parsed["body"]["gruende"]) + + def test_path(self): + rel = relative_path(self.parsed["meta"]) + self.assertEqual(rel, Path("bverfg/2020/1-bvr-1585-19.md")) + + def test_norm_gg(self): + gesetze = {n["gesetz"] for n in self.parsed["meta"]["normen"]} + self.assertIn("gg", gesetze) + + +class TestAzSlug(unittest.TestCase): + def test_simple(self): + self.assertEqual(az_slug("VIII ZR 1/24"), "viii-zr-1-24") + + def test_long_with_doknr(self): + long_az = "A " + "B " * 60 + "1/24" + slug = az_slug(long_az, "KORE123") + self.assertLessEqual(len(slug), 80) + self.assertTrue(slug.endswith("kore123") or "kore123" in slug) + + +class TestIndexNormen(unittest.TestCase): + def test_map_slug(self): + self.assertEqual(map_gesetz_slug("BGB"), "bgb") + self.assertEqual(map_gesetz_slug("GG"), "gg") + self.assertEqual(map_gesetz_slug("InsO"), "inso") + + def test_lawgit_path(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + (root / "laws_md" / "bgb").mkdir(parents=True) + (root / "laws_md" / "bgb" / "§242.md").write_text("# § 242\n", encoding="utf-8") + path, ok = lawgit_path_for(root, "bgb", "242") + self.assertTrue(ok) + self.assertTrue(str(path).endswith("§242.md")) + + def test_frontmatter_normen(self): + md = '''--- +doknr: "X" +normen: + - { gesetz: "bgb", paragraph: "242", roh: "§ 242 BGB" } +--- +# Test +''' + norms = parse_frontmatter_normen(md) + self.assertEqual(len(norms), 1) + self.assertEqual(norms[0]["gesetz"], "bgb") + + +if __name__ == "__main__": + unittest.main()