e1dadc8027
Stage 1.a Browser-Matrix (Task #15) — Multi-Engine Scaffolding: - consent-tester/Dockerfile: firefox + webkit + Xvfb deps - playwright install chromium firefox webkit - services/browser_profiles.py: Registry mit DEFAULT_PROFILES (Chromium-Headed/Firefox-Headed/WebKit-Headed/Mobile-Safari) + EXTRA_PROFILES (Chrome-Channel, Edge, Brave) - services/multi_browser_scanner.py: run_matrix() orchestriert N parallele Scans + worst-of-Aggregation + 3 Sub-Scores (Pre-Consent 50%, Reject-Respekt 30%, Banner-Design 20%) + Hard-Fail-Cap auf <60% bei Pre-Consent/Reject-Verstoß - routes_matrix.py: POST /scan-matrix Endpoint (eigenes Modul, damit main.py unter 500 LOC bleibt) KNOWN: Stage 1.a-Shim ruft alle Profile auf demselben Chromium, echte Engine-Diversität in Stage 1.b (consent_scanner.py Param) Coverage-Gap 3 (Task #17): 2/3 verbleibende GT-Lücken geschlossen: - B9 impressum_multi_entity_check (IMPRESSUM-001): erkennt USt-IdNr/HR/GF-Fehlen pro Entity bei multi-entity Impressen (Elli: USt-IdNr nur bei Elli Mobility, fehlt bei VW Group Charging) - B10 transfer_mechanism_check (TRANSFER-001): pro Non-EU-Vendor in cmp_vendors prüft DSE auf DPF/SCCs/BCRs/Einwilligung im ±400-char-Window. Findet Vendors ohne benannten Mechanismus. - TH-RETENTION-002 (AI-Datenkategorie-Differenzierung) bleibt semantisch-tief, vorgesehen für Specialist-Agents Task #18. Plausibility-LLM Empty-Response-Härtung (Task #16): - BATCH_SIZE 8 → 4, EXCERPT 4000 → 1500 chars, TIMEOUT 60 → 45s - Single-retry mit halbierter Batch wenn LLM empty content zurückgibt — qwen3:30b-a3b rejektiert manchmal ≥6-Item-Prompts unter format='json'. Falls auch Half-Batch empty: log + skip. - Pipeline läuft jetzt nicht mehr 10min in Timeouts. GT-Coverage Sprung: 10/13 → 11/13 (85%). 4/4 HIGH ✓, 5/6 MEDIUM ✓, 2/3 LOW ✓. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
100 lines
3.7 KiB
Python
100 lines
3.7 KiB
Python
"""B9 — Multi-Entity-Impressum-Check.
|
|
|
|
Findings, wenn ein Impressum mehrere Entitäten (mehrere GmbH/AG/UG)
|
|
nennt, aber Pflichtangaben nur bei einer davon vollständig sind.
|
|
|
|
Konkreter Elli-Pattern (GT IMPRESSUM-001):
|
|
- Entity 1: "Elli Mobility GmbH ... USt-IdNr DE814424009 ..."
|
|
- Entity 2: "VW Group Charging GmbH ... [keine USt-IdNr] ..."
|
|
→ USt-IdNr fehlt bei Entity 2.
|
|
|
|
Heuristik:
|
|
1. Entitäten erkennen: jede Match auf "<Name> (GmbH|AG|UG|KG|SE)" als
|
|
Entity-Boundary; Text-Slice von dort bis zur nächsten Entity.
|
|
2. Pro Entity prüfen: USt-IdNr, Handelsregister, Vertretungsberechtigte.
|
|
3. Wenn Entity N ein Feld nennt, das Entity M nicht hat → MEDIUM.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import re
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_ENTITY_PAT = re.compile(
|
|
r"([A-ZÄÖÜ][\w\-\&\s]{1,50}?\s+(?:GmbH|AG|UG|KG|SE|"
|
|
r"e\.V\.|GbR|OHG|Limited|Ltd|LLC))",
|
|
re.IGNORECASE,
|
|
)
|
|
|
|
_USTID_PAT = re.compile(r"\b(?:USt-?Id(?:Nr)?\.?|VAT(?:-?Id)?)\s*[:.\s]\s*"
|
|
r"(DE\d{8,10}|[A-Z]{2}\d{6,12})", re.IGNORECASE)
|
|
_HR_PAT = re.compile(r"\b(?:HR[BA]|Handelsregister|Registergericht)"
|
|
r"\s*[:.\s]*([\w\s\d\-/]{4,80})", re.IGNORECASE)
|
|
_GF_PAT = re.compile(r"(?:Geschäftsführer|Vertretungsberechtigt|"
|
|
r"vertreten\s+durch)\s*[:.\s]+", re.IGNORECASE)
|
|
|
|
|
|
def _slice_entities(text: str) -> list[tuple[str, str]]:
|
|
"""Return [(entity_name, text_slice)] for each detected entity."""
|
|
matches = list(_ENTITY_PAT.finditer(text))
|
|
if len(matches) < 2:
|
|
return []
|
|
slices: list[tuple[str, str]] = []
|
|
for i, m in enumerate(matches):
|
|
start = m.start()
|
|
end = matches[i + 1].start() if i + 1 < len(matches) else len(text)
|
|
slices.append((m.group(1).strip(), text[start:end]))
|
|
return slices
|
|
|
|
|
|
def check_multi_entity_impressum(state: dict) -> list[dict]:
|
|
doc_texts = state.get("doc_texts") or {}
|
|
imp = doc_texts.get("impressum") or ""
|
|
if not imp:
|
|
return []
|
|
slices = _slice_entities(imp)
|
|
if not slices:
|
|
return []
|
|
# Compute features per entity
|
|
features = []
|
|
for name, slc in slices:
|
|
features.append({
|
|
"name": name,
|
|
"ust_id": bool(_USTID_PAT.search(slc)),
|
|
"hr": bool(_HR_PAT.search(slc)),
|
|
"gf": bool(_GF_PAT.search(slc)),
|
|
})
|
|
# If ALL share the same flags → no inconsistency
|
|
findings: list[dict] = []
|
|
for field, label in (
|
|
("ust_id", "USt-IdNr."),
|
|
("hr", "Handelsregister-Eintrag"),
|
|
("gf", "Vertretungsberechtigte"),
|
|
):
|
|
present = [f for f in features if f[field]]
|
|
missing = [f for f in features if not f[field]]
|
|
if present and missing and len(present) >= 1:
|
|
findings.append({
|
|
"check_id": f"IMPRESSUM-MULTI-{field.upper()}",
|
|
"severity": "MEDIUM",
|
|
"severity_reason": "incomplete",
|
|
"title": (
|
|
f"{label} fehlt bei "
|
|
f"{len(missing)} von {len(features)} Entitäten"
|
|
),
|
|
"norm": "§ 5 Abs. 1 TMG (Pflichtangabe pro Diensteanbieter)",
|
|
"entities_present": [f["name"] for f in present],
|
|
"entities_missing": [f["name"] for f in missing],
|
|
"action": (
|
|
f"{label} im Impressum für "
|
|
f"{', '.join(f['name'] for f in missing)} ergänzen. "
|
|
"Pflichtangabe ist pro Diensteanbieter zu erfüllen, "
|
|
"nicht 'eine reicht für alle'."
|
|
),
|
|
})
|
|
if findings:
|
|
logger.info("B9 multi-entity impressum: %d findings", len(findings))
|
|
return findings
|