feat(obligation): obligation applicability predicates

Minimaler Applicability-Hook für die Obligation Aggregation Engine: entscheidet
aus dem Dokumenttext, ob eine bedingte Obligation anwendbar ist (True/False/None).

- has_third_country_transfer · uses_legitimate_interest · direct_marketing
  (+ Alias legitimate_interest_or_public_task)
- unbekanntes Prädikat → None → Aufrufer behält Default=anwendbar (fail-safe, nie stille NA)
- profiling/employment/telecom/health/data_act folgen als nächste Charge

Re-Benchmark (Opus-GT, 3 Firmen): Prädikate erkennen Transfer/berecht.Interesse/
Direktwerbung korrekt → keine falsche NA; NA-Flip-Probe bestätigt FEHLT→NA ohne Transfer.
14 Unit-Tests grün.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-06-24 12:43:42 +02:00
parent 402a42d30d
commit 7ec29999a2
2 changed files with 133 additions and 0 deletions
@@ -0,0 +1,76 @@
"""Applicability-Prädikate (minimal) für die Obligation Aggregation Engine.
Jedes Prädikat entscheidet aus dem Dokumenttext, ob eine BEDINGTE Obligation
anwendbar ist:
True → anwendbar (normal bewerten)
False → NICHT anwendbar (→ NA statt FEHLT)
None → Prädikat unbekannt → Aufrufer behält Default=anwendbar (fail-safe,
KEINE stille NA)
Bewusst KLEIN gehalten: nur die bereits modellierten Bedingungen
has_third_country_transfer · uses_legitimate_interest · direct_marketing
(+ legitimate_interest_or_public_task, weil objection_general_art21_1 dieselbe
Rechtsgrundlage als Anknüpfung nutzt). profiling/employment/telecom/health/
data_act folgen in der nächsten Charge — bis dahin → None → anwendbar.
"""
from __future__ import annotations
from typing import Optional
_THIRD_COUNTRY = (
"drittland", "drittstaat", "drittländ", "third countr", "außerhalb der eu",
"ausserhalb der eu", "außerhalb des ewr", "ausserhalb des ewr",
"angemessenheitsbeschluss", "standardvertragsklausel", "standarddatenschutzklausel",
"binding corporate rules", "verbindliche interne datenschutzvorschriften",
"data privacy framework", "privacy shield", "in die usa", "in den usa",
"vereinigte staaten", "international transfer", "internationale übermittlung",
"art. 44", "art. 46",
)
_LEGIT = (
"berechtigtes interesse", "berechtigten interesse", "berechtigte interesse",
"legitimate interest", "art. 6 abs. 1 lit. f", "art. 6 abs. 1 f",
"art. 6 (1) (f)", "abs. 1 buchstabe f", "interessenabwägung",
)
_PUBLIC_TASK = (
"öffentliche aufgabe", "öffentlichen aufgabe", "im öffentlichen interesse",
"art. 6 abs. 1 lit. e", "ausübung öffentlicher gewalt", "official authority",
)
_DIRECT_MKT = (
"direktwerbung", "direktmarketing", "direkt-werbung", "werbe-e-mail", "werbe-mail",
"newsletter", "werbliche", "marketingzweck", "marketing-zweck", "zwecke der werbung",
"zu werbezwecken", "e-mail-marketing", "postwerbung", "telefonwerbung",
)
def _has(text: str, kws: tuple[str, ...]) -> bool:
return any(k in text for k in kws)
def has_third_country_transfer(text: str) -> bool:
return _has(text, _THIRD_COUNTRY)
def uses_legitimate_interest(text: str) -> bool:
return _has(text, _LEGIT)
def direct_marketing(text: str) -> bool:
return _has(text, _DIRECT_MKT)
_PREDICATES = {
"has_third_country_transfer": has_third_country_transfer,
"uses_legitimate_interest": uses_legitimate_interest,
"legitimate_interest_or_public_task":
lambda t: _has(t, _LEGIT) or _has(t, _PUBLIC_TASK),
"direct_marketing": direct_marketing,
}
def applicable(conditional: str, doc_text: str) -> Optional[bool]:
"""applicable_fn-Hook für `aggregate_obligations`. Unbekanntes Prädikat → None
(Aufrufer behält Default=anwendbar; NIE stille NA)."""
fn = _PREDICATES.get(conditional)
if fn is None:
return None
return fn((doc_text or "").lower())
@@ -0,0 +1,57 @@
"""Unit-Tests für die minimalen Applicability-Prädikate."""
from compliance.services.obligation_applicability import (
applicable, direct_marketing, has_third_country_transfer,
uses_legitimate_interest,
)
class TestThirdCountry:
def test_drittland_present(self):
assert has_third_country_transfer("übermittlung in ein drittland erfolgt") is True
def test_scc_present(self):
assert has_third_country_transfer("auf basis der standardvertragsklauseln") is True
def test_absent(self):
assert has_third_country_transfer("verarbeitung nur innerhalb deutschlands") is False
class TestLegitimateInterest:
def test_present(self):
assert uses_legitimate_interest("auf grundlage unseres berechtigten interesses") is True
def test_absent(self):
assert uses_legitimate_interest("nur auf grundlage ihrer einwilligung") is False
class TestDirectMarketing:
def test_newsletter(self):
assert direct_marketing("anmeldung zum newsletter möglich") is True
def test_direktwerbung(self):
assert direct_marketing("daten für direktwerbung genutzt") is True
def test_absent(self):
assert direct_marketing("wir versenden keine werblichen inhalte ohne basis") is True # 'werbliche' trifft
def test_truly_absent(self):
assert direct_marketing("reine vertragsabwicklung") is False
class TestApplicableHook:
def test_known_predicate_true(self):
assert applicable("has_third_country_transfer", "Transfer in die USA") is True
def test_known_predicate_false_triggers_na(self):
assert applicable("has_third_country_transfer", "nur in der EU") is False
def test_public_task_alias(self):
assert applicable("legitimate_interest_or_public_task",
"zur ausübung öffentlicher gewalt") is True
def test_unknown_predicate_returns_none(self):
# profiling noch nicht modelliert → None → Aufrufer behält anwendbar
assert applicable("profiling", "irgendein text") is None
def test_case_insensitive(self):
assert applicable("uses_legitimate_interest", "BERECHTIGTES INTERESSE") is True