Initialisiere Explor.Code.Law mit RII-Pipeline und Strato-Deploy.
Some checks failed
Deploy explore / deploy (push) Failing after 18s

Entscheidungen als Markdown, Normen-Index und schlankes Git-Sync nach /opt/explore (Gitea Actions, ohne Docker).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-17 12:03:40 +02:00
commit bdaa21dc31
30 changed files with 2652 additions and 0 deletions

View File

@@ -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!"

23
.gitignore vendored Normal file
View File

@@ -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

126
INFRASTRUCTURE.md Normal file
View File

@@ -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) |

69
README.md Normal file
View File

@@ -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).

0
data/.gitkeep Normal file
View File

420
data/normen_index.json Normal file
View File

@@ -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"
}
]
}

60
deploy.sh Executable file
View File

@@ -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"

75
docs/EINARBEITUNG.md Normal file
View File

@@ -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/<slug>/§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/<gesetz>/§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

58
docs/QUELLEN.md Normal file
View File

@@ -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 7080000 Entscheidungen. `extract_rii.py` ist wiederaufsetzbar
(überspringt bekannte `doknr`), mit `--court`, `--since`, `--limit` und
Delay (Default 0,4s).

5
env.example Normal file
View File

@@ -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

308
extract_rii.py Normal file
View File

@@ -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 <gericht>
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())

0
ge_md/.gitkeep Normal file
View File

View File

@@ -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

File diff suppressed because one or more lines are too long

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

245
index_normen.py Normal file
View File

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

10
kommentar/README.md Normal file
View File

@@ -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.

1
requirements.txt Normal file
View File

@@ -0,0 +1 @@
requests>=2.31.0

457
rii_to_markdown.py Normal file
View File

@@ -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/<gericht>/<jahr>/<az-slug>.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("&quot;", '"')
.replace("&amp;", "&")
.replace("&lt;", "<")
.replace("&gt;", ">")
.replace("&nbsp;", " ")
.replace("&#167;", "§")
)
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"<!DOCTYPE[^>]*>", "", xml_content, count=1, flags=re.I)
cleaned = re.sub(r"&(?!(amp|lt|gt|quot|apos|#\d+|#x[0-9a-fA-F]+);)", "&amp;", 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/: <court>/<year>/<az-slug>.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

150
setup-ssh-deploy.sh Executable file
View File

@@ -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 ""

66
sync_to_server.sh Executable file
View File

@@ -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."

32
tests/fixtures/bgh_ix_zr_72_20.xml vendored Normal file
View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE dokument SYSTEM "https://www.rechtsprechung-im-internet.de/dtd/v1/rii-dok.dtd">
<dokument>
<doknr>KORE000000001</doknr>
<ecli>ECLI:DE:BGH:2021:060521UIXZR72.20.0</ecli>
<gertyp>BGH</gertyp>
<gerort>Karlsruhe</gerort>
<spruchkoerper>IX. Zivilsenat</spruchkoerper>
<entsch-datum>2021-05-06</entsch-datum>
<aktenzeichen>IX ZR 72/20</aktenzeichen>
<doktyp>Urteil</doktyp>
<norm>§ 133 InsO
§ 242 BGB</norm>
<titelzeile><P>Vorsatzanfechtung</P></titelzeile>
<leitsatz>
<P>Die erkannte Zahlungsunfähigkeit allein trägt den Vorsatz nicht.</P>
</leitsatz>
<tenor>
<P>Auf die Revision wird das Urteil aufgehoben.</P>
</tenor>
<tatbestand>
<P>Die Klägerin verlangt Rückgewähr.</P>
</tatbestand>
<entscheidungsgruende>
<P>Die Revision ist begründet.</P>
<P>I. Das Berufungsgericht hat angenommen, der Anfechtungsgegner habe vorsätzlich gehandelt.</P>
</entscheidungsgruende>
<vorinstanz>
<gericht>OLG Hamm</gericht>
<aktenzeichen>I-27 U 12/19</aktenzeichen>
</vorinstanz>
</dokument>

25
tests/fixtures/bverfg_1_bvr_1585_19.xml vendored Normal file
View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE dokument SYSTEM "https://www.rechtsprechung-im-internet.de/dtd/v1/rii-dok.dtd">
<dokument>
<doknr>KVRE000000002</doknr>
<ecli>ECLI:DE:BVerfG:2020:rs20200505.1bvr158519</ecli>
<gertyp>BVerfG</gertyp>
<gerort>Karlsruhe</gerort>
<spruchkoerper>1. Senat</spruchkoerper>
<entsch-datum>20200505</entsch-datum>
<aktenzeichen>1 BvR 1585/19</aktenzeichen>
<doktyp>Beschluss</doktyp>
<norm>Art. 2 Abs. 1 GG
Art. 1 Abs. 1 GG
§ 31 BVerfGG</norm>
<titelzeile><P>Recht auf informationelle Selbstbestimmung</P></titelzeile>
<leitsatz>
<P>Das allgemeine Persönlichkeitsrecht schützt auch vor der unkontrollierten Weitergabe von Daten.</P>
</leitsatz>
<tenor>
<P>Die Verfassungsbeschwerde wird nicht zur Entscheidung angenommen.</P>
</tenor>
<gruende>
<P>Die Annahmevoraussetzungen des § 93a Abs. 2 BVerfGG liegen nicht vor.</P>
</gruende>
</dokument>

192
tests/test_rii.py Normal file
View File

@@ -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(
"<dokument><norm>§ 20 StGB, § 224 StGB, Art. 2 Abs. 1 GG</norm></dokument>"
)
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()