diff --git a/backend-compliance/compliance/services/checkers/__init__.py b/backend-compliance/compliance/services/checkers/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/backend-compliance/compliance/services/checkers/base.py b/backend-compliance/compliance/services/checkers/base.py
new file mode 100644
index 00000000..be464636
--- /dev/null
+++ b/backend-compliance/compliance/services/checkers/base.py
@@ -0,0 +1,82 @@
+"""Pruefer-Library — gemeinsames Interface. Siehe docs platform_checker_matrix.md.
+
+Ein Checker prueft EINEN Control gegen EIN Dokument und liefert: vorhanden / fehlt
+/ unklar (+ Evidence). Module (DSE/Impressum/AGB/...) liefern nur Control-Metadaten
+ueber `ControlSpec` (verification_method + decision_method + checker-spezifische
+Config); die Engine routet method-agnostisch zum passenden Checker.
+
+Ziel der Plattform: 14k Controls -> 7 Pruefertypen -> wenige Pruefer. Ein neues
+Modul wird damit ein Klassifizierungs-, kein Forschungsproblem.
+"""
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+from typing import Any, Optional, Protocol, runtime_checkable
+
+
+class VerificationMethod:
+ """Achse 1 — WELCHER Pruefer-Typ (Kategorie)."""
+ FIELD = "FIELD"
+ REFERENCE = "REFERENCE"
+ BEHAVIOR = "BEHAVIOR"
+ PRESENTATION = "PRESENTATION"
+ CONTENT = "CONTENT"
+ PROCESS = "PROCESS"
+ TECHNICAL = "TECHNICAL"
+ CONTRACTUAL = "CONTRACTUAL"
+
+
+class DecisionMethod:
+ """Achse 2 — WIE entschieden wird (konkreter Mechanismus)."""
+ REGEX = "REGEX"
+ EMBEDDING = "EMBEDDING"
+ LLM = "LLM"
+ LINK_RESOLVER = "LINK_RESOLVER"
+ PLAYWRIGHT = "PLAYWRIGHT"
+ AUDIT = "AUDIT"
+ SCANNER = "SCANNER"
+
+
+@dataclass
+class ControlSpec:
+ """Routing-Metadaten + checker-spezifische Config eines Controls. Module fuellen
+ nur die fuer ihren decision_method relevanten Felder."""
+ control_id: str
+ verification_method: str
+ decision_method: str
+ label: str = ""
+ severity: str = "MEDIUM"
+ patterns: list[str] = field(default_factory=list) # FIELD/REGEX, REFERENCE
+ paraphrases: list[str] = field(default_factory=list) # CONTENT (EMBEDDING/LLM)
+ embed_threshold: Optional[float] = None # EMBEDDING (per-Control)
+ topic_regex: str = "" # LLM: Section-Retrieval
+ question: str = "" # LLM: Pruef-Frage
+ extra: dict[str, Any] = field(default_factory=dict)
+
+
+@dataclass
+class DocContext:
+ """Das zu pruefende Artefakt. `text` = Volltext; `url`/`rendered` fuer
+ PRESENTATION/BEHAVIOR (Playwright) — spaeter."""
+ text: str = ""
+ url: str = ""
+ rendered: Any = None
+
+
+@dataclass
+class CheckResult:
+ present: Optional[bool] # True=erfuellt, False=fehlt, None=unklar (fail-safe)
+ evidence: str = ""
+ confidence: float = 0.0
+ source: str = "" # welcher Pruefer/Tier geantwortet hat
+ detail: dict[str, Any] = field(default_factory=dict)
+
+
+@runtime_checkable
+class Checker(Protocol):
+ """Alle Pruefer haben dieselbe Signatur -> die Engine ist method-agnostisch und
+ routet nur ueber ctrl.verification_method / ctrl.decision_method."""
+ verification_method: str
+
+ async def check(self, ctrl: ControlSpec, doc: DocContext) -> CheckResult:
+ ...
diff --git a/backend-compliance/compliance/services/checkers/embedding_checker.py b/backend-compliance/compliance/services/checkers/embedding_checker.py
new file mode 100644
index 00000000..3d3571db
--- /dev/null
+++ b/backend-compliance/compliance/services/checkers/embedding_checker.py
@@ -0,0 +1,51 @@
+"""CONTENT-Pruefer / decision_method=EMBEDDING.
+
+Ist die Pflicht SEMANTISCH im Text vorhanden? Max-Cosinus (Doc-Chunks x Control-
+Paraphrasen) >= per-Control-Schwelle. Deterministisch (festes Embedding-Modell)
+und gecacht. Rettet Recall-FP (Klausel da, anders formuliert).
+
+Faellt der Embedding-Service aus, liefert der Checker present=None (unklar) — der
+Aufrufer behaelt dann das Keyword-Ergebnis (kein Hang, kein Crash).
+(Validiert an AGB: 17 Items, per-Item-Schwelle, 0 Fehl-Rescue.)
+"""
+from __future__ import annotations
+
+import asyncio
+import logging
+
+from .base import CheckResult, ControlSpec, DocContext, VerificationMethod
+
+logger = logging.getLogger(__name__)
+
+# Paraphrasen-Vektoren je Control einmal einbetten + cachen.
+_PARA_CACHE: dict[str, list] = {}
+
+
+class EmbeddingChecker:
+ verification_method = VerificationMethod.CONTENT
+
+ async def check(self, ctrl: ControlSpec, doc: DocContext) -> CheckResult:
+ text = doc.text or ""
+ paras = ctrl.paraphrases or []
+ thr = ctrl.embed_threshold if ctrl.embed_threshold is not None else 0.60
+ if not paras or len(text) < 100:
+ return CheckResult(present=None, source="embedding")
+ try:
+ from compliance.services.mc_embedding_matcher import (
+ DIM, _chunk_text, _cosine, _embed_texts,
+ )
+ if ctrl.control_id not in _PARA_CACHE:
+ pv = await _embed_texts(paras)
+ _PARA_CACHE[ctrl.control_id] = [v for v in pv if v and len(v) == DIM]
+ pvecs = _PARA_CACHE[ctrl.control_id]
+ chunks = _chunk_text(text)
+ cvecs = [v for v in await asyncio.wait_for(
+ _embed_texts(chunks), timeout=90.0) if v and len(v) == DIM]
+ except (Exception, asyncio.TimeoutError) as e:
+ logger.info("embedding checker inaktiv %s: %s", ctrl.control_id, str(e)[:80])
+ return CheckResult(present=None, source="embedding")
+ if not pvecs or not cvecs:
+ return CheckResult(present=None, source="embedding")
+ best = max((_cosine(p, c) for p in pvecs for c in cvecs), default=0.0)
+ return CheckResult(present=best >= thr, confidence=round(best, 3),
+ source="embedding")
diff --git a/backend-compliance/compliance/services/checkers/llm_checker.py b/backend-compliance/compliance/services/checkers/llm_checker.py
new file mode 100644
index 00000000..ec8bc5d0
--- /dev/null
+++ b/backend-compliance/compliance/services/checkers/llm_checker.py
@@ -0,0 +1,73 @@
+"""CONTENT/CONTRACTUAL-Pruefer / decision_method=LLM.
+
+present/absent ueber die LLM-Kaskade (`call_with_cascade`; prod: OVH-120b zuerst).
+Retrieval = GANZE Paragraph-Abschnitte zum Topic (nicht Top-k-Chunks — das war in
+der AGB-Validierung der Schluessel). KEIN DEFECT — Korrektheits-/Defekt-Pruefung
+ist ein separater Modus. present=None bei Fehler (fail-safe: Aufrufer behaelt
+Keyword-Ergebnis). (Validiert an AGB delivery/warranty.)
+"""
+from __future__ import annotations
+
+import json
+import logging
+import re
+
+from .base import CheckResult, ControlSpec, DocContext, VerificationMethod
+
+logger = logging.getLogger(__name__)
+
+_SECTION = re.compile(r"(?m)(?=^\s*(?:§\s*)?\d+[\.\)]\s)")
+_SYS = (
+ "Du bist deutscher Compliance-Rechtsexperte. Entscheide, ob die genannte "
+ "Pflicht in den vorgelegten Abschnitten vorhanden ist. NUR die Abschnitte "
+ 'zaehlen. Antworte NUR JSON: {"verdict":"ERFUELLT|FEHLT","zitat":"woertlich '
+ 'oder leer","begruendung":"1 Satz"}.'
+)
+
+
+def _sections(text: str) -> list[str]:
+ return [s.strip() for s in _SECTION.split(text) if s.strip()]
+
+
+def _parse(txt: str) -> dict:
+ out = (txt or "").strip()
+ if out.startswith("```"):
+ out = out.split("```", 2)[1]
+ out = out[4:] if out.startswith("json") else out
+ a, b = out.find("{"), out.rfind("}")
+ return json.loads(out[a:b + 1] if 0 <= a < b else out)
+
+
+class LLMChecker:
+ verification_method = VerificationMethod.CONTENT
+
+ async def check(self, ctrl: ControlSpec, doc: DocContext) -> CheckResult:
+ text = doc.text or ""
+ if len(text) < 50:
+ return CheckResult(present=None, source="llm")
+ secs = _sections(text)
+ if ctrl.topic_regex:
+ rel = [s for s in secs if re.search(ctrl.topic_regex, s, re.I)][:6] or secs[:6]
+ else:
+ rel = secs[:6]
+ question = ctrl.question or f"Ist die Pflicht '{ctrl.label}' im Text vorhanden?"
+ try:
+ from compliance.services.llm_cascade import call_with_cascade
+ r = await call_with_cascade(
+ _SYS,
+ json.dumps({"frage": question, "abschnitte": rel}, ensure_ascii=False),
+ min_confidence=0.6, max_tokens=500,
+ )
+ obj = _parse(r.get("text"))
+ verdict = obj.get("verdict")
+ zitat = (obj.get("zitat") or "")[:120]
+ if verdict not in ("ERFUELLT", "FEHLT"):
+ return CheckResult(present=None, evidence=zitat, source=r.get("source", "?"))
+ return CheckResult(
+ present=verdict == "ERFUELLT", evidence=zitat,
+ confidence=float(r.get("confidence") or 0.0),
+ source=r.get("source", "llm"),
+ )
+ except Exception as e:
+ logger.info("llm checker fail %s: %s", ctrl.control_id, str(e)[:80])
+ return CheckResult(present=None, source="error")
diff --git a/backend-compliance/compliance/services/checkers/reference_checker.py b/backend-compliance/compliance/services/checkers/reference_checker.py
new file mode 100644
index 00000000..79d011c8
--- /dev/null
+++ b/backend-compliance/compliance/services/checkers/reference_checker.py
@@ -0,0 +1,41 @@
+"""REFERENCE-Pruefer (verification_method=REFERENCE, decision_method=LINK_RESOLVER).
+
+Ist ein klarer Verweis auf ein anderes Pflichtdokument vorhanden (+ optional: loest
+der Link auf)? Deterministisch. Bsp: 'Details in unserer Datenschutzerklaerung'.
+KEIN LLM, kein juristisches Urteil. (Validiert an AGB data_protection: 7/7.)
+
+Die tatsaechliche HTTP-Aufloesung des Links ist ein optionaler Runtime-Schritt
+(online), nicht Teil dieser deterministischen Text-Pruefung — die URL wird hier
+nur extrahiert und in `detail['link']` zurueckgegeben.
+"""
+from __future__ import annotations
+
+import re
+
+from .base import CheckResult, ControlSpec, DocContext, VerificationMethod
+
+_URL = re.compile(r"https?://[^\s)\]]+", re.I)
+
+
+class ReferenceChecker:
+ verification_method = VerificationMethod.REFERENCE
+
+ async def check(self, ctrl: ControlSpec, doc: DocContext) -> CheckResult:
+ text = doc.text or ""
+ pats = ctrl.patterns or []
+ if not pats or not text:
+ return CheckResult(present=False, source="reference")
+ for p in pats:
+ m = re.search(p, text, re.I)
+ if m:
+ window = text[max(0, m.start() - 40): m.end() + 200]
+ url = _URL.search(window) or _URL.search(text)
+ link = url.group(0) if url else None
+ return CheckResult(
+ present=True,
+ evidence=" ".join(m.group(0).split())[:120],
+ confidence=1.0,
+ source="reference",
+ detail={"link": link},
+ )
+ return CheckResult(present=False, source="reference")
diff --git a/backend-compliance/compliance/services/specialist_agents/agb/_embedding_rescue.py b/backend-compliance/compliance/services/specialist_agents/agb/_embedding_rescue.py
new file mode 100644
index 00000000..b6a348aa
--- /dev/null
+++ b/backend-compliance/compliance/services/specialist_agents/agb/_embedding_rescue.py
@@ -0,0 +1,74 @@
+"""EMBEDDING-Rescue (decision_method=EMBEDDING) fuer AGB.
+
+Fuer keyword-durchgefallene EMBEDDING-Items: pruefe, ob die Klausel SEMANTISCH
+(>= per-Item-Schwelle) im Dokument vorkommt — rettet Recall-FP (Klausel da, anders
+formuliert). Referenzvektoren = die Item-Paraphrasen aus `_routing.PARAPHRASES`
+(NICHT der mc_classification-Sidecar wie bei DSE, da AGB eine kuratierte
+Checkliste statt Library-Controls nutzt).
+
+Deterministisch (festes Embedding-Modell -> gleicher Text -> gleicher Vektor) und
+gecacht. Faellt der Embedding-Service aus, liefert die Schicht leer zurueck —
+der Keyword-Layer traegt dann (kein Hang, kein Crash).
+"""
+from __future__ import annotations
+
+import asyncio
+import logging
+
+from . import _routing
+
+logger = logging.getLogger(__name__)
+
+# Paraphrasen-Vektoren werden EINMAL pro Prozess eingebettet und gecacht.
+_PARA_VEC_CACHE: dict[str, list] = {}
+
+
+async def _ensure_para_vecs(item_ids: list[str]) -> dict[str, list]:
+ from compliance.services.mc_embedding_matcher import DIM, _embed_texts
+ todo = [i for i in item_ids
+ if i not in _PARA_VEC_CACHE and _routing.PARAPHRASES.get(i)]
+ for it in todo:
+ vecs = await _embed_texts(_routing.PARAPHRASES[it])
+ _PARA_VEC_CACHE[it] = [v for v in vecs if v and len(v) == DIM]
+ return _PARA_VEC_CACHE
+
+
+async def embedding_rescue(
+ text: str,
+ candidate_ids,
+ embed_timeout: float = 90.0,
+) -> set[str]:
+ """Returns die Teilmenge der `candidate_ids`, die semantisch (>= per-Item-
+ Schwelle) im Text vorkommt. `candidate_ids` = die im Keyword-Layer
+ DURCHGEFALLENEN Items (Recall-Rescue). Nur EMBEDDING-Items werden behandelt.
+ """
+ cands = [c for c in candidate_ids
+ if _routing.decision_method(c) == _routing.EMBEDDING
+ and _routing.PARAPHRASES.get(c)]
+ if not text or len(text) < 100 or not cands:
+ return set()
+ try:
+ from compliance.services.mc_embedding_matcher import (
+ DIM, _chunk_text, _cosine, _embed_texts,
+ )
+ para_vecs = await _ensure_para_vecs(cands)
+ chunks = _chunk_text(text)
+ if not chunks:
+ return set()
+ cvecs = [v for v in await asyncio.wait_for(
+ _embed_texts(chunks), timeout=embed_timeout)
+ if v and len(v) == DIM]
+ except (Exception, asyncio.TimeoutError) as e: # Service down -> kein Rescue
+ logger.info("agb embedding_rescue inaktiv: %s", str(e)[:90])
+ return set()
+ if not cvecs:
+ return set()
+ rescued: set[str] = set()
+ for cid in cands:
+ pv = para_vecs.get(cid) or []
+ if not pv:
+ continue
+ best = max((_cosine(p, c) for p in pv for c in cvecs), default=0.0)
+ if best >= _routing.EMBED_THRESHOLDS.get(cid, 0.60):
+ rescued.add(cid)
+ return rescued
diff --git a/backend-compliance/compliance/services/specialist_agents/agb/_llm_judge.py b/backend-compliance/compliance/services/specialist_agents/agb/_llm_judge.py
new file mode 100644
index 00000000..088a99fe
--- /dev/null
+++ b/backend-compliance/compliance/services/specialist_agents/agb/_llm_judge.py
@@ -0,0 +1,74 @@
+"""LLM-Judge (decision_method=LLM) fuer die 2 semantisch engen AGB-Items
+(delivery_timeframe, warranty_period), bei denen Embedding NICHT trennt.
+
+Retrieval = GANZE Paragraph-Abschnitte (nicht Top-k-Chunks — das war in der
+Validierung der Schluessel: Top-4-Chunks verfehlten z.B. die zalando-1-Jahr-
+Klausel, der ganze Paragraph nicht). Entscheidung ueber die LLM-Kaskade
+(`call_with_cascade`): prod startet bei OVH-120b (stark); dev nur Qwen (schwach,
+bekannte Env-Grenze). NUR present/absent — Defekt-Pruefung ist Stage 3.
+"""
+from __future__ import annotations
+
+import json
+import logging
+import re
+
+from . import _routing
+
+logger = logging.getLogger(__name__)
+
+_SECTION_SPLIT = re.compile(r"(?m)(?=^\s*(?:§\s*)?\d+[\.\)]\s)")
+_SYS = (
+ "Du bist deutscher AGB-Rechtsexperte. Entscheide, ob die genannte Pflicht in "
+ "den vorgelegten AGB-Abschnitten vorhanden ist. NUR die Abschnitte zaehlen. "
+ 'Antworte NUR JSON: {"verdict":"ERFUELLT|FEHLT","zitat":"woertlich oder leer",'
+ '"begruendung":"1 Satz"}.'
+)
+
+
+def _sections(text: str) -> list[str]:
+ return [s.strip() for s in _SECTION_SPLIT.split(text) if s.strip()]
+
+
+def relevant_sections(item_id: str, text: str, limit: int = 6) -> list[str]:
+ """Ganze Abschnitte zum Thema des Items (Topic-Regex). Fallback: erste Abschnitte."""
+ secs = _sections(text)
+ topic = _routing.LLM_TOPIC.get(item_id)
+ if not topic:
+ return secs[:limit]
+ rel = [s for s in secs if re.search(topic, s, re.I)]
+ return rel[:limit] or secs[:limit]
+
+
+def _parse(txt: str) -> dict:
+ out = (txt or "").strip()
+ if out.startswith("```"):
+ out = out.split("```", 2)[1]
+ out = out[4:] if out.startswith("json") else out
+ a, b = out.find("{"), out.rfind("}")
+ return json.loads(out[a:b + 1] if 0 <= a < b else out)
+
+
+async def llm_judge(item_id: str, text: str) -> dict:
+ """Returns {present: bool|None, zitat, begruendung, source}.
+ present=None => Judge konnte nicht entscheiden -> Aufrufer behaelt das
+ Keyword-Ergebnis (fail-safe Richtung Finding)."""
+ from compliance.services.llm_cascade import call_with_cascade
+ question = _routing.LLM_QUESTION.get(item_id, "Ist diese Pflicht im Text vorhanden?")
+ secs = relevant_sections(item_id, text)
+ user = json.dumps({"frage": question, "agb_abschnitte": secs}, ensure_ascii=False)
+ try:
+ r = await call_with_cascade(_SYS, user, min_confidence=0.6, max_tokens=500)
+ obj = _parse(r.get("text"))
+ verdict = obj.get("verdict")
+ if verdict not in ("ERFUELLT", "FEHLT"):
+ return {"present": None, "zitat": "", "begruendung": "unklar", "source": r.get("source", "?")}
+ return {
+ "present": verdict == "ERFUELLT",
+ "zitat": (obj.get("zitat") or "")[:200],
+ "begruendung": (obj.get("begruendung") or "")[:200],
+ "source": r.get("source", "?"),
+ }
+ except Exception as e:
+ logger.info("agb llm_judge fail %s: %s", item_id, str(e)[:80])
+ return {"present": None, "zitat": "", "begruendung": "judge_error", "source": "error"}
diff --git a/backend-compliance/compliance/services/specialist_agents/agb/_reference_check.py b/backend-compliance/compliance/services/specialist_agents/agb/_reference_check.py
new file mode 100644
index 00000000..bff23edd
--- /dev/null
+++ b/backend-compliance/compliance/services/specialist_agents/agb/_reference_check.py
@@ -0,0 +1,34 @@
+"""REFERENCE-Pruefer (verification_method=REFERENCE): ist ein klarer Verweis auf
+ein anderes Pflichtdokument vorhanden — und (optional) loest der Link auf?
+
+Fuer AGB: `data_protection` = Verweis auf die Datenschutzerklaerung. Eine AGB soll
+KEINE Datenschutz-Inhalte mischen; ein Verweis genuegt (§ ... / best practice).
+Deterministisch (Regex), 7/7 gegen Opus-GT — KEIN LLM, kein juristisches Urteil.
+
+Link-Aufloesung (HTTP) ist bewusst NICHT hier: das ist ein Runtime-/Online-Check
+(separater Prozess), nicht Teil der deterministischen Text-Pruefung.
+"""
+from __future__ import annotations
+
+import re
+
+from . import _routing
+
+_URL = re.compile(r"https?://[^\s)\]]+", re.I)
+
+
+def check_reference(item_id: str, text: str) -> dict:
+ """Returns {present: bool, link: str|None}.
+
+ present = ein eindeutiger Verweis auf das referenzierte Dokument steht im Text.
+ link = die in der Naehe gefundene URL (fuer einen spaeteren LINK_CHECK), falls vorhanden.
+ """
+ pat = _routing.REFERENCE_PATTERNS.get(item_id)
+ if not pat or not text:
+ return {"present": False, "link": None}
+ m = re.search(pat, text, re.I)
+ if not m:
+ return {"present": False, "link": None}
+ window = text[max(0, m.start() - 40): m.end() + 200]
+ url = _URL.search(window) or _URL.search(text)
+ return {"present": True, "link": url.group(0) if url else None}
diff --git a/backend-compliance/compliance/services/specialist_agents/agb/_routing.py b/backend-compliance/compliance/services/specialist_agents/agb/_routing.py
new file mode 100644
index 00000000..43b23068
--- /dev/null
+++ b/backend-compliance/compliance/services/specialist_agents/agb/_routing.py
@@ -0,0 +1,144 @@
+"""AGB-Routing — das verification_method / decision_method-Meta-Modell, angewandt
+auf die AGB_CHECKLIST. Siehe docs-src/development/platform_checker_matrix.md.
+
+Pro Checklisten-Item: WELCHER Pruefer (verification_method) und WIE entschieden
+wird (decision_method). Single source of truth; `agb_checks.py` bleibt die reine
+Pflichtangaben-Liste, dieses Modul ist der additive Routing-Overlay.
+
+Validiert 2026-06-20/21 gegen 7-Firmen-Opus-GT (71 % FP -> ~0):
+ - 17 Items EMBEDDING (per-Item-Cosinus-Schwelle; 21 recall-FP gekillt, 0 Fehl-Rescue)
+ - 2 Items LLM (delivery_timeframe, warranty_period; ganze Paragraph-Abschnitte + starkes Modell, present/absent)
+ - 1 Item REFERENCE (data_protection; DSE-Verweis + Link, 7/7 deterministisch)
+ - incorporation_clause MERGED in contract (implizit, kein eigener Pruefer)
+"""
+from __future__ import annotations
+
+# ── decision_method-Werte ────────────────────────────────────────────────
+EMBEDDING = "EMBEDDING"
+LLM = "LLM"
+REFERENCE = "REFERENCE"
+MERGED = "MERGED" # in ein anderes Item aufgegangen -> kein eigener Check
+
+# ── Per-Item Embedding-Rescue-Schwellen ───────────────────────────────────
+# An der 7-Firmen-GT kalibriert. BEWUSST per-Item: eine globale Schwelle trennt
+# bei juristischer Prosa nicht (PASS/FAIL ueberlappen global, trennen per-Item).
+# Vorlaeufig (FAIL n=25 klein) -> vor Prod mit mehr Firmen nachkalibrieren.
+EMBED_THRESHOLDS: dict[str, float] = {
+ "scope": 0.58, "contract": 0.58, "payment": 0.60, "payment_methods": 0.58,
+ "delivery": 0.57, "warranty": 0.58, "termination": 0.60,
+ "termination_period": 0.60, "termination_form": 0.60, "consumer_rights": 0.55,
+ "liability": 0.615, "jurisdiction": 0.585, "dispute_odr_link": 0.67,
+ "choice_of_law_specific": 0.625, "payment_due_date": 0.705,
+ "salvatory_clause": 0.565, "amendment_clause": 0.635,
+}
+
+# ── decision_method je Item (deckt alle 21 Checklisten-IDs ab) ────────────
+DECISION_METHOD: dict[str, str] = {cid: EMBEDDING for cid in EMBED_THRESHOLDS}
+DECISION_METHOD.update({
+ "delivery_timeframe": LLM,
+ "warranty_period": LLM,
+ "data_protection": REFERENCE,
+ "incorporation_clause": MERGED, # -> contract
+})
+
+# ── Applicability-Gate (VOR allen Pruefern; Geschaeftsmodell entscheidet) ──
+ABO_ONLY = {"termination", "termination_period", "termination_form"} # nur Dauerschuld
+B2C_ONLY = {"consumer_rights", "dispute_odr_link"} # nicht reines B2B
+
+# ── Referenz-Paraphrasen (Embedding-Rescue + LLM-Section-Ranking) ──────────
+PARAPHRASES: dict[str, list[str]] = {
+ "scope": ["Diese AGB gelten fuer alle Vertraege zwischen dem Anbieter und dem Kunden.",
+ "Die Angebote richten sich ausschliesslich an Verbraucher, die privat kaufen.",
+ "Geltungsbereich: fuer die Geschaeftsbeziehung gelten die nachfolgenden Bedingungen."],
+ "contract": ["Durch Anklicken des Bestellbuttons gibt der Kunde ein verbindliches Angebot ab.",
+ "Der Vertrag kommt mit Zugang der Bestellbestaetigung zustande.",
+ "Mit der Bestellung erkennt der Kunde diese AGB als Vertragsbestandteil an."],
+ "liability": ["Die Haftung fuer leicht fahrlaessige Pflichtverletzungen ist beschraenkt.",
+ "Wir haften unbeschraenkt fuer Schaeden aus Verletzung von Leben, Koerper, Gesundheit.",
+ "Bei Verletzung wesentlicher Vertragspflichten Haftung auf vorhersehbaren Schaden begrenzt."],
+ "jurisdiction": ["Es gilt das Recht der Bundesrepublik Deutschland unter Ausschluss des UN-Kaufrechts.",
+ "Gerichtsstand fuer alle Streitigkeiten ist der Sitz des Unternehmens.",
+ "Auf die Vertraege findet deutsches Recht Anwendung."],
+ "dispute_odr_link": ["Die EU-Kommission stellt eine Plattform zur Online-Streitbeilegung bereit.",
+ "Zur aussergerichtlichen Streitbeilegung steht die OS-Plattform zur Verfuegung."],
+ "choice_of_law_specific": ["Es gilt deutsches Recht unter Ausschluss des UN-Kaufrechts (CISG).",
+ "Anwendbar ist das Recht der Bundesrepublik Deutschland."],
+ "payment": ["Die Preise sind Endpreise inklusive Mehrwertsteuer; Versandkosten gesondert ausgewiesen.",
+ "Zahlungsbedingungen und Preise richten sich nach den Angaben im Bestellprozess."],
+ "payment_methods": ["Zur Zahlung stehen Vorkasse, Kreditkarte, Lastschrift, Rechnung und PayPal zur Verfuegung.",
+ "Folgende Zahlungsarten werden akzeptiert: Ueberweisung, SEPA-Lastschrift, Kreditkarte."],
+ "payment_due_date": ["Der Kaufpreis ist sofort mit Vertragsschluss faellig.",
+ "Die Zahlung ist bei Bestellung zu leisten.",
+ "Der Rechnungsbetrag wird mit Versand der Ware faellig.",
+ "Bei Kauf auf Rechnung ist der Betrag innerhalb von 14 Tagen zu zahlen."],
+ "delivery": ["Die Lieferung erfolgt an die vom Kunden angegebene Lieferadresse.",
+ "Wir liefern innerhalb Deutschlands; die Leistung wird nach Vertragsschluss erbracht."],
+ "delivery_timeframe": ["Die Lieferzeit betraegt in der Regel 3-5 Werktage.",
+ "Die Ware wird voraussichtlich innerhalb von 2 bis 4 Werktagen geliefert."],
+ "warranty": ["Es gelten die gesetzlichen Maengelhaftungsrechte (Gewaehrleistung).",
+ "Bei Maengeln stehen dem Kunden die gesetzlichen Gewaehrleistungsrechte zu.",
+ "Fuer Sachmaengel haften wir nach den gesetzlichen Bestimmungen."],
+ "warranty_period": ["Die Gewaehrleistungsfrist betraegt zwei Jahre ab Lieferung.",
+ "Die Verjaehrungsfrist fuer Maengelansprueche betraegt zwei Jahre."],
+ "termination": ["Der Vertrag kann von beiden Parteien ordentlich gekuendigt werden.",
+ "Das Abonnement kann jederzeit zum Ende der Laufzeit gekuendigt werden."],
+ "termination_period": ["Die Kuendigungsfrist betraegt einen Monat zum Vertragsende.",
+ "Der Vertrag ist mit einer Frist von vier Wochen kuendbar."],
+ "termination_form": ["Die Kuendigung bedarf der Textform und kann per E-Mail erfolgen.",
+ "Eine Kuendigung ist schriftlich oder per E-Mail moeglich."],
+ "salvatory_clause": ["Sollten einzelne Bestimmungen unwirksam sein, bleibt die Wirksamkeit der uebrigen unberuehrt.",
+ "Die Unwirksamkeit einzelner Klauseln beruehrt nicht die Gueltigkeit der uebrigen AGB."],
+ "amendment_clause": ["Wir behalten uns vor, diese AGB mit Wirkung fuer die Zukunft zu aendern.",
+ "Aenderungen dieser Bedingungen werden dem Kunden rechtzeitig mitgeteilt."],
+ "consumer_rights": ["Die gesetzlichen Rechte des Verbrauchers bleiben unberuehrt.",
+ "Zwingende Verbraucherschutzvorschriften bleiben von diesen Bedingungen unberuehrt."],
+}
+
+# ── LLM-Items: Paragraph-Abschnitts-Retrieval + Pruef-Frage ───────────────
+LLM_TOPIC: dict[str, str] = {
+ "delivery_timeframe": r"liefer",
+ "warranty_period": r"gew(?:ä|ae)hrleist|m(?:ä|ae)ngel|sachm|verj(?:ä|ae)hr|haftungsdauer|garantie",
+}
+LLM_QUESTION: dict[str, str] = {
+ "delivery_timeframe": ("Wird eine KONKRETE Lieferzeit/Lieferfrist genannt (z.B. '3-5 Werktage', "
+ "'innerhalb von 2 Werktagen')? Eine nur allgemeine Lieferregelung ODER ein "
+ "Verweis 'Lieferzeit im Bestellvorgang' ohne konkrete Frist zaehlt NICHT."),
+ "warranty_period": ("Wird eine KONKRETE Gewaehrleistungs-/Verjaehrungsfrist als ZAHL genannt "
+ "(z.B. 'zwei Jahre', 'ein Jahr')? Ein blosser Verweis auf 'gesetzliche "
+ "Verjaehrungsfristen' ohne Zahl zaehlt NICHT."),
+}
+
+# ── REFERENCE-Item data_protection ────────────────────────────────────────
+REFERENCE_PATTERNS: dict[str, str] = {
+ "data_protection": r"datenschutz(erkl(?:ä|ae)rung|bestimmung|hinweis)",
+}
+
+
+def detect_business_model(text: str) -> dict[str, bool]:
+ """Deterministischer Geschaeftsmodell-Detektor fuer das Applicability-Gate.
+ Edge-Case: gemischte Modelle (Webshop + Finanzierung/Service) koennen 'abo'
+ triggern -> dann greift das termination-Gate nicht; bewusst konservativ
+ (lieber eine Kuendigungs-Pruefung zu viel als eine echte Luecke uebersehen)."""
+ tl = text.lower()
+ consumer = ("widerrufsbelehrung" in tl) or ("widerrufsrecht" in tl and "verbraucher" in tl)
+ b2b = (not consumer) and any(s in tl for s in (
+ "geschäftskunden", "ausschließlich an unternehmer", "nur an unternehmer",
+ "lieferbedingungen für geschäftskunden"))
+ abo = any(s in tl for s in (
+ "abonnement", "mindestlaufzeit", "vertragslaufzeit", "verlängert sich",
+ "monatsabo", "jahresabo")) or ("abo" in tl and "kündig" in tl)
+ return {"b2b": b2b, "abo": abo, "b2c": not b2b}
+
+
+def is_applicable(item_id: str, model: dict[str, bool]) -> bool:
+ """Gate: gilt das Item fuer dieses Geschaeftsmodell? (False -> N/A, nicht pruefen)."""
+ if item_id in ABO_ONLY and not model.get("abo"):
+ return False
+ if item_id in B2C_ONLY and model.get("b2b"):
+ return False
+ return True
+
+
+def decision_method(item_id: str) -> str:
+ """decision_method fuer ein Item; Default EMBEDDING (Prosa-Rescue)."""
+ return DECISION_METHOD.get(item_id, EMBEDDING)
diff --git a/docs-src/development/nutzungsbedingungen_mapping.md b/docs-src/development/nutzungsbedingungen_mapping.md
new file mode 100644
index 00000000..5c81641e
--- /dev/null
+++ b/docs-src/development/nutzungsbedingungen_mapping.md
@@ -0,0 +1,43 @@
+# Mapping: Nutzungsbedingungen & Shop-AGB auf die Prüfer-Matrix
+
+> **Zweck:** Beleg der These *„neues Modul = Klassifizierung/Mapping, kein Forschungsprojekt"*. Keine neue Architektur, keine neuen Prüfertypen — nur Zuordnung vorhandener Controls + weniger neuer Items auf die bestehenden Prüfer.
+
+## 1. Shop-AGB = 0 neue Arbeit
+
+Der AGB-Korpus, an dem kalibriert wurde (Zalando, Otto, MediaMarkt, Tchibo, Lieferando), **sind** Shop-AGB. „Shop-AGB" ist damit kein neues Modul, sondern **das AGB-Modul selbst**. Aufwand: **null**.
+
+## 2. Nutzungsbedingungen (Plattform/App-ToS) = Reuse + wenige neue Items
+
+NB sind Vertragsprosa wie AGB, nur ohne Warenkauf-Pflichten und mit Plattform-spezifischen Pflichten. Mapping:
+
+**Aus AGB wiederverwendet (gleiche Prüfer, ggf. neue Paraphrasen):**
+| Item | verification_method | decision_method |
+|---|---|---|
+| scope (Geltungsbereich) | CONTENT | EMBEDDING |
+| liability (Haftung) | CONTENT | EMBEDDING |
+| jurisdiction / choice_of_law | CONTENT | EMBEDDING |
+| data_protection (DSE-Verweis) | REFERENCE | LINK_RESOLVER |
+| salvatory_clause | CONTENT | EMBEDDING |
+| amendment_clause | CONTENT | EMBEDDING |
+| termination (Konto/Account) | CONTENT | EMBEDDING |
+| consumer_rights | CONTENT | EMBEDDING |
+| dispute_odr_link (bei B2C) | REFERENCE | LINK_RESOLVER |
+
+**Nicht anwendbar (Scope-Gate, Waren-Verkauf):** payment*, delivery*, warranty*, contract/incorporation (anderer Vertragsschluss).
+
+**Neu (NB-spezifisch) — alle auf EXISTIERENDE Prüfertypen:**
+| Item | verification_method | decision_method |
+|---|---|---|
+| Nutzungsrechte / zulässige Nutzung | CONTENT | EMBEDDING |
+| Geistiges Eigentum / Schutzrechte | CONTENT | EMBEDDING |
+| Verfügbarkeit (kein Anspruch auf ununterbrochenen Betrieb) | CONTENT | EMBEDDING |
+| Account / Registrierung / Nutzerpflichten | CONTENT | EMBEDDING |
+| Nutzergenerierte Inhalte / Verhaltensregeln | CONTENT | EMBEDDING |
+| Haftung für Links / Drittinhalte | CONTENT | EMBEDDING |
+
+## 3. Ergebnis
+
+- **Shop-AGB:** 0 neue Items, 0 neue Prüfer.
+- **Nutzungsbedingungen:** ~9 Items aus AGB wiederverwendet + ~6 neue Items — **alle auf bestehenden Prüfertypen** (CONTENT/EMBEDDING + REFERENCE). **0 neue Prüfertypen.**
+
+Ein neues Web-Dokument ist damit ein **Mapping-/Klassifizierungs-** und Paraphrasen-Schreibproblem (Stunden–Tage), kein Mess-/Forschungsprojekt (Wochen). Genau die These der Prüfer-Matrix.
diff --git a/docs-src/development/platform_checker_matrix.md b/docs-src/development/platform_checker_matrix.md
new file mode 100644
index 00000000..f542cc6f
--- /dev/null
+++ b/docs-src/development/platform_checker_matrix.md
@@ -0,0 +1,87 @@
+# Prüfer-Matrix — Meta-Modell der Doc-Check-Plattform
+
+> **Status:** Plattformkonzept, **eingefroren 2026-06-21**. Abgeleitet aus 4 kalibrierten Modulen (DSE, Cookie, Impressum, AGB). Erweitert `verification_method.md` (5→8 Klassen) und fügt die `decision_method`-Achse hinzu.
+> **Kernsatz:** *Nicht jedes Control braucht denselben Richter.* Der **Kontrolltyp bestimmt den Prüfer** — nicht alles ist ein Text-/LLM-Problem.
+
+## 0. Die Architektur-Verschiebung
+
+**Vorher (implizit):** `Control → Embedding → LLM → Finding`.
+
+**Jetzt (empirisch bewiesen):**
+```
+Control → [scope-gate] → artifact_type → verification_method → decision_method
+ → passender Prüfer → Evidence → Finding (severity-getiert)
+```
+
+Vier strukturell verschiedene Dokumenttypen führten immer wieder auf dieselbe Meta-Struktur. Das ist größer als jeder Einzel-Fix: es ist mit hoher Wahrscheinlichkeit das Routing-Prinzip für alle ~14.000 Master Controls.
+
+## 1. Empirische Basis (4 Module)
+
+| Modul | dominanter Prüfer | Beleg |
+|---|---|---|
+| DSE | CONTENT (LLM/Embedding) | Kriterien-Kalibrierung, FP 11→6 % |
+| Cookie-Banner | BEHAVIOR | Enforcement / Dark-Pattern (Playwright) |
+| Cookie-Policy | CONTENT + REFERENCE | Inhalt + Verweise |
+| Impressum | FIELD + PRESENTATION (+ SCOPE-Gate) | Feld-Matcher FP 0 %, Präsentation re-routed |
+| AGB | CONTENT (KEYWORD→EMBEDDING→LLM) + REFERENCE (+ SCOPE-Gate) | 71 % FP → ~0; LLM nur 2/21 Items |
+
+## 2. Achse 1 — `verification_method` (welcher Prüfer-TYP)
+
+| verification_method | Prüfer | Leitfrage | Beleg | Reifegrad |
+|---|---|---|---|---|
+| **CONTENT** | Embedding + LLM-Kaskade | Was steht (als Offenlegung) im Text? | DSE, Cookie-Policy | kalibriert |
+| **FIELD** | Regex / Extraktion (Feldmatrix) | Welche Pflichtfelder existieren + sind valide? | Impressum (HRB, USt-IdNr, Anschrift) | ✓ FP 0 % |
+| **REFERENCE** | Link-Resolver | Gibt es einen klaren Verweis/Link, löst er auf? | AGB `data_protection` | ✓ 7/7 |
+| **BEHAVIOR** | Playwright + API | Manipuliert die UI die Entscheidung? | Cookie-Banner (Reject=Accept, Pre-Consent-Cookies) | Matrix vorhanden |
+| **PRESENTATION** | Playwright UI-Sensor | Auffindbar / sichtbar / erreichbar? | Impressum „leicht erkennbar" | re-routed |
+| **PROCESS** | Audit / Evidence | Gibt es einen internen Nachweis? | VVT, TOM, interne Richtlinie | Checkliste |
+| **TECHNICAL** | Scanner (Repo / Netz / Config) | Ist die technische Maßnahme implementiert? | geplant: CRA, NIS2, ISO 27001 | offen |
+| **CONTRACTUAL** | Clause-Engine | Ist die Klausel vorhanden + rechtskonform? | AGB (delivery/warranty; Defekte → Stage 3) | teilweise |
+
+**CONTENT vs CONTRACTUAL:** CONTENT = Offenlegungs-Prosa (DSE nennt Zwecke). CONTRACTUAL = Vertragsklauseln (AGB-Haftung/Lieferung). Beide können Embedding+LLM nutzen — die Trennung ist die Rechtsnatur + die spätere Defekt-Prüfung (Klausel rechtswidrig?).
+
+**PRESENTATION ≠ BEHAVIOR:** beide Playwright, andere Rechtslogik. PRESENTATION = Auffindbarkeit/Sichtbarkeit; BEHAVIOR = Entscheidungs-Manipulation/Dark-Pattern.
+
+## 3. Achse 2 — `decision_method` (WIE innerhalb CONTENT/CONTRACTUAL entschieden wird)
+
+Die AGB-Entdeckung: **Controls INNERHALB eines Prüfer-Typs brauchen verschiedene Entscheider.** Eskalation nur bei Bedarf (Kostendisziplin):
+
+| decision_method | Mechanismus | Wann | Beleg (AGB) |
+|---|---|---|---|
+| **KEYWORD** | Regex-Match | Pflicht eindeutig formuliert | Keyword-Layer |
+| **EMBEDDING** | per-Item-Cosinus-Schwelle (Doc-Chunks × Item-Paraphrasen) | Prosa, semantisch trennbar | 13/21 Items, 0 Fehl-Rescue |
+| **LLM** | Clause-Retrieval (**ganze §-Abschnitte**) + starkes Modell, present/absent | semantisch eng (Embedding trennt nicht) | 2/21 Items (delivery/warranty), 14/14 |
+
+`CONTENT_SIMPLE` = KEYWORD/EMBEDDING reicht; `CONTENT_COMPLEX` = LLM nötig. AGB-Bilanz: **81 % deterministisch, 19 % LLM-fähig**, LLM real nur bei Keyword-Miss.
+
+## 4. Durable Per-Control-Metadaten (das Routing-Vokabular)
+
+| Feld | Zweck |
+|---|---|
+| `artifact_type` | gegen welches Artefakt geprüft wird → Scanner-Routing |
+| `obligation_type` | Rechtsnatur: Pflicht / Empfehlung / Kann → Tier |
+| `check_intent` | was die Prüfung bezweckt |
+| `reference_allowed` | darf per Verweis erfüllt werden → REFERENCE statt CONTENT |
+| `scope` / `scope_requires` | Applicability-Gate (Geschäftsmodell, Rechtsform) — **vor** allen Prüfern |
+| `verification_method` | Achse 1 (Prüfer-Typ) |
+| `decision_method` | Achse 2 (Entscheider innerhalb CONTENT/CONTRACTUAL) |
+| `severity` | HIGH / MEDIUM / LOW → Finding vs Empfehlung |
+
+## 5. Hart erarbeitete Plattform-Prinzipien
+
+1. **Route, don't uniformly-LLM** — verschiedene Controls, verschiedene Prüfer.
+2. Eskaliere **KEYWORD → EMBEDDING → LLM nur bei Bedarf** (AGB: 17/21 ohne LLM).
+3. Embedding: **per-Item-Schwellen** (globale Schwelle scheitert bei juristischer Prosa — PASS/FAIL überlappen global, trennen per-Item).
+4. LLM-Judge: **ganze §-Abschnitte** schlagen Top-k-Chunks; **starken Tier pinnen** (billig-zuerst-Kaskade eskaliert selbstbewusst-falsche Antworten NICHT, weil die Confidence-Heuristik genauigkeits-blind ist); **present/absent** trennen von der Defekt-Prüfung.
+5. **REFERENCE (Link) ist ein eigener billiger Prüfer** — keinen „siehe Datenschutzerklärung"-Verweis durch ein LLM jagen.
+6. **SCOPE-Gate (Applicability) ist vor allen Prüfern** — N/A-Controls werden nie geprüft.
+7. **Severity → Finding vs Empfehlung** (Tier, nicht droppen).
+8. *Was im Text nicht beweisbar ist, gehört nicht in den Text-Check.*
+
+## 6. Schema-Status
+
+Kein DB-Eingriff (DB eingefroren). `verification_method` + `decision_method` als **abgeleitete Tags** in `control_classification` (aus `artifact_type` / `obligation_type` / `check_intent` + Item-Kalibrierung). `canonical_controls.verification_method` existiert (~4 % befüllt, gröbere Enterprise-Taxonomie) — **nicht** das Doc-Check-Routing.
+
+## 7. Verbindlichkeit
+
+Dies ist der **Vertrag**, gegen den implementiert wird. Die AGB-Integration und die nächsten Module (Nutzungsbedingungen, Widerruf, CRA, MaschVO, DORA, NIS2, ISO 27001, AI-Act, VVT, TOM) bauen **dieselbe** Routing-Schicht — nicht modul-lokal. Reihenfolge: **(1) diese Matrix einfrieren → (2) AGB integrieren → (3) Nutzungsbedingungen → (4) Widerruf.**
diff --git a/docs-src/development/platform_evidence_v1.md b/docs-src/development/platform_evidence_v1.md
new file mode 100644
index 00000000..a2d5d883
--- /dev/null
+++ b/docs-src/development/platform_evidence_v1.md
@@ -0,0 +1,64 @@
+# BreakPilot — Evidenz- & Qualitätsnachweis (Website-Compliance v1)
+
+> **Status:** konsolidierter Freeze-Stand 2026-06-21. Belegbasis aus 4 kalibrierten Modulen (DSE, Cookie, Impressum, AGB). Dient als (a) technischer Freeze-Record und (b) Backbone für Sales/Investoren.
+> **Hinweis:** Zahlen = *gemessene* Validierungsergebnisse gegen Opus-Ground-Truth. Tool-/Prod-Integrationsstand je Modul siehe §7 (validiert ≠ überall schon live).
+
+## 1. Kernaussage
+
+Die meisten Compliance-Tools machen: **Dokument → LLM → Finding** — ein Richter für alles. Das erzeugt systematische False Positives und hat *keine* belastbare Evidenzbasis.
+
+BreakPilot macht: **Dokument → Control-Routing → spezialisierter Prüfer → Finding.**
+
+> Wir haben **für jeden Kontrolltyp den optimalen Prüfer empirisch ermittelt** — mit echten Vorher/Nachher-Zahlen, nicht mit Marketing.
+
+Das ist über 4 strukturell verschiedene Dokumenttypen reproduzierbar belegt — und damit voraussichtlich das Routing-Prinzip für alle ~14.000 Master Controls.
+
+## 2. Die Architektur (zwei Routing-Achsen)
+
+Vollständige Kette: **Regulation → Obligation → Control → verification_method → decision_method → Prüfer → Evidence → Finding → Ticket.**
+
+- **`verification_method`** (Kategorie / welcher Prüfer-Typ): CONTENT · FIELD · REFERENCE · BEHAVIOR · PRESENTATION · PROCESS · TECHNICAL · CONTRACTUAL.
+- **`decision_method`** (konkreter Mechanismus): REGEX · EMBEDDING · LLM · LINK_RESOLVER · PLAYWRIGHT · AUDIT · SCANNER.
+
+Kernregel: *Was im Text nicht beweisbar ist, gehört nicht in den Text-Check.* Scope-Gate (Applicability) läuft vor allen Prüfern; Severity steuert Finding vs. Empfehlung.
+
+## 3. Evidenz je Modul
+
+| Modul | dominanter Prüfer | gemessenes Ergebnis | Hebel | Reife |
+|---|---|---|---|---|
+| **DSE** | CONTENT (Embedding+LLM) | False Positives **11 % → 6 %**; an **8 Firmen** validiert, Generalisierung nachgewiesen (kein Overfit auf einen Assessor); Claude-Tier-Pfad → ~2 % bekannt | Kriterien-Kalibrierung + LLM-Kaskade | **RC** |
+| **Impressum** | FIELD + PRESENTATION (+ Scope-Gate) | **171 falsche Findings → 0** (Scope-Gate); Feldmatrix (Firma/Anschrift/HRB/USt-IdNr/Kontakt) **FP 0 %, Recall 1.0**; 5 Präsentations-Controls an Playwright re-routet | Scope-Gate + deterministischer Feld-Matcher schlägt LLM | **RC** |
+| **Cookie** | BEHAVIOR + CONTENT | Artifact-Type-Trennung **Banner ≠ Richtlinie** validiert (Controls liefen am falschen Artefakt → re-routet); Browser-Verhaltens-Matrix (Enforcement, Dark-Pattern, Reject=Accept) | Artifact-Type-Routing + Playwright-Verhaltenssensor | Wave-1 (GT-Stab. offen) |
+| **AGB** | CONTENT + REFERENCE + LLM | **71 % FP → ~0** (7-Firmen-Opus-GT): 49 Findings / 35 falsch → bereinigt; Embedding-Rescue **21 Recall-FP gekillt, 0 Fehl-Rescue**; LLM-Judge (ganze §-Abschnitte) **14/14**; Reference-Check **7/7** | **decision_method pro Item** (17 EMBEDDING, 2 LLM, 1 REFERENCE) | Architektur validiert |
+
+## 4. Warum die Zahlen belastbar sind (Methodik-Rigor)
+
+- **Ground Truth mit dem stärksten Modell** (Opus-4-8), nicht mit billigen Modellen.
+- **Prove-don't-handwave:** echte FP/FN-Zählungen, Vorher/Nachher, keine Behauptungen.
+- **Generalisierung statt Overfit:** Mehr-Firmen-GT (DSE 8, AGB 7) + explizite Leitplanken gegen Ein-Assessor-Overfit.
+- **Mehrfach-Referenz-Validierung:** bei AGB 3-Wege (Opus-GT × Claude-Eigenbewertung × Laufzeit-Kaskade) — deckte sogar einen Fehler in der GT selbst auf.
+- **Stichprobe vor Aufbau:** vor jeder teuren Klassifikation/Batch zuerst stratifizierte Stichprobe geprüft (verhinderte mehrfach Aufbau auf falschem Fundament).
+
+## 5. Die Schlüssel-Entdeckung (AGB)
+
+Verschiedene Controls **innerhalb desselben Moduls** brauchen verschiedene Richter. Belege:
+- Eine **globale** Embedding-Schwelle scheitert bei juristischer Prosa; **per-Item-Schwellen** trennen sauber.
+- **Whole-Section-Retrieval** (ganze §-Abschnitte) schlägt Top-k-Chunks für den LLM-Judge deutlich.
+- Ein **billig-zuerst-Kaskaden-LLM** taugt nicht als Richter (eskaliert selbstbewusst-falsche Antworten nicht) — für harte Items starken Tier pinnen.
+- Ein **Verweis** („siehe Datenschutzerklärung") ist ein REFERENCE/Link-Check, **kein** LLM-Fall.
+
+## 6. Wettbewerbspositionierung
+
+| | Typisches Tool | BreakPilot |
+|---|---|---|
+| Prüfansatz | ein LLM für alles | Control-Routing → spezialisierter Prüfer |
+| False Positives | systematisch (LLM auf Nicht-Text-Pflichten) | je Kontrolltyp minimiert (gemessen) |
+| Evidenzbasis | keine | Mehr-Firmen-GT, reproduzierbare Zahlen |
+| Skalierung neuer Regulierungen | jedes Mal neu | Mapping auf bestehende Prüfer-Matrix |
+
+## 7. Reifegrad, Ehrlichkeit & Roadmap
+
+- **Validiert (Messung):** alle 4 Module oben.
+- **Live im Tool:** DSE-Kriterien (prod). Impressum-Scope/Feldmatrix, Cookie-Artifact-Type und AGB-C-lean sind **validiert, aber noch nicht überall ins Produkt integriert** → Demo-Integration ist der nächste Schritt (Vorher/Nachher live zeigbar machen).
+- **Website-/Marketing-Compliance: abgeschlossen** (DSE/Impressum/Cookie/AGB + Architektur). Restliche Web-Doc-Typen (Nutzungsbedingungen, Shop-AGB, Legal Notice, Social-Media) = **Mapping**, keine neue Architektur.
+- **Nächste große Etappe (nach Sales):** industrielle Compliance (CRA, Maschinenverordnung, NIS2, DORA, ISO 27001, TISAX, AI Act) — neue Prüfertypen TECHNICAL/PROCESS/EVIDENCE/SYSTEM; die Prüfer-Matrix wird dort wiederverwendet.
diff --git a/docs-src/development/platform_validation_v1.md b/docs-src/development/platform_validation_v1.md
new file mode 100644
index 00000000..717d9eaf
--- /dev/null
+++ b/docs-src/development/platform_validation_v1.md
@@ -0,0 +1,74 @@
+# Plattform-Validierung der Doc-Check-Kalibrierung — `platform_validation_v1`
+
+> **Status:** Plattform-Methodik validiert über 3 strukturell verschiedene Dokumentklassen (2026-06-19).
+> **Zweck:** Nicht ein Modul dokumentieren, sondern den **Kalibrierungsprozess** und die **empirische Fehlerkarte** der Engine — damit die *Ursachen* erhalten bleiben (nicht nur die Messwerte). Erkenntnis > Metrik.
+
+## 1. Was hier validiert wurde
+
+Vor dieser Runde war unklar, ob der Restfehler der Doc-Check-Engine aus dem **LLM**, dem **Embedding**, dem **Prompt**, der **Applicability** oder dem **Control-Katalog** stammt — alles vermischt. Nach DSE + Cookie + Impressum existiert eine **belastbare Taxonomie der Fehlerursachen**, und der **Kalibrierungsprozess** hat in drei sehr unterschiedlichen Domänen geliefert. Das ist die eigentliche Errungenschaft — größer als jede einzelne Zahl.
+
+## 2. Der Kalibrierungsprozess (wiederverwendbarer Kern)
+
+1. **Opus-GT** je `(Firma × Control)` über 5–9 repräsentative Firmen (stärkstes Modell, NICHT Haiku).
+2. **Engine-Messung** (Keyword → BGE-M3-Embedding → robuster LLM-Judge) vs GT.
+3. **FP-Cluster** — wiederkehrende Controls statt Einzel-Findings (systematisch ≠ zufällig).
+4. **Ursachen-Klassifikation** je FP: `SCOPE` / `ARTIFACT_TYPE` / `CRITERIA` / `JUDGE`.
+5. **Fix** der dominanten Ursache (versioniert, mit Rechtsnotiz).
+6. **Re-Messung** — Pflicht: FP↓ **und** FN stabil. Plus **Anti-Overfit** auf ungesehenen Firmen.
+
+## 3. Plattform-Fehlerkarte (Kernergebnis)
+
+| Modul | Dominante Ursache | Hebel | Ergebnis | Status |
+|---|---|---|---|---|
+| **DSE** | Kriterien zu streng | Kriterien-Kalibrierung (11 Controls) | FP 11 % → **6 %**, FN ~7 %; **generalisiert** (8 Firmen; fresh FP 7 % / FN 5 %) | Release-Candidate |
+| **Cookie** | `artifact_type` (Banner ≠ Richtlinie) | 31 Banner-Controls → `COOKIE_BANNER`; 21 Kriterien (Kategorie statt Pro-Cookie, Zitat optional), Pro-Cookie = Best-Practice | Precision 0,81 → **0,95**, Recall 0,26 → **0,44**, verpasste Lücken → **0 %**, abs. FP 71 → 54 | Wave-1 (dev) |
+| **Impressum** | **Scope** (GT-NA 48 %) + **Feld-Extraktion** + **Präsentation** | Scope-Gate (14 raus) + **Feldmatrix-Matcher** (Fakten) + **PRESENTATION_CHECK**-Re-Route (5) | roh: SCOPE-FP 105 / JUDGE-FP 66 → **Text-Check FP 0 % / FN 2 %** | Release-Candidate |
+
+## 4. Meta-Befunde
+
+- **Die generische Architektur bewährt sich.** Jede Domäne hat ein *anderes* dominantes Problem — `artifact_type` / `obligation_type` / `scope` tragen unterschiedlich stark. Eine gute generische Architektur erzeugt nicht überall denselben Effekt, sondern löst je Domäne ein anderes Problem. Genau das ist eingetreten.
+- **Die Zielarchitektur ist domänen-adaptiv, nicht uniform.** „Embedding → OVH → Claude" ist nicht überall richtig: bei **Prosa** (DSE/Cookie) ist die LLM-Kaskade der Hebel; bei **strukturierten Faktendokumenten** (Impressum) ist das LLM sogar schwach (es verfehlt Adressen/Felder, die *dastehen*) → dort schlagen **Scope-Gate + deterministischer Feld-Matcher** den LLM-Judge.
+- **Wiederkehrendes Anti-Muster:** „vermeintlicher Judge-Fehler → eigentlich Katalog-Fehler" (Scope, Präsentations-statt-Inhalt, Fehl-Typisierung). Erst NACH den Katalog-Fixes ist der Rest ein *echter* Judge-Fehler.
+
+## 4b. Die `verification_method`-Achse (Synthese — die eigentliche Lehre)
+
+Nicht jede Compliance-Pflicht ist ein Textproblem. Die 5 entdeckten Fehlerklassen mappen auf **5 Prüfer-Typen** — eine neue Routing-Metadaten-Achse `verification_method`, die einem Control sagt, *welcher Prüfer* zuständig ist (nicht alles an den LLM):
+
+| `verification_method` | Prüfer | Frage | Beispiel | Status |
+|---|---|---|---|---|
+| **CONTENT** | Embedding + LLM-Kaskade (OVH→Claude) | Was steht da? | DSE nennt Zwecke; Cookie-Policy | DSE/Cookie kalibriert |
+| **FIELD** | Regex/Parser (Feldmatrix) | Welche Felder existieren? | HRB, USt-IdNr, Adresse | Impressum-Fakten ✓ (FP 0 %) |
+| **PRESENTATION** | Playwright (Sichtbarkeit/Erreichbarkeit) | Ist es auffindbar/wahrnehmbar? | Impressum leicht erkennbar, ständig verfügbar; Footer nicht verdeckt | Re-Route gemacht; Check offen |
+| **BEHAVIOR** | Playwright + API (Interaktion) | Manipuliert es die Entscheidung? | Reject = Accept, Consent VOR Cookie, kein Dark Pattern | Cookie-Banner-Matrix existiert |
+| **PROCESS** | Audit/Nachweis | Gibt es internen Nachweis? | VVT, interne Richtlinie, Audit-Entscheidung | Org-Checkliste |
+
+**PRESENTATION ≠ BEHAVIOR** (beide Playwright, andere Rechtslogik): Präsentation = *Auffindbarkeit/Sichtbarkeit/Zugänglichkeit* (Impressum leicht erkennbar); Behavior = *Entscheidungs-Manipulation/Dark-Pattern* (Reject versteckt). Getrennt halten.
+
+**Playwright wird damit vom Crawler zum Compliance-Sensor:** es prüft, was kein LLM kann — `display:none`, `font-size:4px`, Cookie-Layer verdeckt den Footer. LLM sieht `` und sagt „erfüllt"; Playwright sieht die Verdeckung und sagt „nicht erfüllt".
+
+**Kern-Regel der Architektur:** *Was im Text nicht beweisbar ist, gehört nicht in den Text-Check.* → route per `verification_method`. Sobald die Klassen sauber getrennt sind, sinken die FP fast automatisch (Impressum: SCOPE+JUDGE 171 → Text-Check-FP 0).
+
+**Schema-Status:** `canonical_controls.verification_method` existiert (nur ~4 % befüllt, andere/gröbere Taxonomie document/code_review/tool/hybrid), `doc_check_controls` hat sie nicht. Die hier definierte Doc-Check-Routing-Achse ist **aus `control_classification` (artifact_type/obligation_type/check_intent) ableitbar** → kein Schema-Eingriff (eingefroren) nötig; als abgeleitetes Tag in `control_classification` führen.
+
+## 5. Mess-Disziplin (prove-don't-handwave)
+
+- GT mit dem stärksten Modell (`claude-opus-4-8`), nicht Haiku (zu lasch).
+- Robust gegen LLM-Leerantworten: Retry + `INSUFFICIENT_EVIDENCE`/Eskalation statt FEHLT (ein realer Produktions-Bug, der die FP künstlich aufblähte).
+- **Anti-Overfit:** Kriterien am Gesetz kalibrieren, dann auf *ungesehenen* Firmen gegenprüfen (DSE: 5 Original + 3 frische → stabile Zahlen = kein Overfit).
+- OVH ist stochastisch (±~Rauschen je Lauf) und strenger als Opus → der Rest-FP konvergiert über Module auf **OVH-Über-Strenge**.
+- **Zirkularitäts-Leitplanke:** Claude = Opus-GT-Modell → ein Claude-Tier-Sim misst die *Kaskaden-Reichweite* (erreicht Opus-Niveau), nicht eine unabhängige Validierung.
+
+## 6. Offen (Reihenfolge)
+
+1. **Claude-Tier-Sim (DSE + Cookie):** quantifiziert den verbleibenden **reinen** Judge-Fehler nach allen Katalog-Fixes — die letzte große unbekannte Variable. Erwartung: kleiner als roh, weil viel „Judge" sich als Katalog entpuppte.
+2. **Impressum-Fix:** Rechtsform-Scope-Gate (#33) verdrahten + deterministischer Feld-Matcher + Re-Messung.
+3. **Cookie Wave-2** (Cluster-E) + Produktions-Re-Route der 31 Banner-Controls (`control_classification`).
+4. **Produktivschaltung** DSE + Cookie (zuletzt; verify-first DB-Write).
+
+## 7. Artefakte
+
+- DSE: `docs-src/development/dse_v1_validation.md`, `dse_criteria_changelog.json`/`dse_criteria_backup.json`.
+- Cookie: `cookie_criteria_changelog.json`/`cookie_criteria_backup.json`/`cookie_best_practice.json` (Container `/tmp`), Cluster-Map.
+- Impressum: `impressum_fp_by_cause.json` (SCOPE/JUDGE-Split).
+- Gedächtnis: `project_engine_quality.md` (Detail je Modul). Werkzeuge: `cc_gt_opus_*`, `cc_engine_*`, `cc_*_candidates*` (alle macmini `/tmp`).
+- **Alle Control-Änderungen nur auf macmini-dev**, versioniert, reversibel; Prod-Schaltung ausstehend.
diff --git a/docs-src/development/verification_method.md b/docs-src/development/verification_method.md
new file mode 100644
index 00000000..6aa94aed
--- /dev/null
+++ b/docs-src/development/verification_method.md
@@ -0,0 +1,59 @@
+# `verification_method` — die Prüfer-Routing-Achse
+
+> **Status:** Architektur-Achse (2026-06-19), abgeleitet aus der 3-Modul-Kalibrierung (DSE / Cookie / Impressum).
+> **Kernsatz:** *Nicht jede Compliance-Pflicht ist ein Textproblem.* `verification_method` sagt einem Control, **welcher Prüfer** zuständig ist — damit nicht alles am LLM hängt.
+
+## 1. Warum diese Achse existiert
+
+Die Kalibrierung von drei strukturell verschiedenen Dokumentklassen zeigte drei **verschiedene** dominante Fehlerursachen — und alle ließen sich auf die *Wahl des falschen Prüfers* zurückführen:
+
+- **DSE** (Prosa): LLM-Urteil zu streng → Kriterien-Kalibrierung. Prüfer war richtig (LLM), Kriterien falsch.
+- **Cookie** (Banner ≠ Richtlinie): Controls am falschen Artefakt geprüft → `artifact_type`-Re-Route.
+- **Impressum** (Faktendokument): LLM verfehlt Felder, die *dastehen* (Adresse, HRB) → deterministischer Feld-Matcher schlägt den LLM. Und 5 Controls waren **gar nicht im Text beweisbar** (Erreichbarkeit/Verfügbarkeit) → gehören an Playwright, nicht an den Text-Check.
+
+**Regel:** *Was im Text nicht beweisbar ist, gehört nicht in den Text-Check.* Sobald die Klassen sauber getrennt sind, sinken die False Positives fast automatisch (Beleg Impressum: SCOPE+JUDGE 171 Roh-FP → Text-Check-FP 0).
+
+## 2. Die fünf Klassen
+
+| `verification_method` | Prüfer | Leitfrage | Beispiel | Reifegrad |
+|---|---|---|---|---|
+| **CONTENT** | Embedding-Recall + LLM-Kaskade (OVH→Claude) | Was steht da? | DSE nennt Verarbeitungszwecke; Cookie-Richtlinie | DSE/Cookie kalibriert |
+| **FIELD** | Regex / Parser (Feldmatrix) | Welche Felder existieren + sind valide? | HRB, USt-IdNr, Anschrift, E-Mail+Telefon | Impressum-Fakten ✓ (FP 0 %) |
+| **PRESENTATION** | Playwright (Rendering-Sensor) | Ist es auffindbar / wahrnehmbar / erreichbar? | Impressum „leicht erkennbar", ständig verfügbar, Footer nicht verdeckt | Re-Route gemacht, Checker offen |
+| **BEHAVIOR** | Playwright + API (Interaktion) | Manipuliert die UI die Entscheidung? | Reject = Accept, Cookies VOR Consent, Dark Pattern | Cookie-Banner-Matrix vorhanden |
+| **PROCESS** | Audit / Nachweis | Gibt es einen internen Nachweis? | VVT, interne Richtlinie, Audit-Entscheidung | Org-Checkliste |
+
+## 3. PRESENTATION ≠ BEHAVIOR
+
+Beide nutzen Playwright, prüfen aber **verschiedene Rechtslogik** — getrennt halten:
+
+- **PRESENTATION** = Auffindbarkeit / Sichtbarkeit / Zugänglichkeit. Beispiel: Impressum-Link erreichbar, nicht in 4px-Schrift, nicht hinter `display:none`, nicht dauerhaft vom Cookie-Layer verdeckt.
+- **BEHAVIOR** = Entscheidungs-Manipulation / Dark-Pattern. Beispiel: „Ablehnen" versteckt, Vorauswahl gesetzt, Consent technisch ignoriert.
+
+## 4. Playwright als Compliance-Sensor (nicht Crawler)
+
+Playwright prüft, was **kein** LLM kann: Der LLM sieht `` und urteilt „erfüllt"; der Sensor sieht, dass das Element verdeckt / unsichtbar / unerreichbar ist und urteilt „nicht erfüllt". Drei technische Prüfer langfristig:
+
+- **Content-Checker** → LLM (CONTENT)
+- **Structure-Checker** → Regex/Parser (FIELD)
+- **Presentation-Checker** → Playwright (PRESENTATION + BEHAVIOR)
+
+## 5. Schema-Status & Verortung
+
+- `canonical_controls.verification_method` **existiert**, aber nur ~4 % befüllt und mit *anderer*, gröberer Taxonomie (`document` / `code_review` / `tool` / `hybrid` — generische Enterprise-Verifikation, nicht das Doc-Check-Routing).
+- `doc_check_controls` hat **keine** `verification_method`-Spalte.
+- → Die hier definierte Doc-Check-Routing-Achse ist **neu**, aber **ableitbar** aus den schon vorhandenen `control_classification`-Achsen (`artifact_type` / `obligation_type` / `check_intent`). **Kein** Schema-Eingriff nötig (DB ist eingefroren) — als abgeleitetes Tag in `control_classification` führen.
+
+Heuristik für die Ableitung (Startpunkt, nicht final):
+
+| Signal | → verification_method |
+|---|---|
+| `artifact_type = COOKIE_BANNER`, Interaktionspflicht | BEHAVIOR |
+| Pflicht zu Erreichbarkeit / Sichtbarkeit / „ständig verfügbar" | PRESENTATION |
+| Faktenfeld (Anschrift, Register, Kennung) | FIELD |
+| `obligation_type` Prozess / Nachweis ohne Außenwirkung | PROCESS |
+| sonst (inhaltliche Offenlegung in Prosa) | CONTENT |
+
+## 6. Warum das über die 3 Module hinaus zählt
+
+Für die nächsten Module (CRA, Maschinenverordnung, NIS2, TISAX, ISO 27001) ist diese Achse vermutlich fast so wichtig wie `artifact_type`: viele dieser Pflichten sind **PROCESS** oder **BEHAVIOR**, kein Textinhalt. Wer sie an den LLM-Text-Check hängt, erzeugt systematische False Positives. Das ist die eigentliche Erkenntnis der Kalibrierung: **nicht** dass DSE/Cookie/Impressum funktionieren, sondern dass klar wurde, *welcher Prüfer für welche Art von Pflicht zuständig ist*.