feat(agent): Impressum Rechtsform-Gates + USt-optional (Phase 3)
Die 8 Audit-Klassifizierungs-Felder (scan_context) treiben jetzt den business_scope der Agenten (vorher gespeichert, aber nicht genutzt). Rechtsform-Gates als opt-out (excludes_scope): Verein -> kein Handelsregister-Finding, e.K. -> kein Vertretungsberechtigte-Finding; unbekannte Rechtsform bleibt anwendbar. USt-IdNr optional -> fehlt = kein Finding. Rechts-Zuordnung vom Domain-Experten bestaetigt. - _classification.py: scan_context_to_scope (8 Felder -> scope-Tokens) - mcs.py: MC.excludes_scope + MC.optional; IMP-MC-004/006 Gate-Tokens; IMP-MC-005 optional; scope_matches respektiert excludes_scope - agent.py: optional -> kein Finding bei Abwesenheit - _agent_outputs.py: scope = scan_context vereinigt LLM-Profil-Fallback - Tests gruen: v3 25, Groundtruth 13, CI-Pfad 14 (+ SSE-Loop-Fix) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
"""Phase 3: Normalisierung der 8 Audit-Klassifizierungs-Felder
|
||||
(scan_context aus dem PreScanWizard) → business_scope-Tokens.
|
||||
|
||||
EINZIGER Normalisierungspunkt: beide Quellen (SDK-Profil/Scope ODER der
|
||||
standalone Compliance-Check) füllen denselben business_scope, den
|
||||
scope_matches() in den Agenten konsumiert. Schließt die Drift, dass die
|
||||
8 Felder gespeichert, aber nicht an die Agenten gegeben wurden.
|
||||
|
||||
Rechts-Zuordnung mit dem User (Domain-Experte) bestätigt 2026-06-10:
|
||||
- industry=healthcare → NICHT pauschal regulated_profession (Krankenhaus-
|
||||
GmbH ≠ Apotheke). regulated_profession kommt nur aus expliziter
|
||||
Erkennung (LLM-Profil is_regulated_profession), nicht aus der Branche.
|
||||
- Handelsregister: gmbh/ug/ag/kg/ohg/gmbh_co_kg/ek (e.K. ist registerpflichtig).
|
||||
- Vertretungsberechtigte: + verein/stiftung, aber OHNE ek (Inhaber genügt).
|
||||
- USt-IdNr: kein Rechtsform-Gate (Kleinunternehmer §19 haben keine).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
# Rechtsformen OHNE Handelsregister-Eintrag → Handelsregister-MC n/a.
|
||||
# (HR-pflichtig sind gmbh/ug/ag/kg/ohg/gmbh_co_kg/ek — die schließen wir
|
||||
# NICHT aus. Opt-out, damit Entry-Points ohne legal_form anwendbar bleiben.)
|
||||
_NON_HANDELSREGISTER_FORMS = frozenset({"verein", "stiftung", "behoerde", "other"})
|
||||
# Rechtsformen OHNE gesondertes Vertretungsorgan (e.K. = Inhaber selbst).
|
||||
_NON_VERTRETUNG_FORMS = frozenset({"ek", "behoerde", "other"})
|
||||
|
||||
|
||||
def scan_context_to_scope(scan_context: dict | None) -> list[str]:
|
||||
"""8 Wizard-Felder → business_scope-Tokens (für scope_matches)."""
|
||||
sc = scan_context or {}
|
||||
industry = str(sc.get("industry") or "").lower()
|
||||
business_model = str(sc.get("business_model") or "").lower()
|
||||
direct_sales = str(sc.get("direct_sales") or "").lower()
|
||||
legal_form = str(sc.get("legal_form") or "").lower()
|
||||
|
||||
scope: set[str] = set()
|
||||
# ── Branche / Geschäftsmodell ──
|
||||
if industry == "ecommerce" or direct_sales == "yes":
|
||||
scope.add("ecommerce")
|
||||
if business_model in ("b2c", "both"):
|
||||
scope.add("b2c")
|
||||
if industry == "insurance":
|
||||
scope.add("insurance")
|
||||
if industry == "banking":
|
||||
scope.add("financial_services")
|
||||
if industry == "automotive":
|
||||
scope.add("automotive")
|
||||
if industry == "media":
|
||||
scope.add("editorial") # §18 MStV (pragmatisch)
|
||||
if legal_form == "behoerde" or industry == "public":
|
||||
scope.add("public_authority")
|
||||
# industry=healthcare → bewusst KEIN regulated_profession.
|
||||
|
||||
# ── Rechtsform-Gates (opt-out) ──
|
||||
# Nur explizit AUSSCHLIESSEN; ohne/unbekanntes legal_form bleibt die
|
||||
# Angabe anwendbar (z.B. Agent-Test ohne Wizard verschluckt nichts).
|
||||
if legal_form in _NON_HANDELSREGISTER_FORMS:
|
||||
scope.add("kein_handelsregister")
|
||||
if legal_form in _NON_VERTRETUNG_FORMS:
|
||||
scope.add("keine_vertretung")
|
||||
|
||||
return sorted(scope)
|
||||
@@ -125,6 +125,14 @@ class ImpressumAgent(BaseSpecialistAgent):
|
||||
mc_id=mc.mc_id, status="ok", reason="Pattern-Treffer",
|
||||
))
|
||||
continue
|
||||
if mc.optional:
|
||||
# fehlt + optional → KEIN Finding (z.B. USt-IdNr;
|
||||
# Kleinunternehmer §19 haben legitim keine).
|
||||
coverage.append(McCoverage(
|
||||
mc_id=mc.mc_id, status="na",
|
||||
reason="optional — nicht angegeben",
|
||||
))
|
||||
continue
|
||||
sev = _SEV_TO_ENUM.get(mc.severity_if_missing, Severity.MEDIUM)
|
||||
findings.append(Finding(
|
||||
check_id=f"IMP-{mc.field_id}",
|
||||
|
||||
@@ -23,8 +23,15 @@ class MC:
|
||||
patterns: tuple[Pattern[str], ...] = field(default_factory=tuple)
|
||||
severity_if_missing: str = "MEDIUM" # HIGH | MEDIUM | LOW | INFO
|
||||
requires_scope: tuple[str, ...] = field(default_factory=tuple)
|
||||
# Opt-out: NICHT anwendbar, wenn eines dieser Tokens im Scope liegt
|
||||
# (z.B. Einzelunternehmer für Vertretungsberechtigte). Default = immer
|
||||
# anwendbar → Entry-Points ohne legal_form verschlucken nichts.
|
||||
excludes_scope: tuple[str, ...] = field(default_factory=tuple)
|
||||
# Wenn True: bei Scope-Mismatch nicht-applicable melden, sonst skip
|
||||
explicit_na: bool = True
|
||||
# Wenn True: fehlt die Angabe → KEIN Finding (z.B. USt-IdNr —
|
||||
# Kleinunternehmer §19 haben legitim keine). Nur wenn vorhanden relevant.
|
||||
optional: bool = False
|
||||
|
||||
|
||||
MCS: tuple[MC, ...] = (
|
||||
@@ -78,6 +85,7 @@ MCS: tuple[MC, ...] = (
|
||||
label="Handelsregister-Eintrag",
|
||||
norm="§ 5 Abs. 1 Nr. 4 TMG",
|
||||
severity_if_missing="HIGH",
|
||||
excludes_scope=("kein_handelsregister",),
|
||||
patterns=(
|
||||
re.compile(r"\bHR[BA]\s+\d", re.IGNORECASE),
|
||||
re.compile(r"Handelsregister", re.IGNORECASE),
|
||||
@@ -89,6 +97,7 @@ MCS: tuple[MC, ...] = (
|
||||
label="USt-IdNr",
|
||||
norm="§ 5 Abs. 1 Nr. 6 TMG",
|
||||
severity_if_missing="MEDIUM",
|
||||
optional=True,
|
||||
patterns=(
|
||||
re.compile(
|
||||
r"\b(?:USt-?Id(?:Nr)?\.?|VAT(?:-?Id)?)\s*[:.\s]",
|
||||
@@ -103,6 +112,7 @@ MCS: tuple[MC, ...] = (
|
||||
label="Vertretungsberechtigte Person",
|
||||
norm="§ 5 Abs. 1 Nr. 1 TMG (juristische Personen)",
|
||||
severity_if_missing="HIGH",
|
||||
excludes_scope=("keine_vertretung",),
|
||||
patterns=(
|
||||
re.compile(
|
||||
r"(?:Gesch(?:ae|ä)ftsf(?:ue|ü)hr(?:er|ung|erin)|"
|
||||
@@ -214,6 +224,10 @@ MC_IDS: tuple[str, ...] = tuple(m.mc_id for m in MCS)
|
||||
|
||||
def scope_matches(mc: MC, scope: set[str], is_automotive: bool) -> bool:
|
||||
"""Entscheidet ob die MC auf den Business-Scope anwendbar ist."""
|
||||
# Opt-out zuerst: explizit ausgeschlossene Rechtsformen (z.B.
|
||||
# Einzelunternehmer für Vertretungsberechtigte) → nicht anwendbar.
|
||||
if mc.excludes_scope and any(s in scope for s in mc.excludes_scope):
|
||||
return False
|
||||
if not mc.requires_scope:
|
||||
return True
|
||||
if mc.field_id == "aufsichtsbehoerde" and is_automotive:
|
||||
|
||||
Reference in New Issue
Block a user