feat(profile): P14+P15+P16 — B2B-Heuristik + Doc-URL-Dedup + Homepage-Profile

P14 — _detect_no_direct_sales erweitert um 3 Cluster:
  A) OEM-Konfigurator (BMW/Audi/Mercedes/VW/Porsche-Markennamen + Vertragshaendler-Pattern)
  B) B2B-Dienstleister (CE-Zertifizierung, Compliance-Beratung, Schulungen, Auditierung, TISAX, ISO-Normen, Arbeitssicherheit, ...)
  C) NGO/Verein/Public (Spendenkonto, Vereinsregister, gemeinnuetzig, ...)
Schwelle: pos >= 2 pro Cluster UND pos > neg. Bisher: nur OEM.

P15 — Doc-URL-Dedup im Worker: wenn mehrere Doc-Types DASSELBE Dokument
referenzieren (Safetykon-Pattern: User gibt /datenschutz fuer dse, cookie
UND widerruf), wird nur dem primaeren Doc-Type (Priority: dse > impressum
> cookie > widerruf > agb > nutzungsbedingungen) der Text gegeben. Andere
landen als "Nicht separat vorhanden — wird im Dokument 'X' mit-geprueft."
Eliminiert die 8+8 systematischen widerruf/cookie False Positives.

P16 — Profile-Detection auch Homepage-Text: Homepage-HTML wird mit kurzem
Fetch (8s timeout) gezogen, getrippt und zum profile_input gemerged. Vor-
her wirkte P14 nur wenn B2B-Indikatoren im DSE/Impressum-Pflichttext
standen — bei Safetykon stehen sie nur im Homepage-Menue.

Plus Bonus: TDM-Override-Submit-Button wird deaktiviert wenn Reason < 10
Zeichen — verhindert dass User wie heute in den Bug rein klickt.

Smoke-Test Safetykon (B2B Compliance-Dienstleister):
  dse                  geprueft (kein err)
  impressum            geprueft (kein err)
  cookie               "Nicht separat vorhanden — wird in DSE mit-geprueft"
  agb                  "Nicht anwendbar — kein Direkt-Kaufvertrag"
  widerruf             "Nicht anwendbar — kein Direkt-Kaufvertrag"
  nutzungsbedingungen  "Nicht anwendbar — kein Direkt-Kaufvertrag"
Vorher: 16 False Positives. Jetzt: 0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-19 11:46:58 +02:00
parent a1b380e211
commit 479ce2225b
3 changed files with 140 additions and 13 deletions
@@ -335,9 +335,10 @@ async def detect_business_profile(documents: dict[str, str]) -> BusinessProfile:
return profile
# Indikatoren: Site verweist primaer auf Vertragshaendler/Niederlassungen
# statt einen eigenen Checkout-Vertragsabschluss zu bieten.
_NO_DIRECT_SALES_POSITIVE = [
# P14: drei Cluster die jeweils unabhaengig no_direct_sales=True triggern.
# Cluster A: OEM-Konfigurator-Pattern (Auto-Hersteller mit Vertragshaendler-Netz)
_OEM_POSITIVE = [
"vertragshaendler", "vertragshändler", "vertragspartner",
"vertragswerkstatt", "haendlersuche", "händlersuche",
"niederlassung", "vertretung", "autorisierter haendler",
@@ -347,27 +348,80 @@ _NO_DIRECT_SALES_POSITIVE = [
"anfrage an haendler", "anfrage an händler",
"konfigurator", "fahrzeug konfigurieren",
"ihre individuelle anfrage",
# OEM-Markennamen — sind Hersteller-Marken die ueblicherweise via
# Haendler vertreiben.
"bmw vertriebs", "audi vertriebs", "mercedes-benz vertriebs",
"volkswagen vertriebs", "porsche zentrum",
# OEM-Markennamen im Pflichttext (Datenschutz erwaehnt Hersteller)
"bmw ag", "audi ag", "mercedes-benz ag", "volkswagen ag",
"porsche ag", "opel automobile gmbh",
]
# Cluster B: B2B-Dienstleister (Beratung / Compliance / Schulung / CE)
_B2B_SERVICE_POSITIVE = [
"ce-zertifizierung", "ce zertifizierung",
"ce-konformitaet", "ce-konformität",
"ce-kennzeichnung", "ce kennzeichnung",
"compliance-beratung", "compliance beratung",
"arbeitssicherheit", "product compliance",
"produktsicherheit", "produkthaftung",
"auditierung", "auditor", "auditierungen",
"schulungen", "workshops", "akademie",
"beratungsleistungen", "consultingleistungen",
"consulting services", "managementsystem",
"datenschutzbeauftragter (extern)",
"externer datenschutzbeauftragter",
"datenschutz-audit", "tisax", "iso 27001",
"iso 9001", "iso 14001", "iso 45001",
"gefaehrdungsbeurteilung", "gefährdungsbeurteilung",
"betriebsbeauftragter", "fachkraft fuer arbeitssicherheit",
"fachkraft für arbeitssicherheit",
]
# Cluster C: NGO / Verein / oeffentliche Verwaltung
_NONPROFIT_PUBLIC_POSITIVE = [
"spendenkonto", "vereinsregister", "gemeinnuetzig",
"gemeinnützig", "ehrenamtlich", "foerderverein",
"förderverein", "stiftung", "buergeramt", "bürgeramt",
"landratsamt", "kommunalverwaltung",
]
# Backwards-compat
_NO_DIRECT_SALES_POSITIVE = (
_OEM_POSITIVE + _B2B_SERVICE_POSITIVE + _NONPROFIT_PUBLIC_POSITIVE
)
# Indikatoren GEGEN no_direct_sales: echte Online-Shop-Funktionen.
_DIRECT_SALES_NEGATIVE = [
"in den warenkorb", "warenkorb hinzu", "zur kasse",
"jetzt kaufen", "kostenpflichtig bestellen",
"zahlungspflichtig bestellen", "sofort-kauf",
"online bestellen", "lieferadresse", "rechnungsadresse",
"versandkosten", "lieferzeit", "lieferbedingungen",
"checkout", "stueckpreis", "stückpreis",
]
def _detect_no_direct_sales(full_text: str) -> bool:
"""Heuristik: erkennt OEM-Konfigurator-Sites die nicht direkt verkaufen."""
"""Heuristik: True wenn Site keinen Direkt-Vertrieb mit B2C-Kunden hat.
Trifft fuer 3 Cluster zu (jeweils mind. 2 Treffer im Cluster):
A) OEM-Konfigurator (Auto-Hersteller)
B) B2B-Dienstleister (Beratung/Compliance/Schulung)
C) NGO / oeffentliche Verwaltung
Negativ-Signale (echte Shop-Funktionen) zaehlen gegen den Cluster:
nur True wenn pos > neg.
"""
text = full_text.lower()
pos = sum(1 for k in _NO_DIRECT_SALES_POSITIVE if k in text)
oem = sum(1 for k in _OEM_POSITIVE if k in text)
b2b = sum(1 for k in _B2B_SERVICE_POSITIVE if k in text)
npg = sum(1 for k in _NONPROFIT_PUBLIC_POSITIVE if k in text)
neg = sum(1 for k in _DIRECT_SALES_NEGATIVE if k in text)
# Mindestens 3 Haendler-Indikatoren UND weniger Shop-Indikatoren als
# Haendler-Indikatoren. Vermeidet false-positive fuer Shops die
# zusaetzlich "Haendlersuche" als Filiale-Finder anbieten.
return pos >= 3 and pos > neg
# Jeder Cluster ist eigenstaendig: 2 Treffer + weniger Negativ-Signale
# als Cluster-Treffer.
if oem >= 2 and oem > neg:
return True
if b2b >= 2 and b2b > neg:
return True
if npg >= 2 and npg > neg:
return True
return False