fix(dse+linter): Drittland-Applicability, kein na-Detail, kurze Titel, Linter-Wortgrenzen

- Linter: FORBIDDEN_OUTPUT_TERMS per Wortgrenze → 'Schutzgarantien'/'geeignete
  Garantien' (Art. 46) passieren, 'garantiert'-Claims bleiben geblockt.
- DSE: L2-Detail wird übersprungen statt 'na', wenn die L1-Pflichtangabe fehlt
  (kein irreführendes 'nicht anwendbar' für z.B. Transfermechanismus).
- DSE: Drittland → HIGH bei dokumentiertem Drittlandtransfer (scan_context via
  AgentInput.context) — BMW (Konzern, US-Provider) ist kein weiches MEDIUM.
- DSE: Titel/Maßnahme kurz (treibt den Recommendation-Titel); ausführliche
  Begründung als evidence — behebt 120-Zeichen-abgeschnittene Überschriften.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-06-11 13:43:24 +02:00
parent 6b41eec176
commit 3c6deac1c5
5 changed files with 95 additions and 19 deletions
@@ -26,7 +26,9 @@ def test_dse_detects_core_obligations():
"bei der Aufsichtsbehoerde. ") * 3
out = _run(text)
assert out.agent == "dse"
assert out.mc_total == 33 # ART13_CHECKLIST komplett
# 10 L1-Pflichtangaben immer + L2-Details deren Parent vorhanden ist
# (fehlende Parents → L2 übersprungen, kein 'na'-Rauschen).
assert 10 <= out.mc_total <= 33
ok = [c.label for c in out.mc_coverage if c.status == "ok"]
assert any("Verantwortlich" in lbl for lbl in ok)
assert any("Rechtsgrundlage" in lbl for lbl in ok)
@@ -42,3 +44,22 @@ def test_dse_short_text_skips():
out = _run("zu kurz")
assert out.confidence == 0.0
assert all(c.status == "skipped" for c in out.mc_coverage)
def test_third_country_high_when_applicable_no_na_detail_short_action():
# Text ohne Drittland-Abschnitt + Scan-Kontext drittland=ja:
# - third_country (L1) fehlt → HIGH (nicht weiches MEDIUM)
# - Transfermechanismus (L2) → KEIN 'na' (übersprungen, Parent deckt ab)
# - Titel/Maßnahme kurz (kein 280-Zeichen-Hint als Recommendation-Titel)
text = ("Datenschutz. Verantwortlich ist die Muster GmbH, info@muster.de. "
"Zwecke und Rechtsgrundlage Art. 6. Speicherdauer. Ihre Rechte. ") * 4
out = asyncio.run(REGISTRY.get("dse").evaluate(AgentInput(
doc_type="dse", text=text,
context={"scan_context": {"third_country_transfer": "yes"}})))
tc = [f for f in out.findings if "Drittland" in f.title]
assert tc and tc[0].severity == "HIGH"
assert not any(c.status == "na" and "Transfermechanismus" in c.label
for c in out.mc_coverage)
assert all(len(f.action) < 110 for f in out.findings)
# Detail-Begründung bleibt als evidence erhalten
assert any(f.evidence for f in out.findings)
@@ -0,0 +1,32 @@
"""Disclaimer-Linter: Wort-Grenzen — Rechtsbegriffe passieren, Claims geblockt."""
from __future__ import annotations
from datetime import datetime, timezone
from compliance.services.specialist_agents._base import (
AgentOutput,
Finding,
Severity,
lint_output,
)
def _out(action: str) -> AgentOutput:
now = datetime.now(timezone.utc)
f = Finding(check_id="X", agent="t", agent_version="1",
severity=Severity.MEDIUM, title="Titel", action=action)
return AgentOutput(agent="t", agent_version="1", started_at=now,
finished_at=now, duration_ms=0, findings=[f])
def test_schutzgarantien_not_scrubbed():
out = lint_output(_out("Geeignete Schutzgarantien nach Art. 46 angeben."))
assert "Schutzgarantien" in out.findings[0].action
assert "neutraler Wortlaut" not in out.findings[0].action
def test_garantiert_claim_still_blocked():
out = lint_output(_out("Dies ist garantiert konform."))
assert "garantiert" not in out.findings[0].action.lower()
assert "neutraler Wortlaut" in out.findings[0].action