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:
@@ -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
|
||||||
Reference in New Issue
Block a user