docs+feat(platform): Pruefer-Matrix-Foundation einfrieren (Evidenz, Mapping, Checker-Library, AGB-Kalibrierung)

Know-how-Freeze der Website-Compliance-Runde (DSE/Cookie/Impressum/AGB). docs: platform_evidence_v1 (Evidenz-/Qualitaetsnachweis, echte Zahlen), nutzungsbedingungen_mapping (neues Modul = Mapping, empirisch belegt), platform_checker_matrix (Meta-Modell verification_method x decision_method), verification_method, platform_validation_v1. code: checkers/ (reusable Pruefer-Library base+reference+embedding+llm, im Container validiert), agb/ (decision_method-Routing + Checker-Prototypen, 71% FP -> ~0 validiert). Dev-only, kein Prod-Push; Benchmark-GTs/Korpora im internen Archiv (data-retention).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-06-21 09:23:21 +02:00
parent 6b9c7984b4
commit 9d79cf1576
14 changed files with 900 additions and 0 deletions
@@ -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:
...
@@ -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")
@@ -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")
@@ -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")
@@ -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
@@ -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"}
@@ -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}
@@ -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)
@@ -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 (StundenTage), kein Mess-/Forschungsprojekt (Wochen). Genau die These der Prüfer-Matrix.
@@ -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.**
@@ -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.
@@ -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 59 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 `<a href="/impressum">` 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.
@@ -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 `<a href="/impressum">` 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*.