No description
  • Python 95.3%
  • PowerShell 2.4%
  • Shell 2%
  • Dockerfile 0.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Rey Koxha 5f18bc2ad4
All checks were successful
Quality / Lint + Typpruefung (push) Successful in 43s
Quality / Tests (Python 3.11) (push) Successful in 2m4s
Quality / Tests (Python 3.12) (push) Successful in 2m19s
Security / Secret Scan (push) Successful in 4s
Security / Dependency Audit (push) Successful in 1m42s
Build / Deploy-Artefakte validieren (push) Successful in 19s
Build / Release-Quality (Python 3.11) (push) Successful in 2m2s
Build / Release-Quality (Python 3.12) (push) Successful in 2m11s
Build / Engine-Image bauen (push) Successful in 3s
Build / Release-Security (push) Successful in 2m18s
Build / Release veröffentlichen (nur vMAJOR.MINOR.PATCH) (push) Successful in 1h6m37s
fix: fetch annotated release tags on Forgejo
2026-08-22 02:54:11 +02:00
.forgejo fix: fetch annotated release tags on Forgejo 2026-08-22 02:54:11 +02:00
benchmarks Finetune-A/B unter neuer Policy: negativ -- Guardrail-Entscheid bestaetigt 2026-06-10 20:43:56 +02:00
cards Card-Anker freigegeben (Engine-Zone, 1015) + Absatz-Cards ehrlich etikettiert 2026-06-06 01:03:21 +02:00
catalog /R40/ Komposita-Alias-Mining: kuratierbare Vorschlaege aus dem Korpus 2026-06-10 17:57:48 +02:00
corpus Discovery-Harness (Pass 1) + Authority-Klasse Gesetz/Verordnung 2026-06-04 15:26:43 +02:00
deploy feat: add managed embedding refresh operations 2026-08-11 23:35:17 +02:00
docs fix: make managed source graph build offline 2026-08-12 17:53:43 +02:00
examples Initial commit: Risk-Layer 2026-06-02 00:06:11 +02:00
fixtures Semantik-Schicht: RRF-Fusion + sim-Rettung (R@5 0,560 -> 0,840 auf Gold) 2026-06-10 18:37:07 +02:00
release-input fix: make managed source graph build offline 2026-08-12 17:53:43 +02:00
risk_layer fix: reuse pinned CPU engine safely 2026-08-21 03:52:26 +02:00
scripts feat: enable AMD GPU embeddings on Windows 2026-08-21 03:24:04 +02:00
tests fix: reuse pinned CPU engine safely 2026-08-21 03:52:26 +02:00
tools feat: unify deployment, updates, and backups 2026-07-15 17:06:20 +02:00
.dockerignore fix: make managed source graph build offline 2026-08-12 17:53:43 +02:00
.gitattributes fix: make managed source graph build offline 2026-08-12 17:53:43 +02:00
.gitignore feat: enable AMD GPU embeddings on Windows 2026-08-21 03:24:04 +02:00
CHANGELOG.md fix: fetch annotated release tags on Forgejo 2026-08-22 02:54:11 +02:00
docker-compose.dev.yml feat: add managed embedding refresh operations 2026-08-11 23:35:17 +02:00
docker-compose.yml feat: add managed embedding refresh operations 2026-08-11 23:35:17 +02:00
Dockerfile feat: expose managed CPU LLM backend 2026-08-20 05:05:18 +02:00
Dockerfile.release feat: support managed source image builds 2026-08-12 16:55:23 +02:00
FEATURES.md docs: clarify host timestamp responsibility 2026-08-21 17:45:14 +02:00
INTEGRATION_TAXTRONIK.md feat: expose managed CPU LLM backend 2026-08-20 05:05:18 +02:00
LICENSE Update FEATURES and README 2026-06-02 00:19:46 +02:00
pyproject.toml feat: enable AMD GPU embeddings on Windows 2026-08-21 03:24:04 +02:00
README.md feat: enable AMD GPU embeddings on Windows 2026-08-21 03:24:04 +02:00
requirements-embedding-linux-cpu-py312-lock.txt fix: restore Linux CI and dependency audit 2026-07-15 19:31:56 +02:00
requirements-embedding-linux-cpu-py312.in fix: restore Linux CI and dependency audit 2026-07-15 19:31:56 +02:00
requirements-embedding-windows-amd-py312-lock.txt feat: enable AMD GPU embeddings on Windows 2026-08-21 03:24:04 +02:00
requirements-embedding-windows-amd-py312.in feat: enable AMD GPU embeddings on Windows 2026-08-21 03:24:04 +02:00
requirements-embedding-windows-cpu-py312-lock.txt fix: restore Linux CI and dependency audit 2026-07-15 19:31:56 +02:00
requirements-embedding-windows-cpu-py312.in feat: enable AMD GPU embeddings on Windows 2026-08-21 03:24:04 +02:00
requirements-lock.txt fix: restore Linux CI and dependency audit 2026-07-15 19:31:56 +02:00
requirements-ner-lock.txt fix: restore Linux CI and dependency audit 2026-07-15 19:31:56 +02:00
requirements-ner.in Haerte Engine, Modelle und Lieferkette 2026-07-13 16:46:22 +02:00
requirements-quality-lock.txt Haerte Engine, Modelle und Lieferkette 2026-07-13 16:46:22 +02:00
requirements-quality.in Haerte Engine, Modelle und Lieferkette 2026-07-13 16:46:22 +02:00
requirements-quanten-linux-py312-lock.txt fix: restore CI portability and security audit 2026-08-08 18:00:29 +02:00
requirements-quanten-linux-py312.in fix: restore CI portability and security audit 2026-08-08 18:00:29 +02:00
requirements-quanten-windows-py312-lock.txt fix: restore CI portability and security audit 2026-08-08 18:00:29 +02:00
requirements-quanten-windows-py312.in fix: restore CI portability and security audit 2026-08-08 18:00:29 +02:00
requirements-runtime-lock.txt fix: restore Linux CI and dependency audit 2026-07-15 19:31:56 +02:00
requirements-runtime.in fix: restore Linux CI and dependency audit 2026-07-15 19:31:56 +02:00
requirements-security-lock.txt Haerte Engine, Modelle und Lieferkette 2026-07-13 16:46:22 +02:00
requirements-security.in Haerte Engine, Modelle und Lieferkette 2026-07-13 16:46:22 +02:00
requirements.in fix: restore Linux CI and dependency audit 2026-07-15 19:31:56 +02:00
signal fix: bootstrap local checkout without ops profile 2026-07-18 00:06:51 +02:00
signal.ps1 fix: bootstrap local checkout without ops profile 2026-07-18 00:06:51 +02:00

Signal — Schlüsselbegriff-Assistenz für die steuerliche Risikosteuerung

Der stabile technische HTTP-/Paketname bleibt aus Bestandsgründen risk_layer beziehungsweise Risk-Layer-API.

Die Engine liest steuerrechtliche Texte und markiert die unbestimmten Rechtsbegriffe, an denen subsumiert werden muss. Sie beantwortet die Rechtsfrage nicht — sie stellt sicher, dass die Sollbruchstelle gesehen wird und die zugehörige Definitionsquelle einen Klick entfernt liegt.

Das System lenkt Aufmerksamkeit, es übernimmt keine Subsumtion. Die Bewertung schuldet der Berufsträger höchstpersönlich (§§ 33, 57 StBerG).

Umsetzung des Lastenhefts „Schlüsselbegriff-Assistenz" samt Erweiterungen. Mehrere deterministische Schichten erkennen auch umschriebene Sachverhalte ohne LLM (Muster, Trigger), ein semantischer Vorschlags-Modus (BGE-M3-Embedding) fängt Paraphrasen, und Fundstellen werden als Normketten (Kaskaden) geführt.

📋 Vollständiges Feature-Inventar inkl. benannter Grenzen: FEATURES.md.

Leitprinzip aller Schichten: Das System lenkt Aufmerksamkeit, es übernimmt keine Subsumtion. Definitionen und Normzuordnungen stammen aus dem lokalen Korpus/Katalog, nie aus Modellgedächtnis. Das Embedding-Modell liefert nur Vektoren; verglichen wird gegen die eingebetteten Gesetzestexte.


Aktueller Architekturstand: Scope-aware Retrieval

Der semantische Teil des Risk-Layers ist nicht als globaler Flat-Index gedacht. Eval-Läufe haben gezeigt: Werden alle Paraphrasen-Anker ungefiltert in denselben Kandidatenraum gelegt, entstehen cross-law- und intra-law-Verdrängungen. Der produktive Zielpfad ist deshalb:

Scope / Rechtsraum
→ Kandidaten innerhalb des Scopes
→ Reranker
→ Norm-Aggregation

Freeze-Stand: Innerhalb des Scopes rerankt ein Normpaket je Norm (Norm-Titel + §-Textauszug + Top-k Cards) statt einzelner Anker. Das repariert intra-law-Geschwister- kollisionen und signalverteilte Normen — kann aber scharfe Einzelanker verwässern (gemessen netto positiv, Precision-Trade-off offen dokumentiert, s. FEATURES.md). Der Flat-Index ist nur Baseline/Diagnose, nicht der finale Produktivpfad.

Der Scope kann produktiv aus dem Host-Workflow bzw. aus Beraterauswahl stammen. Open-World-Scope-Schätzung aus bloßem Text bleibt ein Fallback-/Forschungsthema.

Kuratierte Cards sind keine Rechtsentscheidung. Sie liefern Evidenz für eine Norm. Bewertet, freigegeben und produktiv übernommen werden sie erst nach fachlicher Kuration durch den Berufsträger.


Was es ist — eine headless Engine

Der Risk-Layer ist eine zustandslose Analyse-Engine mit zwei Zugängen:

  • HTTP-Vertrag /v1/* (web.py + api_v1.py) — stabil, zustandslos, Bearer-Auth. Zum Einbetten in ein Host-System (z. B. TaxTronik): Text rein, Analyse raus, kein Persist. Oberfläche, Mandanten-Persistenz und Audit übernimmt der Host.
  • CLI (cli.py) — Analyse, Katalog-/Korpus-/Graph-/Embedding-Build, Zitat-Parsing, Rechtsprechungs-Abgleich. Für Automatisierung (n8n) und Wartung.

Es gibt keine eingebaute GUI und keinen eigenen Audit-Log — beides Aufgabe des Host-Systems. § 203 StGB ist konstitutiv: zur Laufzeit verlässt nichts die Maschine außer dem bewusst freigegebenen Recherche-Export (s. u.).


Schnellstart

Voraussetzung: Python ≥ 3.11. Laufzeitabhängigkeiten: PyYAML (Kern) + numpy (Schicht 1.7). Nur Standardbibliothek für HTTP/CLI — kein FastAPI, kein Node, kein Build-Schritt. Optionaler Ausbau (lazy): sentence-transformers (BGE-M3-Semantik), llama-cpp/llama-server (Schicht 2 LLM), spacy (NER-Anonymisierung + breitere Lemmatisierung).

Der Checkout hat eine Zero-Config-Oberfläche. Linux verwendet ./signal, Windows .\signal.ps1. Der erste Aufruf legt .signal/, definitionen/, Backupziel samt Sentinel und .engine_token lokal an und führt danach das plattformgerechte Setup aus. Dabei wird ein verwalteter Service eingerichtet und sofort mit Engine und LLM gestartet:

./signal bootstrap
./signal configure
./signal start
./signal status
./signal backup
./signal update
./signal stop

Dabei ist bootstrap ausschließlich der vollständige Erstaufbau. Ein späteres update führt git pull --ff-only aus, installiert daraus nur das kleine Anwendungs-Wheel neu und startet den bereits provisionierten Dienst; Modelle, Korpusbau und Service-Installation werden nicht wiederholt. Backups werden bewusst separat mit ./signal backup erstellt. deploy existiert im Checkout nicht und bleibt dem profilierten, manifestgebundenen Release-Betrieb vorbehalten.

Mit ./signal configure lassen sich Bind-Adresse, Docker-Zugriff, Implementierung und Service-Unit später interaktiv ändern, ohne Modelle, Abhängigkeiten oder Korpus erneut aufzubauen.

Betrieb zusammen mit TaxTronik

Für den einfachen Standardbetrieb veröffentlicht jeder stabile Signal-Release zusätzlich ein versioniertes, self-contained CPU-Image. Es enthält Engine, Katalog, Graph, Embedding-Runtime, die hash-gepinnte Quantenlos-Runtime und das manifestierte lokale BGE-M3-Modell in einem read-only Release-Layer. TaxTronik kann dieses Image bei SIGNAL_DEPLOYMENT=managed selbst beziehen, Secrets provisionieren und zusammen mit der Kanzleikonsole aktualisieren; ein Host-Checkout oder separater Festwissen-Mount ist dafür nicht erforderlich.

Solange noch kein OCI-Release veröffentlicht ist, kann TaxTronik denselben verwalteten CPU-Pfad mit SIGNAL_DEPLOY_CHANNEL=source direkt aus einem kontrollierten Git-Checkout bauen. Dafür stellt scripts/build-managed-image.sh <lokales-image:tag> Runtime einschließlich der hash-gepinnten Quantenlos-Extras, Graph und das manifestierte lokale Modell zusammen. Graph und Trigger entstehen aus dem im selben Commit versionierten Normquellen-Snapshot; der Zielserver muss dafür nicht auf externe Gesetzesportale zugreifen. Nur der Modellsnapshot benötigt beim ersten, noch nicht gecachten Source-Build Netz. Der Source-Build berechnet absichtlich keinen Embedding-Index: Der rechenintensive Aufbau bleibt der ausdrücklichen Bestätigung in den TaxTronik-Einstellungen vorbehalten.

Native Installationen bleiben der vorgesehene Weg für AMD/ROCm und besondere GPU-Setups. Sie werden in TaxTronik als SIGNAL_DEPLOYMENT=external angebunden. Dieser Modus überträgt TaxTronik ausdrücklich keine Lifecycle-Rechte: Setup, Service und Updates bleiben vollständig beim Signal-Betreiber.

Ein explizites --config beziehungsweise ein checkout-frei installierter Wrapper schaltet auf den manifestgebundenen Produktionspfad. Dessen Vorlagen liegen unter deploy/risk-layer-ops.example.json (Container/Linux). Native Vorlagen liegen daneben für Linux und Windows:

Im profilierten Release-Betrieb werden Backupziel und .risk-layer-backup-target.json extern provisioniert und genau ein veröffentlichtes Release unter <release_home>/incoming abgelegt. Diese Produktionsanforderungen blockieren den lokalen Checkout-Bootstrap nicht.

Das extern bereitgestellte Backupziel wird durch .risk-layer-backup-target.json an die backup_target_id des Profils gebunden; doctor --fix erzeugt weder Ziel noch Sentinel. So wird ein fehlender oder falscher Mount vor Backup, Restore-Vorher-Sicherung und Retention abgelehnt.

Die Arbeitskopien im Repo-Root sind die lokale Operatoroberfläche, nicht selbst der Release-Vertrauensanker. Im veröffentlichten Paket werden Wrapper, Profile, Scheduler und Zipapp gemeinsam im bindenden Manifest gehasht. Checkout-frei wird dieselbe Oberfläche zum Beispiel so aufgerufen: python3.12 -I risk-layer-release-tools.pyz ops --config PFAD status, oder über einen stabil installierten Wrapper auf genau diese Zipapp.

Deploy, Update, Backup, Restore und Rollback folgen derselben Form. Der kurze Einstieg steht unter docs/operations/, das technische Detail-Runbook dokumentiert die Recovery-/Expertenebene.

./scripts/setup.sh und .\scripts\setup.ps1 sind lokale Checkout-Bootstraps; sie sind keine produktiven In-place-Updater. Der lokale Setup-Pfad provisioniert die NER-Schicht der Anonymisierung (spaCy + de_core_news_lg) automatisch mit — sie stärkt das §203-Gate. Opt-out für schlanke/Offline-Installationen: --ohne-ner bzw. -OhneNer; manueller Fallback: python -m pip install --require-hashes --only-binary=:all: -r requirements-ner-lock.txt. Zur Laufzeit erzwingt RISK_LAYER_NER=aus den deterministischen Regex-Pfad.

python -m pip install --require-hashes --only-binary=:all: -r requirements-runtime-lock.txt
python -m pip install --no-build-isolation --no-deps -e .

# (a) Headless-Engine: bedient ausschließlich das authentifizierte /v1
python -m risk_layer.web        # http://127.0.0.1:8000/v1/
#   Fuer Docker-Zugriff auf eine native Host-Engine gezielt eine Docker-Bridge-IP
#   binden, nicht 0.0.0.0. Das Deploy-Skript fragt diesen Fall interaktiv ab.

# (b) CLI: Sachverhalt analysieren → Konsolen-Zusammenfassung
python -m risk_layer.cli analyse examples/beispiel_organschaft.txt

# optional: semantische Vorschläge (BGE-M3), Python 3.12
# Linux:
python -m pip install --require-hashes --only-binary=:all: -r requirements-embedding-linux-cpu-py312-lock.txt
# Windows ohne unterstützte GPU: requirements-embedding-windows-cpu-py312-lock.txt
# Windows 11 + unterstützte AMD Radeon (ROCm 7.2.1):
python -m pip install --no-build-isolation --require-hashes -r requirements-embedding-windows-amd-py312-lock.txt
set RISK_LAYER_EMB_DEVICE=cuda
risk-layer embedding build      # einmalig — baut den Korpus-Index (+ Paraphrasen-Anker)

Produktion und optionale Backends werden getrennt gelockt: der schlanke requirements-runtime-lock.txt enthält keine Testwerkzeuge; Embedding und Quanten besitzen je einen unter Linux beziehungsweise Windows/Python 3.12 erzeugten Hash-Lock. Die Deploy-Skripte lösen keine ML-/Qiskit-Ranges mehr zur Installationszeit auf. Unter Windows nutzt das Setup für offiziell unterstützte AMD-Radeon-GPUs den separaten ROCm-7.2.1/PyTorch-2.9.1-Lock; andere Hosts fallen auf den CPU-Lock zurück. PyTorch adressiert auch ROCm-Geräte über cuda.

Sicherer Autostart

Service-Prozesse dürfen nicht privilegiert aus dem benutzerschreibbaren Checkout laufen. Der produktive Standardpfad verwendet den im Betreiberprofil konfigurierten release-aware Dienst beziehungsweise Windows-Task; dessen einmalige Einrichtung ist im technischen Detail-Runbook beschrieben. Der parameterlose Checkout-Pfad ./signal bootstrap ruft unter Linux intern das Setup mit Service-Provisionierung auf. In einem root-verwalteten Checkout unter beispielsweise /opt/signal legt Root die System-Unit, das Secret und das gesperrte Systemkonto risk-layer an; der Engine-Prozess selbst läuft unprivilegiert unter diesem Konto. Ein Aufruf als normaler Benutzer erzeugt stattdessen dessen systemd-User-Unit. Danach steuern ./signal start, stop und restart automatisch den passenden Scope. Für den User-Unit-Start schon vor der Anmeldung kann ein Administrator einmalig sudo loginctl enable-linger BENUTZER setzen. Beide Units setzen zusätzlich systemd-Sandboxing.

Unter Windows verwendet der Standardpfad .\signal.ps1 bootstrap und richtet den begrenzten Benutzer-Task ebenfalls automatisch ein; start, stop und restart steuern ihn anschließend über dieselbe Oberfläche. Das interne beziehungsweise kompatible .\deploy\install-service.ps1 installiert einen sofort gestarteten, auf RunLevel Limited begrenzten Task für den aktuellen Benutzer; Autostart erfolgt bei dessen Anmeldung und es wird kein Kennwort gespeichert. Der Unicode-sichere PowerShell-Wrapper verweigert den Start, wenn .engine_token fehlt, unlesbar oder kürzer als 32 Zeichen ist. Ein Boot-Dienst aus dem benutzerschreibbaren Checkout wird absichtlich nicht angeboten. Migration einer älteren Dienst-/SYSTEM-Installation: zuerst aus einer administrativen PowerShell .\deploy\install-service.ps1 -Remove, danach als normaler Benutzer den begrenzten Task neu installieren.

Produktive Releases, Container und Rollback

docker-compose.yml ist ein reines Produktions-Deployment: kein lokaler Build, kein latest, sondern ein zwingender OCI-Digest sowie ein read-only gemountetes, manifestiertes Festwissen-Release. Lokale Checkout-Builds verwenden ausdrücklich docker-compose.dev.yml. Das LLM läuft optional im Compose-Profil llm und ist keine Startabhängigkeit des deterministischen Engine-Kerns.

Der Forgejo-Workflow veröffentlicht nur aus einem annotierten vMAJOR.MINOR.PATCH-Tag. Das native Bundle bindet Wheel, gehashte Plattform-Locks, Offline-Wheelhouses für Linux/Windows, Korpus, Graph, Trigger, Katalog und den produktiven Embedding-Index einschließlich des am festen Commit manifestierten lokalen BGE-M3-Modells einzeln an release.json; der lokale Release-Manager baut daraus je Generation eine Offline-Venv und hält einen atomaren current/previous-Zustand. Der produktive OCI-SemVer-Tag wird erst nach allen anderen Uploads gesetzt. Normale Bedienung: Betriebsübersicht; sichere Extraktion und Recovery: technisches Detail-Runbook.

Als Bibliothek:

from risk_layer import analyse
report = analyse(open("sachverhalt.txt", encoding="utf-8").read())
print(report.zusammenfassung())
for karte in report.luecken:
    print("Lücke:", karte.rechercheauftrag)

/v1-Endpunkte (der Integrationsvertrag, Engine-Version 1.4.0)

GET /v1/health · GET /v1/status · POST /v1/analyse · GET /v1/katalog · POST /v1/katalog/definiere · POST /v1/katalog/review · GET /v1/radar · GET /v1/normgraph/{aufloesen,suche} · POST /v1/embedding/suche · GET /v1/embedding/status · POST /v1/embedding/{refresh,cancel,schedule} · GET /v1/llm/status · POST /v1/llm/start. Details + Beispiel-Antworten: INTEGRATION_TAXTRONIK.md und fixtures/.

curl -s http://127.0.0.1:8000/v1/analyse \
  -H "Authorization: Bearer $RISK_LAYER_TOKEN" -H "Content-Type: application/json" \
  -d '{"text":"Eine Schätzung der Besteuerungsgrundlagen droht wegen § 42 AO.","mitLLM":false}'

POST /v1/analyse liefert stabile Zeichen-Offsets (spans[].start/end) in den exakt übergebenen Text + text_hash (= sha256). Die schnelle Phase (mitLLM:false) ist deterministisch — gleicher Text → gleiche Spans. Darüber rendert das Host-Overlay. Auth: Bearer-Token (RISK_LAYER_TOKEN), konstant-zeitig geprüft; ein Nicht-Loopback-Bind ohne Token wird abgelehnt.

Degradations-Transparenz (engine_status, seit 1.2.0): GET /v1/status liefert einen konsistenten Engine-Snapshot (Schichten aktiv/grund, Katalogversion, Graph-/Index-Fingerprint, index_aktuell, NER-Verfügbarkeit, LLM-host:port); jede /v1/analyse-Antwort trägt denselben Block plus die effektiven Optionen des Laufs. Ein aktivierter Embedding-Index aktualisiert nur diesen Teil atomar; eine laufende Analyse behält ihren vorherigen Snapshot. Der Host legt ihn mit dem Ergebnis in seiner Audit-Chain ab — der Degradations-Nachweis je Analyse (deterministisch, keine Zeitstempel — bricht keine Hash-Ketten).

Env-Variablen: RISK_LAYER_TOKEN (Pflicht-Auth bei Netz-Bind), RISK_LAYER_OPERATOR_TOKEN (separates Secret, mindestens 32 UTF-8-Bytes, nur für mutierende Embedding-Operatorrouten), RISK_LAYER_EMBEDDING_DIR (beschreibbares persistentes Generationen-/Operatorstate-Overlay; ein manifestierter corpus/embedding bleibt der Legacy-Fallback), RISK_LAYER_WEBHOOK_ALLOW (Host[:Port]-Allowlist für den Recherche-Webhook; außerhalb von Loopback ist HTTPS Pflicht, SSRF-/DNS-Rebinding-Schutz), RISK_LAYER_EMB_DEVICE (Embedding-Encoder-Gerät, Default cuda), RISK_LAYER_NER (aus ⇒ Anonymisierung rein regex-basiert, ohne spaCy-NER), RISK_LAYER_RERANK_SCHWELLE (Abstention-Schwelle des Rerankers, Default 0.10), RISK_LAYER_LLM_BACKEND (auto|cpu|gpu; cpu erzwingt -ngl 0 und meldet den erwarteten Performance-Bottleneck an das Host-System), RISK_LAYER_LLM_TIMEOUT (Timeout der Schicht-2-Generierung in Sekunden; Auflösung: Env-Variable > Benchmark-Kalibrierung .signal/llm_bench.json > Default 300).

Timeout-Kalibrierung (risk-layer modell bench): Auf langsamer Hardware (CPU, schwache GPU) reißt der feste Timeout-Default sonst jede valide, nur späte Schicht-2-Antwort ab. modell bench misst gedrosselt (halbe Threads, Prozess-Priorität unter normal — die Maschine bleibt bedienbar) die reale Prompt- und Generierungs-Geschwindigkeit und persistiert einen daraus berechneten Timeout (Worst-Case-Budget × Sicherheitsfaktor, nie unter 300) nach .signal/llm_bench.json. Das Setup (setup.sh/setup.ps1, auch via ./signal configure) bietet den Lauf interaktiv an; im verwalteten Checkout-Betrieb jederzeit nachholbar per ./signal bench. Läuft bereits ein llama-server, wird gegen diesen gemessen (kein zweites Modell im RAM); schafft die Hardware nicht einmal den Messlauf, empfiehlt der Benchmark explizit, mitLLM deaktiviert zu lassen.


Architektur

Eingabetext
   │
   ▼ INGEST            ingest.py        Normalisierung (offset-erhaltend), Segmente, Hash
   │
   ▼ SCHICHT 1         matcher.py       wörtlicher Katalog-Matcher      ◄── catalog/begriffe.yaml
   ▼ SCHICHT 1b        muster.py        Beschreibungs-/Negativbefund-Muster (Ko-Okkurrenz je Satz)
   ▼ SCHICHT 1.5       triggerindex.py  korpus-trennscharfe Trigger (IDF) → Norm-Kandidaten
   ▼ SCHICHT 1.7       embedding.py     BGE-M3-Semantik → Norm-VORSCHLÄGE (kein hartes Markieren)
   ▼ SCHICHT 2 (opt.)  schicht2.py      LLM-Kandidaten + Span-Verifikation
   ▼ STREIT            muster.py        Subsumtions-Brennpunkte markieren + benachbarte Marken verstärken
   │
   ▼ AUFLÖSUNG         resolver.py      Definition NUR aus Quelle, sonst Lücke; Normketten (Kaskaden)
   │
   ▼ ANTWORT           web.py/api_v1.py /v1-JSON: spans, karten, risiken, semantik, summary, text_hash

Trennlinie der Zuständigkeit: Schichten 1/1b/1.5 und die Definitionsauflösung sind reiner, reproduzierbarer Code — trainingsdaten-unabhängig. Das LLM (Schicht 2) und die Semantik (1.7) dürfen ausschließlich zeigen/vorschlagen, nie definieren oder entscheiden — erzwungen durch Span-Gate (schicht2.py), Existenzprüfung im Graphen und den reinen Vorschlags-Modus der Semantik. Alle Schichten sind additiv (keine überschreibt die vorige).

Datei Verantwortung Lastenheft
risk_layer/ingest.py Normalisierung, Segmentierung, Offset-Map, Hash /LF10//LF30/
risk_layer/catalog.py Katalog laden + validieren + Versionierung /LF220//LF230/
risk_layer/matcher.py deterministischer Schicht-1-Matcher (wörtlich) /LF40//LF80/
risk_layer/muster.py Schicht 1b: Beschreibungs-Muster + Streit-Detektor Erweiterung
risk_layer/triggerindex.py Schicht 1.5: IDF-Trigger-Index (Begriff+Norm, ohne Modell) Erweiterung
risk_layer/embedding.py Schicht 1.7: BGE-M3-Semantik-Index, Paraphrasen-Anker, Vorschlags-Modus Erweiterung
risk_layer/schicht2.py LLM-Kandidaten, Schema/GBNF, Span-Gate /LF90//LF140/
risk_layer/resolver.py Definitionsauflösung / Lücken /LF150//LF160/
risk_layer/pipeline.py Orchestrierung aller Schichten → RunReport; AnalyseOptionen = Single Source of Truth der Lauf-Optionen (CLI/Web/API-Parität)
risk_layer/idschema.py kanonisches Knoten-ID-Schema (norm/verw/urteil) Parser-Spez. 1
risk_layer/lawmap.py Law-Alias-Map (Longest-Match) + Harvest-Hook Parser-Spez. 2.4
risk_layer/zitate.py Norm-/Entscheidungszitat-Parser, Kanonisierung Parser-Spez. 2/3
risk_layer/unknown_gate.py „Unknown als Risiko"-Gate + offline-Proposer Erw. §3 /LF270310/
risk_layer/berater.py Berater-Definitionen (personal/geteilt, Lebenszyklus) Erw. §4 /LF320330/
risk_layer/persistenz.py geteilte Store-Primitive (Slug, atomares Schreiben)
risk_layer/web.py headless /v1-HTTP-Server (stdlib, Bearer-Auth) Integration
risk_layer/api_v1.py stabiler, zustandsloser /v1-Vertrag Integration
risk_layer/cli.py Kommandozeile (analyse, build, zitat, abgleich) Stufe 0
risk_layer/report.py annotierter HTML-/Text-Report (CLI-Ausgabe) /LF180//LF200/
risk_layer/anonymize.py lokale Abstraktion/Anonymisierung (§ 203) Geheimnisschutz
risk_layer/recherche.py Rechercheauftrag-Export (n8n → Cloud-LLM), §203-Re-Check Geheimnisschutz
risk_layer/rss.py + abgleich.py BFH/BMF-RSS + Abgleich-Schleife Erw. §5 /LF340370/
risk_layer/radar.py Norm-Änderungs-Radar (Manifest-Diff → betroffene Festwissens-Anker) Re-Review
risk_layer/llm.py Schicht-2-Provider (llama-server / llama-cpp) Stufe 1 /LF110140/
risk_layer/corpus.py gii-Fetcher + Korpus-Config (aus Katalog) Stufe 2
risk_layer/gii.py gii-XML → Norm-Knoten Parser-Spez. 4
risk_layer/graph.py SQLite-Normgraph + Auflösung + Dangling Stufe 2 /LF170/
catalog/begriffe.yaml Single Source of Truth (Schicht 1) /LF220/

Deterministische Erkennung über die wörtliche Schicht hinaus

Reale Sachverhalte benennen die Rechtsbegriffe selten — sie umschreiben sie, oft als Negativbefund. Drei zusätzliche, trainingsdaten-unabhängige Schichten schließen diese Lücke vor dem LLM:

Schicht 1b — Beschreibungs-Muster (muster.py). Ko-Okkurrenz je Satz: ein Muster trifft, wenn aus jeder geforderten Wortgruppe mindestens ein Glied im selben Satz vorkommt. „Kasse" allein feuert nicht, „Kasse" + „nicht nachvollziehbar" schon → § 146/158 AO. Der Streit-Detektor markiert zusätzlich Subsumtions-Brennpunkte („streitig", „hätte … vermeiden müssen", „nachlässig") und verstärkt die im selben Satz liegenden Begriffsmarken — die FF/IN-Stellen, an denen die Subsumtion kippt.

Schicht 1.5 — Trigger-Index (triggerindex.py). Über den gesamten Korpus vorberechnete, IDF-gewichtete Trigger-Wörter → Norm-Kandidaten. Trennscharf („Kassenführung" ≠ „Unterstützungskasse"), kuratierbar (geprueft-Flag), ohne Modell.

risk-layer trigger build                 # baut den Index aus dem Normgraph
risk-layer trigger lookup "Bareinnahmen fehlen, die Kasse ist nicht nachvollziehbar"

Schicht 1.7 — Semantik / Embedding (embedding.py). BGE-M3 bettet den Gesetzeskorpus sowie kuratierte Anker ein und findet Paraphrasen ohne geteiltes Fachwort. Die Semantik liefert Norm-Vorschläge, keine harten Markierungen. Gemessen gibt es keinen robusten globalen Schwellwert für Auto-Markierungen.

Cards / Anker als Evidenz. Kuratierte Paraphrasen- und Tatbestand-Cards werden als Evidenz für eine Norm geführt. Breite Paragraphen-Anker können bei wachsendem Bestand diffuse Verdrängung erzeugen; für breite Schema-Normen sind deshalb tatbestand- bzw. absatznahe Cards vorzuziehen. Das Modell liefert nur Vektoren; verglichen wird gegen den Korpus — nie Modellgedächtnis.

Scope+Rerank. Der Reranker ist nicht als Reparatur eines beliebig großen Flat-Index gedacht. Der Zielpfad ist scope-aware: Rechtsraum bestimmen, Kandidaten im Scope bilden, dann Reranker und Norm-Aggregation. Oracle-Scope-Läufe zeigen deutlich bessere Stabilität gegen cross-law-Verdrängung; deploybare Scope-Quellen sind Host-Workflow, Beraterauswahl oder später ein lokaler Klassifikator.

risk-layer embedding build               # baut den Index (+ Katalog-Paraphrasen-Anker)
risk-layer embedding suche "Der Chef hat die Lohnsteuer trotz Geld nicht abgeführt"

Im eingerichteten lokalen Checkout ist der bevorzugte Betreiberpfad ./signal embedding-refresh beziehungsweise .\signal.ps1 embedding-refresh. Er baut den BGE-M3-Index isoliert, promotet ihn atomar und bestätigt die Live-Aktivierung nach einem blockierenden Dienstneustart. Bei fehlgeschlagener Readiness werden alter Disk-Stand und Dienst automatisch wiederhergestellt; --no-restart verschiebt die Aktivierung bis zum nächsten Dienststart. Fehlt die optionale Embedding-Laufzeit, wird sie einmalig mit ./signal embedding-refresh --install-dependencies aus dem plattformgebundenen CPU-Hashlock installiert. Profilierte Produktionsinstanzen halten Graph, Katalog und Modell weiterhin unveränderlich; die Laufzeit-API kann daraus ausschließlich ein getrenntes regenerierbares Index-Overlay erzeugen. Setup und Dienststart lassen den Operatorplan standardmäßig ausgeschaltet und starten keinen Neuaufbau. Ein laufender API-Auftrag kann mit seiner Job-ID über POST /v1/embedding/cancel kooperativ zwischen Modell-Batches beendet werden; eine Teilgeneration wird nie aktiviert.

Der Index trägt den Korpus- und Katalogstand sowie seine eindeutige Buildinput-Identität (graph_fingerprint, catalog_version, catalog_fingerprint und input_fingerprint in meta.json). Werden Normgraph oder Katalog danach geändert, deckt pruefe_aktualitaet den veralteten Index beim Start auf (Warnung im Log; maschinell sichtbar als index_aktuell im engine_status) — Abhilfe: embedding build erneut.

Der Index führt die Paragraph-Ebene; tatbestand-/absatznahe Cards für breite Schema-Normen rollen per para_level auf ihre §-Zielnorm hoch (📌 s. FEATURES.md §3). Ein lautes Audit meldet beim Build jeden toten Anker (Paraphrase ohne Index-Ziel).

Normketten — Fundstellen als Kaskaden. Eine Fundstelle ist oft eine Kette („§ 14 KStG iVm § 2 Abs. 2 GewStG"). Treffer und Vorschläge liefern die ganze Kaskade — der GewSt-Durchschlag wird sichtbar, nicht nur die Spitze.


Der Begriffskatalog

catalog/begriffe.yaml ist eine versionierte Datendatei, getrennt vom Code (/LF220/, /NF50/). Eine Katalogänderung erfordert keinen Code-Deploy; sie wird über Git nachvollzogen (/LF230/). Jede Auswertung führt die verwendete Katalogversion in der Antwort mit (katalog_version).

- id: kst_finanzielle_eingliederung
  begriff_kanonisch: "finanzielle Eingliederung"
  aliase: ["Mehrheit der Stimmrechte", "Stimmrechtsmehrheit am Organträger"]
  norm_anker: ["§ 14 Abs. 1 Satz 1 Nr. 1 KStG"]
  # Fundstelle als KETTE (Kaskade) — der Durchschlag auf die GewSt:
  normketten:
    - glieder: ["§ 14 KStG", "§ 2 Abs. 2 Satz 2 GewStG"]
      verknuepfung: "iVm"
      hinweis: "KSt-Organschaft schlägt über § 2 GewStG auf die GewSt durch."
  # Beschreibungs-Muster (Schicht 1b): trifft, wenn aus JEDER Gruppe ein Glied im Satz steht
  ausloeser:
    - gruppen:
        - ["mutter", "konzern", "anteilsmehrheit", "stimmenmehrheit"]
        - ["beherrscht", "durchregier", "wie eine einheit"]
  # Laien-Umschreibungen (Schicht 1.7): werden als Anker mit-eingebettet
  paraphrasen:
    - "Die Konzernmutter hält die Stimmenmehrheit und kann in der Tochter durchregieren."
  definitionsquelle:
    - { typ: "Verwaltung", fundstelle: "R 14.2 KStR" }
  governance_typ: "FP"          # FP formal-periodisch | FF fortlaufend-faktisch | IN inhärent
  schadensintensitaet: "hoch"   # niedrig | mittel | hoch
  kaskadenreichweite: 2         # 1 | 2 | 3
  gefaehrdungsnotiz: "Muss vom Beginn des Wirtschaftsjahrs an ununterbrochen bestehen. …"

norm_anker bleibt die flache Liste aller berührten Normen. normketten, ausloeser und paraphrasen sind optional und additiv — fehlen sie, verhält sich der Eintrag wie bisher (rückwärtskompatibel).

⚠️ Katalogpflege ist der kritische Pfad (/R10/). Eine nicht eingetragene Entscheidung ist eine unsichtbare Lücke. Norm-Anker und Fundstellen des Seed-Katalogs sind vor produktivem Einsatz vom Berufsträger zu verifizieren.

Ein Eintrag ohne definitionsquelle wird bewusst als Lücke (Rechercheauftrag) geführt — niemals wird eine Definition erfunden (/LF160/).


Kanonische IDs & Zitat-Parser (das universelle Join)

Der Zitat-Parser hebt jedes Norm-, Verwaltungs- und Entscheidungszitat auf eine kanonische Knoten-ID — die eine Komponente, die Katalog, Gesetze, Verwaltung und Urteile zusammenhält. Reiner Code, ohne Gesetzeskorpus lauffähig.

python -m risk_layer.cli zitat "§ 2 Abs. 2 Nr. 2 UStG"
#   norm:UStG:2:abs2:nr2          konfidenz=hoch

python -m risk_layer.cli zitat "§§ 14, 17 KStG"        # Liste → mehrere Anker
python -m risk_layer.cli zitat "§ 14 Abs. 1 Nr. 1 bis 3 KStG"   # Bereich expandiert
python -m risk_layer.cli zitat --urteil "BFH v. 18.1.2023  XI R 29/22, BStBl. II 2023, 409"
#   urteil:BFH:XI-R-29/22   (Fundstelle als Alias)
Zitat Kanonische ID
§ 2 Abs. 2 Nr. 2 UStG norm:UStG:2:abs2:nr2
§ 14 Abs. 1 Nr. 3 Satz 4 KStG norm:KStG:14:abs1:nr3:s4
Art. 11 MwStSystRL norm:MwStSystRL:art11
Abschn. 2.8 … UStAE verw:UStAE:2.8
BFH v. 18.1.2023 XI R 29/22 urteil:BFH:XI-R-29/22

Listen/Bereiche/ff. werden expandiert bzw. als offener Bereich geflaggt; relative Verweise („Satz 1") werden bewusst als Risiko markiert (relativ, niedrige Konfidenz) statt geraten. Nicht eindeutig parsebare Fundstellen liefern bewusst keine ID (null) — niemals geraten.

„Unknown als Risiko" & Berater-Definitionen (Erweiterung)

Default-Umkehr: nicht „kenne ich nicht → Schweigen", sondern „kann ich nicht sauber auflösen → Risiko". Zustände je Textstelle: treffer · luecke · unknown_risiko · berater_definiert · verworfen.

Das Unknown-Gate kombiniert Signale (nie nur eines): Norm-Nähe (nutzt den Zitat-Scanner), definitorische Form („im Sinne des", „setzt voraus, dass" …), Negativfilter (Stoppwort/Named Entity/Gesetzeskürzel → Veto), Schwellenlogik. Es läuft offline ohne LLM; ein LLM-Signal ist additiv. Recall-first: im Zweifel eskalieren.

# eskaliert nicht-katalogisierte, juristisch aufgeladene Begriffe (offline)
python -m risk_layer.cli analyse sachverhalt.txt --unknown

# Define-on-the-fly: der Katalog wächst durch Nutzung (CLI oder POST /v1/katalog/definiere)
python -m risk_layer.cli definiere "Betriebsaufspaltung" \
    --text "Aufteilung in Besitz- und Betriebsunternehmen mit Verflechtung." \
    --berater meine_defs --autor rey --norm "§ 15 EStG"

Berater-Definitionen trennen zwei Ebenen hart (Haftung): scope=personal (nur Autor, frei) und scope=geteilt (für andere; erst nach Freigabe kanonisch, Lebenszyklus entwurf → geprüft → freigegeben). Über /v1/katalog/definiere wird vor dem Schreiben gegen identifizierende Angaben geprüft (§ 203) — der geteilte Katalog trägt kein Geheimnis.

Review-Lebenszyklus über /v1: Statuswechsel geteilter Einträge laufen über POST /v1/katalog/review (nur Vorwärts-Übergänge; die Antwort nennt alter_status/neuer_status/pruefer), damit das Host-System die Freigabe in seiner Audit-Chain protokolliert — die Engine auditiert nicht. Das CLI-Kommando definiere ist als Nicht-Audit-Pfad gekennzeichnet (lauter stderr-Hinweis): Operator-Werkzeug, in Produktion gehört der Write durch /v1. Altbestände aus Versionen ohne hartes Vier-Augen-Gate werden beim Laden einzeln auf entwurf zurückgestuft, nie automatisch freigegeben und mit Grund sowie früherem Status in geteilt.yaml markiert. GET /v1/status weist diese Migrationen unter festwissen_status.review_migrationen pro ID aus; jeder quarantänisierte Eintrag muss den regulären Review erneut vollständig durchlaufen.

Stufe 2 — Normgraph aus gesetze-im-internet

Der Korpus ist lokal die Quelle der Wahrheit; zur Laufzeit wird nie ins Netz gegriffen. Das Befüllen ist ein getrennter Build-Schritt:

risk-layer korpus init --voll  # breiter Steuer-Korpus (~21 Gesetze; ohne --voll nur Katalog-Gesetze)
risk-layer korpus fetch        # lädt die xml.zip von gesetze-im-internet.de (einmalig, online)
risk-layer graph build         # baut den SQLite-Normgraph (+ rotiert das Norm-Manifest fürs Radar)
risk-layer graph aufloesen "norm:UStG:2:abs2:nr2"
risk-layer graph dangling      # Verweise auf fehlende Knoten (/T50/, sichtbares Negativwissen)
risk-layer radar               # geänderte/entfallene Normen + betroffene Festwissens-Anker
                               #   (--json maschinenlesbar, --fail-bei-befund für Skripte/CI)

Liegt ein Graph unter corpus/graph.sqlite, hängt die Analyse jedem Treffer den Gesetzeswortlaut aus dem Korpus an die Karte (/LF170/) — Quelle ist die XML, niemals das Modell. Granularität §/Absatz/Nummer; Fassungslogik über gueltig_ab/gueltig_bis (/R30/).

Norm-Änderungs-Radar — Re-Review nach Korpus-Rebuild

Freigegebene Berater-Definitionen veralten sonst still, wenn der Gesetzgeber einen verankerten Paragraphen ändert — der Review-Lebenszyklus kennt nur Vorwärts-Schritte. Jeder graph build rotiert deshalb das Norm-Manifest (norm_manifest.jsonnorm_manifest.vorher.json); risk-layer radar bzw. GET /v1/radar vergleichen die Stände und benennen dringlichkeitssortiert die betroffenen Festwissens-Anker (freigegebene geteilte Definitionen zuerst, dann Kuratierungen und Katalog-Anker). Die Engine berichtet nur — die Review-Aufgaben erzeugt das Host-System und protokolliert sie in seiner Audit-Chain. Eine automatische Status-Rücksetzung gibt es bewusst nicht (Nur-vorwärts-Invariante): ob die Gesetzesänderung die Definition überhaupt berührt, entscheidet der Berufsträger (§§ 33, 57 StBerG).

Stufe 1 — lokales LLM (Schicht 2)

Das LLM ist nur ein zusätzliches Signal; die Sicherheit liegt im Code: Constrained Decoding (JSON-Schema/GBNF) und Span-Verifikation. Jeder LLM-Kandidat durchläuft danach das Unknown-Gate. Im /v1-Vertrag ist Schicht 2 opt-in (mitLLM:true); die Engine verbindet sich dann zum konfigurierten --llm-url (server-seitig, nicht vom Aufrufer wählbar — kein SSRF).

risk-layer modell list                 # Empfehlungen (Default: Granite-4.1-8B Q5_K_M)
risk-layer modell download             # lädt das GGUF (resumebar) nach ./models
risk-layer modell engine --backend auto # lädt die fest gepinnte llama.cpp-Engine
risk-layer modell start                # startet llama-server (Vulkan, alle Layer auf GPU)
risk-layer analyse sachverhalt.txt --unknown    # --no-schicht2 schaltet ab

Ist kein Server erreichbar, läuft die Analyse normal ohne Kandidaten weiter. Modell, Quantisierung, Seed und Temperatur (=0) werden im Provenienzfeld des Reports geführt (das Host-System schreibt daraus sein Audit, /LF140/).

Engine-Supply-Chain: Der automatische Engine-Download unterstützt Windows x64 und Ubuntu/Linux x64. Er verwendet ausschließlich den im Repository festgelegten llama.cpp-Release b9986; Assetname, Größe und SHA-256 stehen in risk_layer/llama_cpp_manifest.py. Es gibt keine Abfrage von latest und keinen Trust-on-first-use: Ein Archiv wird vor dem Entpacken hart gegen den gepinnten Digest geprüft und bei Abweichung gelöscht. Andere Plattformen brechen geschlossen ab. Ein selbst gebautes bzw. separat verwaltetes Binary bleibt per --llm-bin PFAD oder PATH ausdrücklich nutzbar.

Kontrollierter Engine-Updateprozess:

  1. Einen konkreten Tag auf der offiziellen llama.cpp-Release-Seite auswählen und Release/Commit prüfen; niemals eine latest-URL eintragen.
  2. Die Assetnamen, Größen und sha256:-Digests über die offizielle GitHub-API für genau diesen Tag (/repos/ggml-org/llama.cpp/releases/tags/<TAG>) holen. Assets ohne SHA-256-Digest werden nicht aufgenommen.
  3. Jedes aufzunehmende Asset von seiner offiziellen Release-URL laden, den SHA-256 lokal (sha256sum bzw. Get-FileHash -Algorithm SHA256) neu berechnen und mit dem API-Digest vergleichen.
  4. Tag und Manifest gemeinsam reviewen; danach mindestens pytest -q tests/test_modell.py, ruff check risk_layer/modell.py und mypy ausführen. Erst dieser Commit autorisiert den neuen Engine-Stand.

Steuerung über /v1: Das Host-System startet den llama-server per POST /v1/llm/start (idempotent, blockiert nicht — Bereitschaft pollen) und prüft ihn mit GET /v1/llm/status. Der Status liefert neben der Erreichbarkeit auch die Queue — aktive/freie Slots bzw. processing/deferred des llama-server —, damit der Host entscheidet, ob er mitLLM:true jetzt sendet oder wartet. Modell/Binary kommen aus der Engine-Config (kein Request-Input → kein Injection-Vektor). Ein über /v1/llm/start gestarteter llama-server bekommt --slots --metrics automatisch; ein manuell gestarteter braucht diese Flags, sonst bleibt queue null (kein Fehler). Zusätzlich weist backend=cpu den voll funktionsfähigen, aber erwartbar langsamen CPU-Pfad als performance_bottleneck=true aus.

LLM-Norm-Einordnung (Vorschlag, kein Mapping): Zu einer erkannten Stelle darf das Modell eine einschlägige Norm vermuten (norm_vorschlag). Der Normgraph prüft dann ausschließlich die Existenz des Paragraphen (✓ im Korpus / nicht verifizierbar) — es wird nie fest gemappt.

Geheimnisschutz — Rechercheauftrag an ein externes LLM (§ 203 StGB)

Markierte Stellen lassen sich an ein großes (Cloud-)LLM weitergeben — aber nur abstrahiert. Der Ablauf (in anonymize.py + recherche.py, als Bibliothek/CLI nutzbar) erzwingt den Geheimnisschutz:

  1. Lokal abstrahieren (anonymize.py): Firmen, Personen, Beträge, Daten, Steuer-/Konto-Nummern → stabile Platzhalter ([Gesellschaft A] …). Die Rechtsbegriffe bleiben. Die Rückübersetzungstabelle bleibt lokal.
  2. Mensch prüft & schwärzt nach (Pflicht).
  3. Export nur nach Re-Check: findet die Abstraktion noch Identifizierendes, wird der Export verweigert (ohne bewusstes Erzwingen). Hinaus geht nur der abstrahierte Text + die Rechtsbegriffe + die Frage. Der Webhook-Versand ist default-deny (nur Hosts aus RISK_LAYER_WEBHOOK_ALLOW, keine Redirects, außerhalb Loopback nur HTTPS); ohne erfolgreich gelaufene lokale NER bleibt der lokale Download möglich, automatischer Webhook-Versand aber gesperrt; ein identifizierender Restbefund geht selbst mit force nie automatisch hinaus (force erlaubt dann nur den lokalen Download). Die Rückübersetzung wird nie mitexportiert.

Rechtsprechungs-Monitor — BFH/BMF-Abgleich (Erweiterung §5)

risk-layer abgleich zieht die RSS-Feeds von BFH und BMF, extrahiert über den Zitat-Parser die berührten Normen und konfrontiert sie mit den im Katalog / in Berater-Definitionen verankerten Begriffen. Dockt eine neue Entscheidung an denselben Norm-Anker an, wird der Begriff zur Prüfung vorgelegt — konfrontieren, nicht entscheiden (/LF370/). Nur der RSS-Abruf geht ins Netz; bereits gesehene Einträge werden gemerkt.


Sicherheits- und Reproduzierbarkeitsgarantien

Garantie Mechanismus Test
Schicht-1-Recall = 100 % auf dem Katalog deterministisches Matching je Oberfläche test_ab10_*
keine erfundenen Treffer jeder Treffer trägt Katalog-ID + wörtlichen Beleg test_ab20_*
Span-Treue = 100 % (Schicht 2) Pflicht-Gate verifiziere_span test_ab30_*
Definition nie aus dem Modell Resolver greift nur auf Katalogquellen zu test_ab40_*
bit-identische Reproduzierbarkeit rein deterministische Schicht 1 (gleiche /v1-Antwort) test_ab50_*
Versions-/Provenienz-Vollständigkeit Hash + Katalogversion + Schicht-2-Info je Lauf test_ab60_*
Netz-Bind nur authentifiziert Bearer-Token (hmac.compare_digest); kein Token ⇒ nur Loopback test_web_surface
kein Pre-Auth-/Body-DoS Auth vor Body-Read + Body-Cap (8 MiB) + Socket-Timeout test_web_surface
kein Webhook-SSRF webhook_url default-deny (RISK_LAYER_WEBHOOK_ALLOW), keine Redirects test_recherche
trainingsdaten-unabhängig Erkennung ruht auf Korpus/Katalog, nicht Modellwissen
python -m pip install --require-hashes --only-binary=:all: -r requirements-lock.txt
python -m pip install --no-build-isolation --no-deps -e .
python -m pytest -q          # vollständige Suite: /AB10//AB60/, Parser /T10//T40/, /v1-Vertrag +
                             #   Surface/Auth + engine_status, Engine-Analysevertrag,
                             #   AnalyseOptionen, Graph, LLM-Mock + Norm-Vorschlag,
                             #   Muster/Streit, Trigger, Embedding/Paraphrasen-Anker +
                             #   Staleness, Anonymisierung (inkl. NER), Rechercheauftrag
                             #   (SSRF-Allowlist), RSS/Abgleich, Radar, Review, AO

Roadmap

  • Erkennungs-Kern — umgesetzt: Schicht 1 (wörtlich) + 1b (Muster/Streit) + 1.5 (Trigger) + Normketten — deterministisch, ohne LLM, mit den passenden Norm-Ankern. Vollständig in Pipeline, /v1, CLI und Tests.
  • Normgraph (Stufe 2) — umgesetzt: kanonisches ID-Schema + Zitat-Parser, gii-Fetcher/-Ingest, SQLite-Normgraph mit verweist_auf-Kanten + Dangling-Report, Gesetzeswortlaut in den Karten, Fassungslogik-Hook. Reife-Reste: Verwaltungs-/ Urteilskorpus + typisierte wird_ausgelegt_durch-Kanten, Satz-Granularität, Kontext-Stack für relative Verweise.
  • LLM (Stufe 1) — umgesetzt: JSON-Schema/GBNF + Span-Gate + Provider (llama-server). Offen: GGUF-Modell auf der Maschine bereitstellen.
  • Integration (/v1) — umgesetzt: stabiler, zustandsloser Vertrag, Bearer-Auth, headless (kein GUI/Persist/Audit in der Engine — das übernimmt das Host-System). Siehe INTEGRATION_TAXTRONIK.md.
  • Unknown-als-Risiko + Berater-Definitionen — umgesetzt (Zustandsmodell, offline-Gate, Lebenszyklus inkl. POST /v1/katalog/review, define-on-the-fly) inkl. Abgleich-Schleife (BFH/BMF) und Norm-Änderungs-Radar (Re-Review-Trigger nach Korpus-Rebuild, CLI + GET /v1/radar).
  • Semantik (Schicht 1.7/1.7b) — 🟡 umgesetzt mit gemessener Architekturgrenze: BGE-M3 + Cards + Reranker liefern Norm-Vorschläge. Der globale Flat-Index ist nicht der finale Produktivpfad; Zielarchitektur ist Scope+Rerank. Tatbestand-granulare Cards heben bekannte Bedarfsfälle hub-bias-frei, sind aber nicht als Generalisierungsbeweis zu verkaufen. Offen (📌): Scope-Quelle im Produkt, Intra-Law-Kollateral bei dichten Card-Beständen und Generalisierung auf ungesehene Gold-Sets. Details: FEATURES.md.

Optionale Robustheit: spaCy-Lemmatisierung (/LF50/)

Der Matcher ist ohne spaCy bereits morphologie-tolerant. Das Deploy-Skript installiert spaCy + de_core_news_lg standardmäßig mit (Opt-out: --ohne-ner bzw. -OhneNer); manueller Fallback: python -m pip install --require-hashes --only-binary=:all: -r requirements-ner-lock.txtohne das deterministische Verhalten zu ändern.


Datenschutz (/NF10/, /NF60/)

Sämtliche Verarbeitung läuft lokal, ohne Netzaufruf zur Laufzeit (Ausnahme: der bewusst freigegebene, abstrahierte Recherche-Export und der RSS-Abgleich). Es werden keine Mandantendaten an externe Dienste gesendet (§ 203 StGB, AVV). Die /v1-Engine ist zustandslos — sie persistiert keine Mandantendaten; Speicherung und Audit liegen beim Host-System. CLI-Ausgaben (out/, *.report.html) und Secrets (.engine_token, .operator_token, .env) sind in .gitignore und im Docker-Buildkontext zusaetzlich durch .dockerignore ausgenommen.


Weitere Dokumentation

Lebende Dokumente liegen im Repo-Root (dieses README, FEATURES.md, INTEGRATION_TAXTRONIK.md und CHANGELOG.md); eingefrorene Anforderungs- und Entscheidungsdokumente (Lastenheft, Spezifikationen, ADRs) sowie Planungsdokumente liegen unter docs/. Der vollständige Index aller Dokumente mit Zweck und Stand: docs/README.md.

Für Betrieb, Update, Backup und Restore beginnt die Dokumentation unter docs/operations/README.md.