From 7ec29999a21bc019f7bdeb00c0f060292502945d Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Wed, 24 Jun 2026 12:43:42 +0200 Subject: [PATCH] feat(obligation): obligation applicability predicates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../services/obligation_applicability.py | 76 +++++++++++++++++++ .../tests/test_obligation_applicability.py | 57 ++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 backend-compliance/compliance/services/obligation_applicability.py create mode 100644 backend-compliance/tests/test_obligation_applicability.py diff --git a/backend-compliance/compliance/services/obligation_applicability.py b/backend-compliance/compliance/services/obligation_applicability.py new file mode 100644 index 00000000..4d045b9f --- /dev/null +++ b/backend-compliance/compliance/services/obligation_applicability.py @@ -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()) diff --git a/backend-compliance/tests/test_obligation_applicability.py b/backend-compliance/tests/test_obligation_applicability.py new file mode 100644 index 00000000..2a039300 --- /dev/null +++ b/backend-compliance/tests/test_obligation_applicability.py @@ -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