feat(platform): live-wire AGB v2 + DSE v3 + Architektur-Tab #29
+17
-17
@@ -43,7 +43,7 @@ jobs:
|
||||
- name: Checkout
|
||||
run: |
|
||||
apk add --no-cache git bash
|
||||
git clone --depth 200 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
|
||||
git clone --depth 200 --branch ${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
|
||||
if [ "${GITHUB_EVENT_NAME}" = "pull_request" ]; then
|
||||
git fetch --depth 200 origin "${GITHUB_BASE_REF}" || true
|
||||
else
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
- name: Checkout
|
||||
run: |
|
||||
apk add --no-cache git bash
|
||||
git clone --depth 20 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
|
||||
git clone --depth 20 --branch ${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
|
||||
git fetch origin ${GITHUB_BASE_REF}:base
|
||||
- name: Require [guardrail-change] in commits touching guardrails
|
||||
run: |
|
||||
@@ -108,7 +108,7 @@ jobs:
|
||||
- name: Checkout
|
||||
run: |
|
||||
apk add --no-cache git bash
|
||||
git clone --depth 50 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
|
||||
git clone --depth 50 --branch ${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
|
||||
- name: Enforce 500-line hard cap
|
||||
run: |
|
||||
chmod +x scripts/check-loc.sh
|
||||
@@ -123,7 +123,7 @@ jobs:
|
||||
- name: Checkout
|
||||
run: |
|
||||
apk add --no-cache git
|
||||
git clone --depth 50 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
|
||||
git clone --depth 50 --branch ${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
|
||||
- name: Scan for secrets
|
||||
run: |
|
||||
gitleaks detect --source . --no-git \
|
||||
@@ -141,7 +141,7 @@ jobs:
|
||||
- name: Checkout
|
||||
run: |
|
||||
apk add --no-cache git
|
||||
git clone --depth 1 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
|
||||
git clone --depth 1 --branch ${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
|
||||
- name: Lint ai-compliance-sdk
|
||||
run: |
|
||||
[ -d "ai-compliance-sdk" ] || exit 0
|
||||
@@ -162,7 +162,7 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
run: |
|
||||
git clone --depth 1 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
|
||||
git clone --depth 1 --branch ${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
|
||||
- name: Lint (ruff) + type-check (mypy)
|
||||
run: |
|
||||
pip install --quiet ruff mypy
|
||||
@@ -193,7 +193,7 @@ jobs:
|
||||
- name: Checkout
|
||||
run: |
|
||||
apk add --no-cache git
|
||||
git clone --depth 1 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
|
||||
git clone --depth 1 --branch ${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
|
||||
- name: Lint + type-check
|
||||
run: |
|
||||
fail=0
|
||||
@@ -215,7 +215,7 @@ jobs:
|
||||
- name: Checkout
|
||||
run: |
|
||||
apk add --no-cache git
|
||||
git clone --depth 1 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
|
||||
git clone --depth 1 --branch ${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
|
||||
- name: Build Next.js services
|
||||
run: |
|
||||
fail=0
|
||||
@@ -239,7 +239,7 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
run: |
|
||||
git clone --depth 1 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
|
||||
git clone --depth 1 --branch ${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
|
||||
- name: Install Node.js + Go
|
||||
run: |
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - > /dev/null 2>&1
|
||||
@@ -282,7 +282,7 @@ jobs:
|
||||
- name: Checkout
|
||||
run: |
|
||||
apk add --no-cache git curl bash
|
||||
git clone --depth 1 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
|
||||
git clone --depth 1 --branch ${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
|
||||
- name: Install syft + grype
|
||||
run: |
|
||||
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
|
||||
@@ -304,7 +304,7 @@ jobs:
|
||||
- name: Checkout
|
||||
run: |
|
||||
apk add --no-cache git
|
||||
git clone --depth 1 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
|
||||
git clone --depth 1 --branch ${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
|
||||
- name: Test ai-compliance-sdk
|
||||
run: |
|
||||
[ -d "ai-compliance-sdk" ] || exit 0
|
||||
@@ -324,7 +324,7 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
run: |
|
||||
git clone --depth 1 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
|
||||
git clone --depth 1 --branch ${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
|
||||
- name: GT-Bremse measure-coverage report
|
||||
run: |
|
||||
python3 scripts/gt_measure_gap_analysis.py --json /tmp/gt_gap_report.json > /tmp/gt_gap_report.md
|
||||
@@ -355,7 +355,7 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
run: |
|
||||
git clone --depth 1 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
|
||||
git clone --depth 1 --branch ${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
|
||||
- name: Test backend-compliance
|
||||
run: |
|
||||
[ -d "backend-compliance" ] || exit 0
|
||||
@@ -375,7 +375,7 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
run: |
|
||||
git clone --depth 1 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
|
||||
git clone --depth 1 --branch ${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
|
||||
- name: Test document-crawler
|
||||
run: |
|
||||
[ -d "document-crawler" ] || exit 0
|
||||
@@ -395,7 +395,7 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
run: |
|
||||
git clone --depth 1 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
|
||||
git clone --depth 1 --branch ${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
|
||||
- name: Test dsms-gateway
|
||||
run: |
|
||||
[ -d "dsms-gateway" ] || exit 0
|
||||
@@ -417,7 +417,7 @@ jobs:
|
||||
- name: Checkout
|
||||
run: |
|
||||
apk add --no-cache git python3 py3-yaml
|
||||
git clone --depth 1 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
|
||||
git clone --depth 1 --branch ${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
|
||||
- name: Validate every Dockerfile + compose block declares BUILD_SHA
|
||||
run: |
|
||||
python3 - <<'PY'
|
||||
@@ -456,6 +456,6 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
run: |
|
||||
git clone --depth 1 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
|
||||
git clone --depth 1 --branch ${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
|
||||
- name: Validate controls
|
||||
run: python scripts/validate-controls.py
|
||||
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
-e "WORK_DIR=/tmp/rag-ingestion" \
|
||||
-e "RAG_URL=http://bp-core-rag-service:8097/api/v1/documents/upload" \
|
||||
-e "QDRANT_URL=https://qdrant-dev.breakpilot.ai" \
|
||||
-e "QDRANT_API_KEY=z9cKbT74vl1aKPD1QGIlKWfET47VH93u" \
|
||||
-e "QDRANT_API_KEY=${{ secrets.QDRANT_API_KEY }}" \
|
||||
-e "SDK_URL=http://bp-compliance-ai-sdk:8090" \
|
||||
alpine:3.19 \
|
||||
sh -c "
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# gitleaks configuration.
|
||||
# Keeps gitleaks' default ruleset and adds an allowlist for known FALSE POSITIVES
|
||||
# that surfaced once the CI checkout was fixed (secret-scan had never actually run
|
||||
# on a PR before). Real leaked credentials are removed in code, NOT allowlisted.
|
||||
|
||||
[extend]
|
||||
useDefault = true
|
||||
|
||||
[allowlist]
|
||||
description = "Documentation curl examples, env templates, and non-secret identifiers"
|
||||
paths = [
|
||||
# API reference pages — curl examples with placeholder tokens, not real secrets
|
||||
'''developer-portal/app/api/.*''',
|
||||
'''developer-portal/app/development/.*''',
|
||||
# Template env file — placeholder dev values (e.g. breakpilot123)
|
||||
'''\.env\.example$''',
|
||||
# Seed data: "rule_key" identifiers, not credentials
|
||||
'''backend-compliance/compliance/data/template_rule_seed_data\.py$''',
|
||||
# SDK deploy template — MINIO placeholder password
|
||||
'''breakpilot-compliance-sdk/packages/cli/src/commands/deploy\.ts$''',
|
||||
]
|
||||
@@ -0,0 +1,302 @@
|
||||
'use client'
|
||||
|
||||
// Erklärendes Architekturschema des Compliance-Check-Tools — Muster aus dem
|
||||
// CE-Modul (/sdk/iace/.../architektur) übernommen: hand-kurierte Boxen/Pfeile +
|
||||
// Schritt-Akkordeon. Inhalt spiegelt den Code-Pfad (api/agent_check/_orchestrator
|
||||
// + services/specialist_agents). Bewusst statisch (der Doc-Check ist Python, hat
|
||||
// keinen Architektur-Endpoint wie das Go-IACE-Modul) — bei Bedarf später aus einem
|
||||
// Backend-Handler speisbar.
|
||||
|
||||
import { useState, type ReactNode } from 'react'
|
||||
|
||||
function Box({ title, sub, accent }: { title: string; sub?: string; accent?: 'purple' | 'amber' | 'green' | 'gray' }) {
|
||||
const c =
|
||||
accent === 'purple'
|
||||
? 'border-purple-300 bg-purple-50/60 dark:border-purple-700 dark:bg-purple-900/20'
|
||||
: accent === 'amber'
|
||||
? 'border-amber-300 bg-amber-50/60 dark:border-amber-700 dark:bg-amber-900/20'
|
||||
: accent === 'green'
|
||||
? 'border-green-300 bg-green-50/60 dark:border-green-700 dark:bg-green-900/20'
|
||||
: 'border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800'
|
||||
return (
|
||||
<div className={`rounded-lg border ${c} px-2.5 py-1.5`}>
|
||||
<div className="text-[11px] font-medium text-gray-800 dark:text-gray-200 leading-tight">{title}</div>
|
||||
{sub && <div className="text-[10px] text-gray-500 leading-tight mt-0.5">{sub}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Lane({ label, children }: { label: string; children: ReactNode }) {
|
||||
return (
|
||||
<div className="flex-1 min-w-[150px] space-y-2">
|
||||
<div className="text-[10px] font-semibold uppercase tracking-wide text-gray-400 text-center">{label}</div>
|
||||
<div className="space-y-1.5">{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Arrow() {
|
||||
return (
|
||||
<div className="flex items-center justify-center text-gray-300 dark:text-gray-600 shrink-0 px-0.5">
|
||||
<span className="hidden lg:block text-lg">→</span>
|
||||
<span className="lg:hidden text-sm">↓</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type Stage = {
|
||||
id: string
|
||||
title: string
|
||||
summary: string
|
||||
input: string
|
||||
logic: string
|
||||
source: string
|
||||
example: string
|
||||
}
|
||||
|
||||
// Spiegelt run_compliance_check (Phasen A–F) + die Spezialagenten-Schicht.
|
||||
const STAGES: Stage[] = [
|
||||
{
|
||||
id: 'a',
|
||||
title: 'Phase A — Auflösen & Crawl',
|
||||
summary: 'URLs + hochgeladene Dokumente einsammeln, fehlende Pflichtseiten automatisch finden.',
|
||||
input: 'Start-URL, Dokument-Uploads, 8 Wizard-Felder (scan_context)',
|
||||
logic: 'Discovery (Sitemap/Heuristik) + Fetch je Seite, Text-Extraktion pro Doc-Typ',
|
||||
source: 'consent-tester /dsi-discovery, Playwright',
|
||||
example: 'Findet /impressum, /datenschutz, /agb ohne manuelle Eingabe',
|
||||
},
|
||||
{
|
||||
id: 'b',
|
||||
title: 'Phase B — Profil & Dokument-Checks',
|
||||
summary: 'Geschäftsprofil erkennen, jedes Dokument gegen seine Controls prüfen.',
|
||||
input: 'Doc-Texte je Typ + Business-Scope',
|
||||
logic: 'Regex-Runner + MC-Keyword + BGE-M3-Embedding + LLM-Verify (nur unscharf)',
|
||||
source: 'doc_check_controls (DB), mc_classification.db (Embeddings)',
|
||||
example: 'DSE: 267 Text-MCs, Keyword + semantischer Recall',
|
||||
},
|
||||
{
|
||||
id: 'agents',
|
||||
title: 'Spezialagenten (nebenläufig)',
|
||||
summary: 'Pro Dokumenttyp ein typisierter Agent → eigener Ergebnis-Tab, gefüllt per SSE.',
|
||||
input: 'Doc-Text, Scope, scan_context',
|
||||
logic: 'Impressum + AGB + DSE laufen parallel (asyncio.gather), je ein AgentOutput',
|
||||
source: 'api/agent_check/_agent_outputs._TOPIC_AGENTS',
|
||||
example: 'AGB-Tab + DSE-Tab erscheinen, sobald ihr Agent fertig ist',
|
||||
},
|
||||
{
|
||||
id: 'c',
|
||||
title: 'Phase C — Cookie-Banner',
|
||||
summary: 'Consent-Banner + gesetzte Cookies vor/nach Einwilligung live prüfen.',
|
||||
input: 'Live-Seite im Browser',
|
||||
logic: 'Consent-Tester-Scan: Banner, Vendors, Enforcement, Browser-Matrix',
|
||||
source: 'consent-tester /scan',
|
||||
example: 'Cookie vor Einwilligung gesetzt → Verstoß-Kandidat',
|
||||
},
|
||||
{
|
||||
id: 'd',
|
||||
title: 'Phase D — Vendors & Plausibilität',
|
||||
summary: 'Dritt-Dienste extrahieren + Findings auf Plausibilität prüfen.',
|
||||
input: 'Banner-/Seiten-Daten, Findings',
|
||||
logic: 'Vendor-Extraktion (+OCR-Fallback), Plausibilitäts-Check je FAIL',
|
||||
source: 'Cookie-/Vendor-Kataloge, LLM-Kaskade',
|
||||
example: 'Analytics ohne Rechtsgrundlage → bestätigtes Finding',
|
||||
},
|
||||
{
|
||||
id: 'reconcile',
|
||||
title: 'Cross-Finding-Abgleich',
|
||||
summary: 'Findings über Dokumente hinweg abgleichen — Doppel & Scheinverstöße auflösen.',
|
||||
input: 'Alle Modul-Findings',
|
||||
logic: 'Deckt ein anderes Dokument die Pflicht ab, wird das Cross-Finding unterdrückt',
|
||||
source: 'cross_doc_reconcile (B-Wirings)',
|
||||
example: '§36 VSBG im Impressum statt DSE → kein Doppel-Finding',
|
||||
},
|
||||
{
|
||||
id: 'f',
|
||||
title: 'Phase E/F — Bericht & Snapshot',
|
||||
summary: 'Ergebnis persistieren, Snapshot für die Historie speichern.',
|
||||
input: 'Konsolidiertes Ergebnis',
|
||||
logic: 'Mail-Render + DB-Persist + Snapshot (Tab-Ansicht ohne Re-Crawl)',
|
||||
source: 'compliance_check_snapshots',
|
||||
example: 'Historie erneut öffnen, ohne die Seite neu zu crawlen',
|
||||
},
|
||||
]
|
||||
|
||||
type ModuleEngine = { name: string; mechanism: string }
|
||||
const MODULES: ModuleEngine[] = [
|
||||
{ name: 'Impressum', mechanism: 'Scope-Gate + Feld-Matcher (§5 DDG / §18 MStV)' },
|
||||
{ name: 'AGB', mechanism: 'decision_method-Routing: Keyword → Geschäftsmodell-Gate → Embedding/Reference/LLM' },
|
||||
{ name: 'DSE', mechanism: '4-Layer: Regex-Boost → Keyword → BGE-M3-Recall (0.65) → Semantic-Validator' },
|
||||
{ name: 'Cookie-Banner', mechanism: 'Consent-Tester: Banner, Vendors, Enforcement, Browser-Matrix' },
|
||||
]
|
||||
|
||||
type Pruefer = { method: string; mechanism: string; deterministic: string; example: string }
|
||||
// Meta-Modell: jede Pflicht → ein Prüfertyp (decision_method). Wenige
|
||||
// wiederverwendbare Prüfer statt Logik pro Control.
|
||||
const PRUEFER: Pruefer[] = [
|
||||
{ method: 'REGEX', mechanism: 'Kuratierte Muster / Keyword', deterministic: 'ja', example: 'Pflicht-Stichwort im Text' },
|
||||
{ method: 'EMBEDDING', mechanism: 'BGE-M3 Kosinus ≥ Schwelle', deterministic: 'ja (feste Funktion)', example: '„Recht auf Berichtigung" ≈ Umschreibung' },
|
||||
{ method: 'REFERENCE', mechanism: 'Link-/Verweis-Auflösung', deterministic: 'ja', example: 'Verweis auf die Datenschutzerklärung' },
|
||||
{ method: 'LLM', mechanism: 'Kaskade Qwen→OVH→Claude, nur unscharfe Fälle', deterministic: 'nein (eskaliert)', example: 'Speicherdauer inhaltlich erfüllt?' },
|
||||
{ method: 'BEHAVIOR', mechanism: 'Playwright: Live-Verhalten', deterministic: 'ja', example: 'Cookies vor Einwilligung gesetzt?' },
|
||||
{ method: 'SCANNER', mechanism: 'Repo-/Netzwerk-/Prozess-Scan', deterministic: 'ja', example: 'Geplant: technische Nachweise' },
|
||||
]
|
||||
|
||||
function Field({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
|
||||
return (
|
||||
<div>
|
||||
<dt className="text-[10px] uppercase tracking-wide text-gray-400">{label}</dt>
|
||||
<dd className={`text-gray-600 dark:text-gray-300 ${mono ? 'font-mono text-[11px]' : ''}`}>{value}</dd>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function StageRow({ stage, last, open, onToggle }: { stage: Stage; last: boolean; open: boolean; onToggle: () => void }) {
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className={`w-full text-left rounded-lg border p-3 transition-colors ${
|
||||
open
|
||||
? 'border-purple-300 bg-purple-50/60 dark:border-purple-700 dark:bg-purple-900/20'
|
||||
: 'border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700/50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-gray-800 dark:text-gray-200">{stage.title}</div>
|
||||
<div className="text-xs text-gray-500 mt-0.5">{stage.summary}</div>
|
||||
</div>
|
||||
<span className="text-gray-400 text-xs shrink-0">{open ? '▲' : '▼'}</span>
|
||||
</div>
|
||||
{open && (
|
||||
<dl className="mt-3 grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-2 text-xs">
|
||||
<Field label="Input" value={stage.input} />
|
||||
<Field label="Logik" value={stage.logic} />
|
||||
<Field label="Datenquelle" value={stage.source} mono />
|
||||
<Field label="Beispiel" value={stage.example} />
|
||||
</dl>
|
||||
)}
|
||||
</button>
|
||||
{!last && <div className="flex justify-center text-gray-300 dark:text-gray-600 text-xs leading-none py-0.5">↓</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ArchitekturView() {
|
||||
const [open, setOpen] = useState<string | null>('b')
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-gray-900 dark:text-gray-100">Architektur & Datenfluss</h2>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 max-w-3xl mt-1">
|
||||
Nachvollziehbar: <strong>woher jedes Finding stammt</strong> und <strong>wie es geprüft wird</strong>.
|
||||
Die Engine ist überwiegend <strong>deterministisch</strong> (Regex + Embedding); ein LLM entscheidet nur
|
||||
die unscharfen Fälle. Ergebnisse erscheinen pro Modul progressiv und werden am Ende per
|
||||
Cross-Finding-Abgleich bereinigt.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<section className="space-y-2">
|
||||
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300">Datenfluss (Überblick)</h3>
|
||||
<div className="rounded-xl border border-gray-200 dark:border-gray-700 bg-gray-50/50 dark:bg-gray-900/20 p-3 overflow-x-auto">
|
||||
<div className="flex flex-col lg:flex-row gap-1.5 lg:items-stretch min-w-[280px]">
|
||||
<Lane label="Eingabe">
|
||||
<Box title="Website + Dokumente" sub="Impressum · DSE · AGB · Cookies" accent="purple" />
|
||||
<Box title="Wizard-Kontext" sub="8 Felder: Shop, Drittland, Beruf…" accent="purple" />
|
||||
</Lane>
|
||||
<Arrow />
|
||||
<Lane label="Crawl + Text">
|
||||
<Box title="Discovery + Fetch" sub="consent-tester, Playwright" />
|
||||
<Box title="Doc-Text je Typ" />
|
||||
</Lane>
|
||||
<Arrow />
|
||||
<Lane label="Engine (deterministisch)">
|
||||
<div className="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-1.5 space-y-1">
|
||||
{STAGES.map((s) => (
|
||||
<div key={s.id} className="text-[10px] text-gray-600 dark:text-gray-300 leading-tight">
|
||||
{s.title}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Lane>
|
||||
<Arrow />
|
||||
<Lane label="Ausgaben">
|
||||
<Box title="Findings je Modul-Tab" sub="Impressum/AGB/DSE/Cookie" accent="green" />
|
||||
<Box title="Severity + Maßnahme" accent="green" />
|
||||
<Box title="Snapshot + Bericht" sub="ohne Re-Crawl" accent="green" />
|
||||
</Lane>
|
||||
</div>
|
||||
<p className="text-[10px] text-gray-400 mt-2">
|
||||
Links→rechts reproduzierbar. Embedding ist semantisch UND deterministisch (feste Funktion: gleicher
|
||||
Text → gleicher Vektor). Das LLM läuft nur für unscharfe Fälle und eskaliert mit Selbstkonfidenz.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="space-y-2">
|
||||
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300">Pipeline (Schritt für Schritt)</h3>
|
||||
<div className="space-y-1">
|
||||
{STAGES.map((s, i) => (
|
||||
<StageRow
|
||||
key={s.id}
|
||||
stage={s}
|
||||
last={i === STAGES.length - 1}
|
||||
open={open === s.id}
|
||||
onToggle={() => setOpen(open === s.id ? null : s.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="space-y-3">
|
||||
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300">Modul-Engines (live)</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{MODULES.map((m) => (
|
||||
<div key={m.name} className="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-3">
|
||||
<div className="flex items-baseline justify-between gap-2">
|
||||
<span className="text-sm font-medium text-gray-800 dark:text-gray-200">{m.name}</span>
|
||||
<span className="inline-block rounded px-1.5 py-0.5 text-[10px] font-medium bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300">
|
||||
live
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-1">{m.mechanism}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="space-y-3">
|
||||
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300">Prüfer-Matrix (Meta-Modell)</h3>
|
||||
<p className="text-xs text-gray-500 max-w-3xl">
|
||||
Jede Pflicht wird einem <strong>Prüfertyp</strong> zugeordnet — so braucht es nicht pro Control eigene
|
||||
Logik, sondern wenige wiederverwendbare Prüfer.
|
||||
</p>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="text-gray-500 border-b border-gray-200 dark:border-gray-700 text-left">
|
||||
<th className="py-1.5 pr-3">Prüfer</th>
|
||||
<th className="py-1.5 pr-3">Mechanismus</th>
|
||||
<th className="py-1.5 pr-3">Deterministisch</th>
|
||||
<th className="py-1.5">Beispiel</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{PRUEFER.map((p) => (
|
||||
<tr key={p.method} className="border-b border-gray-100 dark:border-gray-700/50 align-top">
|
||||
<td className="py-1.5 pr-3">
|
||||
<code className="text-[11px] bg-gray-100 dark:bg-gray-700 rounded px-1">{p.method}</code>
|
||||
</td>
|
||||
<td className="py-1.5 pr-3 text-gray-600 dark:text-gray-300">{p.mechanism}</td>
|
||||
<td className="py-1.5 pr-3 text-gray-500">{p.deterministic}</td>
|
||||
<td className="py-1.5 text-gray-500">{p.example}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -4,10 +4,12 @@ import React, { useState } from 'react'
|
||||
import { ComplianceCheckTab } from './_components/ComplianceCheckTab'
|
||||
import { ComplianceFAQ } from './_components/ComplianceFAQ'
|
||||
import { SnapshotHistoryList } from './_components/SnapshotHistoryList'
|
||||
import { ArchitekturView } from './_components/ArchitekturView'
|
||||
|
||||
export default function AgentPage() {
|
||||
// Nach einem abgeschlossenen Check die Historie unten neu laden.
|
||||
const [historyKey, setHistoryKey] = useState(0)
|
||||
const [tab, setTab] = useState<'check' | 'architektur'>('check')
|
||||
|
||||
return (
|
||||
<div className="space-y-6 max-w-4xl">
|
||||
@@ -16,11 +18,31 @@ export default function AgentPage() {
|
||||
<p className="text-gray-500 mt-1">Webseiten + Dokumente auf DSGVO-Konformität prüfen.</p>
|
||||
</div>
|
||||
|
||||
<ComplianceCheckTab onComplete={() => setHistoryKey(k => k + 1)} />
|
||||
<div className="flex gap-1 border-b border-gray-200 dark:border-gray-700">
|
||||
{([['check', 'Check'], ['architektur', 'Architektur']] as const).map(([id, label]) => (
|
||||
<button
|
||||
key={id}
|
||||
onClick={() => setTab(id)}
|
||||
className={`px-3 py-2 text-sm font-medium -mb-px border-b-2 transition-colors ${
|
||||
tab === id
|
||||
? 'border-purple-500 text-purple-600 dark:text-purple-400'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 dark:hover:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<SnapshotHistoryList refreshKey={historyKey} />
|
||||
|
||||
<ComplianceFAQ />
|
||||
{tab === 'check' ? (
|
||||
<>
|
||||
<ComplianceCheckTab onComplete={() => setHistoryKey(k => k + 1)} />
|
||||
<SnapshotHistoryList refreshKey={historyKey} />
|
||||
<ComplianceFAQ />
|
||||
</>
|
||||
) : (
|
||||
<ArchitekturView />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ the map). Once the tabs are the source of truth, B18's v1 path retires.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from compliance.services.specialist_agents import REGISTRY, AgentInput
|
||||
@@ -27,6 +28,8 @@ logger = logging.getLogger(__name__)
|
||||
# topic key (matches state["doc_texts"]) -> registered agent_id
|
||||
_TOPIC_AGENTS: dict[str, str] = {
|
||||
"impressum": "impressum",
|
||||
"agb": "agb", # v2: AGBAgent mit decision_method-Routing (71% FP -> ~0)
|
||||
"dse": "dse", # v3: 4-Layer (Regex-Boost/Keyword/BGE-M3-Recall/Semantic)
|
||||
}
|
||||
|
||||
_MIN_TEXT = 100
|
||||
@@ -112,14 +115,17 @@ async def run_agent_outputs(state: dict) -> None:
|
||||
)
|
||||
|
||||
outputs: dict[str, dict] = state.get("agent_outputs") or {}
|
||||
for topic, agent_id in _TOPIC_AGENTS.items():
|
||||
|
||||
async def _run_one(topic: str, agent_id: str):
|
||||
"""Einen Topic-Agent laufen lassen + sein Tab-Event sofort emittieren
|
||||
(Zwischenbefund). Fängt eigene Fehler → ein Agent reißt den Run nicht ab."""
|
||||
text = (doc_texts.get(topic) or "").strip()
|
||||
if len(text) < _MIN_TEXT:
|
||||
continue
|
||||
return None
|
||||
agent = REGISTRY.get(agent_id)
|
||||
if agent is None:
|
||||
logger.warning("agent_outputs: agent '%s' not registered", agent_id)
|
||||
continue
|
||||
return None
|
||||
try:
|
||||
out = await agent.evaluate(AgentInput(
|
||||
doc_type=topic,
|
||||
@@ -128,15 +134,25 @@ async def run_agent_outputs(state: dict) -> None:
|
||||
company_name=company_name,
|
||||
origin_domain=origin_domain,
|
||||
))
|
||||
outputs[topic] = out.model_dump(mode="json")
|
||||
emit(check_id, {"type": "topic", "topic": topic,
|
||||
"output": outputs[topic]})
|
||||
dump = out.model_dump(mode="json")
|
||||
emit(check_id, {"type": "topic", "topic": topic, "output": dump})
|
||||
logger.info(
|
||||
"agent_outputs[%s]: %d findings, confidence %.2f",
|
||||
topic, len(out.findings), out.confidence,
|
||||
)
|
||||
return topic, dump
|
||||
except Exception as e: # noqa: BLE001 — best-effort, never break the run
|
||||
logger.warning("agent_outputs[%s] failed: %s", topic, e)
|
||||
return None
|
||||
|
||||
# Topic-Agenten laufen NEBENLÄUFIG (ihre Embedding-/LLM-Waits überlappen) und
|
||||
# füllen ihren Tab via SSE, sobald sie fertig sind — kein Warten aufs Schlusslicht.
|
||||
results = await asyncio.gather(
|
||||
*(_run_one(topic, agent_id) for topic, agent_id in _TOPIC_AGENTS.items())
|
||||
)
|
||||
for r in results:
|
||||
if r:
|
||||
outputs[r[0]] = r[1]
|
||||
|
||||
if outputs:
|
||||
state["agent_outputs"] = outputs
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
"""Pruefer-Library — gemeinsames Interface. Siehe docs platform_checker_matrix.md.
|
||||
|
||||
Ein Checker prueft EINEN Control gegen EIN Dokument und liefert: vorhanden / fehlt
|
||||
/ unklar (+ Evidence). Module (DSE/Impressum/AGB/...) liefern nur Control-Metadaten
|
||||
ueber `ControlSpec` (verification_method + decision_method + checker-spezifische
|
||||
Config); die Engine routet method-agnostisch zum passenden Checker.
|
||||
|
||||
Ziel der Plattform: 14k Controls -> 7 Pruefertypen -> wenige Pruefer. Ein neues
|
||||
Modul wird damit ein Klassifizierungs-, kein Forschungsproblem.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Optional, Protocol, runtime_checkable
|
||||
|
||||
|
||||
class VerificationMethod:
|
||||
"""Achse 1 — WELCHER Pruefer-Typ (Kategorie)."""
|
||||
FIELD = "FIELD"
|
||||
REFERENCE = "REFERENCE"
|
||||
BEHAVIOR = "BEHAVIOR"
|
||||
PRESENTATION = "PRESENTATION"
|
||||
CONTENT = "CONTENT"
|
||||
PROCESS = "PROCESS"
|
||||
TECHNICAL = "TECHNICAL"
|
||||
CONTRACTUAL = "CONTRACTUAL"
|
||||
|
||||
|
||||
class DecisionMethod:
|
||||
"""Achse 2 — WIE entschieden wird (konkreter Mechanismus)."""
|
||||
REGEX = "REGEX"
|
||||
EMBEDDING = "EMBEDDING"
|
||||
LLM = "LLM"
|
||||
LINK_RESOLVER = "LINK_RESOLVER"
|
||||
PLAYWRIGHT = "PLAYWRIGHT"
|
||||
AUDIT = "AUDIT"
|
||||
SCANNER = "SCANNER"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ControlSpec:
|
||||
"""Routing-Metadaten + checker-spezifische Config eines Controls. Module fuellen
|
||||
nur die fuer ihren decision_method relevanten Felder."""
|
||||
control_id: str
|
||||
verification_method: str
|
||||
decision_method: str
|
||||
label: str = ""
|
||||
severity: str = "MEDIUM"
|
||||
patterns: list[str] = field(default_factory=list) # FIELD/REGEX, REFERENCE
|
||||
paraphrases: list[str] = field(default_factory=list) # CONTENT (EMBEDDING/LLM)
|
||||
embed_threshold: Optional[float] = None # EMBEDDING (per-Control)
|
||||
topic_regex: str = "" # LLM: Section-Retrieval
|
||||
question: str = "" # LLM: Pruef-Frage
|
||||
extra: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DocContext:
|
||||
"""Das zu pruefende Artefakt. `text` = Volltext; `url`/`rendered` fuer
|
||||
PRESENTATION/BEHAVIOR (Playwright) — spaeter."""
|
||||
text: str = ""
|
||||
url: str = ""
|
||||
rendered: Any = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class CheckResult:
|
||||
present: Optional[bool] # True=erfuellt, False=fehlt, None=unklar (fail-safe)
|
||||
evidence: str = ""
|
||||
confidence: float = 0.0
|
||||
source: str = "" # welcher Pruefer/Tier geantwortet hat
|
||||
detail: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class Checker(Protocol):
|
||||
"""Alle Pruefer haben dieselbe Signatur -> die Engine ist method-agnostisch und
|
||||
routet nur ueber ctrl.verification_method / ctrl.decision_method."""
|
||||
verification_method: str
|
||||
|
||||
async def check(self, ctrl: ControlSpec, doc: DocContext) -> CheckResult:
|
||||
...
|
||||
@@ -0,0 +1,51 @@
|
||||
"""CONTENT-Pruefer / decision_method=EMBEDDING.
|
||||
|
||||
Ist die Pflicht SEMANTISCH im Text vorhanden? Max-Cosinus (Doc-Chunks x Control-
|
||||
Paraphrasen) >= per-Control-Schwelle. Deterministisch (festes Embedding-Modell)
|
||||
und gecacht. Rettet Recall-FP (Klausel da, anders formuliert).
|
||||
|
||||
Faellt der Embedding-Service aus, liefert der Checker present=None (unklar) — der
|
||||
Aufrufer behaelt dann das Keyword-Ergebnis (kein Hang, kein Crash).
|
||||
(Validiert an AGB: 17 Items, per-Item-Schwelle, 0 Fehl-Rescue.)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from .base import CheckResult, ControlSpec, DocContext, VerificationMethod
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Paraphrasen-Vektoren je Control einmal einbetten + cachen.
|
||||
_PARA_CACHE: dict[str, list] = {}
|
||||
|
||||
|
||||
class EmbeddingChecker:
|
||||
verification_method = VerificationMethod.CONTENT
|
||||
|
||||
async def check(self, ctrl: ControlSpec, doc: DocContext) -> CheckResult:
|
||||
text = doc.text or ""
|
||||
paras = ctrl.paraphrases or []
|
||||
thr = ctrl.embed_threshold if ctrl.embed_threshold is not None else 0.60
|
||||
if not paras or len(text) < 100:
|
||||
return CheckResult(present=None, source="embedding")
|
||||
try:
|
||||
from compliance.services.mc_embedding_matcher import (
|
||||
DIM, _chunk_text, _cosine, _embed_texts,
|
||||
)
|
||||
if ctrl.control_id not in _PARA_CACHE:
|
||||
pv = await _embed_texts(paras)
|
||||
_PARA_CACHE[ctrl.control_id] = [v for v in pv if v and len(v) == DIM]
|
||||
pvecs = _PARA_CACHE[ctrl.control_id]
|
||||
chunks = _chunk_text(text)
|
||||
cvecs = [v for v in await asyncio.wait_for(
|
||||
_embed_texts(chunks), timeout=90.0) if v and len(v) == DIM]
|
||||
except (Exception, asyncio.TimeoutError) as e:
|
||||
logger.info("embedding checker inaktiv %s: %s", ctrl.control_id, str(e)[:80])
|
||||
return CheckResult(present=None, source="embedding")
|
||||
if not pvecs or not cvecs:
|
||||
return CheckResult(present=None, source="embedding")
|
||||
best = max((_cosine(p, c) for p in pvecs for c in cvecs), default=0.0)
|
||||
return CheckResult(present=best >= thr, confidence=round(best, 3),
|
||||
source="embedding")
|
||||
@@ -0,0 +1,73 @@
|
||||
"""CONTENT/CONTRACTUAL-Pruefer / decision_method=LLM.
|
||||
|
||||
present/absent ueber die LLM-Kaskade (`call_with_cascade`; prod: OVH-120b zuerst).
|
||||
Retrieval = GANZE Paragraph-Abschnitte zum Topic (nicht Top-k-Chunks — das war in
|
||||
der AGB-Validierung der Schluessel). KEIN DEFECT — Korrektheits-/Defekt-Pruefung
|
||||
ist ein separater Modus. present=None bei Fehler (fail-safe: Aufrufer behaelt
|
||||
Keyword-Ergebnis). (Validiert an AGB delivery/warranty.)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
|
||||
from .base import CheckResult, ControlSpec, DocContext, VerificationMethod
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_SECTION = re.compile(r"(?m)(?=^\s*(?:§\s*)?\d+[\.\)]\s)")
|
||||
_SYS = (
|
||||
"Du bist deutscher Compliance-Rechtsexperte. Entscheide, ob die genannte "
|
||||
"Pflicht in den vorgelegten Abschnitten vorhanden ist. NUR die Abschnitte "
|
||||
'zaehlen. Antworte NUR JSON: {"verdict":"ERFUELLT|FEHLT","zitat":"woertlich '
|
||||
'oder leer","begruendung":"1 Satz"}.'
|
||||
)
|
||||
|
||||
|
||||
def _sections(text: str) -> list[str]:
|
||||
return [s.strip() for s in _SECTION.split(text) if s.strip()]
|
||||
|
||||
|
||||
def _parse(txt: str) -> dict:
|
||||
out = (txt or "").strip()
|
||||
if out.startswith("```"):
|
||||
out = out.split("```", 2)[1]
|
||||
out = out[4:] if out.startswith("json") else out
|
||||
a, b = out.find("{"), out.rfind("}")
|
||||
return json.loads(out[a:b + 1] if 0 <= a < b else out)
|
||||
|
||||
|
||||
class LLMChecker:
|
||||
verification_method = VerificationMethod.CONTENT
|
||||
|
||||
async def check(self, ctrl: ControlSpec, doc: DocContext) -> CheckResult:
|
||||
text = doc.text or ""
|
||||
if len(text) < 50:
|
||||
return CheckResult(present=None, source="llm")
|
||||
secs = _sections(text)
|
||||
if ctrl.topic_regex:
|
||||
rel = [s for s in secs if re.search(ctrl.topic_regex, s, re.I)][:6] or secs[:6]
|
||||
else:
|
||||
rel = secs[:6]
|
||||
question = ctrl.question or f"Ist die Pflicht '{ctrl.label}' im Text vorhanden?"
|
||||
try:
|
||||
from compliance.services.llm_cascade import call_with_cascade
|
||||
r = await call_with_cascade(
|
||||
_SYS,
|
||||
json.dumps({"frage": question, "abschnitte": rel}, ensure_ascii=False),
|
||||
min_confidence=0.6, max_tokens=500,
|
||||
)
|
||||
obj = _parse(r.get("text"))
|
||||
verdict = obj.get("verdict")
|
||||
zitat = (obj.get("zitat") or "")[:120]
|
||||
if verdict not in ("ERFUELLT", "FEHLT"):
|
||||
return CheckResult(present=None, evidence=zitat, source=r.get("source", "?"))
|
||||
return CheckResult(
|
||||
present=verdict == "ERFUELLT", evidence=zitat,
|
||||
confidence=float(r.get("confidence") or 0.0),
|
||||
source=r.get("source", "llm"),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.info("llm checker fail %s: %s", ctrl.control_id, str(e)[:80])
|
||||
return CheckResult(present=None, source="error")
|
||||
@@ -0,0 +1,41 @@
|
||||
"""REFERENCE-Pruefer (verification_method=REFERENCE, decision_method=LINK_RESOLVER).
|
||||
|
||||
Ist ein klarer Verweis auf ein anderes Pflichtdokument vorhanden (+ optional: loest
|
||||
der Link auf)? Deterministisch. Bsp: 'Details in unserer Datenschutzerklaerung'.
|
||||
KEIN LLM, kein juristisches Urteil. (Validiert an AGB data_protection: 7/7.)
|
||||
|
||||
Die tatsaechliche HTTP-Aufloesung des Links ist ein optionaler Runtime-Schritt
|
||||
(online), nicht Teil dieser deterministischen Text-Pruefung — die URL wird hier
|
||||
nur extrahiert und in `detail['link']` zurueckgegeben.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
from .base import CheckResult, ControlSpec, DocContext, VerificationMethod
|
||||
|
||||
_URL = re.compile(r"https?://[^\s)\]]+", re.I)
|
||||
|
||||
|
||||
class ReferenceChecker:
|
||||
verification_method = VerificationMethod.REFERENCE
|
||||
|
||||
async def check(self, ctrl: ControlSpec, doc: DocContext) -> CheckResult:
|
||||
text = doc.text or ""
|
||||
pats = ctrl.patterns or []
|
||||
if not pats or not text:
|
||||
return CheckResult(present=False, source="reference")
|
||||
for p in pats:
|
||||
m = re.search(p, text, re.I)
|
||||
if m:
|
||||
window = text[max(0, m.start() - 40): m.end() + 200]
|
||||
url = _URL.search(window) or _URL.search(text)
|
||||
link = url.group(0) if url else None
|
||||
return CheckResult(
|
||||
present=True,
|
||||
evidence=" ".join(m.group(0).split())[:120],
|
||||
confidence=1.0,
|
||||
source="reference",
|
||||
detail={"link": link},
|
||||
)
|
||||
return CheckResult(present=False, source="reference")
|
||||
@@ -0,0 +1,102 @@
|
||||
"""AGB-Routing-Pipeline (C-lean): nimmt das Keyword-Ergebnis des ChecklistAgent
|
||||
und routet keyword-durchgefallene Items per `_routing.decision_method` an die
|
||||
wiederverwendbare Prüfer-Library (Embedding / Reference / LLM). Davor das
|
||||
Geschäftsmodell-Gate (Applicability). Das Re-Tiering (LOW → Empfehlung) +
|
||||
Output-Zusammenbau macht der AGBAgent — hier nur die Routing-Entscheidung.
|
||||
|
||||
Validiert (7-Firmen-Opus-GT): 71 % FP → ~0. agent.py bleibt dünn, dies ist der
|
||||
einzige Ort des C-lean-Flows.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from compliance.services.checkers.base import (
|
||||
ControlSpec,
|
||||
DecisionMethod,
|
||||
DocContext,
|
||||
VerificationMethod,
|
||||
)
|
||||
from compliance.services.checkers.embedding_checker import EmbeddingChecker
|
||||
from compliance.services.checkers.llm_checker import LLMChecker
|
||||
from compliance.services.checkers.reference_checker import ReferenceChecker
|
||||
|
||||
from . import _routing
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Checker sind zustandslos (schwere Imports erst in .check()) → Modul-Singletons.
|
||||
_EMB = EmbeddingChecker()
|
||||
_REF = ReferenceChecker()
|
||||
_LLM = LLMChecker()
|
||||
|
||||
|
||||
def _spec(item_id: str) -> ControlSpec:
|
||||
"""ControlSpec für ein Item aus der AGB-Routing-Config bauen."""
|
||||
dm = _routing.decision_method(item_id)
|
||||
if dm == _routing.REFERENCE:
|
||||
return ControlSpec(
|
||||
control_id=item_id, verification_method=VerificationMethod.REFERENCE,
|
||||
decision_method=DecisionMethod.LINK_RESOLVER,
|
||||
patterns=[_routing.REFERENCE_PATTERNS[item_id]],
|
||||
)
|
||||
if dm == _routing.LLM:
|
||||
return ControlSpec(
|
||||
control_id=item_id, verification_method=VerificationMethod.CONTENT,
|
||||
decision_method=DecisionMethod.LLM,
|
||||
paraphrases=_routing.PARAPHRASES.get(item_id, []),
|
||||
topic_regex=_routing.LLM_TOPIC.get(item_id, ""),
|
||||
question=_routing.LLM_QUESTION.get(item_id, ""),
|
||||
)
|
||||
return ControlSpec(
|
||||
control_id=item_id, verification_method=VerificationMethod.CONTENT,
|
||||
decision_method=DecisionMethod.EMBEDDING,
|
||||
paraphrases=_routing.PARAPHRASES.get(item_id, []),
|
||||
embed_threshold=_routing.EMBED_THRESHOLDS.get(item_id),
|
||||
)
|
||||
|
||||
|
||||
async def _resolves(item_id: str, text: str, skip_llm: bool):
|
||||
"""True = Klausel doch vorhanden (Keyword-Finding auflösen). False/None =
|
||||
Finding behalten (fail-safe: bei Unsicherheit/Service-Ausfall lieber melden)."""
|
||||
dm = _routing.decision_method(item_id)
|
||||
if dm == _routing.MERGED:
|
||||
return True # in ein anderes Item aufgegangen → kein eigenes Finding
|
||||
doc = DocContext(text=text)
|
||||
spec = _spec(item_id)
|
||||
if dm == _routing.REFERENCE:
|
||||
return (await _REF.check(spec, doc)).present
|
||||
if dm == _routing.LLM:
|
||||
if skip_llm:
|
||||
return None # interaktiv: kein LLM → Keyword-Ergebnis behalten
|
||||
return (await _LLM.check(spec, doc)).present
|
||||
return (await _EMB.check(spec, doc)).present
|
||||
|
||||
|
||||
async def run_routed(base_findings: list, text: str, context: dict | None = None):
|
||||
"""Routet die keyword-durchgefallenen Items.
|
||||
|
||||
Returns (kept, resolved_ids, gated_ids):
|
||||
kept = Findings, die nach Gate+Rescue bestehen bleiben
|
||||
resolved_ids = per Embedding/Reference/LLM doch als vorhanden erkannt
|
||||
gated_ids = per Geschäftsmodell nicht anwendbar (N/A)
|
||||
"""
|
||||
context = context or {}
|
||||
skip_llm = bool(context.get("skip_llm"))
|
||||
model = _routing.detect_business_model(text)
|
||||
kept, resolved, gated = [], [], []
|
||||
for f in base_findings:
|
||||
item_id = f.field_id
|
||||
if not _routing.is_applicable(item_id, model):
|
||||
gated.append(item_id)
|
||||
continue
|
||||
try:
|
||||
present = await _resolves(item_id, text, skip_llm)
|
||||
except Exception as e: # noqa: BLE001 — best-effort, Finding behalten
|
||||
logger.info("agb routing %s failed: %s", item_id, str(e)[:80])
|
||||
present = None
|
||||
if present is True:
|
||||
resolved.append(item_id)
|
||||
else:
|
||||
kept.append(f)
|
||||
return kept, resolved, gated
|
||||
@@ -0,0 +1,144 @@
|
||||
"""AGB-Routing — das verification_method / decision_method-Meta-Modell, angewandt
|
||||
auf die AGB_CHECKLIST. Siehe docs-src/development/platform_checker_matrix.md.
|
||||
|
||||
Pro Checklisten-Item: WELCHER Pruefer (verification_method) und WIE entschieden
|
||||
wird (decision_method). Single source of truth; `agb_checks.py` bleibt die reine
|
||||
Pflichtangaben-Liste, dieses Modul ist der additive Routing-Overlay.
|
||||
|
||||
Validiert 2026-06-20/21 gegen 7-Firmen-Opus-GT (71 % FP -> ~0):
|
||||
- 17 Items EMBEDDING (per-Item-Cosinus-Schwelle; 21 recall-FP gekillt, 0 Fehl-Rescue)
|
||||
- 2 Items LLM (delivery_timeframe, warranty_period; ganze Paragraph-Abschnitte + starkes Modell, present/absent)
|
||||
- 1 Item REFERENCE (data_protection; DSE-Verweis + Link, 7/7 deterministisch)
|
||||
- incorporation_clause MERGED in contract (implizit, kein eigener Pruefer)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
# ── decision_method-Werte ────────────────────────────────────────────────
|
||||
EMBEDDING = "EMBEDDING"
|
||||
LLM = "LLM"
|
||||
REFERENCE = "REFERENCE"
|
||||
MERGED = "MERGED" # in ein anderes Item aufgegangen -> kein eigener Check
|
||||
|
||||
# ── Per-Item Embedding-Rescue-Schwellen ───────────────────────────────────
|
||||
# An der 7-Firmen-GT kalibriert. BEWUSST per-Item: eine globale Schwelle trennt
|
||||
# bei juristischer Prosa nicht (PASS/FAIL ueberlappen global, trennen per-Item).
|
||||
# Vorlaeufig (FAIL n=25 klein) -> vor Prod mit mehr Firmen nachkalibrieren.
|
||||
EMBED_THRESHOLDS: dict[str, float] = {
|
||||
"scope": 0.58, "contract": 0.58, "payment": 0.60, "payment_methods": 0.58,
|
||||
"delivery": 0.57, "warranty": 0.58, "termination": 0.60,
|
||||
"termination_period": 0.60, "termination_form": 0.60, "consumer_rights": 0.55,
|
||||
"liability": 0.615, "jurisdiction": 0.585, "dispute_odr_link": 0.67,
|
||||
"choice_of_law_specific": 0.625, "payment_due_date": 0.705,
|
||||
"salvatory_clause": 0.565, "amendment_clause": 0.635,
|
||||
}
|
||||
|
||||
# ── decision_method je Item (deckt alle 21 Checklisten-IDs ab) ────────────
|
||||
DECISION_METHOD: dict[str, str] = {cid: EMBEDDING for cid in EMBED_THRESHOLDS}
|
||||
DECISION_METHOD.update({
|
||||
"delivery_timeframe": LLM,
|
||||
"warranty_period": LLM,
|
||||
"data_protection": REFERENCE,
|
||||
"incorporation_clause": MERGED, # -> contract
|
||||
})
|
||||
|
||||
# ── Applicability-Gate (VOR allen Pruefern; Geschaeftsmodell entscheidet) ──
|
||||
ABO_ONLY = {"termination", "termination_period", "termination_form"} # nur Dauerschuld
|
||||
B2C_ONLY = {"consumer_rights", "dispute_odr_link"} # nicht reines B2B
|
||||
|
||||
# ── Referenz-Paraphrasen (Embedding-Rescue + LLM-Section-Ranking) ──────────
|
||||
PARAPHRASES: dict[str, list[str]] = {
|
||||
"scope": ["Diese AGB gelten fuer alle Vertraege zwischen dem Anbieter und dem Kunden.",
|
||||
"Die Angebote richten sich ausschliesslich an Verbraucher, die privat kaufen.",
|
||||
"Geltungsbereich: fuer die Geschaeftsbeziehung gelten die nachfolgenden Bedingungen."],
|
||||
"contract": ["Durch Anklicken des Bestellbuttons gibt der Kunde ein verbindliches Angebot ab.",
|
||||
"Der Vertrag kommt mit Zugang der Bestellbestaetigung zustande.",
|
||||
"Mit der Bestellung erkennt der Kunde diese AGB als Vertragsbestandteil an."],
|
||||
"liability": ["Die Haftung fuer leicht fahrlaessige Pflichtverletzungen ist beschraenkt.",
|
||||
"Wir haften unbeschraenkt fuer Schaeden aus Verletzung von Leben, Koerper, Gesundheit.",
|
||||
"Bei Verletzung wesentlicher Vertragspflichten Haftung auf vorhersehbaren Schaden begrenzt."],
|
||||
"jurisdiction": ["Es gilt das Recht der Bundesrepublik Deutschland unter Ausschluss des UN-Kaufrechts.",
|
||||
"Gerichtsstand fuer alle Streitigkeiten ist der Sitz des Unternehmens.",
|
||||
"Auf die Vertraege findet deutsches Recht Anwendung."],
|
||||
"dispute_odr_link": ["Die EU-Kommission stellt eine Plattform zur Online-Streitbeilegung bereit.",
|
||||
"Zur aussergerichtlichen Streitbeilegung steht die OS-Plattform zur Verfuegung."],
|
||||
"choice_of_law_specific": ["Es gilt deutsches Recht unter Ausschluss des UN-Kaufrechts (CISG).",
|
||||
"Anwendbar ist das Recht der Bundesrepublik Deutschland."],
|
||||
"payment": ["Die Preise sind Endpreise inklusive Mehrwertsteuer; Versandkosten gesondert ausgewiesen.",
|
||||
"Zahlungsbedingungen und Preise richten sich nach den Angaben im Bestellprozess."],
|
||||
"payment_methods": ["Zur Zahlung stehen Vorkasse, Kreditkarte, Lastschrift, Rechnung und PayPal zur Verfuegung.",
|
||||
"Folgende Zahlungsarten werden akzeptiert: Ueberweisung, SEPA-Lastschrift, Kreditkarte."],
|
||||
"payment_due_date": ["Der Kaufpreis ist sofort mit Vertragsschluss faellig.",
|
||||
"Die Zahlung ist bei Bestellung zu leisten.",
|
||||
"Der Rechnungsbetrag wird mit Versand der Ware faellig.",
|
||||
"Bei Kauf auf Rechnung ist der Betrag innerhalb von 14 Tagen zu zahlen."],
|
||||
"delivery": ["Die Lieferung erfolgt an die vom Kunden angegebene Lieferadresse.",
|
||||
"Wir liefern innerhalb Deutschlands; die Leistung wird nach Vertragsschluss erbracht."],
|
||||
"delivery_timeframe": ["Die Lieferzeit betraegt in der Regel 3-5 Werktage.",
|
||||
"Die Ware wird voraussichtlich innerhalb von 2 bis 4 Werktagen geliefert."],
|
||||
"warranty": ["Es gelten die gesetzlichen Maengelhaftungsrechte (Gewaehrleistung).",
|
||||
"Bei Maengeln stehen dem Kunden die gesetzlichen Gewaehrleistungsrechte zu.",
|
||||
"Fuer Sachmaengel haften wir nach den gesetzlichen Bestimmungen."],
|
||||
"warranty_period": ["Die Gewaehrleistungsfrist betraegt zwei Jahre ab Lieferung.",
|
||||
"Die Verjaehrungsfrist fuer Maengelansprueche betraegt zwei Jahre."],
|
||||
"termination": ["Der Vertrag kann von beiden Parteien ordentlich gekuendigt werden.",
|
||||
"Das Abonnement kann jederzeit zum Ende der Laufzeit gekuendigt werden."],
|
||||
"termination_period": ["Die Kuendigungsfrist betraegt einen Monat zum Vertragsende.",
|
||||
"Der Vertrag ist mit einer Frist von vier Wochen kuendbar."],
|
||||
"termination_form": ["Die Kuendigung bedarf der Textform und kann per E-Mail erfolgen.",
|
||||
"Eine Kuendigung ist schriftlich oder per E-Mail moeglich."],
|
||||
"salvatory_clause": ["Sollten einzelne Bestimmungen unwirksam sein, bleibt die Wirksamkeit der uebrigen unberuehrt.",
|
||||
"Die Unwirksamkeit einzelner Klauseln beruehrt nicht die Gueltigkeit der uebrigen AGB."],
|
||||
"amendment_clause": ["Wir behalten uns vor, diese AGB mit Wirkung fuer die Zukunft zu aendern.",
|
||||
"Aenderungen dieser Bedingungen werden dem Kunden rechtzeitig mitgeteilt."],
|
||||
"consumer_rights": ["Die gesetzlichen Rechte des Verbrauchers bleiben unberuehrt.",
|
||||
"Zwingende Verbraucherschutzvorschriften bleiben von diesen Bedingungen unberuehrt."],
|
||||
}
|
||||
|
||||
# ── LLM-Items: Paragraph-Abschnitts-Retrieval + Pruef-Frage ───────────────
|
||||
LLM_TOPIC: dict[str, str] = {
|
||||
"delivery_timeframe": r"liefer",
|
||||
"warranty_period": r"gew(?:ä|ae)hrleist|m(?:ä|ae)ngel|sachm|verj(?:ä|ae)hr|haftungsdauer|garantie",
|
||||
}
|
||||
LLM_QUESTION: dict[str, str] = {
|
||||
"delivery_timeframe": ("Wird eine KONKRETE Lieferzeit/Lieferfrist genannt (z.B. '3-5 Werktage', "
|
||||
"'innerhalb von 2 Werktagen')? Eine nur allgemeine Lieferregelung ODER ein "
|
||||
"Verweis 'Lieferzeit im Bestellvorgang' ohne konkrete Frist zaehlt NICHT."),
|
||||
"warranty_period": ("Wird eine KONKRETE Gewaehrleistungs-/Verjaehrungsfrist als ZAHL genannt "
|
||||
"(z.B. 'zwei Jahre', 'ein Jahr')? Ein blosser Verweis auf 'gesetzliche "
|
||||
"Verjaehrungsfristen' ohne Zahl zaehlt NICHT."),
|
||||
}
|
||||
|
||||
# ── REFERENCE-Item data_protection ────────────────────────────────────────
|
||||
REFERENCE_PATTERNS: dict[str, str] = {
|
||||
"data_protection": r"datenschutz(erkl(?:ä|ae)rung|bestimmung|hinweis)",
|
||||
}
|
||||
|
||||
|
||||
def detect_business_model(text: str) -> dict[str, bool]:
|
||||
"""Deterministischer Geschaeftsmodell-Detektor fuer das Applicability-Gate.
|
||||
Edge-Case: gemischte Modelle (Webshop + Finanzierung/Service) koennen 'abo'
|
||||
triggern -> dann greift das termination-Gate nicht; bewusst konservativ
|
||||
(lieber eine Kuendigungs-Pruefung zu viel als eine echte Luecke uebersehen)."""
|
||||
tl = text.lower()
|
||||
consumer = ("widerrufsbelehrung" in tl) or ("widerrufsrecht" in tl and "verbraucher" in tl)
|
||||
b2b = (not consumer) and any(s in tl for s in (
|
||||
"geschäftskunden", "ausschließlich an unternehmer", "nur an unternehmer",
|
||||
"lieferbedingungen für geschäftskunden"))
|
||||
abo = any(s in tl for s in (
|
||||
"abonnement", "mindestlaufzeit", "vertragslaufzeit", "verlängert sich",
|
||||
"monatsabo", "jahresabo")) or ("abo" in tl and "kündig" in tl)
|
||||
return {"b2b": b2b, "abo": abo, "b2c": not b2b}
|
||||
|
||||
|
||||
def is_applicable(item_id: str, model: dict[str, bool]) -> bool:
|
||||
"""Gate: gilt das Item fuer dieses Geschaeftsmodell? (False -> N/A, nicht pruefen)."""
|
||||
if item_id in ABO_ONLY and not model.get("abo"):
|
||||
return False
|
||||
if item_id in B2C_ONLY and model.get("b2b"):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def decision_method(item_id: str) -> str:
|
||||
"""decision_method fuer ein Item; Default EMBEDDING (Prosa-Rescue)."""
|
||||
return DECISION_METHOD.get(item_id, EMBEDDING)
|
||||
@@ -1,19 +1,60 @@
|
||||
"""AGBAgent — Allgemeine Geschäftsbedingungen (§§ 305 ff. BGB).
|
||||
|
||||
Thin-Subclass von ChecklistAgent über die kuratierte AGB_CHECKLIST (L1
|
||||
Pflichtangaben + L2 Detailchecks). KEIN Library-Firehose.
|
||||
ChecklistAgent-Subclass: erst L1/L2-Keyword-Pass, dann **C-lean-Routing** — die
|
||||
keyword-durchgefallenen Items werden per `decision_method` an die wiederverwendbare
|
||||
Prüfer-Library geroutet (Embedding / Reference / LLM), davor das Geschäftsmodell-
|
||||
Gate (Applicability), danach Severity-Re-Tiering (LOW → Empfehlung).
|
||||
Validiert gegen 7-Firmen-Opus-GT: 71 % FP → ~0. Config in `_routing`, Flow in `_pipeline`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from compliance.services.doc_checks.agb_checks import AGB_CHECKLIST
|
||||
|
||||
from .._base import AgentInput, AgentOutput, lint_output
|
||||
from .._checklist_agent import ChecklistAgent
|
||||
from .._rollup import rollup
|
||||
from ._pipeline import run_routed
|
||||
|
||||
|
||||
class AGBAgent(ChecklistAgent):
|
||||
CHECKLIST = AGB_CHECKLIST
|
||||
agent_id = "agb"
|
||||
agent_version = "1.0"
|
||||
agent_version = "2.0" # v2: decision_method-Routing (Embedding/Reference/LLM)
|
||||
doc_type = "agb"
|
||||
owned_mc_ids = tuple(c["id"] for c in AGB_CHECKLIST)
|
||||
|
||||
async def evaluate(self, agent_input: AgentInput) -> AgentOutput:
|
||||
# 1) Basis-Keyword-Pass (L1/L2). out.findings = keyword-durchgefallene Items.
|
||||
out = await super().evaluate(agent_input)
|
||||
text = (agent_input.text or "").strip()
|
||||
if len(text) < 100 or not out.findings:
|
||||
return out # zu kurz / nichts zu routen
|
||||
|
||||
# 2) Routing: Gate + Embedding/Reference/LLM-Rescue der Keyword-Misses.
|
||||
kept, resolved, gated = await run_routed(
|
||||
out.findings, text, agent_input.context)
|
||||
resolved_set, gated_set = set(resolved), set(gated)
|
||||
|
||||
# 3) Coverage angleichen: rescued → ok, gated → na.
|
||||
for c in out.mc_coverage:
|
||||
if c.mc_id in resolved_set:
|
||||
c.status, c.reason = "ok", "semantisch vorhanden (Routing)"
|
||||
elif c.mc_id in gated_set:
|
||||
c.status, c.reason = "na", "für Geschäftsmodell nicht anwendbar"
|
||||
|
||||
# 4) Severity-Re-Tiering: HIGH/MEDIUM = Findings, LOW = nur Empfehlung.
|
||||
out.findings = [f for f in kept if f.severity in ("HIGH", "MEDIUM")]
|
||||
out.recommendations = rollup(kept)
|
||||
|
||||
# 5) Aggregat-Kennzahlen neu (Coverage hat sich verschoben).
|
||||
cov = out.mc_coverage
|
||||
out.mc_total = len(cov)
|
||||
out.mc_ok = sum(1 for c in cov if c.status == "ok")
|
||||
out.mc_na = sum(1 for c in cov if c.status == "na")
|
||||
out.mc_high = sum(1 for c in cov if c.status == "high")
|
||||
out.mc_medium = sum(1 for c in cov if c.status == "medium")
|
||||
out.mc_low = sum(1 for c in cov if c.status == "low")
|
||||
out.notes = ((out.notes + " · ") if out.notes else "") + \
|
||||
f"routed: {len(resolved)} rescued, {len(gated)} n/a"
|
||||
return lint_output(out)
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Applicability-Gate fuer den DSE-Scan.
|
||||
|
||||
Schliesst Controls aus dem DSE-FINDINGS-Scan aus, die laut
|
||||
`compliance.control_classification` NICHT gegen eine DSE laufen
|
||||
('DSE' nicht in applicable_artifacts) UND sicher klassifiziert sind
|
||||
(needs_review=false). Diese werden NICHT geloescht, sondern als
|
||||
*organisatorische Checkliste* zurueckgegeben (Routing zu VVT/TOM/Audit).
|
||||
|
||||
Fail-safe: unsichere Klassifikationen (needs_review=true) bleiben im
|
||||
Findings-Scan. Defensiv: fehlt die Tabelle (z.B. Prod ohne Migration),
|
||||
liefert das Gate ein leeres Dict -> es wird NICHT gefiltert.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def load_dse_gate(db_url: str = "") -> dict[str, dict[str, Any]]:
|
||||
"""Liefert {control_id: meta} fuer Controls, die aus dem DSE-Findings-Scan
|
||||
auszuschliessen sind (hochsicher organisatorisch). Leeres Dict = kein Filter.
|
||||
"""
|
||||
dsn = (db_url or os.getenv("DATABASE_URL")
|
||||
or os.getenv("COMPLIANCE_DATABASE_URL") or "")
|
||||
if not dsn:
|
||||
return {}
|
||||
try:
|
||||
import asyncpg
|
||||
conn = await asyncpg.connect(dsn)
|
||||
try:
|
||||
rows = await conn.fetch(
|
||||
"""SELECT control_id, obligation_type, check_intent,
|
||||
applicable_artifacts, reference_allowed
|
||||
FROM compliance.control_classification
|
||||
WHERE is_active AND NOT needs_review
|
||||
AND NOT ('DSE' = ANY(applicable_artifacts))""")
|
||||
finally:
|
||||
await conn.close()
|
||||
except Exception as e: # Tabelle fehlt / DB nicht erreichbar -> kein Filter
|
||||
logger.info("dse classification gate inaktiv: %s", str(e)[:90])
|
||||
return {}
|
||||
return {
|
||||
r["control_id"]: {
|
||||
"obligation_type": r["obligation_type"],
|
||||
"check_intent": r["check_intent"],
|
||||
"applicable_artifacts": list(r["applicable_artifacts"] or []),
|
||||
"reference_allowed": r["reference_allowed"],
|
||||
}
|
||||
for r in rows if r["control_id"]
|
||||
}
|
||||
|
||||
|
||||
def apply_gate(
|
||||
controls: list[dict[str, Any]],
|
||||
gate: dict[str, dict[str, Any]],
|
||||
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
||||
"""Teilt geladene Controls in (findings_controls, organizational).
|
||||
|
||||
findings_controls: laufen normal durch den DSE-Scan.
|
||||
organizational: aus dem Scan genommen, als Checkliste ausgegeben
|
||||
(control_id + title + Klassifikations-Metadaten fuer das Routing).
|
||||
"""
|
||||
kept: list[dict[str, Any]] = []
|
||||
organizational: list[dict[str, Any]] = []
|
||||
for c in controls:
|
||||
cid = c.get("control_id")
|
||||
meta = gate.get(cid) if cid else None
|
||||
if meta:
|
||||
organizational.append({
|
||||
"control_id": cid,
|
||||
"title": c.get("title"),
|
||||
**meta,
|
||||
})
|
||||
else:
|
||||
kept.append(c)
|
||||
return kept, organizational
|
||||
@@ -0,0 +1,170 @@
|
||||
"""Deterministische semantische Recall-Schicht für den DSE-Check.
|
||||
|
||||
WARUM: Reines Keyword-Matching hat schlechten Recall (eine Pflicht lässt sich
|
||||
auf viele Arten formulieren). Der frühere Regex-Boost war zu stumpf (über-passt
|
||||
auf vollständigen Dokumenten). BGE-M3-Embeddings erkennen den SINN — und sind
|
||||
dabei DETERMINISTISCH: ein Embedding-Modell ist eine feste Funktion, gleicher
|
||||
Text → gleicher Vektor → gleiches Pass/Fail bei fester Schwelle. Reproduzierbar,
|
||||
auditierbar, kein Keyword-Katalog, kein generatives LLM zur Checkzeit.
|
||||
|
||||
Design:
|
||||
- Doc wird EINMAL pro Dokument-Hash eingebettet (teuer: ~37s/64k-Doc), die
|
||||
Per-Control-Scores werden gecacht (/data) → Folge-Checks sind instant.
|
||||
- Reachability-Guard: ist der Embedding-Service nicht erreichbar, liefert die
|
||||
Schicht leer zurück (der deterministische Keyword-Layer trägt) — KEIN Hang.
|
||||
- Schwelle ist die einzige Stellschraube (DSE-Default 0.65, an BMW-GT kalibriert;
|
||||
braucht Mehr-Firmen-Kalibrierung gegen Overfitting — bewusst konservativ).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sqlite3
|
||||
from typing import Iterable
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# DSE-Schwelle: an BMW-Haiku-GT vermessen (PASS-Median 0.648 / FAIL-Median 0.612).
|
||||
# 0.65 = präzisionsfreundlich (wenig Über-Pass). Per ENV überschreibbar für
|
||||
# spätere Mehr-Firmen-Kalibrierung, ohne Code-Änderung.
|
||||
DSE_EMBED_THRESHOLD = float(os.getenv("DSE_EMBED_THRESHOLD", "0.65"))
|
||||
_CACHE_PATH = os.getenv("DSE_EMBED_CACHE", "/data/dse_embed_cache.json")
|
||||
_SIDECAR_DB = os.getenv("MC_CLASS_DB", "/data/mc_classification.db")
|
||||
|
||||
|
||||
def _doc_hash(text: str) -> str:
|
||||
return hashlib.sha256(text.encode("utf-8", "ignore")).hexdigest()[:20]
|
||||
|
||||
|
||||
def _load_cache() -> dict:
|
||||
try:
|
||||
with open(_CACHE_PATH, encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def _save_cache(cache: dict) -> None:
|
||||
try:
|
||||
# LRU-Kappung: max 30 Dokumente im Cache (Scores sind klein)
|
||||
if len(cache) > 30:
|
||||
for k in list(cache.keys())[:-30]:
|
||||
cache.pop(k, None)
|
||||
tmp = _CACHE_PATH + ".tmp"
|
||||
with open(tmp, "w", encoding="utf-8") as f:
|
||||
json.dump(cache, f)
|
||||
os.replace(tmp, _CACHE_PATH)
|
||||
except Exception as e:
|
||||
logger.warning("dse embed-cache save failed: %s", e)
|
||||
|
||||
|
||||
def _load_control_vecs(cids: Iterable[str]) -> dict[str, list[float]]:
|
||||
from compliance.services.mc_embedding_matcher import _blob_to_vec
|
||||
cid_list = [c for c in cids if c]
|
||||
if not cid_list:
|
||||
return {}
|
||||
try:
|
||||
with sqlite3.connect(_SIDECAR_DB) as c:
|
||||
ph = ",".join("?" * len(cid_list))
|
||||
rows = c.execute(
|
||||
f"SELECT control_id, embedding FROM mc_classification "
|
||||
f"WHERE control_id IN ({ph}) AND doc_type='dse' "
|
||||
f"AND check_type='text' AND embedding IS NOT NULL",
|
||||
cid_list,
|
||||
).fetchall()
|
||||
return {cid: _blob_to_vec(b) for cid, b in rows}
|
||||
except Exception as e:
|
||||
logger.warning("dse control-vec load failed: %s", e)
|
||||
return {}
|
||||
|
||||
|
||||
async def _embedding_reachable(timeout: float = 2.0) -> bool:
|
||||
"""Schneller TCP-Connect zum Embedding-Service. Verhindert, dass ein toter
|
||||
Service den Check blockiert (macmini-Lehrer-Last hat das Embedding früher
|
||||
verstopft)."""
|
||||
url = os.getenv("EMBEDDING_URL", "http://embedding-service:8087")
|
||||
hostport = url.split("://", 1)[-1].split("/", 1)[0]
|
||||
host, _, port = hostport.partition(":")
|
||||
port = int(port or "8087")
|
||||
try:
|
||||
fut = asyncio.open_connection(host, port)
|
||||
reader, writer = await asyncio.wait_for(fut, timeout=timeout)
|
||||
writer.close()
|
||||
try:
|
||||
await writer.wait_closed()
|
||||
except Exception:
|
||||
pass
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warning("dse embedding-service nicht erreichbar (%s) — "
|
||||
"deterministischer Layer trägt", e)
|
||||
return False
|
||||
|
||||
|
||||
async def _compute_scores(text: str, all_cids: list[str]) -> dict[str, float]:
|
||||
"""Bettet das Dokument EINMAL ein und liefert max-Cosinus je Control."""
|
||||
from compliance.services.mc_embedding_matcher import (
|
||||
_chunk_text, _cosine, _embed_texts, DIM,
|
||||
)
|
||||
mc_vecs = _load_control_vecs(all_cids)
|
||||
if not mc_vecs:
|
||||
return {}
|
||||
chunks = _chunk_text(text)
|
||||
if not chunks:
|
||||
return {}
|
||||
chunk_vecs = await _embed_texts(chunks)
|
||||
chunk_vecs = [v for v in chunk_vecs if v and len(v) == DIM]
|
||||
if not chunk_vecs:
|
||||
return {}
|
||||
return {
|
||||
cid: round(float(max((_cosine(mv, cv) for cv in chunk_vecs),
|
||||
default=0.0)), 4)
|
||||
for cid, mv in mc_vecs.items()
|
||||
}
|
||||
|
||||
|
||||
async def embedding_recall(
|
||||
text: str,
|
||||
candidate_cids: Iterable[str],
|
||||
threshold: float | None = None,
|
||||
embed_timeout: float = 90.0,
|
||||
) -> set[str]:
|
||||
"""Returns die candidate control_ids, die semantisch (>= Schwelle) im Doc
|
||||
vorkommen. Deterministisch + gecacht. Leeres Set, wenn Service down/Fehler.
|
||||
|
||||
candidate_cids: die im Keyword-Layer DURCHGEFALLENEN Controls (Recall-Rescue).
|
||||
"""
|
||||
cands = [c for c in candidate_cids if c]
|
||||
if not text or len(text) < 100 or not cands:
|
||||
return set()
|
||||
thr = DSE_EMBED_THRESHOLD if threshold is None else threshold
|
||||
|
||||
h = _doc_hash(text)
|
||||
cache = _load_cache()
|
||||
scores = cache.get(h)
|
||||
|
||||
if scores is None:
|
||||
if not await _embedding_reachable():
|
||||
return set()
|
||||
try:
|
||||
scores = await asyncio.wait_for(
|
||||
_compute_scores(text, cands), timeout=embed_timeout)
|
||||
except (Exception, asyncio.TimeoutError) as e:
|
||||
logger.warning("dse embedding_recall skipped: %s", e)
|
||||
return set()
|
||||
if not scores:
|
||||
return set()
|
||||
cache[h] = scores
|
||||
_save_cache(cache)
|
||||
logger.info("dse embedding_recall: doc %s eingebettet (%d Scores)",
|
||||
h, len(scores))
|
||||
else:
|
||||
logger.info("dse embedding_recall: Cache-Treffer doc %s", h)
|
||||
|
||||
cand_set = set(cands)
|
||||
return {cid for cid, s in scores.items()
|
||||
if cid in cand_set and s >= thr}
|
||||
@@ -1,29 +1,286 @@
|
||||
"""DSEAgent — Datenschutzerklärung / Datenschutzinformation (Art. 13/14 DSGVO).
|
||||
"""DSE-Agent v3 — Datenschutzerklärung / Datenschutzinformation (Art. 13/14
|
||||
DSGVO), baut auf doc_check_controls (267 text-MCs aus DB).
|
||||
|
||||
Thin-Subclass von ChecklistAgent über die kuratierte ART13_CHECKLIST (KEIN
|
||||
90k-Library-Firehose). Einzige Spezialität: Drittland wird bei dokumentiertem
|
||||
Drittlandtransfer (Scan-Kontext) zu HIGH angehoben.
|
||||
Volle Parität zu impressum/ + cookie_policy/ (User-Vorgabe 2026-06-17):
|
||||
Layer 0 — Regex-Boost (kuratierte Art-13-Patterns aus mcs.py / ART13_CHECKLIST)
|
||||
Layer 1 — Keyword-Match aus pass_criteria der DSE-DB-MCs (deterministisch)
|
||||
Layer 2 — BGE-M3 Embedding-Match
|
||||
Layer 3 — Semantic-Validator (LLM) für offene HIGH/MEDIUM-Fails + Auto-Learning
|
||||
|
||||
Die kuratierten Patterns gehen NICHT verloren — sie boosten (Layer 0) die DB-
|
||||
Controls (z.B. präzises "keine Drittlandübermittlung" → drittland-MC PASS, kein
|
||||
False-Positive). DSE-Spezialität bleibt: Drittland → HIGH bei dokumentiertem
|
||||
Transfer (scan_context).
|
||||
|
||||
Output-Layer (Linter / Rollup / Methodik-UI) bleibt 1:1.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from compliance.services.doc_checks.dse_checks import ART13_CHECKLIST
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from .._base import AgentInput
|
||||
from .._checklist_agent import ChecklistAgent
|
||||
from .._base import (
|
||||
AgentInput,
|
||||
AgentOutput,
|
||||
BaseSpecialistAgent,
|
||||
EscalationLog,
|
||||
EvidenceSource,
|
||||
Finding,
|
||||
McCoverage,
|
||||
Severity,
|
||||
SourceType,
|
||||
lint_output,
|
||||
)
|
||||
from .._pattern_library import record as record_pattern
|
||||
from .._rollup import rollup
|
||||
from .._semantic_validator import build_rename_action, validate_present
|
||||
from .mcs import MC_IDS, MCS
|
||||
from .regex_boost import BOOST_KEYWORDS
|
||||
from .v3_engine import run_v3_pipeline
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DSEAgent(ChecklistAgent):
|
||||
CHECKLIST = ART13_CHECKLIST
|
||||
_SEV_TO_ENUM = {
|
||||
"CRITICAL": Severity.HIGH,
|
||||
"HIGH": Severity.HIGH,
|
||||
"MEDIUM": Severity.MEDIUM,
|
||||
"LOW": Severity.LOW,
|
||||
"INFO": Severity.INFO,
|
||||
}
|
||||
|
||||
# Drittland-Vokabeln für die scan_context-Heraufstufung (Art. 13(1)(f)).
|
||||
_THIRD_COUNTRY_KW = tuple(set(
|
||||
BOOST_KEYWORDS.get("third_country", ())
|
||||
+ BOOST_KEYWORDS.get("third_country_mechanism", ())
|
||||
))
|
||||
|
||||
|
||||
def _build_measure(label: str, norm: str) -> str:
|
||||
"""Maßnahme (Imperativ) statt Pruef-Frage als action."""
|
||||
base = (label or "").strip().rstrip(".")
|
||||
if not base:
|
||||
return ("Datenschutz-Pflichtangabe ergänzen und gegen Art. 13/14 "
|
||||
"DSGVO prüfen.")
|
||||
msg = f"Pflichtangabe ergänzen: {base}."
|
||||
if norm:
|
||||
msg += f" Rechtsgrundlage: {norm}."
|
||||
return msg
|
||||
|
||||
|
||||
def _is_third_country_topic(result: dict) -> bool:
|
||||
"""Ist dieses DB-MC thematisch ein Drittland-Control?"""
|
||||
parts: list[str] = [str(result.get("label") or "").lower()]
|
||||
for c in (result.get("_pass_criteria") or []):
|
||||
if c:
|
||||
parts.append(str(c).lower())
|
||||
blob = " ".join(parts)
|
||||
hits = sum(1 for kw in _THIRD_COUNTRY_KW if kw in blob)
|
||||
return hits >= 1
|
||||
|
||||
|
||||
class DSEAgent(BaseSpecialistAgent):
|
||||
agent_id = "dse"
|
||||
agent_version = "1.0"
|
||||
agent_version = "3.0"
|
||||
doc_type = "dse"
|
||||
owned_mc_ids = tuple(c["id"] for c in ART13_CHECKLIST)
|
||||
owned_mc_ids = MC_IDS
|
||||
|
||||
def _severity_override(self, c: dict, agent_input: AgentInput):
|
||||
def _third_country_transfer(self, agent_input: AgentInput) -> bool:
|
||||
sc = (agent_input.context or {}).get("scan_context") or {}
|
||||
tc = str(sc.get("third_country_transfer", "")).lower() in (
|
||||
return str(sc.get("third_country_transfer", "")).lower() in (
|
||||
"yes", "true", "1", "ja")
|
||||
if tc and c["id"] in ("third_country", "third_country_mechanism"):
|
||||
return "HIGH"
|
||||
return None
|
||||
|
||||
async def evaluate(self, agent_input: AgentInput) -> AgentOutput:
|
||||
start = datetime.now(timezone.utc)
|
||||
text = (agent_input.text or "").strip()
|
||||
scope = set(agent_input.business_scope or [])
|
||||
coverage: list[McCoverage] = []
|
||||
findings: list[Finding] = []
|
||||
esc_logs: list[EscalationLog] = []
|
||||
notes_parts: list[str] = []
|
||||
|
||||
if len(text) < 100:
|
||||
for mc in MCS:
|
||||
coverage.append(McCoverage(
|
||||
mc_id=mc.mc_id, status="skipped",
|
||||
label=mc.label, reason="text too short",
|
||||
))
|
||||
return self._finalize(
|
||||
start, findings, esc_logs, coverage,
|
||||
confidence=0.0,
|
||||
notes="DSE-Text zu kurz oder leer.",
|
||||
)
|
||||
|
||||
tc_transfer = self._third_country_transfer(agent_input)
|
||||
# Embedding-Recall (Layer 2) läuft IMMER — deterministisch, gecacht
|
||||
# (pro Doc-Hash → Folge-Views instant) und Reachability-gegated
|
||||
# (kein Hang, wenn der Service fehlt). Ersetzt den über-passenden Boost.
|
||||
results, telemetry = await run_v3_pipeline(text, scope)
|
||||
notes_parts.append(
|
||||
f"v3-pipeline: {telemetry.get('total_mcs', 0)} DB-MCs · "
|
||||
f"{telemetry.get('layer_1_pass', 0)} Keyword-Treffer · "
|
||||
f"{telemetry.get('embedding_passes', 0)} semantisch (Embedding)"
|
||||
)
|
||||
if telemetry.get("sector_dropped") or telemetry.get("offtopic_dropped"):
|
||||
notes_parts.append(
|
||||
f"Scope-Filter: {telemetry.get('sector_dropped', 0)} "
|
||||
f"Branchen-MCs, {telemetry.get('offtopic_dropped', 0)} "
|
||||
"themenfremde MCs entfernt"
|
||||
)
|
||||
|
||||
seen: set[str] = set()
|
||||
for r in results:
|
||||
mc_id = r.get("control_id") or ""
|
||||
if not mc_id or mc_id in seen:
|
||||
continue
|
||||
seen.add(mc_id)
|
||||
passed = bool(r.get("passed"))
|
||||
sev = _SEV_TO_ENUM.get(
|
||||
(r.get("severity") or "MEDIUM").upper(), Severity.MEDIUM,
|
||||
)
|
||||
# DSE-Spezialität: Drittland → HIGH bei dokumentiertem Transfer.
|
||||
sev_reason = "db_mc_failed"
|
||||
if tc_transfer and _is_third_country_topic(r):
|
||||
sev = Severity.HIGH
|
||||
sev_reason = "db_mc_failed_third_country_transfer"
|
||||
coverage.append(McCoverage(
|
||||
mc_id=mc_id,
|
||||
status="ok" if passed else sev.value.lower(),
|
||||
reason=str(r.get("matched_text") or r.get("hint") or "")[:120],
|
||||
))
|
||||
if passed:
|
||||
continue
|
||||
label = r.get("label") or r.get("hint") or ""
|
||||
norm_str = str(r.get("regulation") or "").strip()
|
||||
if r.get("article"):
|
||||
norm_str = (norm_str + f" Art. {r.get('article')}").strip()
|
||||
if not norm_str:
|
||||
norm_str = "DSGVO Art. 13/14"
|
||||
findings.append(Finding(
|
||||
check_id=f"DSE-DBMC-{mc_id}",
|
||||
agent=self.agent_id,
|
||||
agent_version=self.agent_version,
|
||||
field_id=mc_id,
|
||||
severity=sev,
|
||||
severity_reason=sev_reason,
|
||||
title=str(label)[:200] or f"DB-MC {mc_id} nicht erfüllt",
|
||||
norm=norm_str,
|
||||
evidence="",
|
||||
action=_build_measure(str(label), norm_str)[:400],
|
||||
confidence=0.9,
|
||||
sources=[EvidenceSource(
|
||||
source_type=SourceType.MC,
|
||||
source_id=mc_id,
|
||||
detail=str(r.get("source") or "keyword_match")[:120],
|
||||
confidence=0.9,
|
||||
)],
|
||||
))
|
||||
|
||||
# Boost-Coverage: meine Pattern-Treffer (regex-boost field_ids).
|
||||
boost_ids = set(telemetry.get("layer_0_field_ids") or [])
|
||||
for mc in MCS:
|
||||
coverage.append(McCoverage(
|
||||
mc_id=mc.mc_id,
|
||||
status="ok" if mc.field_id in boost_ids else "na",
|
||||
label=mc.label,
|
||||
reason=("regex-boost hit"
|
||||
if mc.field_id in boost_ids
|
||||
else "kein Pattern-Treffer (kein Veto)"),
|
||||
))
|
||||
|
||||
if not (agent_input.context or {}).get("skip_llm"):
|
||||
await self._semantic_demote(text, findings, coverage)
|
||||
|
||||
confs = [f.confidence for f in findings if f.confidence] or [0.95]
|
||||
overall = sum(confs) / len(confs)
|
||||
|
||||
return self._finalize(
|
||||
start, findings, esc_logs, coverage,
|
||||
confidence=overall, notes=" · ".join(notes_parts),
|
||||
)
|
||||
|
||||
async def _semantic_demote(
|
||||
self, text: str, findings: list[Finding],
|
||||
coverage: list[McCoverage],
|
||||
) -> None:
|
||||
"""LLM-Layer für HIGH/MEDIUM-DB-MCs: Label-Mismatch-Check.
|
||||
Bei Fund → HIGH/MEDIUM → LOW + Rename-Action."""
|
||||
candidates = [
|
||||
f for f in findings
|
||||
if f.severity in (Severity.HIGH.value, Severity.MEDIUM.value)
|
||||
and f.severity_reason in (
|
||||
"db_mc_failed", "db_mc_failed_third_country_transfer")
|
||||
]
|
||||
if not candidates:
|
||||
return
|
||||
result = await validate_present(
|
||||
text, [(f.field_id, f.title[:80]) for f in candidates],
|
||||
)
|
||||
if not result:
|
||||
return
|
||||
for finding in candidates:
|
||||
row = result.get(finding.field_id)
|
||||
if not row or not row.get("found"):
|
||||
continue
|
||||
if row.get("confidence", 0) < 0.6:
|
||||
continue
|
||||
label_used = row.get("label_used") or "abweichendes Label"
|
||||
conf = float(row.get("confidence") or 0.8)
|
||||
finding.severity = Severity.LOW.value
|
||||
finding.severity_reason = "label_mismatch"
|
||||
finding.title = (
|
||||
f"Label '{label_used}' weicht von Standard ab"
|
||||
)
|
||||
finding.evidence = str(row.get("evidence") or "")[:200]
|
||||
finding.action = build_rename_action(
|
||||
finding.field_id, label_used,
|
||||
)
|
||||
finding.confidence = conf
|
||||
finding.sources.append(EvidenceSource(
|
||||
source_type=SourceType.LLM_LOCAL,
|
||||
source_id="semantic_validator",
|
||||
detail=f"LLM-confirmed: '{label_used}'",
|
||||
confidence=conf,
|
||||
))
|
||||
for c in coverage:
|
||||
if c.mc_id == finding.field_id:
|
||||
c.status = "low"
|
||||
c.reason = f"label_mismatch: '{label_used}'"
|
||||
try:
|
||||
record_pattern(
|
||||
field_id=finding.field_id,
|
||||
label_used=label_used,
|
||||
confidence=conf,
|
||||
agent_id=self.agent_id,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning("pattern-library record failed: %s", e)
|
||||
|
||||
def _finalize(
|
||||
self, start: datetime, findings: list[Finding],
|
||||
esc_logs: list[EscalationLog], coverage: list[McCoverage],
|
||||
confidence: float, notes: str = "",
|
||||
) -> AgentOutput:
|
||||
end = datetime.now(timezone.utc)
|
||||
recs = rollup(findings)
|
||||
out = AgentOutput(
|
||||
agent=self.agent_id,
|
||||
agent_version=self.agent_version,
|
||||
started_at=start,
|
||||
finished_at=end,
|
||||
duration_ms=int((end - start).total_seconds() * 1000),
|
||||
findings=findings,
|
||||
recommendations=recs,
|
||||
mc_coverage=coverage,
|
||||
escalation_log=esc_logs,
|
||||
confidence=confidence,
|
||||
notes=notes,
|
||||
mc_total=len(coverage),
|
||||
mc_ok=sum(1 for c in coverage if c.status == "ok"),
|
||||
mc_na=sum(1 for c in coverage if c.status == "na"),
|
||||
mc_high=sum(1 for c in coverage if c.status == "high"),
|
||||
mc_medium=sum(1 for c in coverage if c.status == "medium"),
|
||||
mc_low=sum(1 for c in coverage if c.status == "low"),
|
||||
)
|
||||
return lint_output(out)
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
"""DSE-Tiefenprüfung: LLM-Kaskade auf die UNSCHARFEN Findings.
|
||||
|
||||
User-Architektur (2026-06-18): die deterministische Engine (Keyword + Embedding)
|
||||
triagiert. Eindeutige Fälle (sehr hoher/niedriger Embedding-Score) bleiben
|
||||
deterministisch. Die UNSCHARFE Mitte + grenzwertig-Bestandene gehen durch die
|
||||
Kaskade — denn dort entstehen sowohl 'verpasste Lücken' (schlimmster Fehler) als
|
||||
auch Falsch-Findings (Rework).
|
||||
|
||||
Eskalation auf ANTWORT-UNSICHERHEIT (nicht JSON-Gültigkeit): jedes Tier liefert
|
||||
{erfuellt, confidence, begruendung}. Confidence < Schwelle → nächstes Tier.
|
||||
Tier 1: Qwen 35B (lokal, schnell, billig)
|
||||
Tier 2: OVH gpt-oss-120B
|
||||
Tier 3: Claude — NUR mit Freigabe (allow_claude), sonst 'needs_freigabe'.
|
||||
|
||||
Judging-Leitplanken (User-Vorgaben):
|
||||
- Speicherdauer nur erfüllt bei konkreter Höchstdauer ODER echtem,
|
||||
nachvollziehbarem Kriterium — NICHT zirkulär ('bis Zweck wegfällt').
|
||||
- Ohne ausreichenden Kontext → eher nicht erfüllt (nichts fehlen lassen).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
from compliance.services.llm_cascade import (
|
||||
_call_anthropic, _call_ollama, _call_ovh,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Unscharfe Embedding-Zone (kalibriert an 5-Firmen-GT 2026-06-18): außerhalb ist
|
||||
# die Engine sicher genug, innen entscheidet der LLM.
|
||||
FUZZY_LO = float(os.getenv("DSE_FUZZY_LO", "0.50"))
|
||||
FUZZY_HI = float(os.getenv("DSE_FUZZY_HI", "0.72"))
|
||||
# Selbstkonfidenz-Schwelle: darunter → eskalieren.
|
||||
ESC_CONF = float(os.getenv("DSE_ESC_CONF", "0.75"))
|
||||
|
||||
_JUDGE_SYS = (
|
||||
"Du bist ein erfahrener DSGVO-Datenschutz-Auditor. Du prüfst, ob eine "
|
||||
"konkrete Pflicht in einer Datenschutzerklärung (DSE) ERFÜLLT ist. "
|
||||
"Sei streng wie ein Fachanwalt: lieber 'nicht erfüllt' wenn unklar — eine "
|
||||
"übersehene Lücke ist schlimmer als ein Hinweis zu viel. "
|
||||
"Speicherdauer ist NUR erfüllt bei konkreter Höchstdauer ODER einem echten, "
|
||||
"nachvollziehbaren Kriterium; zirkuläre Formeln ('bis der Zweck wegfällt') "
|
||||
"erfüllen die Pflicht NICHT. "
|
||||
'Antworte AUSSCHLIESSLICH als JSON: '
|
||||
'{"erfuellt": true|false, "confidence": 0.0-1.0, "begruendung": "kurz"}'
|
||||
)
|
||||
|
||||
|
||||
def _build_user(doc_text: str, title: str, criteria: list) -> str:
|
||||
crit = "; ".join(str(c) for c in (criteria or []) if c)[:600]
|
||||
return (
|
||||
f"PFLICHT: {title}\n"
|
||||
f"Erfüllt, wenn: {crit}\n\n"
|
||||
f"DATENSCHUTZERKLÄRUNG (Auszug):\n{doc_text[:14000]}\n\n"
|
||||
"Ist die Pflicht im Text inhaltlich erfüllt?"
|
||||
)
|
||||
|
||||
|
||||
def _parse(text: str) -> dict | None:
|
||||
if not text:
|
||||
return None
|
||||
s, e = text.find("{"), text.rfind("}")
|
||||
if s < 0 or e <= s:
|
||||
return None
|
||||
try:
|
||||
o = json.loads(text[s:e + 1])
|
||||
return {
|
||||
"erfuellt": bool(o.get("erfuellt")),
|
||||
"confidence": float(o.get("confidence") or 0.0),
|
||||
"begruendung": str(o.get("begruendung") or "")[:300],
|
||||
}
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
async def judge_control(
|
||||
doc_text: str, title: str, criteria: list, allow_claude: bool = False,
|
||||
) -> dict:
|
||||
"""Tiered judgment mit Selbstkonfidenz-Eskalation. Returns
|
||||
{erfuellt, confidence, source, begruendung, needs_freigabe}."""
|
||||
user = _build_user(doc_text, title, criteria)
|
||||
tiers = [("qwen", _call_ollama), ("ovh_120b", _call_ovh)]
|
||||
best: dict | None = None
|
||||
for name, call in tiers:
|
||||
try:
|
||||
if name == "qwen":
|
||||
txt = await call(_JUDGE_SYS, user, max_tokens=400,
|
||||
timeout=60, think=False)
|
||||
else:
|
||||
txt = await call(_JUDGE_SYS, user, max_tokens=400)
|
||||
except Exception as e:
|
||||
logger.warning("deep_check tier %s failed: %s", name, e)
|
||||
txt = ""
|
||||
p = _parse(txt)
|
||||
if p:
|
||||
p["source"] = name
|
||||
best = p
|
||||
if p["confidence"] >= ESC_CONF:
|
||||
return {**p, "needs_freigabe": False}
|
||||
# Tier 3: Claude — nur mit Freigabe
|
||||
if not allow_claude:
|
||||
if best:
|
||||
return {**best, "needs_freigabe": True}
|
||||
return {"erfuellt": False, "confidence": 0.0, "source": "none",
|
||||
"begruendung": "Unsicher — Anwalt/Claude-Freigabe nötig",
|
||||
"needs_freigabe": True}
|
||||
try:
|
||||
txt = await _call_anthropic(_JUDGE_SYS, user, max_tokens=400)
|
||||
p = _parse(txt)
|
||||
if p:
|
||||
return {**p, "source": "anthropic_claude", "needs_freigabe": False}
|
||||
except Exception as e:
|
||||
logger.warning("deep_check claude failed: %s", e)
|
||||
if best:
|
||||
return {**best, "needs_freigabe": False}
|
||||
return {"erfuellt": False, "confidence": 0.0, "source": "none",
|
||||
"begruendung": "Kein LLM-Ergebnis", "needs_freigabe": False}
|
||||
|
||||
|
||||
def is_fuzzy(score: float, kw_pass: bool) -> bool:
|
||||
"""Unscharf = im Embedding-Graubereich UND nicht durch Keyword klar bestätigt.
|
||||
Klar-bestanden (kw) bleibt deterministisch; klar-hoch/niedrig auch."""
|
||||
if kw_pass:
|
||||
return False
|
||||
return FUZZY_LO <= score <= FUZZY_HI
|
||||
@@ -0,0 +1,78 @@
|
||||
"""Machine-Check-Definitionen für den DSE-Agent (Layer-0 Regex-Boost).
|
||||
|
||||
Eine MC = ein abgegrenztes Art-13/14-DSGVO-Pflichtfeld mit deterministischen
|
||||
Patterns. Quelle der Patterns ist die EINE kuratierte ART13_CHECKLIST
|
||||
(doc_checks/dse_checks.py) — hier nur in das MC-Format gehoben, damit der
|
||||
Regex-Boost (regex_boost.py) und die v3-Engine (v3_engine.py) dieselbe Struktur
|
||||
nutzen wie impressum/ + cookie_policy/. KEINE Pattern-Duplikation: die Patterns
|
||||
bleiben in dse_checks.py, dieses Modul kompiliert sie nur.
|
||||
|
||||
Owner = dse-agent.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Pattern
|
||||
|
||||
from compliance.services.doc_checks.dse_checks import ART13_CHECKLIST
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MC:
|
||||
"""Eine Machine-Check-Definition (Boost-Pattern für ein DSE-Feld)."""
|
||||
mc_id: str # DSE-MC-001 ...
|
||||
field_id: str # controller, legal_basis, third_country ...
|
||||
label: str
|
||||
norm: str
|
||||
patterns: tuple[Pattern[str], ...] = field(default_factory=tuple)
|
||||
severity_if_missing: str = "MEDIUM"
|
||||
level: int = 1
|
||||
|
||||
|
||||
_NORM_RE = re.compile(r"\((Art\.[^)]+|§\s*\d+[^)]*)\)")
|
||||
|
||||
|
||||
def _norm_of(label: str) -> str:
|
||||
m = _NORM_RE.search(label or "")
|
||||
return m.group(1).strip() if m else "Art. 13/14 DSGVO"
|
||||
|
||||
|
||||
def _compile(patterns: list[str]) -> tuple[Pattern[str], ...]:
|
||||
out: list[Pattern[str]] = []
|
||||
for p in patterns or ():
|
||||
try:
|
||||
out.append(re.compile(p, re.IGNORECASE | re.MULTILINE))
|
||||
except re.error:
|
||||
continue
|
||||
return tuple(out)
|
||||
|
||||
|
||||
def _build_mcs() -> tuple[MC, ...]:
|
||||
"""Hebt die ART13_CHECKLIST in das MC-Format (Boost-Pattern pro Feld)."""
|
||||
mcs: list[MC] = []
|
||||
for i, c in enumerate(ART13_CHECKLIST, start=1):
|
||||
mcs.append(MC(
|
||||
mc_id=f"DSE-MC-{i:03d}",
|
||||
field_id=c["id"],
|
||||
label=c["label"],
|
||||
norm=_norm_of(c["label"]),
|
||||
patterns=_compile(c.get("patterns", [])),
|
||||
severity_if_missing=c.get("severity", "MEDIUM"),
|
||||
level=c.get("level", 1),
|
||||
))
|
||||
return tuple(mcs)
|
||||
|
||||
|
||||
MCS: tuple[MC, ...] = _build_mcs()
|
||||
|
||||
# Public list of all MC-IDs for the Registry / owned_mc_ids.
|
||||
MC_IDS: tuple[str, ...] = tuple(m.mc_id for m in MCS)
|
||||
|
||||
|
||||
def scope_matches(mc: MC, scope: set[str]) -> bool:
|
||||
"""Art-13/14-Pflichten gelten universell für jede DSE — keine Branchen-
|
||||
Gating auf Boost-Ebene (anders als Impressum mit Kammerberufen). Das
|
||||
Sektor-Gate über den control_id-Prefix passiert in der v3-Engine."""
|
||||
return True
|
||||
@@ -0,0 +1,179 @@
|
||||
"""Layer-0 Regex-Boost für den DSE-Agent — die kuratierten Art-13/14-Patterns
|
||||
als deterministische Vor-Stufe vor dem Keyword-Match aus doc_check_controls.
|
||||
|
||||
Analog zu impressum/regex_boost.py + cookie_policy/regex_boost.py:
|
||||
- run_v3_pipeline lädt die 267 text-MCs (doc_type='dse') und macht
|
||||
Keyword-Match aus deren pass_criteria.
|
||||
- MEIN Beitrag (Layer 0): die präzisen Art-13-Patterns (mcs.py / aus
|
||||
ART13_CHECKLIST) laufen ZUERST. Trifft ein Pattern → das thematisch
|
||||
passende DB-MC wird zu PASS geboostet (auch wenn der Keyword-Match unklar
|
||||
war). Mapping: field_id → typische Wörter in der pass_criteria der DB-MC.
|
||||
|
||||
Damit gehen die kuratierten DSE-Patterns nicht verloren, sondern boosten das
|
||||
DB-Control-System (statt es zu ersetzen).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from .mcs import MCS, scope_matches
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# field_id (aus ART13_CHECKLIST) → Wörter, wie sie in der pass_criteria der
|
||||
# zugehörigen DSE-DB-MCs vorkommen. Treffen ≥2 dieser Wörter in den criteria
|
||||
# eines DB-MC, gehört es zu diesem Feld → Boost. Die Vokabeln sind an den
|
||||
# real beobachteten DB-Kriterien ausgerichtet (DATA/SEC/AUTH-Controls).
|
||||
BOOST_KEYWORDS: dict[str, tuple[str, ...]] = {
|
||||
"controller": (
|
||||
"verantwortlich", "verantwortliche stelle", "verantwortlichen",
|
||||
"kontaktdaten des verantwortlichen", "name und kontaktdaten",
|
||||
"firmenname", "rechtsform", "anschrift", "ladungsfähige",
|
||||
"identität des verantwortlichen", "controller",
|
||||
),
|
||||
"dpo": (
|
||||
"datenschutzbeauftragt", "datenschutzbeauftragter",
|
||||
"datenschutzbeauftragte", "data protection officer",
|
||||
"kontaktdaten des datenschutz", "benennung", "art. 37",
|
||||
),
|
||||
"purposes": (
|
||||
"zweck", "zwecke", "verarbeitungszweck", "zweck der verarbeitung",
|
||||
"zwecke der verarbeitung", "verarbeitungstätigkeit", "purpose",
|
||||
"zweckbindung", "erhebung",
|
||||
),
|
||||
"legal_basis": (
|
||||
"rechtsgrundlage", "berechtigte interesse", "berechtigtes interesse",
|
||||
"einwilligung", "vertragserfüllung", "vertragserfuellung",
|
||||
"interessenabwägung", "interessenabwaegung", "art. 6",
|
||||
"rechtmäßigkeit", "rechtmaessigkeit", "erforderlich",
|
||||
),
|
||||
"recipients": (
|
||||
"empfänger", "empfaenger", "empfängerkategorien",
|
||||
"empfaengerkategorien", "weitergabe", "übermittlung an",
|
||||
"auftragsverarbeit", "auftragsverarbeiter", "dienstleister",
|
||||
"dritte", "drittempfänger", "kategorien von empfängern",
|
||||
),
|
||||
"third_country": (
|
||||
"drittland", "drittstaat", "drittländer", "drittlaender",
|
||||
"standardvertragsklausel", "angemessenheitsbeschluss",
|
||||
"übermittlung in ein drittland", "geeignete garantien",
|
||||
"schutzgarantien", "data privacy framework", "ewr",
|
||||
"internationale übermittlung", "transfermechanismus",
|
||||
),
|
||||
"third_country_mechanism": (
|
||||
"standardvertragsklausel", "angemessenheitsbeschluss",
|
||||
"geeignete garantien", "data privacy framework",
|
||||
"schutzgarantien", "art. 46", "art. 45",
|
||||
),
|
||||
"retention": (
|
||||
"speicherdauer", "aufbewahrungsfrist", "aufbewahrungsdauer",
|
||||
"löschfrist", "loeschfrist", "speicherbegrenzung", "löschung",
|
||||
"loeschung", "dauer der speicherung", "kriterien für die festlegung",
|
||||
"speicherfrist", "aufbewahrung",
|
||||
),
|
||||
"retention_periods": (
|
||||
"aufbewahrungsfrist", "löschfrist", "speicherdauer",
|
||||
"gesetzliche frist", "handelsrechtlich", "steuerrechtlich",
|
||||
),
|
||||
"rights": (
|
||||
"betroffenenrecht", "betroffenenrechte", "recht auf auskunft",
|
||||
"auskunft", "berichtigung", "löschung", "loeschung",
|
||||
"einschränkung", "einschraenkung", "datenübertragbarkeit",
|
||||
"datenuebertragbarkeit", "widerspruch", "widerruf",
|
||||
"rechte der betroffenen", "art. 15", "art. 17", "art. 21",
|
||||
),
|
||||
"complaint": (
|
||||
"beschwerderecht", "aufsichtsbehörde", "aufsichtsbehoerde",
|
||||
"datenschutzbehörde", "datenschutzbehoerde", "beschwerde",
|
||||
"recht auf beschwerde", "art. 77", "zuständige aufsichtsbehörde",
|
||||
),
|
||||
"rights_art22_profiling": (
|
||||
"automatisierte entscheidung", "profiling", "art. 22",
|
||||
"automatisierte einzelentscheidung", "scoring",
|
||||
),
|
||||
"dse_version_date": (
|
||||
"stand", "letzte aktualisierung", "zuletzt geändert",
|
||||
"gültig ab", "gueltig ab", "version", "versionsdatum",
|
||||
"aktualität", "nachweisbarkeit",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def compute_regex_boosts(text: str, business_scope: set[str] | None = None) -> set[str]:
|
||||
"""Welche DSE-field_ids haben die kuratierten Patterns erkannt?
|
||||
|
||||
Returns die Menge gehit'ter field_ids, über die später entschieden wird,
|
||||
ob ein DB-MC darüber automatisch passed werden kann. business_scope wird
|
||||
akzeptiert (Signatur-Parität mit impressum), für DSE aber nicht gegated —
|
||||
Art-13-Pflichten sind universell.
|
||||
"""
|
||||
if not text or len(text) < 50:
|
||||
return set()
|
||||
scope = business_scope or set()
|
||||
hits: set[str] = set()
|
||||
for mc in MCS:
|
||||
if not scope_matches(mc, scope):
|
||||
continue
|
||||
if any(p.search(text) for p in mc.patterns):
|
||||
hits.add(mc.field_id)
|
||||
return hits
|
||||
|
||||
|
||||
def boost_matches_db_mc(
|
||||
boosts: set[str],
|
||||
pass_criteria: list,
|
||||
fail_criteria: list | None = None,
|
||||
) -> str | None:
|
||||
"""Hat ein gebooster field_id ≥2 Keyword-Überlapp mit den pass/fail_criteria
|
||||
eines DB-MC? Returns field_id (höchster Match-Count) oder None."""
|
||||
if not boosts:
|
||||
return None
|
||||
crit_parts: list[str] = []
|
||||
for c in (pass_criteria or []):
|
||||
if c:
|
||||
crit_parts.append(str(c).lower())
|
||||
for c in (fail_criteria or []):
|
||||
if c:
|
||||
crit_parts.append(str(c).lower())
|
||||
if not crit_parts:
|
||||
return None
|
||||
crit_text = " ".join(crit_parts)
|
||||
best: tuple[int, str] | None = None
|
||||
for field_id in boosts:
|
||||
kws = BOOST_KEYWORDS.get(field_id) or ()
|
||||
match_count = sum(1 for kw in kws if kw in crit_text)
|
||||
if match_count >= 2:
|
||||
if best is None or match_count > best[0]:
|
||||
best = (match_count, field_id)
|
||||
return best[1] if best else None
|
||||
|
||||
|
||||
def criteria_on_topic(
|
||||
pass_criteria: list | None,
|
||||
fail_criteria: list | None = None,
|
||||
min_hits: int = 2,
|
||||
) -> bool:
|
||||
"""Deterministischer Themen-Gate: gehört ein DB-MC überhaupt ins DSE-
|
||||
Themenfeld (Art 13/14)? ≥min_hits unterschiedliche Schlüsselwörter aus
|
||||
IRGENDEINEM DSE-Feld in den kombinierten criteria. Fängt fremd-getaggte
|
||||
MCs ab. Leere Kriterien → on-topic behalten (konservativ)."""
|
||||
crit_parts: list[str] = []
|
||||
for c in (pass_criteria or []):
|
||||
if c:
|
||||
crit_parts.append(str(c).lower())
|
||||
for c in (fail_criteria or []):
|
||||
if c:
|
||||
crit_parts.append(str(c).lower())
|
||||
if not crit_parts:
|
||||
return True
|
||||
crit_text = " ".join(crit_parts)
|
||||
hits: set[str] = set()
|
||||
for kws in BOOST_KEYWORDS.values():
|
||||
for kw in kws:
|
||||
if kw in crit_text:
|
||||
hits.add(kw)
|
||||
if len(hits) >= min_hits:
|
||||
return True
|
||||
return False
|
||||
@@ -0,0 +1,209 @@
|
||||
"""v3-Engine: läuft die 4-Layer-Pipeline auf einem DSE-Text (Art. 13/14 DSGVO).
|
||||
|
||||
Layer 0 — Regex-Boost (die kuratierten Art-13-Patterns aus mcs.py)
|
||||
Layer 1 — MC-Laden + Keyword-Match. Das LADEN delegiert an die Main-Tool-
|
||||
Engine (rag_document_checker._load_controls, doc_type='dse'):
|
||||
eine Quelle der Wahrheit inkl. P72-Scope, check_type='text'
|
||||
(267 von 571) und fits_doc_type/scope_requires aus dem Sidecar.
|
||||
Layer 2 — BGE-M3 Embedding-Match (mc_embedding_matcher, shared)
|
||||
Layer 0 Override — failed MCs, deren criteria zu einem gebooster field_id
|
||||
passen, werden zu PASS überschrieben.
|
||||
|
||||
Zusätzlich am Agent-Rand: subtraktives Sektor-/Themen-Gate (_filter_controls)
|
||||
— das Sektor-Gate (Branchen-Prefix GOV/FIN/MED…) verwirft branchenfremde MCs,
|
||||
das Themen-Gate fremd-getaggte. Analog impressum/v3_engine.py.
|
||||
|
||||
Output: Liste Result-Dicts kompatibel mit rag_document_checker. Der Agent
|
||||
konvertiert sie zu Finding-Objekten.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from .regex_boost import (
|
||||
compute_regex_boosts,
|
||||
criteria_on_topic,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Branchen-Prefix -> erwarteter Scope-Token. Reuse aus dem Mail-V2-Scope-
|
||||
# Filter, damit Agent-Pfad und Report-Pfad dieselbe Quelle nutzen. Import
|
||||
# defensiv: faellt der Mail-Pfad weg, bleibt der Agent lauffaehig.
|
||||
try:
|
||||
from compliance.services.mail_render_v2._scope_filter import (
|
||||
SECTOR_PREFIXES,
|
||||
)
|
||||
except Exception: # pragma: no cover - defensiver Fallback
|
||||
SECTOR_PREFIXES = {}
|
||||
|
||||
|
||||
async def run_v3_pipeline(
|
||||
text: str,
|
||||
business_scope: set[str],
|
||||
db_url: str = "",
|
||||
skip_embedding: bool = False,
|
||||
) -> tuple[list[dict[str, Any]], dict[str, Any]]:
|
||||
"""Returns (results, telemetry).
|
||||
|
||||
results: pro DB-MC ein dict {control_id, passed, severity, ...}
|
||||
telemetry: counters für Frontend-Anzeige (Layer-Aufschlüsselung)
|
||||
|
||||
skip_embedding: Layer-2 (BGE-M3 Recall) überspringen. Nur für Unit-Tests
|
||||
ohne Embedding-Service. Im Betrieb läuft die Recall-Schicht: sie ist
|
||||
gecacht (pro Doc-Hash) und Reachability-gegated, blockiert also nie.
|
||||
"""
|
||||
if not text or len(text) < 100:
|
||||
return [], {"reason": "text too short"}
|
||||
|
||||
# Layer 0: kuratierte Art-13-Patterns
|
||||
boosts = compute_regex_boosts(text, business_scope)
|
||||
boost_field_ids = sorted(boosts)
|
||||
logger.info("dse v3 Layer-0 boosts: %d hits — %s",
|
||||
len(boost_field_ids), boost_field_ids)
|
||||
|
||||
# Layer 1: MC-Laden DELEGIERT an die Main-Tool-Engine (Scope-Schutz inkl.).
|
||||
try:
|
||||
from compliance.services.rag_document_checker import _load_controls
|
||||
controls = await _load_controls(
|
||||
"dse", db_url, 0, business_scope,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning("dse v3 load via main-tool engine failed: %s", e)
|
||||
controls = []
|
||||
_normalize_criteria(controls)
|
||||
# Agent-Rand-Backstop: Sektor-Gate (Branchen-Prefix) + Themen-Gate.
|
||||
controls, drop_stats = _filter_controls(controls, business_scope)
|
||||
# Applicability-Gate: hochsichere organisatorische Controls (laut
|
||||
# control_classification NICHT DSE, needs_review=false) aus dem
|
||||
# FINDINGS-Scan nehmen -> organisatorische Checkliste statt False-FEHLT.
|
||||
# Fail-safe: needs_review bleiben drin. Defensiv: fehlt die Tabelle, kein
|
||||
# Filter (Prod-sicher). Siehe _classification_gate.
|
||||
from ._classification_gate import apply_gate, load_dse_gate
|
||||
gate = await load_dse_gate(db_url)
|
||||
organizational: list[dict[str, Any]] = []
|
||||
if gate:
|
||||
controls, organizational = apply_gate(controls, gate)
|
||||
results: list[dict[str, Any]] = []
|
||||
if controls:
|
||||
try:
|
||||
from compliance.services.rag_document_checker import (
|
||||
_check_mc_deterministic,
|
||||
)
|
||||
text_lower = text.lower().replace("\xad", "")
|
||||
for mc in controls:
|
||||
r = _check_mc_deterministic(text_lower, mc)
|
||||
if r:
|
||||
r["_pass_criteria"] = mc.get("pass_criteria")
|
||||
r["_fail_criteria"] = mc.get("fail_criteria")
|
||||
results.append(r)
|
||||
except Exception as e:
|
||||
logger.warning("layer-1 keyword check failed: %s", e)
|
||||
results = []
|
||||
|
||||
layer_1_pass = sum(1 for r in results if r.get("passed"))
|
||||
|
||||
# Layer 2: DETERMINISTISCHE semantische Recall-Schicht (BGE-M3, gecacht).
|
||||
# Ersetzt den früheren Regex-Boost, der auf vollständigen DSE-Dokumenten
|
||||
# massiv über-passte (BMW: 71/94 Boost-Overrides → 49% Übereinstimmung).
|
||||
# Embedding ist genauer (BMW-GT: KW|EMB@0.65 = 75%) UND deterministisch
|
||||
# (feste Funktion, reproduzierbar — kein Keyword-Katalog, kein LLM).
|
||||
embedding_passes = 0
|
||||
if not skip_embedding:
|
||||
failed_cids = [r.get("control_id") for r in results
|
||||
if r and not r.get("passed") and r.get("control_id")]
|
||||
if failed_cids:
|
||||
try:
|
||||
from ._embedding_recall import embedding_recall
|
||||
semantic = await embedding_recall(text, failed_cids)
|
||||
except Exception as e:
|
||||
logger.warning("dse embedding recall failed: %s", e)
|
||||
semantic = set()
|
||||
for r in results:
|
||||
if (r.get("control_id") in semantic
|
||||
and not r.get("passed")):
|
||||
r["passed"] = True
|
||||
r["matched_text"] = "[semantisch erkannt — Embedding]"
|
||||
r["source"] = (r.get("source") or "") + "+embedding"
|
||||
embedding_passes += 1
|
||||
|
||||
telemetry = {
|
||||
"layer_0_field_hits": len(boost_field_ids),
|
||||
"layer_0_field_ids": boost_field_ids,
|
||||
"layer_1_pass": layer_1_pass,
|
||||
"embedding_passes": embedding_passes,
|
||||
"total_mcs": len(results),
|
||||
"sector_dropped": drop_stats.get("sector_dropped", 0),
|
||||
"offtopic_dropped": drop_stats.get("offtopic_dropped", 0),
|
||||
"gate_excluded": len(organizational),
|
||||
"organizational_checklist": organizational,
|
||||
}
|
||||
logger.info("dse v3 telemetry: %s", telemetry)
|
||||
return results, telemetry
|
||||
|
||||
|
||||
def _filter_controls(
|
||||
controls: list[dict[str, Any]],
|
||||
business_scope: set[str],
|
||||
) -> tuple[list[dict[str, Any]], dict[str, int]]:
|
||||
"""Subtraktiver Scope-Filter VOR der Bewertung.
|
||||
|
||||
1. Sektor-Gate — MCs deren control_id-Prefix eine Branche bezeichnet
|
||||
(FIN/GOV/MED/INS/EDU/LEG/REL/POL), die NICHT im business_scope liegt
|
||||
UND die nicht on-topic ist, werden verworfen.
|
||||
2. Themen-Gate — MCs ohne DSE-Themenüberlapp werden verworfen.
|
||||
|
||||
Rein subtraktiv: entfernt nur falsch-positive Kandidaten.
|
||||
"""
|
||||
scope_lc = {s.lower() for s in (business_scope or set())}
|
||||
kept: list[dict[str, Any]] = []
|
||||
sector_dropped = 0
|
||||
offtopic_dropped = 0
|
||||
for c in controls:
|
||||
cid = c.get("control_id") or ""
|
||||
prefix = cid.split("-")[0].upper() if "-" in cid else ""
|
||||
on_topic = criteria_on_topic(c.get("pass_criteria"),
|
||||
c.get("fail_criteria"))
|
||||
required = SECTOR_PREFIXES.get(prefix)
|
||||
# Sektor-Gate nur fuer NICHT-on-topic Controls: ein klar DSE-
|
||||
# thematischer Control (z.B. GOV-Prefix aus der Domain-Erkennung)
|
||||
# darf nicht am Branchen-Prefix scheitern.
|
||||
if required and not (scope_lc & required) and not on_topic:
|
||||
sector_dropped += 1
|
||||
continue
|
||||
if not on_topic:
|
||||
offtopic_dropped += 1
|
||||
continue
|
||||
kept.append(c)
|
||||
if sector_dropped or offtopic_dropped:
|
||||
logger.info(
|
||||
"dse v3 scope-filter: -%d Branchen-MCs, -%d themenfremde MCs "
|
||||
"(scope=%s)", sector_dropped, offtopic_dropped,
|
||||
sorted(scope_lc) or "leer",
|
||||
)
|
||||
return kept, {
|
||||
"sector_dropped": sector_dropped,
|
||||
"offtopic_dropped": offtopic_dropped,
|
||||
}
|
||||
|
||||
|
||||
def _normalize_criteria(controls: list[dict[str, Any]]) -> None:
|
||||
"""asyncpg liefert JSONB-Spalten (pass_criteria/fail_criteria) als
|
||||
Roh-String. In echte Listen parsen, damit Sektor-/Themen-Gate und der
|
||||
Boost-Layer Element-weise iterieren."""
|
||||
import json
|
||||
for c in controls:
|
||||
for key in ("pass_criteria", "fail_criteria"):
|
||||
v = c.get(key)
|
||||
if isinstance(v, list):
|
||||
continue
|
||||
if isinstance(v, str):
|
||||
try:
|
||||
parsed = json.loads(v)
|
||||
c[key] = parsed if isinstance(parsed, list) else [v]
|
||||
except Exception:
|
||||
c[key] = [v] if v else []
|
||||
else:
|
||||
c[key] = []
|
||||
@@ -1,12 +1,27 @@
|
||||
"""AGBAgent — kuratierte §§-305-ff-BGB-Checkliste (ChecklistAgent-Subclass)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
"""AGBAgent (v2, routed). Embedding/LLM offline-gestubbt → kein Netzwerk."""
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
import compliance.services.specialist_agents.agb._pipeline as pipeline
|
||||
from compliance.services.checkers.base import CheckResult
|
||||
from compliance.services.specialist_agents import REGISTRY, AgentInput
|
||||
|
||||
|
||||
class _Stub:
|
||||
def __init__(self, present):
|
||||
self._p = present
|
||||
|
||||
async def check(self, ctrl, doc):
|
||||
return CheckResult(present=self._p)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _offline(monkeypatch):
|
||||
monkeypatch.setattr(pipeline, "_EMB", _Stub(None))
|
||||
monkeypatch.setattr(pipeline, "_LLM", _Stub(None))
|
||||
|
||||
|
||||
def _run(text: str):
|
||||
return asyncio.run(
|
||||
REGISTRY.get("agb").evaluate(AgentInput(doc_type="agb", text=text)))
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
"""AGB routed-Pipeline: Gate, Reference-/Embedding-Rescue, LLM-skip, Re-Tiering.
|
||||
Embedding + LLM offline-gestubbt → deterministisch, kein Netzwerk (Reference = echtes Regex)."""
|
||||
import asyncio
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
import compliance.services.specialist_agents.agb._pipeline as pipeline
|
||||
from compliance.services.checkers.base import CheckResult
|
||||
from compliance.services.specialist_agents._base import AgentInput
|
||||
from compliance.services.specialist_agents.agb.agent import AGBAgent
|
||||
|
||||
|
||||
class _Stub:
|
||||
def __init__(self, present):
|
||||
self._p = present
|
||||
|
||||
async def check(self, ctrl, doc):
|
||||
return CheckResult(present=self._p)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _offline(monkeypatch):
|
||||
monkeypatch.setattr(pipeline, "_EMB", _Stub(None))
|
||||
monkeypatch.setattr(pipeline, "_LLM", _Stub(None))
|
||||
|
||||
|
||||
def _routed(field_ids, text, context=None):
|
||||
findings = [SimpleNamespace(field_id=fid) for fid in field_ids]
|
||||
return asyncio.run(pipeline.run_routed(findings, text, context or {}))
|
||||
|
||||
|
||||
def test_gate_termination_na_for_oneoff_shop():
|
||||
text = "Widerrufsbelehrung: Sie koennen binnen 14 Tagen widerrufen. " * 5
|
||||
kept, resolved, gated = _routed(["termination", "termination_form"], text)
|
||||
assert set(gated) == {"termination", "termination_form"}
|
||||
assert kept == []
|
||||
|
||||
|
||||
def test_reference_rescues_data_protection():
|
||||
text = "Einzelheiten zur Verarbeitung in unserer Datenschutzerklaerung. " * 5
|
||||
kept, resolved, gated = _routed(["data_protection"], text)
|
||||
assert "data_protection" in resolved and kept == []
|
||||
|
||||
|
||||
def test_embedding_rescue_resolves(monkeypatch):
|
||||
monkeypatch.setattr(pipeline, "_EMB", _Stub(True))
|
||||
kept, resolved, gated = _routed(["scope"], "x" * 200)
|
||||
assert "scope" in resolved
|
||||
|
||||
|
||||
def test_llm_skipped_keeps_finding():
|
||||
kept, resolved, gated = _routed(["delivery_timeframe"], "x" * 200, {"skip_llm": True})
|
||||
assert [f.field_id for f in kept] == ["delivery_timeframe"] and resolved == []
|
||||
|
||||
|
||||
def test_evaluate_retiers_low_out_of_findings():
|
||||
text = ("Allgemeine Geschaeftsbedingungen. Vertragsschluss durch Bestellung. "
|
||||
"Haftung beschraenkt. Gerichtsstand Muenchen. ") * 6
|
||||
out = asyncio.run(AGBAgent().evaluate(AgentInput(doc_type="agb", text=text)))
|
||||
assert out.agent == "agb" and out.agent_version == "2.0"
|
||||
assert all(f.severity in ("HIGH", "MEDIUM") for f in out.findings)
|
||||
@@ -0,0 +1,14 @@
|
||||
"""AGB muss im LIVE-Pfad verdrahtet sein (_TOPIC_AGENTS), nicht nur per Snapshot."""
|
||||
from compliance.api.agent_check._agent_outputs import _TOPIC_AGENTS
|
||||
|
||||
|
||||
def test_agb_wired_into_live_topic_agents():
|
||||
assert _TOPIC_AGENTS.get("agb") == "agb"
|
||||
|
||||
|
||||
def test_dse_wired_into_live_topic_agents():
|
||||
assert _TOPIC_AGENTS.get("dse") == "dse"
|
||||
|
||||
|
||||
def test_impressum_still_wired():
|
||||
assert _TOPIC_AGENTS.get("impressum") == "impressum"
|
||||
@@ -0,0 +1,83 @@
|
||||
"""Unit-Tests der Prüfer-Library. Embedding + LLM gemockt → kein Netzwerk."""
|
||||
import asyncio
|
||||
|
||||
import compliance.services.llm_cascade as cascade_mod
|
||||
import compliance.services.mc_embedding_matcher as emb_mod
|
||||
from compliance.services.checkers.base import (
|
||||
ControlSpec,
|
||||
DecisionMethod,
|
||||
DocContext,
|
||||
VerificationMethod,
|
||||
)
|
||||
from compliance.services.checkers.embedding_checker import EmbeddingChecker
|
||||
from compliance.services.checkers.llm_checker import LLMChecker
|
||||
from compliance.services.checkers.reference_checker import ReferenceChecker
|
||||
|
||||
|
||||
def _run(coro):
|
||||
return asyncio.run(coro)
|
||||
|
||||
|
||||
def test_reference_present_and_absent():
|
||||
rc = ReferenceChecker()
|
||||
spec = ControlSpec("data_protection", VerificationMethod.REFERENCE,
|
||||
DecisionMethod.LINK_RESOLVER,
|
||||
patterns=[r"datenschutz(erkl|bestimmung|hinweis)"])
|
||||
r = _run(rc.check(spec, DocContext(
|
||||
text="Details in unserer Datenschutzerklaerung: https://x.de/datenschutz")))
|
||||
assert r.present is True
|
||||
assert r.detail.get("link", "").startswith("https://")
|
||||
r2 = _run(rc.check(spec, DocContext(text="Keine Angabe zum Datenschutz-Thema.")))
|
||||
assert r2.present is False
|
||||
|
||||
|
||||
def test_embedding_threshold(monkeypatch):
|
||||
monkeypatch.setattr(emb_mod, "DIM", 3, raising=False)
|
||||
monkeypatch.setattr(emb_mod, "_chunk_text", lambda t: [t], raising=False)
|
||||
|
||||
async def _embed(texts):
|
||||
return [[1.0, 0.0, 0.0] for _ in texts]
|
||||
|
||||
monkeypatch.setattr(emb_mod, "_embed_texts", _embed, raising=False)
|
||||
ec = EmbeddingChecker()
|
||||
spec = ControlSpec("scope_t", VerificationMethod.CONTENT, DecisionMethod.EMBEDDING,
|
||||
paraphrases=["Geltungsbereich"], embed_threshold=0.58)
|
||||
monkeypatch.setattr(emb_mod, "_cosine", lambda a, b: 0.90, raising=False)
|
||||
r = _run(ec.check(spec, DocContext(text="x" * 200)))
|
||||
assert r.present is True and r.confidence >= 0.58
|
||||
monkeypatch.setattr(emb_mod, "_cosine", lambda a, b: 0.20, raising=False)
|
||||
r2 = _run(ec.check(spec, DocContext(text="x" * 200)))
|
||||
assert r2.present is False
|
||||
|
||||
|
||||
def test_embedding_offline_returns_none(monkeypatch):
|
||||
async def _boom(texts):
|
||||
raise ConnectionError("embedding-service down")
|
||||
|
||||
monkeypatch.setattr(emb_mod, "_embed_texts", _boom, raising=False)
|
||||
ec = EmbeddingChecker()
|
||||
spec = ControlSpec("scope_off", VerificationMethod.CONTENT, DecisionMethod.EMBEDDING,
|
||||
paraphrases=["x"], embed_threshold=0.6)
|
||||
r = _run(ec.check(spec, DocContext(text="y" * 200)))
|
||||
assert r.present is None # fail-safe
|
||||
|
||||
|
||||
def test_llm_present_and_absent(monkeypatch):
|
||||
lc = LLMChecker()
|
||||
spec = ControlSpec("delivery_timeframe", VerificationMethod.CONTENT, DecisionMethod.LLM,
|
||||
topic_regex=r"liefer", question="Konkrete Lieferfrist?")
|
||||
doc = DocContext(text=("1. Lieferung\nDie Ware wird innerhalb von 2 Werktagen "
|
||||
"geliefert.\n") * 4)
|
||||
|
||||
async def _erfuellt(system, user, **kw):
|
||||
return {"text": '{"verdict":"ERFUELLT","zitat":"2 Werktagen","begruendung":"x"}',
|
||||
"source": "qwen", "confidence": 0.7}
|
||||
|
||||
monkeypatch.setattr(cascade_mod, "call_with_cascade", _erfuellt, raising=False)
|
||||
assert _run(lc.check(spec, doc)).present is True
|
||||
|
||||
async def _fehlt(system, user, **kw):
|
||||
return {"text": '{"verdict":"FEHLT"}', "source": "qwen"}
|
||||
|
||||
monkeypatch.setattr(cascade_mod, "call_with_cascade", _fehlt, raising=False)
|
||||
assert _run(lc.check(spec, doc)).present is False
|
||||
@@ -1,65 +1,153 @@
|
||||
"""DSEAgent — kuratierte Art-13/14-Checkliste (kein Library-Firehose)."""
|
||||
"""DSE-Agent v3 — DB-Controls (doc_check_controls) via run_v3_pipeline +
|
||||
kuratierter Art-13-Regex-Boost (Layer 0). Volle Parität zu impressum/cookie.
|
||||
|
||||
Die Tests prüfen die deterministischen Bausteine (regex_boost/mcs) ohne DB und
|
||||
den Agent-Pfad mit gemocktem run_v3_pipeline (CI hat keine DB).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
import compliance.services.specialist_agents.dse.agent as dse_agent
|
||||
from compliance.services.specialist_agents import REGISTRY, AgentInput
|
||||
from compliance.services.specialist_agents.dse.mcs import MCS, MC_IDS
|
||||
from compliance.services.specialist_agents.dse.regex_boost import (
|
||||
boost_matches_db_mc,
|
||||
compute_regex_boosts,
|
||||
criteria_on_topic,
|
||||
)
|
||||
|
||||
_DSE_SAMPLE = (
|
||||
"Datenschutzerklaerung. Verantwortlich im Sinne der DSGVO ist die Muster "
|
||||
"GmbH, Musterstrasse 1, 12345 Berlin. E-Mail: info@muster.de. "
|
||||
"Datenschutzbeauftragter: dsb@muster.de. Zwecke der Verarbeitung und "
|
||||
"Rechtsgrundlage Art. 6 Abs. 1 lit. f berechtigtes Interesse. Empfaenger "
|
||||
"Ihrer Daten sind Auftragsverarbeiter. Speicherdauer der Daten richtet "
|
||||
"sich nach Aufbewahrungsfristen. Sie haben das Recht auf Auskunft, das "
|
||||
"Recht auf Berichtigung, das Recht auf Loeschung sowie ein "
|
||||
"Widerspruchsrecht. Beschwerde bei der Aufsichtsbehoerde moeglich. Stand: "
|
||||
"Januar 2026. ") * 3
|
||||
|
||||
|
||||
def _run(text: str):
|
||||
return asyncio.run(
|
||||
REGISTRY.get("dse").evaluate(AgentInput(doc_type="dse", text=text)))
|
||||
|
||||
|
||||
# ── Registrierung ────────────────────────────────────────────────────────
|
||||
def test_dse_agent_registered():
|
||||
assert REGISTRY.get("dse") is not None
|
||||
agent = REGISTRY.get("dse")
|
||||
assert agent is not None
|
||||
assert agent.agent_version == "3.0"
|
||||
assert agent.doc_type == "dse"
|
||||
|
||||
|
||||
def test_dse_detects_core_obligations():
|
||||
text = (
|
||||
"Datenschutzerklaerung. Verantwortlich im Sinne der DSGVO ist die "
|
||||
"Muster GmbH, Musterstrasse 1, 12345 Berlin. E-Mail: info@muster.de. "
|
||||
"Datenschutzbeauftragter: dsb@muster.de. Zwecke der Verarbeitung und "
|
||||
"Rechtsgrundlage Art. 6 Abs. 1. Empfaenger Ihrer Daten. Speicherdauer "
|
||||
"der Daten. Ihre Rechte: Auskunft, Loeschung, Widerspruch, Beschwerde "
|
||||
"bei der Aufsichtsbehoerde. ") * 3
|
||||
out = _run(text)
|
||||
assert out.agent == "dse"
|
||||
# 10 L1-Pflichtangaben immer + L2-Details deren Parent vorhanden ist
|
||||
# (fehlende Parents → L2 übersprungen, kein 'na'-Rauschen).
|
||||
assert 10 <= out.mc_total <= 33
|
||||
ok = [c.label for c in out.mc_coverage if c.status == "ok"]
|
||||
assert any("Verantwortlich" in lbl for lbl in ok)
|
||||
assert any("Rechtsgrundlage" in lbl for lbl in ok)
|
||||
def test_owned_mc_ids_match_checklist():
|
||||
# owned_mc_ids = die Boost-Pattern-IDs (aus ART13_CHECKLIST gehoben).
|
||||
assert MC_IDS == tuple(m.mc_id for m in MCS)
|
||||
assert len(MC_IDS) >= 10 # mind. die 10 L1-Pflichtfelder + L2
|
||||
|
||||
|
||||
def test_dse_missing_obligations_are_findings():
|
||||
out = _run("Lorem ipsum dolor sit amet consectetur adipiscing elit. " * 6)
|
||||
assert out.findings
|
||||
assert any(f.severity == "HIGH" for f in out.findings)
|
||||
# ── Layer-0 Regex-Boost (deterministisch, ohne DB) ───────────────────────
|
||||
def test_regex_boost_detects_core_fields():
|
||||
boosts = compute_regex_boosts(_DSE_SAMPLE)
|
||||
# Die zentralen Art-13-Felder müssen erkannt werden.
|
||||
for field in ("controller", "legal_basis", "rights", "complaint",
|
||||
"retention", "dse_version_date"):
|
||||
assert field in boosts, f"{field} nicht erkannt: {sorted(boosts)}"
|
||||
|
||||
|
||||
def test_regex_boost_empty_on_short_text():
|
||||
assert compute_regex_boosts("zu kurz") == set()
|
||||
|
||||
|
||||
def test_criteria_on_topic_accepts_dse_rejects_foreign():
|
||||
dse_crit = ["Rechtsgrundlage gemäß Art. 6 DSGVO benannt",
|
||||
"Speicherdauer und Löschfrist angegeben"]
|
||||
assert criteria_on_topic(dse_crit) is True
|
||||
foreign = ["Bestellbestätigung wird per E-Mail versendet",
|
||||
"Versandkosten werden im Warenkorb angezeigt"]
|
||||
assert criteria_on_topic(foreign) is False
|
||||
# leere Kriterien → konservativ on-topic behalten
|
||||
assert criteria_on_topic([]) is True
|
||||
|
||||
|
||||
def test_boost_matches_db_mc_third_country():
|
||||
boosts = {"third_country", "controller"}
|
||||
crit = ["Standardvertragsklauseln für Drittland benannt",
|
||||
"Geeignete Garantien bei Übermittlung in ein Drittland"]
|
||||
assert boost_matches_db_mc(boosts, crit) == "third_country"
|
||||
# ohne passende Boosts → None
|
||||
assert boost_matches_db_mc(set(), crit) is None
|
||||
|
||||
|
||||
# ── Agent-Pfad mit gemocktem run_v3_pipeline ─────────────────────────────
|
||||
def _mock_v3(results, telemetry=None):
|
||||
async def _fake(text, scope, db_url="", skip_embedding=False):
|
||||
return results, (telemetry or {
|
||||
"total_mcs": len(results), "layer_0_field_hits": 0,
|
||||
"layer_0_field_ids": [], "layer_0_boost_overrides": 0,
|
||||
"sector_dropped": 0, "offtopic_dropped": 0})
|
||||
return _fake
|
||||
|
||||
|
||||
def _run(text, context=None):
|
||||
return asyncio.run(REGISTRY.get("dse").evaluate(
|
||||
AgentInput(doc_type="dse", text=text, context=context or {})))
|
||||
|
||||
|
||||
def test_dse_short_text_skips():
|
||||
out = _run("zu kurz")
|
||||
assert out.confidence == 0.0
|
||||
assert all(c.status == "skipped" for c in out.mc_coverage)
|
||||
assert out.mc_coverage and all(
|
||||
c.status == "skipped" for c in out.mc_coverage)
|
||||
|
||||
|
||||
def test_third_country_high_when_applicable_no_na_detail_short_action():
|
||||
# Text ohne Drittland-Abschnitt + Scan-Kontext drittland=ja:
|
||||
# - third_country (L1) fehlt → HIGH (nicht weiches MEDIUM)
|
||||
# - Transfermechanismus (L2) → KEIN 'na' (übersprungen, Parent deckt ab)
|
||||
# - Titel/Maßnahme kurz (kein 280-Zeichen-Hint als Recommendation-Titel)
|
||||
text = ("Datenschutz. Verantwortlich ist die Muster GmbH, info@muster.de. "
|
||||
"Zwecke und Rechtsgrundlage Art. 6. Speicherdauer. Ihre Rechte. ") * 4
|
||||
out = asyncio.run(REGISTRY.get("dse").evaluate(AgentInput(
|
||||
doc_type="dse", text=text,
|
||||
context={"scan_context": {"third_country_transfer": "yes"}})))
|
||||
tc = [f for f in out.findings if "Drittland" in f.title]
|
||||
assert tc and tc[0].severity == "HIGH"
|
||||
assert not any(c.status == "na" and "Transfermechanismus" in c.label
|
||||
for c in out.mc_coverage)
|
||||
assert all(len(f.action) < 110 for f in out.findings)
|
||||
# Detail-Begründung bleibt als evidence erhalten
|
||||
assert any(f.evidence for f in out.findings)
|
||||
def test_dse_findings_from_failed_db_mc(monkeypatch):
|
||||
results = [{
|
||||
"control_id": "DATA-525-A17", "passed": False, "severity": "HIGH",
|
||||
"label": "Berechtigte Interessen ausweisen", "regulation": None,
|
||||
"article": None, "_pass_criteria": ["berechtigtes interesse benannt"],
|
||||
"matched_text": "", "source": "keyword_match",
|
||||
}, {
|
||||
"control_id": "AUTH-2051-A11", "passed": True, "severity": "LOW",
|
||||
"label": "Prägnante Form", "regulation": None, "article": None,
|
||||
"_pass_criteria": [], "matched_text": "ok",
|
||||
}]
|
||||
monkeypatch.setattr(dse_agent, "run_v3_pipeline", _mock_v3(results))
|
||||
out = _run(_DSE_SAMPLE, context={"skip_llm": True})
|
||||
fids = {f.field_id for f in out.findings}
|
||||
assert "DATA-525-A17" in fids # failed → Finding
|
||||
assert "AUTH-2051-A11" not in fids # passed → kein Finding
|
||||
f = next(f for f in out.findings if f.field_id == "DATA-525-A17")
|
||||
assert f.severity == "HIGH"
|
||||
assert f.norm == "DSGVO Art. 13/14" # NULL-regulation → Fallback-Norm
|
||||
assert len(f.action) < 410
|
||||
|
||||
|
||||
def test_dse_third_country_override_to_high(monkeypatch):
|
||||
# MEDIUM-Drittland-MC → HIGH bei dokumentiertem Transfer (scan_context).
|
||||
results = [{
|
||||
"control_id": "DATA-900-A01", "passed": False, "severity": "MEDIUM",
|
||||
"label": "Drittlandtransfer Schutzgarantien benennen",
|
||||
"regulation": None, "article": None,
|
||||
"_pass_criteria": ["standardvertragsklauseln", "drittland garantien"],
|
||||
"matched_text": "", "source": "keyword_match",
|
||||
}]
|
||||
monkeypatch.setattr(dse_agent, "run_v3_pipeline", _mock_v3(results))
|
||||
out = _run(_DSE_SAMPLE, context={
|
||||
"skip_llm": True,
|
||||
"scan_context": {"third_country_transfer": "yes"}})
|
||||
f = next(f for f in out.findings if f.field_id == "DATA-900-A01")
|
||||
assert f.severity == "HIGH"
|
||||
assert f.severity_reason == "db_mc_failed_third_country_transfer"
|
||||
|
||||
|
||||
def test_dse_no_transfer_keeps_medium(monkeypatch):
|
||||
results = [{
|
||||
"control_id": "DATA-900-A01", "passed": False, "severity": "MEDIUM",
|
||||
"label": "Drittlandtransfer Schutzgarantien benennen",
|
||||
"regulation": None, "article": None,
|
||||
"_pass_criteria": ["standardvertragsklauseln", "drittland garantien"],
|
||||
"matched_text": "", "source": "keyword_match",
|
||||
}]
|
||||
monkeypatch.setattr(dse_agent, "run_v3_pipeline", _mock_v3(results))
|
||||
out = _run(_DSE_SAMPLE, context={"skip_llm": True})
|
||||
f = next(f for f in out.findings if f.field_id == "DATA-900-A01")
|
||||
assert f.severity == "MEDIUM"
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
"""Tests fuer das DSE-Applicability-Gate (_classification_gate).
|
||||
|
||||
Deckt die reine Split-Logik (apply_gate) und das defensive Verhalten von
|
||||
load_dse_gate ohne DB ab. Die DB-Abfrage selbst ist I/O und wird hier nicht
|
||||
gegen eine echte DB getestet (defensiver Pfad: kein DSN -> leeres Dict)."""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from compliance.services.specialist_agents.dse._classification_gate import (
|
||||
apply_gate,
|
||||
load_dse_gate,
|
||||
)
|
||||
|
||||
|
||||
def test_apply_gate_splits_findings_and_organizational():
|
||||
controls = [
|
||||
{"control_id": "AUTH-2051-A02", "title": "Speicherdauer nennen"},
|
||||
{"control_id": "AUTH-2049-A01", "title": "VVT fuehren"},
|
||||
]
|
||||
gate = {
|
||||
"AUTH-2049-A01": {
|
||||
"obligation_type": "EVIDENCE",
|
||||
"check_intent": "DIRECT_EVIDENCE",
|
||||
"applicable_artifacts": ["VVT", "AUDIT"],
|
||||
"reference_allowed": "NO",
|
||||
}
|
||||
}
|
||||
kept, organizational = apply_gate(controls, gate)
|
||||
assert [c["control_id"] for c in kept] == ["AUTH-2051-A02"]
|
||||
assert len(organizational) == 1
|
||||
org = organizational[0]
|
||||
assert org["control_id"] == "AUTH-2049-A01"
|
||||
assert org["title"] == "VVT fuehren"
|
||||
assert org["applicable_artifacts"] == ["VVT", "AUDIT"]
|
||||
assert org["check_intent"] == "DIRECT_EVIDENCE"
|
||||
|
||||
|
||||
def test_apply_gate_empty_gate_keeps_all():
|
||||
controls = [{"control_id": "X-1"}, {"control_id": "X-2"}]
|
||||
kept, organizational = apply_gate(controls, {})
|
||||
assert len(kept) == 2
|
||||
assert organizational == []
|
||||
|
||||
|
||||
def test_load_dse_gate_without_dsn_is_defensive():
|
||||
"""Kein DSN + keine Env -> leeres Dict (kein Filter), kein Fehler."""
|
||||
saved = (
|
||||
os.environ.pop("DATABASE_URL", None),
|
||||
os.environ.pop("COMPLIANCE_DATABASE_URL", None),
|
||||
)
|
||||
try:
|
||||
result = asyncio.run(load_dse_gate(""))
|
||||
assert result == {}
|
||||
finally:
|
||||
if saved[0] is not None:
|
||||
os.environ["DATABASE_URL"] = saved[0]
|
||||
if saved[1] is not None:
|
||||
os.environ["COMPLIANCE_DATABASE_URL"] = saved[1]
|
||||
@@ -0,0 +1,67 @@
|
||||
"""DSE Embedding-Recall — deterministische semantische Schicht (gecacht).
|
||||
|
||||
Testet die reine Logik OHNE Embedding-Service: Cache-Treffer-Pfad,
|
||||
Schwellen-Filter, Kandidaten-Schnitt, Reachability-Guard. Das Einbetten selbst
|
||||
(Embedding-Service) ist Integration und wird auf macmini/Prod validiert.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
import compliance.services.specialist_agents.dse._embedding_recall as er
|
||||
|
||||
|
||||
_TEXT = ("Datenschutzerklaerung der Muster GmbH. " * 20) # > 100 Zeichen
|
||||
|
||||
|
||||
def _seed_cache(tmp_path, scores: dict[str, float]) -> str:
|
||||
p = tmp_path / "dse_embed_cache.json"
|
||||
p.write_text(json.dumps({er._doc_hash(_TEXT): scores}))
|
||||
return str(p)
|
||||
|
||||
|
||||
def test_doc_hash_deterministic():
|
||||
# feste Funktion: gleicher Text → gleicher Hash (Reproduzierbarkeit)
|
||||
assert er._doc_hash(_TEXT) == er._doc_hash(_TEXT)
|
||||
assert er._doc_hash("a") != er._doc_hash("b")
|
||||
|
||||
|
||||
def test_cache_hit_threshold_filter(tmp_path, monkeypatch):
|
||||
# Cache-Treffer: kein Embedding-Service nötig. Nur Scores >= Schwelle UND
|
||||
# in den Kandidaten werden zurückgegeben.
|
||||
scores = {"DATA-1": 0.71, "DATA-2": 0.60, "AUTH-3": 0.68, "SEC-4": 0.50}
|
||||
monkeypatch.setenv("DSE_EMBED_CACHE", _seed_cache(tmp_path, scores))
|
||||
monkeypatch.setattr(er, "_CACHE_PATH", str(tmp_path / "dse_embed_cache.json"))
|
||||
|
||||
cands = ["DATA-1", "DATA-2", "AUTH-3", "SEC-4"]
|
||||
out = asyncio.run(er.embedding_recall(_TEXT, cands, threshold=0.65))
|
||||
# >=0.65: DATA-1 (0.71), AUTH-3 (0.68). NICHT DATA-2 (0.60), SEC-4 (0.50).
|
||||
assert out == {"DATA-1", "AUTH-3"}
|
||||
|
||||
|
||||
def test_cache_hit_candidate_intersection(tmp_path, monkeypatch):
|
||||
# Nur Kandidaten (durchgefallene Controls) zählen — andere ignoriert.
|
||||
scores = {"DATA-1": 0.90, "DATA-2": 0.90}
|
||||
monkeypatch.setattr(er, "_CACHE_PATH", str(tmp_path / "c.json"))
|
||||
(tmp_path / "c.json").write_text(json.dumps({er._doc_hash(_TEXT): scores}))
|
||||
out = asyncio.run(er.embedding_recall(_TEXT, ["DATA-1"], threshold=0.65))
|
||||
assert out == {"DATA-1"} # DATA-2 nicht in Kandidaten
|
||||
|
||||
|
||||
def test_empty_inputs():
|
||||
assert asyncio.run(er.embedding_recall("zu kurz", ["X"])) == set()
|
||||
assert asyncio.run(er.embedding_recall(_TEXT, [])) == set()
|
||||
|
||||
|
||||
def test_service_down_returns_empty(tmp_path, monkeypatch):
|
||||
# Kein Cache + Service nicht erreichbar → leer (deterministischer Layer trägt),
|
||||
# KEIN Hang.
|
||||
monkeypatch.setattr(er, "_CACHE_PATH", str(tmp_path / "none.json"))
|
||||
|
||||
async def _unreachable(timeout=2.0):
|
||||
return False
|
||||
monkeypatch.setattr(er, "_embedding_reachable", _unreachable)
|
||||
out = asyncio.run(er.embedding_recall(_TEXT, ["DATA-1"]))
|
||||
assert out == set()
|
||||
@@ -0,0 +1,153 @@
|
||||
# DSE-Engine — Validierung & Versionierung `DSE_v1`
|
||||
|
||||
> **Status:** Release-Candidate (qualitativ validiert, nicht „fertig")
|
||||
> **Datum:** 2026-06-19
|
||||
> **Modul:** Datenschutzerklärung (`doc_type = dse`)
|
||||
> **Zweck dieses Dokuments:** nachvollziehbar festhalten, *wie* die DSE-Engine gemessen
|
||||
> wurde, *welche* Controls *warum* korrigiert wurden und *welcher* reproduzierbare Prozess
|
||||
> daraus entstanden ist — als Referenz für Cookie-Richtlinie, Impressum, AGB, CRA, NIS2, KI-VO usw.
|
||||
|
||||
## 1. Kurzfazit
|
||||
|
||||
Die zentrale Aussage ist nicht „FP 11 % → 6 %", sondern: **die False Positives wurden
|
||||
halbiert, ohne den Schutz vor echten Lücken zu verlieren** (deterministisch nachgewiesen,
|
||||
0 verschluckte Lücken).
|
||||
|
||||
| KPI (vs Opus-GT, 5 Firmen, 432 anwendbare Controls) | vorher | `DSE_v1` |
|
||||
|---|---|---|
|
||||
| Falsche Findings (Engine FEHLT / GT ERFÜLLT) | 51 (11 %) | **26 (6 %)** |
|
||||
| Verpasste Lücken (Engine ERFÜLLT / GT FEHLT) | 32 (7 %) | **~31 (~7 %, stabil)** |
|
||||
| Recall | 0,76 | **0,88** |
|
||||
| Precision | 0,83 | **0,84** |
|
||||
|
||||
Ursache der falschen Findings war **nicht** ein zu schwaches LLM, sondern ~11 Controls,
|
||||
die mehr verlangten als das Gesetz. Die Korrektur der Regel verbessert OVH, Claude,
|
||||
Embeddings und menschliche Auditoren gleichzeitig — höchster ROI.
|
||||
|
||||
## 2. Testkorpus (Anti-Overfit)
|
||||
|
||||
Fünf repräsentative, real abgerufene Datenschutzerklärungen unterschiedlicher Größe/Branche:
|
||||
|
||||
| Firma | Charakter | DSE-Größe |
|
||||
|---|---|---|
|
||||
| BMW | Großkonzern, sehr umfangreich | ~64 k Zeichen |
|
||||
| Mercedes-Benz | Großkonzern | ~19 k |
|
||||
| ELLI (VW) | Tochter/Energie | ~26 k |
|
||||
| ETO | B2B-Mittelstand | ~26 k |
|
||||
| SafetyKon | kleiner B2B, dünne DSE | ~5 k |
|
||||
|
||||
Die dünne SafetyKon-DSE ist bewusst der Härtetest gegen **zu lasche** Kriterien.
|
||||
|
||||
## 3. Ground-Truth-Methode
|
||||
|
||||
- **Orakel:** `claude-opus-4-8` (stärkstes verfügbares Modell — NICHT Haiku, das auf 5-Firmen-DSE
|
||||
zu lasch/„N/A-blind" war). Pro `(Firma × Control)` ein Urteil `ERFUELLT | FEHLT | NA`.
|
||||
- Strenge juristische Leitplanken im Prompt (Speicherdauer: zirkuläre Formeln erfüllen nicht;
|
||||
berechtigtes Interesse eines Dritten ≠ eigenes; EU-Kommission-Erwähnung ≠ Angemessenheitsbeschluss).
|
||||
- 456 Urteile, davon 24 `NA` (nicht anwendbar) → 432 anwendbare als Messbasis.
|
||||
- Wichtige Einschränkung: das Opus-GT trägt auf einigen der korrigierten Controls **dieselbe
|
||||
Überstrenge** — siehe §6 (FN-Nachweis). Auf diesen Controls ist die Engine inzwischen
|
||||
*näher am Gesetz* als das GT.
|
||||
|
||||
## 4. Gemessene Engine-Architektur
|
||||
|
||||
Dreistufig, Verdikt je `(Firma × Control)`:
|
||||
|
||||
1. **Keyword** (deterministisch) → trifft zu ⇒ ERFÜLLT.
|
||||
2. **BGE-M3-Embedding-Recall**, Score < 0,50 ⇒ FEHLT (deterministisch, gecacht).
|
||||
3. **LLM-Judge** (OVH `gpt-oss-120b`) auf den Embedding-Passes (Score ≥ 0,50) — fängt die
|
||||
„semantisch nah, aber nicht erfüllt"-Über-Passes des Embeddings.
|
||||
|
||||
**Robustheit (Pflicht):** OVH liefert unter Concurrency teils leere Antworten. Eine leere
|
||||
LLM-Antwort darf **niemals** als FEHLT gewertet werden. `DSE_v1` misst mit Retry-on-empty,
|
||||
Concurrency 3, `max_tokens` 600; bei dauerhaft leer → `UNSICHER` (= 4-Status
|
||||
`INSUFFICIENT_EVIDENCE` / Eskalation), nicht FEHLT.
|
||||
|
||||
## 5. Messverlauf (ehrlich, inkl. Sackgassen)
|
||||
|
||||
| Stufe | verpasste Lücken | falsche Findings | Anmerkung |
|
||||
|---|---|---|---|
|
||||
| nur Keyword + Embedding @0,58 | 32 % | 3 % | Embedding allein **ungenügend** (Über-Passes) |
|
||||
| + OVH-LLM (55k-Truncation) | 5 % | 19 % | LLM löst „nichts fehlt"; FP scheinbar hoch |
|
||||
| + Volltext (kein 55k-Cut) | 6 % | 14 % | Truncation hatte BMW-FP aufgebläht |
|
||||
| + Robust (Retry, kein Leer→FEHLT) | 7 % | 11 % | Artefaktfreie Basis |
|
||||
| **+ Kriterien-Fix = `DSE_v1`** | **~7 %** | **6 %** | siehe §7 |
|
||||
|
||||
**Schlüssel-Befund (Produkt-relevant):** ein großer Teil der ursprünglichen falschen Findings
|
||||
waren **leere OVH-Antworten**, die als FEHLT verbucht wurden — ein echter Pipeline-Bug, nicht
|
||||
ein Inhaltsproblem. Lehre für die Kaskaden-Verdrahtung: leere/Timeout-Antwort → Retry →
|
||||
`INSUFFICIENT_EVIDENCE` / Claude-Eskalation.
|
||||
|
||||
## 6. Die korrigierten Controls (juristischer Review)
|
||||
|
||||
12 systematische FP-Controls (firmenübergreifend wiederkehrend) wurden juristisch reviewt
|
||||
(Gesetz-Forderung vs. Control-Forderung) und in drei Klassen geteilt. **11 korrigiert,
|
||||
1 (`DATA-1611-A04`, Klasse A) unangetastet.** Volle Alt→Neu-Diffs:
|
||||
`dse_criteria_changelog.json` (Repo-Wurzel). Backup für Restore: `dse_criteria_backup.json`.
|
||||
|
||||
### Klasse B — Control verlangte mehr als das Gesetz (7)
|
||||
|
||||
| Control | Rechtsnorm | Kern-Korrektur |
|
||||
|---|---|---|
|
||||
| `DATA-2260-A01` | Art. 13(1)(c) | „*primärer*/einzelner Zweck" → Zweck(e) im Plural genügen |
|
||||
| `AUTH-3737-A06` | Art. 13(1)(c)+(e) | keine Zweck/Rechtsgrundlage-Matrix *je* Übermittlung |
|
||||
| `DATA-2992-A03` | Art. 13(1)(e) | Empfänger/Kategorien + Zweck; keine AV-Distinktion/Vertraulichkeit |
|
||||
| `DATA-1624-A03` | Art. 13(1)(f) | Garantie + Zugang via **Link ODER** Kontakt; keine Schutzwirkungs-Beschreibung |
|
||||
| `DATA-1619-A03` | Art. 13(1)(c) | Rechtsgrundlage je Zweck; Artikel-Zitat *nicht* zwingend |
|
||||
| `DATA-424-A09` | Art. 20 | Recht erwähnen genügt; Format (CSV/JSON) *nicht* in der DSE |
|
||||
| `GOV-3300-A06` | Art. 20 | wie A09 (Dedupe-Kandidat zu `DATA-424-A09`) |
|
||||
|
||||
### Klasse C — Control mehrdeutig / Pflichten vermischt (4)
|
||||
|
||||
| Control | Rechtsnorm | Kern-Korrektur |
|
||||
|---|---|---|
|
||||
| `AI-1560-A01` | Art. 13(1)(c) vs (2)(a) | Speicherdauer-Forderung entfernt (eigene Pflicht) |
|
||||
| `SEC-3444-A04` | Art. 13(1)(c) | Titel/Frage „*beschränken*" (Verhalten) → „offenlegen" |
|
||||
| `DATA-1624-A06` | Art. 13(1)(f) | Schutzwirkungs-Beschreibung raus; ⚠️ Near-Duplikat zu `DATA-1624-A03` |
|
||||
| `DATA-2812-A05` | Art. 17 / §25 TTDSG | Titel „*implementieren*" → „offenlegen"; Verweis auf Cookie-Einstellungen genügt |
|
||||
|
||||
### Klasse A — Control korrekt, LLM/Artefakt (1, nicht geändert)
|
||||
`DATA-1611-A04` (Art. 13(1)(c)): Kriterien rechtlich sauber; die FP waren OVH-Leerantworten.
|
||||
|
||||
## 7. FN-Sicherheitsnachweis (kein Aufweichen)
|
||||
|
||||
Lockerere Kriterien dürfen keine echten Lücken durchwinken. Nach dem Fix stieg FN scheinbar
|
||||
32 → 36. Kausaltest (11 korrigierte Controls × 5 Firmen) + **deterministische Textprüfung**:
|
||||
|
||||
- **0 echte verschluckte Lücken.** Alle 5 „neuen FN" sind Fälle, in denen die Engine jetzt
|
||||
*korrekt* ist und das **Opus-GT zu streng war** — im DSE-Text belegt (bmw nennt „Art. 20 DSGVO",
|
||||
elli „EU-Standardvertragsklauseln … USA", mercedes Empfänger+Weitergabe, eto konkrete Zwecke).
|
||||
- **11 echte Lücken weiterhin gefangen** (TN), v.a. SafetyKons dünne DSE → die Kriterien sind
|
||||
nicht zahnlos geworden.
|
||||
|
||||
⇒ Die wahre Engine-FN bleibt bei ~7 % (stabil); die scheinbaren +4 sind GT-Überstrenge.
|
||||
|
||||
## 8. Reproduzierbarer Kalibrierungsprozess (das eigentliche Ergebnis)
|
||||
|
||||
Auf jedes weitere Modul anwendbar (Cookie, Impressum, AGB, CRA, NIS2, KI-VO, DORA, MaschVO):
|
||||
|
||||
1. **Opus-GT** je `(Firma × Control)` über 5 repräsentative Firmen bauen.
|
||||
2. **Engine messen** (Keyword → Embedding → robuster LLM-Judge) vs GT.
|
||||
3. **FP clustern** — wiederkehrende Controls statt Einzel-Findings (systematisch ≠ zufällig).
|
||||
4. **Gesetz-vs-Control-Review** der Top-Cluster → Klassen A (LLM-Fehler) / B (zu streng) / C (mehrdeutig).
|
||||
5. **Kriterien korrigieren** (B+C), versioniert, mit Rechtsnotiz im Changelog.
|
||||
6. **Re-Messung** — Pflicht: FP gesunken **und** FN stabil (FN-Kausaltest gegen Über-Lockerung).
|
||||
|
||||
## 9. Bekannte Grenzen / offene Punkte
|
||||
|
||||
- **OVH stochastisch** (kein Seed): ±~4 Findings Lauf-zu-Lauf. Für harte Zahlen Mehrfachlauf/Mehrheit.
|
||||
- **GT-Überstrenge** auf einigen korrigierten Controls → „8 % FN" überzeichnet leicht (wahr ~7 %).
|
||||
- **Dedupe offen** (separater Catalog-Schritt, nicht gelöscht): `DATA-1624-A06`↔`A03`,
|
||||
`DATA-424-A09`↔`GOV-3300`.
|
||||
- **Nur macmini-dev.** Kriterien-Änderungen sind reversibel (Backup) und noch **nicht** auf Prod.
|
||||
- **Restliche FP-Tail** (~17 außerhalb der Top-12) bei 6 % belassen — weitere Optimierung
|
||||
schlechter ROI; operativer Hebel ist der Claude-Freigabe-Tier (Kaskade), nicht Regel-Tuning.
|
||||
|
||||
## 10. Artefakte & Reproduktion
|
||||
|
||||
- GT-Verdikte: `/tmp/gt_opus_dse.json` (Container) · Kandidaten/Scores: `/tmp/multi_company_gt.json`
|
||||
- Changelog (Alt→Neu + Rechtsnotiz): `dse_criteria_changelog.json` · Restore: `dse_criteria_backup.json`
|
||||
- Skripte (MacBook `/tmp`, Ausführung via `docker exec -i bp-compliance-backend python3 -`):
|
||||
`cc_gt_opus_dse.py` (GT) · `cc_engine_llm_dse3.py` (robuste Messung) ·
|
||||
`cc_apply_criteria.py` (Korrektur + Versionierung) · `cc_check_fn.py` (FN-Kausaltest) ·
|
||||
`cc_verify_fn.py` (deterministische Textprüfung)
|
||||
@@ -0,0 +1,43 @@
|
||||
# Mapping: Nutzungsbedingungen & Shop-AGB auf die Prüfer-Matrix
|
||||
|
||||
> **Zweck:** Beleg der These *„neues Modul = Klassifizierung/Mapping, kein Forschungsprojekt"*. Keine neue Architektur, keine neuen Prüfertypen — nur Zuordnung vorhandener Controls + weniger neuer Items auf die bestehenden Prüfer.
|
||||
|
||||
## 1. Shop-AGB = 0 neue Arbeit
|
||||
|
||||
Der AGB-Korpus, an dem kalibriert wurde (Zalando, Otto, MediaMarkt, Tchibo, Lieferando), **sind** Shop-AGB. „Shop-AGB" ist damit kein neues Modul, sondern **das AGB-Modul selbst**. Aufwand: **null**.
|
||||
|
||||
## 2. Nutzungsbedingungen (Plattform/App-ToS) = Reuse + wenige neue Items
|
||||
|
||||
NB sind Vertragsprosa wie AGB, nur ohne Warenkauf-Pflichten und mit Plattform-spezifischen Pflichten. Mapping:
|
||||
|
||||
**Aus AGB wiederverwendet (gleiche Prüfer, ggf. neue Paraphrasen):**
|
||||
| Item | verification_method | decision_method |
|
||||
|---|---|---|
|
||||
| scope (Geltungsbereich) | CONTENT | EMBEDDING |
|
||||
| liability (Haftung) | CONTENT | EMBEDDING |
|
||||
| jurisdiction / choice_of_law | CONTENT | EMBEDDING |
|
||||
| data_protection (DSE-Verweis) | REFERENCE | LINK_RESOLVER |
|
||||
| salvatory_clause | CONTENT | EMBEDDING |
|
||||
| amendment_clause | CONTENT | EMBEDDING |
|
||||
| termination (Konto/Account) | CONTENT | EMBEDDING |
|
||||
| consumer_rights | CONTENT | EMBEDDING |
|
||||
| dispute_odr_link (bei B2C) | REFERENCE | LINK_RESOLVER |
|
||||
|
||||
**Nicht anwendbar (Scope-Gate, Waren-Verkauf):** payment*, delivery*, warranty*, contract/incorporation (anderer Vertragsschluss).
|
||||
|
||||
**Neu (NB-spezifisch) — alle auf EXISTIERENDE Prüfertypen:**
|
||||
| Item | verification_method | decision_method |
|
||||
|---|---|---|
|
||||
| Nutzungsrechte / zulässige Nutzung | CONTENT | EMBEDDING |
|
||||
| Geistiges Eigentum / Schutzrechte | CONTENT | EMBEDDING |
|
||||
| Verfügbarkeit (kein Anspruch auf ununterbrochenen Betrieb) | CONTENT | EMBEDDING |
|
||||
| Account / Registrierung / Nutzerpflichten | CONTENT | EMBEDDING |
|
||||
| Nutzergenerierte Inhalte / Verhaltensregeln | CONTENT | EMBEDDING |
|
||||
| Haftung für Links / Drittinhalte | CONTENT | EMBEDDING |
|
||||
|
||||
## 3. Ergebnis
|
||||
|
||||
- **Shop-AGB:** 0 neue Items, 0 neue Prüfer.
|
||||
- **Nutzungsbedingungen:** ~9 Items aus AGB wiederverwendet + ~6 neue Items — **alle auf bestehenden Prüfertypen** (CONTENT/EMBEDDING + REFERENCE). **0 neue Prüfertypen.**
|
||||
|
||||
Ein neues Web-Dokument ist damit ein **Mapping-/Klassifizierungs-** und Paraphrasen-Schreibproblem (Stunden–Tage), kein Mess-/Forschungsprojekt (Wochen). Genau die These der Prüfer-Matrix.
|
||||
@@ -0,0 +1,87 @@
|
||||
# Prüfer-Matrix — Meta-Modell der Doc-Check-Plattform
|
||||
|
||||
> **Status:** Plattformkonzept, **eingefroren 2026-06-21**. Abgeleitet aus 4 kalibrierten Modulen (DSE, Cookie, Impressum, AGB). Erweitert `verification_method.md` (5→8 Klassen) und fügt die `decision_method`-Achse hinzu.
|
||||
> **Kernsatz:** *Nicht jedes Control braucht denselben Richter.* Der **Kontrolltyp bestimmt den Prüfer** — nicht alles ist ein Text-/LLM-Problem.
|
||||
|
||||
## 0. Die Architektur-Verschiebung
|
||||
|
||||
**Vorher (implizit):** `Control → Embedding → LLM → Finding`.
|
||||
|
||||
**Jetzt (empirisch bewiesen):**
|
||||
```
|
||||
Control → [scope-gate] → artifact_type → verification_method → decision_method
|
||||
→ passender Prüfer → Evidence → Finding (severity-getiert)
|
||||
```
|
||||
|
||||
Vier strukturell verschiedene Dokumenttypen führten immer wieder auf dieselbe Meta-Struktur. Das ist größer als jeder Einzel-Fix: es ist mit hoher Wahrscheinlichkeit das Routing-Prinzip für alle ~14.000 Master Controls.
|
||||
|
||||
## 1. Empirische Basis (4 Module)
|
||||
|
||||
| Modul | dominanter Prüfer | Beleg |
|
||||
|---|---|---|
|
||||
| DSE | CONTENT (LLM/Embedding) | Kriterien-Kalibrierung, FP 11→6 % |
|
||||
| Cookie-Banner | BEHAVIOR | Enforcement / Dark-Pattern (Playwright) |
|
||||
| Cookie-Policy | CONTENT + REFERENCE | Inhalt + Verweise |
|
||||
| Impressum | FIELD + PRESENTATION (+ SCOPE-Gate) | Feld-Matcher FP 0 %, Präsentation re-routed |
|
||||
| AGB | CONTENT (KEYWORD→EMBEDDING→LLM) + REFERENCE (+ SCOPE-Gate) | 71 % FP → ~0; LLM nur 2/21 Items |
|
||||
|
||||
## 2. Achse 1 — `verification_method` (welcher Prüfer-TYP)
|
||||
|
||||
| verification_method | Prüfer | Leitfrage | Beleg | Reifegrad |
|
||||
|---|---|---|---|---|
|
||||
| **CONTENT** | Embedding + LLM-Kaskade | Was steht (als Offenlegung) im Text? | DSE, Cookie-Policy | kalibriert |
|
||||
| **FIELD** | Regex / Extraktion (Feldmatrix) | Welche Pflichtfelder existieren + sind valide? | Impressum (HRB, USt-IdNr, Anschrift) | ✓ FP 0 % |
|
||||
| **REFERENCE** | Link-Resolver | Gibt es einen klaren Verweis/Link, löst er auf? | AGB `data_protection` | ✓ 7/7 |
|
||||
| **BEHAVIOR** | Playwright + API | Manipuliert die UI die Entscheidung? | Cookie-Banner (Reject=Accept, Pre-Consent-Cookies) | Matrix vorhanden |
|
||||
| **PRESENTATION** | Playwright UI-Sensor | Auffindbar / sichtbar / erreichbar? | Impressum „leicht erkennbar" | re-routed |
|
||||
| **PROCESS** | Audit / Evidence | Gibt es einen internen Nachweis? | VVT, TOM, interne Richtlinie | Checkliste |
|
||||
| **TECHNICAL** | Scanner (Repo / Netz / Config) | Ist die technische Maßnahme implementiert? | geplant: CRA, NIS2, ISO 27001 | offen |
|
||||
| **CONTRACTUAL** | Clause-Engine | Ist die Klausel vorhanden + rechtskonform? | AGB (delivery/warranty; Defekte → Stage 3) | teilweise |
|
||||
|
||||
**CONTENT vs CONTRACTUAL:** CONTENT = Offenlegungs-Prosa (DSE nennt Zwecke). CONTRACTUAL = Vertragsklauseln (AGB-Haftung/Lieferung). Beide können Embedding+LLM nutzen — die Trennung ist die Rechtsnatur + die spätere Defekt-Prüfung (Klausel rechtswidrig?).
|
||||
|
||||
**PRESENTATION ≠ BEHAVIOR:** beide Playwright, andere Rechtslogik. PRESENTATION = Auffindbarkeit/Sichtbarkeit; BEHAVIOR = Entscheidungs-Manipulation/Dark-Pattern.
|
||||
|
||||
## 3. Achse 2 — `decision_method` (WIE innerhalb CONTENT/CONTRACTUAL entschieden wird)
|
||||
|
||||
Die AGB-Entdeckung: **Controls INNERHALB eines Prüfer-Typs brauchen verschiedene Entscheider.** Eskalation nur bei Bedarf (Kostendisziplin):
|
||||
|
||||
| decision_method | Mechanismus | Wann | Beleg (AGB) |
|
||||
|---|---|---|---|
|
||||
| **KEYWORD** | Regex-Match | Pflicht eindeutig formuliert | Keyword-Layer |
|
||||
| **EMBEDDING** | per-Item-Cosinus-Schwelle (Doc-Chunks × Item-Paraphrasen) | Prosa, semantisch trennbar | 13/21 Items, 0 Fehl-Rescue |
|
||||
| **LLM** | Clause-Retrieval (**ganze §-Abschnitte**) + starkes Modell, present/absent | semantisch eng (Embedding trennt nicht) | 2/21 Items (delivery/warranty), 14/14 |
|
||||
|
||||
`CONTENT_SIMPLE` = KEYWORD/EMBEDDING reicht; `CONTENT_COMPLEX` = LLM nötig. AGB-Bilanz: **81 % deterministisch, 19 % LLM-fähig**, LLM real nur bei Keyword-Miss.
|
||||
|
||||
## 4. Durable Per-Control-Metadaten (das Routing-Vokabular)
|
||||
|
||||
| Feld | Zweck |
|
||||
|---|---|
|
||||
| `artifact_type` | gegen welches Artefakt geprüft wird → Scanner-Routing |
|
||||
| `obligation_type` | Rechtsnatur: Pflicht / Empfehlung / Kann → Tier |
|
||||
| `check_intent` | was die Prüfung bezweckt |
|
||||
| `reference_allowed` | darf per Verweis erfüllt werden → REFERENCE statt CONTENT |
|
||||
| `scope` / `scope_requires` | Applicability-Gate (Geschäftsmodell, Rechtsform) — **vor** allen Prüfern |
|
||||
| `verification_method` | Achse 1 (Prüfer-Typ) |
|
||||
| `decision_method` | Achse 2 (Entscheider innerhalb CONTENT/CONTRACTUAL) |
|
||||
| `severity` | HIGH / MEDIUM / LOW → Finding vs Empfehlung |
|
||||
|
||||
## 5. Hart erarbeitete Plattform-Prinzipien
|
||||
|
||||
1. **Route, don't uniformly-LLM** — verschiedene Controls, verschiedene Prüfer.
|
||||
2. Eskaliere **KEYWORD → EMBEDDING → LLM nur bei Bedarf** (AGB: 17/21 ohne LLM).
|
||||
3. Embedding: **per-Item-Schwellen** (globale Schwelle scheitert bei juristischer Prosa — PASS/FAIL überlappen global, trennen per-Item).
|
||||
4. LLM-Judge: **ganze §-Abschnitte** schlagen Top-k-Chunks; **starken Tier pinnen** (billig-zuerst-Kaskade eskaliert selbstbewusst-falsche Antworten NICHT, weil die Confidence-Heuristik genauigkeits-blind ist); **present/absent** trennen von der Defekt-Prüfung.
|
||||
5. **REFERENCE (Link) ist ein eigener billiger Prüfer** — keinen „siehe Datenschutzerklärung"-Verweis durch ein LLM jagen.
|
||||
6. **SCOPE-Gate (Applicability) ist vor allen Prüfern** — N/A-Controls werden nie geprüft.
|
||||
7. **Severity → Finding vs Empfehlung** (Tier, nicht droppen).
|
||||
8. *Was im Text nicht beweisbar ist, gehört nicht in den Text-Check.*
|
||||
|
||||
## 6. Schema-Status
|
||||
|
||||
Kein DB-Eingriff (DB eingefroren). `verification_method` + `decision_method` als **abgeleitete Tags** in `control_classification` (aus `artifact_type` / `obligation_type` / `check_intent` + Item-Kalibrierung). `canonical_controls.verification_method` existiert (~4 % befüllt, gröbere Enterprise-Taxonomie) — **nicht** das Doc-Check-Routing.
|
||||
|
||||
## 7. Verbindlichkeit
|
||||
|
||||
Dies ist der **Vertrag**, gegen den implementiert wird. Die AGB-Integration und die nächsten Module (Nutzungsbedingungen, Widerruf, CRA, MaschVO, DORA, NIS2, ISO 27001, AI-Act, VVT, TOM) bauen **dieselbe** Routing-Schicht — nicht modul-lokal. Reihenfolge: **(1) diese Matrix einfrieren → (2) AGB integrieren → (3) Nutzungsbedingungen → (4) Widerruf.**
|
||||
@@ -0,0 +1,64 @@
|
||||
# BreakPilot — Evidenz- & Qualitätsnachweis (Website-Compliance v1)
|
||||
|
||||
> **Status:** konsolidierter Freeze-Stand 2026-06-21. Belegbasis aus 4 kalibrierten Modulen (DSE, Cookie, Impressum, AGB). Dient als (a) technischer Freeze-Record und (b) Backbone für Sales/Investoren.
|
||||
> **Hinweis:** Zahlen = *gemessene* Validierungsergebnisse gegen Opus-Ground-Truth. Tool-/Prod-Integrationsstand je Modul siehe §7 (validiert ≠ überall schon live).
|
||||
|
||||
## 1. Kernaussage
|
||||
|
||||
Die meisten Compliance-Tools machen: **Dokument → LLM → Finding** — ein Richter für alles. Das erzeugt systematische False Positives und hat *keine* belastbare Evidenzbasis.
|
||||
|
||||
BreakPilot macht: **Dokument → Control-Routing → spezialisierter Prüfer → Finding.**
|
||||
|
||||
> Wir haben **für jeden Kontrolltyp den optimalen Prüfer empirisch ermittelt** — mit echten Vorher/Nachher-Zahlen, nicht mit Marketing.
|
||||
|
||||
Das ist über 4 strukturell verschiedene Dokumenttypen reproduzierbar belegt — und damit voraussichtlich das Routing-Prinzip für alle ~14.000 Master Controls.
|
||||
|
||||
## 2. Die Architektur (zwei Routing-Achsen)
|
||||
|
||||
Vollständige Kette: **Regulation → Obligation → Control → verification_method → decision_method → Prüfer → Evidence → Finding → Ticket.**
|
||||
|
||||
- **`verification_method`** (Kategorie / welcher Prüfer-Typ): CONTENT · FIELD · REFERENCE · BEHAVIOR · PRESENTATION · PROCESS · TECHNICAL · CONTRACTUAL.
|
||||
- **`decision_method`** (konkreter Mechanismus): REGEX · EMBEDDING · LLM · LINK_RESOLVER · PLAYWRIGHT · AUDIT · SCANNER.
|
||||
|
||||
Kernregel: *Was im Text nicht beweisbar ist, gehört nicht in den Text-Check.* Scope-Gate (Applicability) läuft vor allen Prüfern; Severity steuert Finding vs. Empfehlung.
|
||||
|
||||
## 3. Evidenz je Modul
|
||||
|
||||
| Modul | dominanter Prüfer | gemessenes Ergebnis | Hebel | Reife |
|
||||
|---|---|---|---|---|
|
||||
| **DSE** | CONTENT (Embedding+LLM) | False Positives **11 % → 6 %**; an **8 Firmen** validiert, Generalisierung nachgewiesen (kein Overfit auf einen Assessor); Claude-Tier-Pfad → ~2 % bekannt | Kriterien-Kalibrierung + LLM-Kaskade | **RC** |
|
||||
| **Impressum** | FIELD + PRESENTATION (+ Scope-Gate) | **171 falsche Findings → 0** (Scope-Gate); Feldmatrix (Firma/Anschrift/HRB/USt-IdNr/Kontakt) **FP 0 %, Recall 1.0**; 5 Präsentations-Controls an Playwright re-routet | Scope-Gate + deterministischer Feld-Matcher schlägt LLM | **RC** |
|
||||
| **Cookie** | BEHAVIOR + CONTENT | Artifact-Type-Trennung **Banner ≠ Richtlinie** validiert (Controls liefen am falschen Artefakt → re-routet); Browser-Verhaltens-Matrix (Enforcement, Dark-Pattern, Reject=Accept) | Artifact-Type-Routing + Playwright-Verhaltenssensor | Wave-1 (GT-Stab. offen) |
|
||||
| **AGB** | CONTENT + REFERENCE + LLM | **71 % FP → ~0** (7-Firmen-Opus-GT): 49 Findings / 35 falsch → bereinigt; Embedding-Rescue **21 Recall-FP gekillt, 0 Fehl-Rescue**; LLM-Judge (ganze §-Abschnitte) **14/14**; Reference-Check **7/7** | **decision_method pro Item** (17 EMBEDDING, 2 LLM, 1 REFERENCE) | Architektur validiert |
|
||||
|
||||
## 4. Warum die Zahlen belastbar sind (Methodik-Rigor)
|
||||
|
||||
- **Ground Truth mit dem stärksten Modell** (Opus-4-8), nicht mit billigen Modellen.
|
||||
- **Prove-don't-handwave:** echte FP/FN-Zählungen, Vorher/Nachher, keine Behauptungen.
|
||||
- **Generalisierung statt Overfit:** Mehr-Firmen-GT (DSE 8, AGB 7) + explizite Leitplanken gegen Ein-Assessor-Overfit.
|
||||
- **Mehrfach-Referenz-Validierung:** bei AGB 3-Wege (Opus-GT × Claude-Eigenbewertung × Laufzeit-Kaskade) — deckte sogar einen Fehler in der GT selbst auf.
|
||||
- **Stichprobe vor Aufbau:** vor jeder teuren Klassifikation/Batch zuerst stratifizierte Stichprobe geprüft (verhinderte mehrfach Aufbau auf falschem Fundament).
|
||||
|
||||
## 5. Die Schlüssel-Entdeckung (AGB)
|
||||
|
||||
Verschiedene Controls **innerhalb desselben Moduls** brauchen verschiedene Richter. Belege:
|
||||
- Eine **globale** Embedding-Schwelle scheitert bei juristischer Prosa; **per-Item-Schwellen** trennen sauber.
|
||||
- **Whole-Section-Retrieval** (ganze §-Abschnitte) schlägt Top-k-Chunks für den LLM-Judge deutlich.
|
||||
- Ein **billig-zuerst-Kaskaden-LLM** taugt nicht als Richter (eskaliert selbstbewusst-falsche Antworten nicht) — für harte Items starken Tier pinnen.
|
||||
- Ein **Verweis** („siehe Datenschutzerklärung") ist ein REFERENCE/Link-Check, **kein** LLM-Fall.
|
||||
|
||||
## 6. Wettbewerbspositionierung
|
||||
|
||||
| | Typisches Tool | BreakPilot |
|
||||
|---|---|---|
|
||||
| Prüfansatz | ein LLM für alles | Control-Routing → spezialisierter Prüfer |
|
||||
| False Positives | systematisch (LLM auf Nicht-Text-Pflichten) | je Kontrolltyp minimiert (gemessen) |
|
||||
| Evidenzbasis | keine | Mehr-Firmen-GT, reproduzierbare Zahlen |
|
||||
| Skalierung neuer Regulierungen | jedes Mal neu | Mapping auf bestehende Prüfer-Matrix |
|
||||
|
||||
## 7. Reifegrad, Ehrlichkeit & Roadmap
|
||||
|
||||
- **Validiert (Messung):** alle 4 Module oben.
|
||||
- **Live im Tool:** DSE-Kriterien (prod). Impressum-Scope/Feldmatrix, Cookie-Artifact-Type und AGB-C-lean sind **validiert, aber noch nicht überall ins Produkt integriert** → Demo-Integration ist der nächste Schritt (Vorher/Nachher live zeigbar machen).
|
||||
- **Website-/Marketing-Compliance: abgeschlossen** (DSE/Impressum/Cookie/AGB + Architektur). Restliche Web-Doc-Typen (Nutzungsbedingungen, Shop-AGB, Legal Notice, Social-Media) = **Mapping**, keine neue Architektur.
|
||||
- **Nächste große Etappe (nach Sales):** industrielle Compliance (CRA, Maschinenverordnung, NIS2, DORA, ISO 27001, TISAX, AI Act) — neue Prüfertypen TECHNICAL/PROCESS/EVIDENCE/SYSTEM; die Prüfer-Matrix wird dort wiederverwendet.
|
||||
@@ -0,0 +1,74 @@
|
||||
# Plattform-Validierung der Doc-Check-Kalibrierung — `platform_validation_v1`
|
||||
|
||||
> **Status:** Plattform-Methodik validiert über 3 strukturell verschiedene Dokumentklassen (2026-06-19).
|
||||
> **Zweck:** Nicht ein Modul dokumentieren, sondern den **Kalibrierungsprozess** und die **empirische Fehlerkarte** der Engine — damit die *Ursachen* erhalten bleiben (nicht nur die Messwerte). Erkenntnis > Metrik.
|
||||
|
||||
## 1. Was hier validiert wurde
|
||||
|
||||
Vor dieser Runde war unklar, ob der Restfehler der Doc-Check-Engine aus dem **LLM**, dem **Embedding**, dem **Prompt**, der **Applicability** oder dem **Control-Katalog** stammt — alles vermischt. Nach DSE + Cookie + Impressum existiert eine **belastbare Taxonomie der Fehlerursachen**, und der **Kalibrierungsprozess** hat in drei sehr unterschiedlichen Domänen geliefert. Das ist die eigentliche Errungenschaft — größer als jede einzelne Zahl.
|
||||
|
||||
## 2. Der Kalibrierungsprozess (wiederverwendbarer Kern)
|
||||
|
||||
1. **Opus-GT** je `(Firma × Control)` über 5–9 repräsentative Firmen (stärkstes Modell, NICHT Haiku).
|
||||
2. **Engine-Messung** (Keyword → BGE-M3-Embedding → robuster LLM-Judge) vs GT.
|
||||
3. **FP-Cluster** — wiederkehrende Controls statt Einzel-Findings (systematisch ≠ zufällig).
|
||||
4. **Ursachen-Klassifikation** je FP: `SCOPE` / `ARTIFACT_TYPE` / `CRITERIA` / `JUDGE`.
|
||||
5. **Fix** der dominanten Ursache (versioniert, mit Rechtsnotiz).
|
||||
6. **Re-Messung** — Pflicht: FP↓ **und** FN stabil. Plus **Anti-Overfit** auf ungesehenen Firmen.
|
||||
|
||||
## 3. Plattform-Fehlerkarte (Kernergebnis)
|
||||
|
||||
| Modul | Dominante Ursache | Hebel | Ergebnis | Status |
|
||||
|---|---|---|---|---|
|
||||
| **DSE** | Kriterien zu streng | Kriterien-Kalibrierung (11 Controls) | FP 11 % → **6 %**, FN ~7 %; **generalisiert** (8 Firmen; fresh FP 7 % / FN 5 %) | Release-Candidate |
|
||||
| **Cookie** | `artifact_type` (Banner ≠ Richtlinie) | 31 Banner-Controls → `COOKIE_BANNER`; 21 Kriterien (Kategorie statt Pro-Cookie, Zitat optional), Pro-Cookie = Best-Practice | Precision 0,81 → **0,95**, Recall 0,26 → **0,44**, verpasste Lücken → **0 %**, abs. FP 71 → 54 | Wave-1 (dev) |
|
||||
| **Impressum** | **Scope** (GT-NA 48 %) + **Feld-Extraktion** + **Präsentation** | Scope-Gate (14 raus) + **Feldmatrix-Matcher** (Fakten) + **PRESENTATION_CHECK**-Re-Route (5) | roh: SCOPE-FP 105 / JUDGE-FP 66 → **Text-Check FP 0 % / FN 2 %** | Release-Candidate |
|
||||
|
||||
## 4. Meta-Befunde
|
||||
|
||||
- **Die generische Architektur bewährt sich.** Jede Domäne hat ein *anderes* dominantes Problem — `artifact_type` / `obligation_type` / `scope` tragen unterschiedlich stark. Eine gute generische Architektur erzeugt nicht überall denselben Effekt, sondern löst je Domäne ein anderes Problem. Genau das ist eingetreten.
|
||||
- **Die Zielarchitektur ist domänen-adaptiv, nicht uniform.** „Embedding → OVH → Claude" ist nicht überall richtig: bei **Prosa** (DSE/Cookie) ist die LLM-Kaskade der Hebel; bei **strukturierten Faktendokumenten** (Impressum) ist das LLM sogar schwach (es verfehlt Adressen/Felder, die *dastehen*) → dort schlagen **Scope-Gate + deterministischer Feld-Matcher** den LLM-Judge.
|
||||
- **Wiederkehrendes Anti-Muster:** „vermeintlicher Judge-Fehler → eigentlich Katalog-Fehler" (Scope, Präsentations-statt-Inhalt, Fehl-Typisierung). Erst NACH den Katalog-Fixes ist der Rest ein *echter* Judge-Fehler.
|
||||
|
||||
## 4b. Die `verification_method`-Achse (Synthese — die eigentliche Lehre)
|
||||
|
||||
Nicht jede Compliance-Pflicht ist ein Textproblem. Die 5 entdeckten Fehlerklassen mappen auf **5 Prüfer-Typen** — eine neue Routing-Metadaten-Achse `verification_method`, die einem Control sagt, *welcher Prüfer* zuständig ist (nicht alles an den LLM):
|
||||
|
||||
| `verification_method` | Prüfer | Frage | Beispiel | Status |
|
||||
|---|---|---|---|---|
|
||||
| **CONTENT** | Embedding + LLM-Kaskade (OVH→Claude) | Was steht da? | DSE nennt Zwecke; Cookie-Policy | DSE/Cookie kalibriert |
|
||||
| **FIELD** | Regex/Parser (Feldmatrix) | Welche Felder existieren? | HRB, USt-IdNr, Adresse | Impressum-Fakten ✓ (FP 0 %) |
|
||||
| **PRESENTATION** | Playwright (Sichtbarkeit/Erreichbarkeit) | Ist es auffindbar/wahrnehmbar? | Impressum leicht erkennbar, ständig verfügbar; Footer nicht verdeckt | Re-Route gemacht; Check offen |
|
||||
| **BEHAVIOR** | Playwright + API (Interaktion) | Manipuliert es die Entscheidung? | Reject = Accept, Consent VOR Cookie, kein Dark Pattern | Cookie-Banner-Matrix existiert |
|
||||
| **PROCESS** | Audit/Nachweis | Gibt es internen Nachweis? | VVT, interne Richtlinie, Audit-Entscheidung | Org-Checkliste |
|
||||
|
||||
**PRESENTATION ≠ BEHAVIOR** (beide Playwright, andere Rechtslogik): Präsentation = *Auffindbarkeit/Sichtbarkeit/Zugänglichkeit* (Impressum leicht erkennbar); Behavior = *Entscheidungs-Manipulation/Dark-Pattern* (Reject versteckt). Getrennt halten.
|
||||
|
||||
**Playwright wird damit vom Crawler zum Compliance-Sensor:** es prüft, was kein LLM kann — `display:none`, `font-size:4px`, Cookie-Layer verdeckt den Footer. LLM sieht `<a href="/impressum">` und sagt „erfüllt"; Playwright sieht die Verdeckung und sagt „nicht erfüllt".
|
||||
|
||||
**Kern-Regel der Architektur:** *Was im Text nicht beweisbar ist, gehört nicht in den Text-Check.* → route per `verification_method`. Sobald die Klassen sauber getrennt sind, sinken die FP fast automatisch (Impressum: SCOPE+JUDGE 171 → Text-Check-FP 0).
|
||||
|
||||
**Schema-Status:** `canonical_controls.verification_method` existiert (nur ~4 % befüllt, andere/gröbere Taxonomie document/code_review/tool/hybrid), `doc_check_controls` hat sie nicht. Die hier definierte Doc-Check-Routing-Achse ist **aus `control_classification` (artifact_type/obligation_type/check_intent) ableitbar** → kein Schema-Eingriff (eingefroren) nötig; als abgeleitetes Tag in `control_classification` führen.
|
||||
|
||||
## 5. Mess-Disziplin (prove-don't-handwave)
|
||||
|
||||
- GT mit dem stärksten Modell (`claude-opus-4-8`), nicht Haiku (zu lasch).
|
||||
- Robust gegen LLM-Leerantworten: Retry + `INSUFFICIENT_EVIDENCE`/Eskalation statt FEHLT (ein realer Produktions-Bug, der die FP künstlich aufblähte).
|
||||
- **Anti-Overfit:** Kriterien am Gesetz kalibrieren, dann auf *ungesehenen* Firmen gegenprüfen (DSE: 5 Original + 3 frische → stabile Zahlen = kein Overfit).
|
||||
- OVH ist stochastisch (±~Rauschen je Lauf) und strenger als Opus → der Rest-FP konvergiert über Module auf **OVH-Über-Strenge**.
|
||||
- **Zirkularitäts-Leitplanke:** Claude = Opus-GT-Modell → ein Claude-Tier-Sim misst die *Kaskaden-Reichweite* (erreicht Opus-Niveau), nicht eine unabhängige Validierung.
|
||||
|
||||
## 6. Offen (Reihenfolge)
|
||||
|
||||
1. **Claude-Tier-Sim (DSE + Cookie):** quantifiziert den verbleibenden **reinen** Judge-Fehler nach allen Katalog-Fixes — die letzte große unbekannte Variable. Erwartung: kleiner als roh, weil viel „Judge" sich als Katalog entpuppte.
|
||||
2. **Impressum-Fix:** Rechtsform-Scope-Gate (#33) verdrahten + deterministischer Feld-Matcher + Re-Messung.
|
||||
3. **Cookie Wave-2** (Cluster-E) + Produktions-Re-Route der 31 Banner-Controls (`control_classification`).
|
||||
4. **Produktivschaltung** DSE + Cookie (zuletzt; verify-first DB-Write).
|
||||
|
||||
## 7. Artefakte
|
||||
|
||||
- DSE: `docs-src/development/dse_v1_validation.md`, `dse_criteria_changelog.json`/`dse_criteria_backup.json`.
|
||||
- Cookie: `cookie_criteria_changelog.json`/`cookie_criteria_backup.json`/`cookie_best_practice.json` (Container `/tmp`), Cluster-Map.
|
||||
- Impressum: `impressum_fp_by_cause.json` (SCOPE/JUDGE-Split).
|
||||
- Gedächtnis: `project_engine_quality.md` (Detail je Modul). Werkzeuge: `cc_gt_opus_*`, `cc_engine_*`, `cc_*_candidates*` (alle macmini `/tmp`).
|
||||
- **Alle Control-Änderungen nur auf macmini-dev**, versioniert, reversibel; Prod-Schaltung ausstehend.
|
||||
@@ -0,0 +1,59 @@
|
||||
# `verification_method` — die Prüfer-Routing-Achse
|
||||
|
||||
> **Status:** Architektur-Achse (2026-06-19), abgeleitet aus der 3-Modul-Kalibrierung (DSE / Cookie / Impressum).
|
||||
> **Kernsatz:** *Nicht jede Compliance-Pflicht ist ein Textproblem.* `verification_method` sagt einem Control, **welcher Prüfer** zuständig ist — damit nicht alles am LLM hängt.
|
||||
|
||||
## 1. Warum diese Achse existiert
|
||||
|
||||
Die Kalibrierung von drei strukturell verschiedenen Dokumentklassen zeigte drei **verschiedene** dominante Fehlerursachen — und alle ließen sich auf die *Wahl des falschen Prüfers* zurückführen:
|
||||
|
||||
- **DSE** (Prosa): LLM-Urteil zu streng → Kriterien-Kalibrierung. Prüfer war richtig (LLM), Kriterien falsch.
|
||||
- **Cookie** (Banner ≠ Richtlinie): Controls am falschen Artefakt geprüft → `artifact_type`-Re-Route.
|
||||
- **Impressum** (Faktendokument): LLM verfehlt Felder, die *dastehen* (Adresse, HRB) → deterministischer Feld-Matcher schlägt den LLM. Und 5 Controls waren **gar nicht im Text beweisbar** (Erreichbarkeit/Verfügbarkeit) → gehören an Playwright, nicht an den Text-Check.
|
||||
|
||||
**Regel:** *Was im Text nicht beweisbar ist, gehört nicht in den Text-Check.* Sobald die Klassen sauber getrennt sind, sinken die False Positives fast automatisch (Beleg Impressum: SCOPE+JUDGE 171 Roh-FP → Text-Check-FP 0).
|
||||
|
||||
## 2. Die fünf Klassen
|
||||
|
||||
| `verification_method` | Prüfer | Leitfrage | Beispiel | Reifegrad |
|
||||
|---|---|---|---|---|
|
||||
| **CONTENT** | Embedding-Recall + LLM-Kaskade (OVH→Claude) | Was steht da? | DSE nennt Verarbeitungszwecke; Cookie-Richtlinie | DSE/Cookie kalibriert |
|
||||
| **FIELD** | Regex / Parser (Feldmatrix) | Welche Felder existieren + sind valide? | HRB, USt-IdNr, Anschrift, E-Mail+Telefon | Impressum-Fakten ✓ (FP 0 %) |
|
||||
| **PRESENTATION** | Playwright (Rendering-Sensor) | Ist es auffindbar / wahrnehmbar / erreichbar? | Impressum „leicht erkennbar", ständig verfügbar, Footer nicht verdeckt | Re-Route gemacht, Checker offen |
|
||||
| **BEHAVIOR** | Playwright + API (Interaktion) | Manipuliert die UI die Entscheidung? | Reject = Accept, Cookies VOR Consent, Dark Pattern | Cookie-Banner-Matrix vorhanden |
|
||||
| **PROCESS** | Audit / Nachweis | Gibt es einen internen Nachweis? | VVT, interne Richtlinie, Audit-Entscheidung | Org-Checkliste |
|
||||
|
||||
## 3. PRESENTATION ≠ BEHAVIOR
|
||||
|
||||
Beide nutzen Playwright, prüfen aber **verschiedene Rechtslogik** — getrennt halten:
|
||||
|
||||
- **PRESENTATION** = Auffindbarkeit / Sichtbarkeit / Zugänglichkeit. Beispiel: Impressum-Link erreichbar, nicht in 4px-Schrift, nicht hinter `display:none`, nicht dauerhaft vom Cookie-Layer verdeckt.
|
||||
- **BEHAVIOR** = Entscheidungs-Manipulation / Dark-Pattern. Beispiel: „Ablehnen" versteckt, Vorauswahl gesetzt, Consent technisch ignoriert.
|
||||
|
||||
## 4. Playwright als Compliance-Sensor (nicht Crawler)
|
||||
|
||||
Playwright prüft, was **kein** LLM kann: Der LLM sieht `<a href="/impressum">` und urteilt „erfüllt"; der Sensor sieht, dass das Element verdeckt / unsichtbar / unerreichbar ist und urteilt „nicht erfüllt". Drei technische Prüfer langfristig:
|
||||
|
||||
- **Content-Checker** → LLM (CONTENT)
|
||||
- **Structure-Checker** → Regex/Parser (FIELD)
|
||||
- **Presentation-Checker** → Playwright (PRESENTATION + BEHAVIOR)
|
||||
|
||||
## 5. Schema-Status & Verortung
|
||||
|
||||
- `canonical_controls.verification_method` **existiert**, aber nur ~4 % befüllt und mit *anderer*, gröberer Taxonomie (`document` / `code_review` / `tool` / `hybrid` — generische Enterprise-Verifikation, nicht das Doc-Check-Routing).
|
||||
- `doc_check_controls` hat **keine** `verification_method`-Spalte.
|
||||
- → Die hier definierte Doc-Check-Routing-Achse ist **neu**, aber **ableitbar** aus den schon vorhandenen `control_classification`-Achsen (`artifact_type` / `obligation_type` / `check_intent`). **Kein** Schema-Eingriff nötig (DB ist eingefroren) — als abgeleitetes Tag in `control_classification` führen.
|
||||
|
||||
Heuristik für die Ableitung (Startpunkt, nicht final):
|
||||
|
||||
| Signal | → verification_method |
|
||||
|---|---|
|
||||
| `artifact_type = COOKIE_BANNER`, Interaktionspflicht | BEHAVIOR |
|
||||
| Pflicht zu Erreichbarkeit / Sichtbarkeit / „ständig verfügbar" | PRESENTATION |
|
||||
| Faktenfeld (Anschrift, Register, Kennung) | FIELD |
|
||||
| `obligation_type` Prozess / Nachweis ohne Außenwirkung | PROCESS |
|
||||
| sonst (inhaltliche Offenlegung in Prosa) | CONTENT |
|
||||
|
||||
## 6. Warum das über die 3 Module hinaus zählt
|
||||
|
||||
Für die nächsten Module (CRA, Maschinenverordnung, NIS2, TISAX, ISO 27001) ist diese Achse vermutlich fast so wichtig wie `artifact_type`: viele dieser Pflichten sind **PROCESS** oder **BEHAVIOR**, kein Textinhalt. Wer sie an den LLM-Text-Check hängt, erzeugt systematische False Positives. Das ist die eigentliche Erkenntnis der Kalibrierung: **nicht** dass DSE/Cookie/Impressum funktionieren, sondern dass klar wurde, *welcher Prüfer für welche Art von Pflicht zuständig ist*.
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"DATA-2260-A01": {
|
||||
"title": "Primären Verarbeitungszweck schriftlich und verständlich dokumentieren",
|
||||
"check_question": "Ist der primäre Verarbeitungszweck schriftlich und verständlich dokumentiert?",
|
||||
"pass_criteria": "[\"primärer Verarbeitungszweck verständlich beschrieben\", \"Zweck der Datenerhebung nachvollziehbar genannt\"]",
|
||||
"fail_criteria": "[\"Primärzweck nicht schriftlich dokumentiert\", \"Unverständliche oder zu technische Formulierung\", \"Zu allgemeine Beschreibung ohne konkrete Bezüge\"]"
|
||||
},
|
||||
"AUTH-3737-A06": {
|
||||
"title": "Zwecke von Datenübermittlungen dokumentieren",
|
||||
"check_question": "Sind die Zwecke aller Datenübermittlungen transparent und nachvollziehbar dokumentiert?",
|
||||
"pass_criteria": "[\"Explizite Zweckangabe für jede Datenübermittlung (z.B. 'Vertragserfüllung', 'Rechtliche Verpflichtung')\", \"Rechtsgrundlage für die jeweilige Übermittlung (Art. 6 DSGVO oder spezifische Norm)\", \"Empfänger und Empfängerkategorie mit Zweckbindung\", \"Dokumentation der Zwecke in verständlicher Form für Betroffene\", \"Unterscheidung zwischen verschiedenen Übermittlungszwecken\"]",
|
||||
"fail_criteria": "[\"Generische Zweckangaben wie 'geschäftliche Zwecke' ohne Konkretisierung\", \"Fehlende Rechtsgrundlage für die Übermittlung\", \"Keine Dokumentation der Zwecke oder nur mündliche Absprachen\"]"
|
||||
},
|
||||
"DATA-2992-A03": {
|
||||
"title": "Weiterübertragung an Drittparteien dokumentieren (Zweck, Rechtsgrundlage)",
|
||||
"check_question": "Dokumentiert die Datenschutzinformation für jede Weiterübertragung an Drittparteien den Zweck und die Rechtsgrundlage?",
|
||||
"pass_criteria": "[\"Für jeden Drittpartner-Transfer: Expliziter Zweck dokumentiert (z.B. 'Zahlungsabwicklung', 'Kundenservice')\", \"Rechtsgrundlage für die Weiterübertragung genannt (z.B. 'Vertragserfüllung mit Kunde', 'Einwilligung des Betroffenen')\", \"Unterscheidung zwischen Auftragsverarbeiter und eigenverantwortlichem Verantwortlicher\", \"Informationen zu Weitergabebeschränkungen oder Vertraulichkeitsverpflichtungen\"]",
|
||||
"fail_criteria": "[\"Drittparteien genannt, aber Zweck oder Rechtsgrundlage fehlt\", \"Pauschalaussage wie 'Daten werden an Partner weitergegeben' ohne Spezifizierung\", \"Keine Unterscheidung zwischen verschiedenen Weiterübertragungsszenarien\"]"
|
||||
},
|
||||
"DATA-1624-A03": {
|
||||
"title": "Verweis auf Garantien für Drittlandtransfer bereitstellen",
|
||||
"check_question": "Werden betroffene Personen über alternative Garantien für Drittlandtransfers (falls kein Angemessenheitsbeschluss) informiert und auf diese verwiesen?",
|
||||
"pass_criteria": "[\"Aufzählung der angewendeten Transfermechanismen (z.B. 'Standardvertragsklauseln', 'Binding Corporate Rules', 'Zertifizierungen')\", \"Konkrete Beschreibung jedes Mechanismus und dessen Schutzwirkung in verständlicher Sprache\", \"Angabe, wie Betroffene die Garantiedokumente einsehen können (mit Kontaktdaten oder Link)\", \"Hinweis auf Rechte der Betroffenen (z.B. Recht auf Beschwerde, Recht auf Auskunft über Schutzmaßnahmen)\"]",
|
||||
"fail_criteria": "[\"Nur Nennung von Transfermechanismen ohne Erklärung oder Zugriff auf Dokumente\", \"Unvollständige Aufzählung (z.B. nur SCCs erwähnt, aber auch BCR verwendet)\", \"Garantien werden erwähnt, sind aber nicht tatsächlich implementiert oder dokumentiert\"]"
|
||||
},
|
||||
"DATA-1619-A03": {
|
||||
"title": "Verarbeitungszwecke und Rechtsgrundlage offenlegen",
|
||||
"check_question": "Sind Verarbeitungszwecke und Rechtsgrundlagen klar und verständlich offengelegt?",
|
||||
"pass_criteria": "[\"Konkrete Verarbeitungszwecke benannt (z.B. 'Vertragserfüllung', 'Rechnungsstellung', 'Kundenservice')\", \"Spezifische Rechtsgrundlage mit Artikel genannt (z.B. 'Art. 6 Abs. 1 Buchstabe b DSGVO')\", \"Unterscheidung zwischen verschiedenen Verarbeitungszwecken mit jeweiliger Rechtsgrundlage\", \"Verständliche Sprache ohne juristische Fachbegriffe oder mit Erklärung\", \"Trennung von Pflichtangaben und freiwilligen Verarbeitungen\"]",
|
||||
"fail_criteria": "[\"Zweck nur allgemein formuliert ('geschäftliche Zwecke', 'interne Nutzung')\", \"Rechtsgrundlage fehlt oder nur 'DSGVO' ohne Artikel und Absatz\", \"Mehrere Zwecke ohne klare Zuordnung zu Rechtsgrundlagen\", \"Unverständliche juristische Formulierungen ohne Erklärung\"]"
|
||||
},
|
||||
"DATA-424-A09": {
|
||||
"title": "Datenübertragbarkeit bei Einwilligung oder Vertrag ermöglichen",
|
||||
"check_question": "Dokumentiert die Datenschutzinformation die Bereitstellung von Daten in maschinenlesbarem Format für Fälle mit Einwilligung oder Vertrag als Rechtsgrundlage?",
|
||||
"pass_criteria": "[\"Datenübertragbarkeit bei Einwilligung oder Vertrag erwähnt\", \"maschinenlesbares Format genannt\"]",
|
||||
"fail_criteria": "[\"Maschinenlesbare Formate werden nicht angeboten\", \"Keine Differenzierung nach Rechtsgrundlagen\", \"Abruf nur in unstrukturierten Formaten (z.B. PDF) möglich\"]"
|
||||
},
|
||||
"GOV-3300-A06": {
|
||||
"title": "Daten in maschinenlesbaren Formaten bei Datenportierung bereitstellen",
|
||||
"check_question": "Stellt die Datenschutzinformation sicher, dass Betroffene ihre Daten bei Datenportierungsanfragen in maschinenlesbaren Formaten erhalten?",
|
||||
"pass_criteria": "[\"Recht auf Datenübertragbarkeit erwähnt\", \"strukturiertes oder maschinenlesbares Format genannt\"]",
|
||||
"fail_criteria": "[\"Nur Bereitstellung in nicht-maschinenlesbaren Formaten (PDF, Papier)\", \"Vage Aussagen zu 'gängigen Formaten' ohne konkrete Nennung\", \"Einschränkung auf proprietäre oder nicht-standardisierte Formate\"]"
|
||||
},
|
||||
"AI-1560-A01": {
|
||||
"title": "Zwecke der Datenverwendung dokumentieren",
|
||||
"check_question": "Sind die Zwecke der Datenverwendung transparent und DSGVO-konform dokumentiert?",
|
||||
"pass_criteria": "[\"Schriftliche Dokumentation aller Verarbeitungszwecke\", \"Verständliche Darstellung für Betroffene (keine Fachjargon ohne Erklärung)\", \"Einhaltung des Zweckbindungsprinzips (Zwecke sind spezifisch und nicht beliebig erweiterbar)\", \"Dokumentation der Zwecke in der Datenschutzerklärung oder Datenschutzinformation\", \"Angabe von Speicherdauer in Bezug auf Verarbeitungszwecke\"]",
|
||||
"fail_criteria": "[\"Unklare oder mehrdeutige Zweckbeschreibungen\", \"Fehlende Dokumentation in Datenschutzerklärung\", \"Zu breite Zweckdefinitionen, die Zweckentfremdung ermöglichen\"]"
|
||||
},
|
||||
"SEC-3444-A04": {
|
||||
"title": "Sekundärverarbeitungen auf Notwendigkeit beschränken",
|
||||
"check_question": "Beschränkt die Datenschutzinformation Sekundärverarbeitungen von Adressendaten auf die ursprünglichen Zwecke und notwendige Folgemaßnahmen?",
|
||||
"pass_criteria": "[\"ursprünglicher Verarbeitungszweck benannt\", \"Zweckbindung der Daten angegeben\"]",
|
||||
"fail_criteria": "[\"Uneingeschränkte Erlaubnis zur Datennutzung für beliebige Zwecke\", \"Keine Differenzierung zwischen ursprünglichem und neuem Zweck\", \"Fehlende Nennung konkreter Folgemaßnahmen\"]"
|
||||
},
|
||||
"DATA-1624-A06": {
|
||||
"title": "Übermittlung von Drittland-Schutzgarantie-Informationen verifizieren",
|
||||
"check_question": "Informiert die Datenschutzinformation betroffene Personen über die angewendeten Schutzmechanismen bei Datenübermittlungen in Drittländer (Adequacy Decisions, SCCs, BCRs)?",
|
||||
"pass_criteria": "[\"Explizite Nennung der angewendeten Schutzmechanismen (z.B. 'Adequacy Decision der EU-Kommission', 'Standarddatenschutzklauseln', 'Binding Corporate Rules')\", \"Angabe der betroffenen Drittländer oder Regionen\", \"Beschreibung der Garantien und Schutzmaßnahmen für die Datenübermittlung\", \"Verweis auf Dokumentation oder Rechtsgrundlagen (z.B. Verträge, Entscheidungen)\", \"Information über Rechte der betroffenen Person bei Drittlandtransfers\"]",
|
||||
"fail_criteria": "[\"Nur pauschale Aussage 'Daten werden geschützt übermittelt' ohne Nennung konkreter Mechanismen\", \"Aufzählung von Drittländern ohne Angabe der Schutzmechanismen\", \"Fehlende Differenzierung zwischen verschiedenen Übermittlungsszenarien\"]"
|
||||
},
|
||||
"DATA-2812-A05": {
|
||||
"title": "Löschungsrecht für Cookies und Speicherdaten implementieren",
|
||||
"check_question": "Wird in der Datenschutzinformation das Recht auf Löschung von in Cookies und Speichermechanismen abgelegten personenbezogenen Daten beschrieben?",
|
||||
"pass_criteria": "[\"Recht auf Löschung von Cookie- oder Speicherdaten beschrieben\", \"Verwaltung oder Löschung von Cookies angesprochen\"]",
|
||||
"fail_criteria": "[\"Cookies werden nicht erwähnt oder als unvermeidbar dargestellt\", \"Keine Anleitung zur Löschung oder Verwaltung von Cookies\", \"Keine Möglichkeit zur Ablehnung oder zum Widerruf von Cookies beschrieben\"]"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
[
|
||||
{
|
||||
"control_id": "DATA-2260-A01",
|
||||
"klasse": "B",
|
||||
"legal_note": "Art. 13(1)(c) DSGVO verlangt 'die Zwecke' (Plural) — keinen einzelnen 'primären' Zweck und keine Priorisierung. Mehrere genannte Zwecke erfüllen die Pflicht.",
|
||||
"changed_fields": [
|
||||
"title",
|
||||
"check_question",
|
||||
"pass_criteria",
|
||||
"fail_criteria"
|
||||
],
|
||||
"old": {
|
||||
"title": "Primären Verarbeitungszweck schriftlich und verständlich dokumentieren",
|
||||
"check_question": "Ist der primäre Verarbeitungszweck schriftlich und verständlich dokumentiert?",
|
||||
"pass_criteria": "[\"primärer Verarbeitungszweck verständlich beschrieben\", \"Zweck der Datenerhebung nachvollziehbar genannt\"]",
|
||||
"fail_criteria": "[\"Primärzweck nicht schriftlich dokumentiert\", \"Unverständliche oder zu technische Formulierung\", \"Zu allgemeine Beschreibung ohne konkrete Bezüge\"]"
|
||||
},
|
||||
"new": {
|
||||
"title": "Verarbeitungszweck(e) schriftlich und verständlich dokumentieren",
|
||||
"check_question": "Sind die Verarbeitungszwecke schriftlich und verständlich genannt?",
|
||||
"pass_criteria": [
|
||||
"Verarbeitungszweck(e) verständlich beschrieben",
|
||||
"Zweck der Datenerhebung nachvollziehbar genannt"
|
||||
],
|
||||
"fail_criteria": [
|
||||
"Verarbeitungszwecke nicht genannt",
|
||||
"Unverständliche oder zu technische Formulierung",
|
||||
"Nur pauschale Floskel ohne konkreten Zweckbezug"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"control_id": "AUTH-3737-A06",
|
||||
"klasse": "B",
|
||||
"legal_note": "Art. 13(1)(c)+(e) verlangt Zwecke + Empfänger(kategorien) — keine vollständige Zweck/Rechtsgrundlage-Matrix je einzelner Übermittlung.",
|
||||
"changed_fields": [
|
||||
"pass_criteria",
|
||||
"fail_criteria"
|
||||
],
|
||||
"old": {
|
||||
"pass_criteria": "[\"Explizite Zweckangabe für jede Datenübermittlung (z.B. 'Vertragserfüllung', 'Rechtliche Verpflichtung')\", \"Rechtsgrundlage für die jeweilige Übermittlung (Art. 6 DSGVO oder spezifische Norm)\", \"Empfänger und Empfängerkategorie mit Zweckbindung\", \"Dokumentation der Zwecke in verständlicher Form für Betroffene\", \"Unterscheidung zwischen verschiedenen Übermittlungszwecken\"]",
|
||||
"fail_criteria": "[\"Generische Zweckangaben wie 'geschäftliche Zwecke' ohne Konkretisierung\", \"Fehlende Rechtsgrundlage für die Übermittlung\", \"Keine Dokumentation der Zwecke oder nur mündliche Absprachen\"]"
|
||||
},
|
||||
"new": {
|
||||
"pass_criteria": [
|
||||
"Zwecke der Datenübermittlungen genannt",
|
||||
"Empfänger oder Empfängerkategorien angegeben",
|
||||
"verständliche Darstellung für Betroffene"
|
||||
],
|
||||
"fail_criteria": [
|
||||
"keine Angabe von Übermittlungszwecken",
|
||||
"weder Empfänger noch Empfängerkategorien genannt",
|
||||
"nur pauschale Floskel ('geschäftliche Zwecke') ohne jeden Bezug"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"control_id": "DATA-2992-A03",
|
||||
"klasse": "B",
|
||||
"legal_note": "Art. 13(1)(e) verlangt Empfänger/Kategorien; die DSE muss keine AV-/Verantwortlicher-Distinktion, keine Vertraulichkeitszusage und keine Rechtsgrundlage je Empfänger ausweisen.",
|
||||
"changed_fields": [
|
||||
"pass_criteria",
|
||||
"fail_criteria"
|
||||
],
|
||||
"old": {
|
||||
"pass_criteria": "[\"Für jeden Drittpartner-Transfer: Expliziter Zweck dokumentiert (z.B. 'Zahlungsabwicklung', 'Kundenservice')\", \"Rechtsgrundlage für die Weiterübertragung genannt (z.B. 'Vertragserfüllung mit Kunde', 'Einwilligung des Betroffenen')\", \"Unterscheidung zwischen Auftragsverarbeiter und eigenverantwortlichem Verantwortlicher\", \"Informationen zu Weitergabebeschränkungen oder Vertraulichkeitsverpflichtungen\"]",
|
||||
"fail_criteria": "[\"Drittparteien genannt, aber Zweck oder Rechtsgrundlage fehlt\", \"Pauschalaussage wie 'Daten werden an Partner weitergegeben' ohne Spezifizierung\", \"Keine Unterscheidung zwischen verschiedenen Weiterübertragungsszenarien\"]"
|
||||
},
|
||||
"new": {
|
||||
"pass_criteria": [
|
||||
"Weitergabe an Dritte offengelegt (Empfänger oder Kategorien)",
|
||||
"Zweck der Weitergabe genannt"
|
||||
],
|
||||
"fail_criteria": [
|
||||
"Weitergabe verschwiegen",
|
||||
"weder Empfänger/Kategorie noch Zweck genannt",
|
||||
"nur pauschal 'Daten werden an Partner weitergegeben' ohne jeden Bezug"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"control_id": "DATA-1624-A03",
|
||||
"klasse": "B",
|
||||
"legal_note": "Art. 13(1)(f) verlangt Verweis auf geeignete Garantien + wie eine Kopie erhältlich/wo verfügbar ist. Ein Link genügt; eine verständliche Beschreibung der Schutzwirkung ist nicht gefordert.",
|
||||
"changed_fields": [
|
||||
"pass_criteria",
|
||||
"fail_criteria"
|
||||
],
|
||||
"old": {
|
||||
"pass_criteria": "[\"Aufzählung der angewendeten Transfermechanismen (z.B. 'Standardvertragsklauseln', 'Binding Corporate Rules', 'Zertifizierungen')\", \"Konkrete Beschreibung jedes Mechanismus und dessen Schutzwirkung in verständlicher Sprache\", \"Angabe, wie Betroffene die Garantiedokumente einsehen können (mit Kontaktdaten oder Link)\", \"Hinweis auf Rechte der Betroffenen (z.B. Recht auf Beschwerde, Recht auf Auskunft über Schutzmaßnahmen)\"]",
|
||||
"fail_criteria": "[\"Nur Nennung von Transfermechanismen ohne Erklärung oder Zugriff auf Dokumente\", \"Unvollständige Aufzählung (z.B. nur SCCs erwähnt, aber auch BCR verwendet)\", \"Garantien werden erwähnt, sind aber nicht tatsächlich implementiert oder dokumentiert\"]"
|
||||
},
|
||||
"new": {
|
||||
"pass_criteria": [
|
||||
"geeignete Garantie genannt (z.B. SCC/BCR/Zertifizierung)",
|
||||
"Zugang zu den Garantien angegeben (Link ODER Kontakt)"
|
||||
],
|
||||
"fail_criteria": [
|
||||
"Drittlandtransfer ohne Nennung einer Garantie",
|
||||
"Garantie genannt, aber keinerlei Möglichkeit sie einzusehen (kein Link und kein Kontakt)"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"control_id": "DATA-1619-A03",
|
||||
"klasse": "B",
|
||||
"legal_note": "Art. 13(1)(c) verlangt die Rechtsgrundlage; die Nennung des konkreten Artikels (Art. 6 Abs. 1 lit. ...) ist gute Praxis, aber nicht zwingend; ebenso wenig die Trennung Pflicht/freiwillig.",
|
||||
"changed_fields": [
|
||||
"pass_criteria",
|
||||
"fail_criteria"
|
||||
],
|
||||
"old": {
|
||||
"pass_criteria": "[\"Konkrete Verarbeitungszwecke benannt (z.B. 'Vertragserfüllung', 'Rechnungsstellung', 'Kundenservice')\", \"Spezifische Rechtsgrundlage mit Artikel genannt (z.B. 'Art. 6 Abs. 1 Buchstabe b DSGVO')\", \"Unterscheidung zwischen verschiedenen Verarbeitungszwecken mit jeweiliger Rechtsgrundlage\", \"Verständliche Sprache ohne juristische Fachbegriffe oder mit Erklärung\", \"Trennung von Pflichtangaben und freiwilligen Verarbeitungen\"]",
|
||||
"fail_criteria": "[\"Zweck nur allgemein formuliert ('geschäftliche Zwecke', 'interne Nutzung')\", \"Rechtsgrundlage fehlt oder nur 'DSGVO' ohne Artikel und Absatz\", \"Mehrere Zwecke ohne klare Zuordnung zu Rechtsgrundlagen\", \"Unverständliche juristische Formulierungen ohne Erklärung\"]"
|
||||
},
|
||||
"new": {
|
||||
"pass_criteria": [
|
||||
"konkrete Verarbeitungszwecke benannt",
|
||||
"Rechtsgrundlage je Zweck genannt (Artikel-Zitat nicht zwingend)"
|
||||
],
|
||||
"fail_criteria": [
|
||||
"Zweck nur pauschal ('geschäftliche Zwecke')",
|
||||
"keine Rechtsgrundlage genannt"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"control_id": "DATA-424-A09",
|
||||
"klasse": "B",
|
||||
"legal_note": "Art. 20 DSGVO — die DSE informiert über das Recht; das konkrete Exportformat (CSV/JSON/XML) ist Umsetzungsfrage und muss in der DSE nicht benannt werden.",
|
||||
"changed_fields": [
|
||||
"title",
|
||||
"check_question",
|
||||
"pass_criteria",
|
||||
"fail_criteria"
|
||||
],
|
||||
"old": {
|
||||
"title": "Datenübertragbarkeit bei Einwilligung oder Vertrag ermöglichen",
|
||||
"check_question": "Dokumentiert die Datenschutzinformation die Bereitstellung von Daten in maschinenlesbarem Format für Fälle mit Einwilligung oder Vertrag als Rechtsgrundlage?",
|
||||
"pass_criteria": "[\"Datenübertragbarkeit bei Einwilligung oder Vertrag erwähnt\", \"maschinenlesbares Format genannt\"]",
|
||||
"fail_criteria": "[\"Maschinenlesbare Formate werden nicht angeboten\", \"Keine Differenzierung nach Rechtsgrundlagen\", \"Abruf nur in unstrukturierten Formaten (z.B. PDF) möglich\"]"
|
||||
},
|
||||
"new": {
|
||||
"title": "Recht auf Datenübertragbarkeit (Einwilligung/Vertrag) offenlegen",
|
||||
"check_question": "Informiert die Datenschutzinformation über das Recht auf Datenübertragbarkeit (bei Einwilligung oder Vertrag)?",
|
||||
"pass_criteria": [
|
||||
"Recht auf Datenübertragbarkeit erwähnt (bei Einwilligung oder Vertrag)"
|
||||
],
|
||||
"fail_criteria": [
|
||||
"Recht auf Datenübertragbarkeit nicht erwähnt"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"control_id": "GOV-3300-A06",
|
||||
"klasse": "B",
|
||||
"legal_note": "Wie DATA-424-A09 (Art. 20): Format-Nennung in der DSE nicht gefordert. Dedupe-Kandidat zu DATA-424-A09.",
|
||||
"changed_fields": [
|
||||
"pass_criteria",
|
||||
"fail_criteria"
|
||||
],
|
||||
"old": {
|
||||
"pass_criteria": "[\"Recht auf Datenübertragbarkeit erwähnt\", \"strukturiertes oder maschinenlesbares Format genannt\"]",
|
||||
"fail_criteria": "[\"Nur Bereitstellung in nicht-maschinenlesbaren Formaten (PDF, Papier)\", \"Vage Aussagen zu 'gängigen Formaten' ohne konkrete Nennung\", \"Einschränkung auf proprietäre oder nicht-standardisierte Formate\"]"
|
||||
},
|
||||
"new": {
|
||||
"pass_criteria": [
|
||||
"Recht auf Datenübertragbarkeit erwähnt"
|
||||
],
|
||||
"fail_criteria": [
|
||||
"Recht auf Datenübertragbarkeit nicht erwähnt"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"control_id": "AI-1560-A01",
|
||||
"klasse": "C",
|
||||
"legal_note": "Zweck-Offenlegung (Art. 13(1)(c)) und Speicherdauer (Art. 13(2)(a)) sind verschiedene Pflichten; die Speicherdauer-Forderung gehört nicht in den Zweck-Control und wird entfernt.",
|
||||
"changed_fields": [
|
||||
"pass_criteria",
|
||||
"fail_criteria"
|
||||
],
|
||||
"old": {
|
||||
"pass_criteria": "[\"Schriftliche Dokumentation aller Verarbeitungszwecke\", \"Verständliche Darstellung für Betroffene (keine Fachjargon ohne Erklärung)\", \"Einhaltung des Zweckbindungsprinzips (Zwecke sind spezifisch und nicht beliebig erweiterbar)\", \"Dokumentation der Zwecke in der Datenschutzerklärung oder Datenschutzinformation\", \"Angabe von Speicherdauer in Bezug auf Verarbeitungszwecke\"]",
|
||||
"fail_criteria": "[\"Unklare oder mehrdeutige Zweckbeschreibungen\", \"Fehlende Dokumentation in Datenschutzerklärung\", \"Zu breite Zweckdefinitionen, die Zweckentfremdung ermöglichen\"]"
|
||||
},
|
||||
"new": {
|
||||
"pass_criteria": [
|
||||
"Verarbeitungszwecke in der Datenschutzerklärung genannt",
|
||||
"verständliche Darstellung für Betroffene"
|
||||
],
|
||||
"fail_criteria": [
|
||||
"Verarbeitungszwecke nicht genannt",
|
||||
"nur unklare/zu breite Zweckfloskeln, die jede Nutzung erlauben"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"control_id": "SEC-3444-A04",
|
||||
"klasse": "C",
|
||||
"legal_note": "In der DSE wird die Zweckbindung OFFENGELEGT (Art. 13(1)(c)); ob Sekundärverarbeitung tatsächlich 'beschränkt' wird, ist eine Verhaltensfrage und aus dem Text nicht prüfbar. Titel/Frage an den Offenlegungs-Charakter angeglichen.",
|
||||
"changed_fields": [
|
||||
"title",
|
||||
"check_question",
|
||||
"pass_criteria",
|
||||
"fail_criteria"
|
||||
],
|
||||
"old": {
|
||||
"title": "Sekundärverarbeitungen auf Notwendigkeit beschränken",
|
||||
"check_question": "Beschränkt die Datenschutzinformation Sekundärverarbeitungen von Adressendaten auf die ursprünglichen Zwecke und notwendige Folgemaßnahmen?",
|
||||
"pass_criteria": "[\"ursprünglicher Verarbeitungszweck benannt\", \"Zweckbindung der Daten angegeben\"]",
|
||||
"fail_criteria": "[\"Uneingeschränkte Erlaubnis zur Datennutzung für beliebige Zwecke\", \"Keine Differenzierung zwischen ursprünglichem und neuem Zweck\", \"Fehlende Nennung konkreter Folgemaßnahmen\"]"
|
||||
},
|
||||
"new": {
|
||||
"title": "Zweckbindung der Datenverarbeitung offenlegen",
|
||||
"check_question": "Legt die Datenschutzinformation den ursprünglichen Zweck und die Zweckbindung der Daten offen?",
|
||||
"pass_criteria": [
|
||||
"ursprünglicher Verarbeitungszweck benannt",
|
||||
"Zweckbindung der Daten angegeben"
|
||||
],
|
||||
"fail_criteria": [
|
||||
"kein Zweck genannt",
|
||||
"uneingeschränkte Nutzung für beliebige Zwecke erlaubt"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"control_id": "DATA-1624-A06",
|
||||
"klasse": "C",
|
||||
"legal_note": "Art. 13(1)(f): Beschreibung der Schutzwirkung nicht gefordert. ⚠️ Near-Duplikat zu DATA-1624-A03 → Dedupe als separater Catalog-Schritt empfohlen (hier NICHT gelöscht).",
|
||||
"changed_fields": [
|
||||
"title",
|
||||
"check_question",
|
||||
"pass_criteria",
|
||||
"fail_criteria"
|
||||
],
|
||||
"old": {
|
||||
"title": "Übermittlung von Drittland-Schutzgarantie-Informationen verifizieren",
|
||||
"check_question": "Informiert die Datenschutzinformation betroffene Personen über die angewendeten Schutzmechanismen bei Datenübermittlungen in Drittländer (Adequacy Decisions, SCCs, BCRs)?",
|
||||
"pass_criteria": "[\"Explizite Nennung der angewendeten Schutzmechanismen (z.B. 'Adequacy Decision der EU-Kommission', 'Standarddatenschutzklauseln', 'Binding Corporate Rules')\", \"Angabe der betroffenen Drittländer oder Regionen\", \"Beschreibung der Garantien und Schutzmaßnahmen für die Datenübermittlung\", \"Verweis auf Dokumentation oder Rechtsgrundlagen (z.B. Verträge, Entscheidungen)\", \"Information über Rechte der betroffenen Person bei Drittlandtransfers\"]",
|
||||
"fail_criteria": "[\"Nur pauschale Aussage 'Daten werden geschützt übermittelt' ohne Nennung konkreter Mechanismen\", \"Aufzählung von Drittländern ohne Angabe der Schutzmechanismen\", \"Fehlende Differenzierung zwischen verschiedenen Übermittlungsszenarien\"]"
|
||||
},
|
||||
"new": {
|
||||
"title": "Schutzgarantien bei Drittlandübermittlung offenlegen",
|
||||
"check_question": "Informiert die Datenschutzinformation über die angewendeten Schutzmechanismen bei Drittlandtransfers?",
|
||||
"pass_criteria": [
|
||||
"Schutzmechanismus genannt (Adäquanzbeschluss/SCC/BCR)",
|
||||
"betroffene Drittländer oder Regionen angegeben",
|
||||
"Zugang/Verweis zur Garantie angegeben (Link oder Kontakt)"
|
||||
],
|
||||
"fail_criteria": [
|
||||
"Drittlandtransfer ohne Nennung eines Schutzmechanismus",
|
||||
"nur pauschal 'Daten werden geschützt übermittelt'"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"control_id": "DATA-2812-A05",
|
||||
"klasse": "C",
|
||||
"legal_note": "DSE-Offenlegung des Lösch-/Verwaltungswegs (Art. 17 / §25 TTDSG-Kontext); ein Verweis auf Cookie-Einstellungen/Banner oder Browser genügt — eine Schritt-für-Schritt-Anleitung ist nicht gefordert. FRAGE war bereits disclosure-framed → bleibt DSE.",
|
||||
"changed_fields": [
|
||||
"title",
|
||||
"pass_criteria",
|
||||
"fail_criteria"
|
||||
],
|
||||
"old": {
|
||||
"title": "Löschungsrecht für Cookies und Speicherdaten implementieren",
|
||||
"pass_criteria": "[\"Recht auf Löschung von Cookie- oder Speicherdaten beschrieben\", \"Verwaltung oder Löschung von Cookies angesprochen\"]",
|
||||
"fail_criteria": "[\"Cookies werden nicht erwähnt oder als unvermeidbar dargestellt\", \"Keine Anleitung zur Löschung oder Verwaltung von Cookies\", \"Keine Möglichkeit zur Ablehnung oder zum Widerruf von Cookies beschrieben\"]"
|
||||
},
|
||||
"new": {
|
||||
"title": "Recht auf Löschung von Cookie-/Speicherdaten offenlegen",
|
||||
"pass_criteria": [
|
||||
"Recht auf Löschung/Verwaltung von Cookie- bzw. Speicherdaten beschrieben",
|
||||
"Hinweis auf Verwaltungs-/Löschweg (Cookie-Einstellungen, Banner oder Browser) — Verweis/Link genügt"
|
||||
],
|
||||
"fail_criteria": [
|
||||
"Cookies/Speicherdaten ohne jeden Hinweis auf Löschung/Verwaltung",
|
||||
"Löschung/Verwaltung ausdrücklich verweigert"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -18,6 +18,7 @@ Run with --dry-run to preview deletions without executing.
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import requests
|
||||
@@ -33,7 +34,7 @@ TARGETS = {
|
||||
},
|
||||
"production": {
|
||||
"url": "https://qdrant-dev.breakpilot.ai",
|
||||
"api_key": "z9cKbT74vl1aKPD1QGIlKWfET47VH93u",
|
||||
"api_key": os.environ.get("QDRANT_API_KEY"),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -2,16 +2,23 @@
|
||||
# Emit per-service + aggregate change flags for the CI / build workflows.
|
||||
#
|
||||
# Reads:
|
||||
# BASE_SHA — diff base. Empty / unreachable → emit everything as true.
|
||||
# BASE_SHA — diff base. Empty / unreachable / diff-failure → emit everything as true.
|
||||
# HEAD_SHA — diff target. Defaults to HEAD.
|
||||
#
|
||||
# Writes key=value lines to $GITHUB_OUTPUT (defaults to /dev/stdout for local runs).
|
||||
#
|
||||
# ROBUSTNESS CONTRACT: this script must ALWAYS emit a full set of outputs. A
|
||||
# missing/empty output makes the job's `outputs:` mapping evaluate to a Go format
|
||||
# error (`%!t(string=)`) which fails detect-changes AND every job that `needs` it
|
||||
# (cascade). So we do NOT use `set -e` (an aborting git/grep must not kill the
|
||||
# script before it emits), we treat any base/diff failure as "rebuild all", and an
|
||||
# EXIT trap emits rebuild-all + forces exit 0 if we ever exit early/unexpectedly.
|
||||
#
|
||||
# Keys emitted:
|
||||
# admin, backend, sdk, portal, tts, crawler, dsms_gateway, dsms_node
|
||||
# any_python, any_node, any
|
||||
|
||||
set -euo pipefail
|
||||
set -uo pipefail
|
||||
|
||||
BASE_SHA="${BASE_SHA:-}"
|
||||
HEAD_SHA="${HEAD_SHA:-HEAD}"
|
||||
@@ -31,17 +38,27 @@ emit_all_true() {
|
||||
done
|
||||
}
|
||||
|
||||
# Safety net: never let the job end with undefined outputs. If we exit before
|
||||
# DONE=1 (any error / early termination), emit rebuild-all and exit 0 so the
|
||||
# step still succeeds — rebuild-all is the safe over-approximation.
|
||||
DONE=0
|
||||
trap '[ "$DONE" = 1 ] || emit_all_true "safety-net (unexpected exit)"; exit 0' EXIT
|
||||
|
||||
if [ -z "$BASE_SHA" ]; then
|
||||
emit_all_true "no BASE_SHA provided"
|
||||
exit 0
|
||||
DONE=1; exit 0
|
||||
fi
|
||||
|
||||
if ! git rev-parse --verify "${BASE_SHA}^{commit}" >/dev/null 2>&1; then
|
||||
emit_all_true "BASE_SHA ${BASE_SHA} unreachable"
|
||||
exit 0
|
||||
DONE=1; exit 0
|
||||
fi
|
||||
|
||||
if ! changed=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA" 2>/dev/null); then
|
||||
emit_all_true "git diff against ${BASE_SHA} failed"
|
||||
DONE=1; exit 0
|
||||
fi
|
||||
|
||||
changed=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA" || true)
|
||||
echo "Changed files since ${BASE_SHA}:"
|
||||
echo "${changed:-(none)}"
|
||||
echo "---"
|
||||
@@ -91,3 +108,5 @@ else
|
||||
emit any false
|
||||
echo " any: false"
|
||||
fi
|
||||
|
||||
DONE=1
|
||||
|
||||
@@ -6,6 +6,7 @@ Uses persistent HTTP sessions and rate limiting for hosted Qdrant.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import requests
|
||||
@@ -13,7 +14,7 @@ from urllib.parse import urljoin
|
||||
|
||||
SOURCE_URL = "http://macmini:6333"
|
||||
TARGET_URL = "https://qdrant-dev.breakpilot.ai"
|
||||
TARGET_API_KEY = "z9cKbT74vl1aKPD1QGIlKWfET47VH93u"
|
||||
TARGET_API_KEY = os.environ.get("QDRANT_API_KEY", "")
|
||||
BATCH_SIZE = 20
|
||||
RATE_LIMIT_DELAY = 0.3 # seconds between batches
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"""Delete eu_2023_988 duplicate from production Qdrant."""
|
||||
import httpx
|
||||
import os
|
||||
|
||||
PROD_URL = "https://qdrant-dev.breakpilot.ai"
|
||||
HEADERS = {"api-key": "z9cKbT74vl1aKPD1QGIlKWfET47VH93u"}
|
||||
HEADERS = {"api-key": os.environ.get("QDRANT_API_KEY", "")}
|
||||
|
||||
# Delete
|
||||
resp = httpx.post(
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,207 @@
|
||||
Datenschutz
|
||||
|
||||
Information zum Datenschutz für Microsoft Teams
|
||||
|
||||
Informationen zum Datenschutz bei Nutzung der Website
|
||||
|
||||
Datenschutzerklärung
|
||||
1. Datenschutz auf einen Blick
|
||||
Allgemeine Hinweise
|
||||
Die folgenden Hinweise geben einen einfachen Überblick darüber, was mit Ihren personenbezogenen Daten passiert, wenn Sie diese Website besuchen. Personenbezogene Daten sind alle Daten, mit denen Sie persönlich identifiziert werden können. Ausführliche Informationen zum Thema Datenschutz entnehmen Sie unserer unter diesem Text aufgeführten Datenschutzerklärung.
|
||||
|
||||
Datenerfassung auf dieser Website
|
||||
Wer ist verantwortlich für die Datenerfassung auf dieser Website?
|
||||
Die Datenverarbeitung auf dieser Website erfolgt durch den Websitebetreiber. Dessen Kontaktdaten können Sie dem Abschnitt „Hinweis zur Verantwortlichen Stelle" in dieser Datenschutzerklärung entnehmen.
|
||||
|
||||
Wie erfassen wir Ihre Daten?
|
||||
Ihre Daten werden zum einen dadurch erhoben, dass Sie uns diese mitteilen. Hierbei kann es sich z. B. um Daten handeln, die Sie in ein Kontaktformular eingeben.
|
||||
|
||||
Andere Daten werden automatisch oder nach Ihrer Einwilligung beim Besuch der Website durch unsere IT-Systeme erfasst. Das sind vor allem technische Daten (z. B. Internetbrowser, Betriebssystem oder Uhrzeit des Seitenaufrufs). Die Erfassung dieser Daten erfolgt automatisch, sobald Sie diese Website betreten.
|
||||
|
||||
Wofür nutzen wir Ihre Daten?
|
||||
Ein Teil der Daten wird erhoben, um eine fehlerfreie Bereitstellung der Website zu gewährleisten. Andere Daten können zur Analyse Ihres Nutzerverhaltens verwendet werden.
|
||||
|
||||
Welche Rechte haben Sie bezüglich Ihrer Daten?
|
||||
Sie haben jederzeit das Recht, unentgeltlich Auskunft über Herkunft, Empfänger und Zweck Ihrer gespeicherten personenbezogenen Daten zu erhalten. Sie haben außerdem ein Recht, die Berichtigung oder Löschung dieser Daten zu verlangen. Wenn Sie eine Einwilligung zur Datenverarbeitung erteilt haben, können Sie diese Einwilligung jederzeit für die Zukunft widerrufen. Außerdem haben Sie das Recht, unter bestimmten Umständen die Einschränkung der Verarbeitung Ihrer personenbezogenen Daten zu verlangen. Des Weiteren steht Ihnen ein Beschwerderecht bei der zuständigen Aufsichtsbehörde zu.
|
||||
|
||||
Hierzu sowie zu weiteren Fragen zum Thema Datenschutz können Sie sich jederzeit an uns wenden.
|
||||
|
||||
Analyse-Tools und Tools von Drittanbietern
|
||||
Beim Besuch dieser Website kann Ihr Surf-Verhalten statistisch ausgewertet werden. Das geschieht vor allem mit sogenannten Analyseprogrammen.
|
||||
|
||||
Detaillierte Informationen zu diesen Analyseprogrammen finden Sie in der folgenden Datenschutzerklärung.
|
||||
|
||||
2. Hosting
|
||||
Wir hosten die Inhalte unserer Website bei folgendem Anbieter:
|
||||
|
||||
All-Inkl
|
||||
Anbieter ist die ALL-INKL.COM - Neue Medien Münnich, Inh. René Münnich, Hauptstraße 68, 02742 Friedersdorf (nachfolgend All-Inkl). Details entnehmen Sie der Datenschutzerklärung von All-Inkl: https://all-inkl.com/datenschutzinformationen/.
|
||||
|
||||
Die Verwendung von All-Inkl erfolgt auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO. Wir haben ein berechtigtes Interesse an einer möglichst zuverlässigen Darstellung unserer Website. Sofern eine entsprechende Einwilligung abgefragt wurde, erfolgt die Verarbeitung ausschließlich auf Grundlage von Art. 6 Abs. 1 lit. a DSGVO und § 25 Abs. 1 TDDDG, soweit die Einwilligung die Speicherung von Cookies oder den Zugriff auf Informationen im Endgerät des Nutzers (z. B. Device-Fingerprinting) im Sinne des TDDDG umfasst. Die Einwilligung ist jederzeit widerrufbar.
|
||||
|
||||
Auftragsverarbeitung
|
||||
Wir haben einen Vertrag über Auftragsverarbeitung (AVV) zur Nutzung des oben genannten Dienstes geschlossen. Hierbei handelt es sich um einen datenschutzrechtlich vorgeschriebenen Vertrag, der gewährleistet, dass dieser die personenbezogenen Daten unserer Websitebesucher nur nach unseren Weisungen und unter Einhaltung der DSGVO verarbeitet.
|
||||
|
||||
3. Allgemeine Hinweise und Pflichtinformationen
|
||||
Datenschutz
|
||||
Die Betreiber dieser Seiten nehmen den Schutz Ihrer persönlichen Daten sehr ernst. Wir behandeln Ihre personenbezogenen Daten vertraulich und entsprechend den gesetzlichen Datenschutzvorschriften sowie dieser Datenschutzerklärung.
|
||||
|
||||
Wenn Sie diese Website benutzen, werden verschiedene personenbezogene Daten erhoben. Personenbezogene Daten sind Daten, mit denen Sie persönlich identifiziert werden können. Die vorliegende Datenschutzerklärung erläutert, welche Daten wir erheben und wofür wir sie nutzen. Sie erläutert auch, wie und zu welchem Zweck das geschieht.
|
||||
|
||||
Wir weisen darauf hin, dass die Datenübertragung im Internet (z. B. bei der Kommunikation per E-Mail) Sicherheitslücken aufweisen kann. Ein lückenloser Schutz der Daten vor dem Zugriff durch Dritte ist nicht möglich.
|
||||
|
||||
Hinweis zur verantwortlichen Stelle
|
||||
Die verantwortliche Stelle für die Datenverarbeitung auf dieser Website ist:
|
||||
|
||||
ETO GRUPPE TECHNOLOGIES GmbH
|
||||
Hardtring 8
|
||||
78333 Stockach
|
||||
DEUTSCHLAND
|
||||
|
||||
Telefon: +49 7771 809-0
|
||||
E-Mail: info@etogruppe.com
|
||||
|
||||
Verantwortliche Stelle ist die natürliche oder juristische Person, die allein oder gemeinsam mit anderen über die Zwecke und Mittel der Verarbeitung von personenbezogenen Daten (z. B. Namen, E-Mail-Adressen o. Ä.) entscheidet.
|
||||
|
||||
Speicherdauer
|
||||
Soweit innerhalb dieser Datenschutzerklärung keine speziellere Speicherdauer genannt wurde, verbleiben Ihre personenbezogenen Daten bei uns, bis der Zweck für die Datenverarbeitung entfällt. Wenn Sie ein berechtigtes Löschersuchen geltend machen oder eine Einwilligung zur Datenverarbeitung widerrufen, werden Ihre Daten gelöscht, sofern wir keine anderen rechtlich zulässigen Gründe für die Speicherung Ihrer personenbezogenen Daten haben (z. B. steuer- oder handelsrechtliche Aufbewahrungsfristen); im letztgenannten Fall erfolgt die Löschung nach Fortfall dieser Gründe.
|
||||
|
||||
Allgemeine Hinweise zu den Rechtsgrundlagen der Datenverarbeitung auf dieser Website
|
||||
Sofern Sie in die Datenverarbeitung eingewilligt haben, verarbeiten wir Ihre personenbezogenen Daten auf Grundlage von Art. 6 Abs. 1 lit. a DSGVO bzw. Art. 9 Abs. 2 lit. a DSGVO, sofern besondere Datenkategorien nach Art. 9 Abs. 1 DSGVO verarbeitet werden. Im Falle einer ausdrücklichen Einwilligung in die Übertragung personenbezogener Daten in Drittstaaten erfolgt die Datenverarbeitung außerdem auf Grundlage von Art. 49 Abs. 1 lit. a DSGVO. Sofern Sie in die Speicherung von Cookies oder in den Zugriff auf Informationen in Ihr Endgerät (z. B. via Device-Fingerprinting) eingewilligt haben, erfolgt die Datenverarbeitung zusätzlich auf Grundlage von § 25 Abs. 1 TDDDG. Die Einwilligung ist jederzeit widerrufbar. Sind Ihre Daten zur Vertragserfüllung oder zur Durchführung vorvertraglicher Maßnahmen erforderlich, verarbeiten wir Ihre Daten auf Grundlage des Art. 6 Abs. 1 lit. b DSGVO. Des Weiteren verarbeiten wir Ihre Daten, sofern diese zur Erfüllung einer rechtlichen Verpflichtung erforderlich sind auf Grundlage von Art. 6 Abs. 1 lit. c DSGVO. Die Datenverarbeitung kann ferner auf Grundlage unseres berechtigten Interesses nach Art. 6 Abs. 1 lit. f DSGVO erfolgen. Über die jeweils im Einzelfall einschlägigen Rechtsgrundlagen wird in den folgenden Absätzen dieser Datenschutzerklärung informiert.
|
||||
|
||||
Datenschutzbeauftragter
|
||||
Wir haben einen Datenschutzbeauftragten benannt.
|
||||
|
||||
ETO GRUPPE TECHNOLOGIES GmbH
|
||||
Hardtring 8
|
||||
78333 Stockach
|
||||
|
||||
Telefon: Telefon: +49 7771 809-0
|
||||
E-Mail: info@etogruppe.com
|
||||
|
||||
Hinweis zur Datenweitergabe in datenschutzrechtlich nicht sichere Drittstaaten sowie die Weitergabe an US-Unternehmen, die nicht DPF-zertifiziert sind
|
||||
Wir verwenden unter anderem Tools von Unternehmen mit Sitz in datenschutzrechtlich nicht sicheren Drittstaaten sowie US-Tools, deren Anbieter nicht nach dem EU-US-Data Privacy Framework (DPF) zertifiziert sind. Wenn diese Tools aktiv sind, können Ihre personenbezogene Daten in diese Staaten übertragen und dort verarbeitet werden. Wir weisen darauf hin, dass in datenschutzrechtlich unsicheren Drittstaaten kein mit der EU vergleichbares Datenschutzniveau garantiert werden kann.
|
||||
|
||||
Wir weisen darauf hin, dass die USA als sicherer Drittstaat grundsätzlich ein mit der EU vergleichbares Datenschutzniveau aufweisen. Eine Datenübertragung in die USA ist danach zulässig, wenn der Empfänger eine Zertifizierung unter dem „EU-US Data Privacy Framework" (DPF) besitzt oder über geeignete zusätzliche Garantien verfügt. Informationen zu Übermittlungen an Drittstaaten einschließlich der Datenempfänger finden Sie in dieser Datenschutzerklärung.
|
||||
|
||||
Empfänger von personenbezogenen Daten
|
||||
Im Rahmen unserer Geschäftstätigkeit arbeiten wir mit verschiedenen externen Stellen zusammen. Dabei ist teilweise auch eine Übermittlung von personenbezogenen Daten an diese externen Stellen erforderlich. Wir geben personenbezogene Daten nur dann an externe Stellen weiter, wenn dies im Rahmen einer Vertragserfüllung erforderlich ist, wenn wir gesetzlich hierzu verpflichtet sind (z. B. Weitergabe von Daten an Steuerbehörden), wenn wir ein berechtigtes Interesse nach Art. 6 Abs. 1 lit. f DSGVO an der Weitergabe haben oder wenn eine sonstige Rechtsgrundlage die Datenweitergabe erlaubt. Beim Einsatz von Auftragsverarbeitern geben wir personenbezogene Daten unserer Kunden nur auf Grundlage eines gültigen Vertrags über Auftragsverarbeitung weiter. Im Falle einer gemeinsamen Verarbeitung wird ein Vertrag über gemeinsame Verarbeitung geschlossen.
|
||||
|
||||
Widerruf Ihrer Einwilligung zur Datenverarbeitung
|
||||
Viele Datenverarbeitungsvorgänge sind nur mit Ihrer ausdrücklichen Einwilligung möglich. Sie können eine bereits erteilte Einwilligung jederzeit widerrufen. Die Rechtmäßigkeit der bis zum Widerruf erfolgten Datenverarbeitung bleibt vom Widerruf unberührt.
|
||||
|
||||
Widerspruchsrecht gegen die Datenerhebung in besonderen Fällen sowie gegen Direktwerbung (Art. 21 DSGVO)
|
||||
Wenn die Datenverarbeitung auf Grundlage von Art. 6 Abs. 1 lit. e oder f DSGVO erfolgt, haben Sie jederzeit das Recht, aus Gründen, die sich aus Ihrer besonderen Situation ergeben, gegen die Verarbeitung Ihrer personenbezogenen Daten Widerspruch einzulegen; dies gilt auch für ein auf diese Bestimmungen gestütztes Profiling. Die jeweilige Rechtsgrundlage, auf denen eine Verarbeitung beruht, entnehmen Sie dieser Datenschutzerklärung. Wenn Sie Widerspruch einlegen, werden wir Ihre betroffenen personenbezogenen Daten nicht mehr verarbeiten, es sei denn, wir können zwingende schutzwürdige Gründe für die Verarbeitung nachweisen, die Ihre Interessen, Rechte und Freiheiten überwiegen oder die Verarbeitung dient der Geltendmachung, Ausübung oder Verteidigung von Rechtsansprüchen (Widerspruch nach Art. 21 Abs. 1 DSGVO).
|
||||
|
||||
Werden Ihre personenbezogenen Daten verarbeitet, um Direktwerbung zu betreiben, so haben Sie das Recht, jederzeit Widerspruch gegen die Verarbeitung Sie betreffender personenbezogener Daten zum Zwecke derartiger Werbung einzulegen; dies gilt auch für das Profiling, soweit es mit solcher Direktwerbung in Verbindung steht. Wenn Sie widersprechen, werden Ihre personenbezogenen Daten anschließend nicht mehr zum Zwecke der Direktwerbung verwendet (Widerspruch nach Art. 21 Abs. 2 DSGVO).
|
||||
|
||||
Beschwerderecht bei der zuständigen Aufsichtsbehörde
|
||||
Im Falle von Verstößen gegen die DSGVO steht den Betroffenen ein Beschwerderecht bei einer Aufsichtsbehörde, insbesondere in dem Mitgliedstaat ihres gewöhnlichen Aufenthalts, ihres Arbeitsplatzes oder des Orts des mutmaßlichen Verstoßes zu. Das Beschwerderecht besteht unbeschadet anderweitiger verwaltungsrechtlicher oder gerichtlicher Rechtsbehelfe.
|
||||
|
||||
Recht auf Datenübertragbarkeit
|
||||
Sie haben das Recht, Daten, die wir auf Grundlage Ihrer Einwilligung oder in Erfüllung eines Vertrags automatisiert verarbeiten, an sich oder an einen Dritten in einem gängigen, maschinenlesbaren Format aushändigen zu lassen. Sofern Sie die direkte Übertragung der Daten an einen anderen Verantwortlichen verlangen, erfolgt dies nur, soweit es technisch machbar ist.
|
||||
|
||||
Auskunft, Berichtigung und Löschung
|
||||
Sie haben im Rahmen der geltenden gesetzlichen Bestimmungen jederzeit das Recht auf unentgeltliche Auskunft über Ihre gespeicherten personenbezogenen Daten, deren Herkunft und Empfänger und den Zweck der Datenverarbeitung und ggf. ein Recht auf Berichtigung oder Löschung dieser Daten. Hierzu sowie zu weiteren Fragen zum Thema personenbezogene Daten können Sie sich jederzeit an uns wenden.
|
||||
|
||||
Recht auf Einschränkung der Verarbeitung
|
||||
Sie haben das Recht, die Einschränkung der Verarbeitung Ihrer personenbezogenen Daten zu verlangen. Hierzu können Sie sich jederzeit an uns wenden. Das Recht auf Einschränkung der Verarbeitung besteht in folgenden Fällen:
|
||||
|
||||
Wenn Sie die Richtigkeit Ihrer bei uns gespeicherten personenbezogenen Daten bestreiten, benötigen wir in der Regel Zeit, um dies zu überprüfen. Für die Dauer der Prüfung haben Sie das Recht, die Einschränkung der Verarbeitung Ihrer personenbezogenen Daten zu verlangen.
|
||||
Wenn die Verarbeitung Ihrer personenbezogenen Daten unrechtmäßig geschah/geschieht, können Sie statt der Löschung die Einschränkung der Datenverarbeitung verlangen.
|
||||
Wenn wir Ihre personenbezogenen Daten nicht mehr benötigen, Sie sie jedoch zur Ausübung, Verteidigung oder Geltendmachung von Rechtsansprüchen benötigen, haben Sie das Recht, statt der Löschung die Einschränkung der Verarbeitung Ihrer personenbezogenen Daten zu verlangen.
|
||||
Wenn Sie einen Widerspruch nach Art. 21 Abs. 1 DSGVO eingelegt haben, muss eine Abwägung zwischen Ihren und unseren Interessen vorgenommen werden. Solange noch nicht feststeht, wessen Interessen überwiegen, haben Sie das Recht, die Einschränkung der Verarbeitung Ihrer personenbezogenen Daten zu verlangen.
|
||||
Wenn Sie die Verarbeitung Ihrer personenbezogenen Daten eingeschränkt haben, dürfen diese Daten – von ihrer Speicherung abgesehen – nur mit Ihrer Einwilligung oder zur Geltendmachung, Ausübung oder Verteidigung von Rechtsansprüchen oder zum Schutz der Rechte einer anderen natürlichen oder juristischen Person oder aus Gründen eines wichtigen öffentlichen Interesses der Europäischen Union oder eines Mitgliedstaats verarbeitet werden.
|
||||
|
||||
SSL- bzw. TLS-Verschlüsselung
|
||||
Diese Seite nutzt aus Sicherheitsgründen und zum Schutz der Übertragung vertraulicher Inhalte, wie zum Beispiel Bestellungen oder Anfragen, die Sie an uns als Seitenbetreiber senden, eine SSL- bzw. TLS-Verschlüsselung. Eine verschlüsselte Verbindung erkennen Sie daran, dass die Adresszeile des Browsers von „http://" auf „https://" wechselt und an dem Schloss-Symbol in Ihrer Browserzeile.
|
||||
|
||||
Wenn die SSL- bzw. TLS-Verschlüsselung aktiviert ist, können die Daten, die Sie an uns übermitteln, nicht von Dritten mitgelesen werden.
|
||||
|
||||
4. Datenerfassung auf dieser Website
|
||||
Cookies
|
||||
Unsere Internetseiten verwenden so genannte „Cookies". Cookies sind kleine Datenpakete und richten auf Ihrem Endgerät keinen Schaden an. Sie werden entweder vorübergehend für die Dauer einer Sitzung (Session-Cookies) oder dauerhaft (permanente Cookies) auf Ihrem Endgerät gespeichert. Session-Cookies werden nach Ende Ihres Besuchs automatisch gelöscht. Permanente Cookies bleiben auf Ihrem Endgerät gespeichert, bis Sie diese selbst löschen oder eine automatische Löschung durch Ihren Webbrowser erfolgt.
|
||||
|
||||
Cookies können von uns (First-Party-Cookies) oder von Drittunternehmen stammen (sog. Third-Party-Cookies). Third-Party-Cookies ermöglichen die Einbindung bestimmter Dienstleistungen von Drittunternehmen innerhalb von Webseiten (z. B. Cookies zur Abwicklung von Zahlungsdienstleistungen).
|
||||
|
||||
Cookies haben verschiedene Funktionen. Zahlreiche Cookies sind technisch notwendig, da bestimmte Webseitenfunktionen ohne diese nicht funktionieren würden (z. B. die Warenkorbfunktion oder die Anzeige von Videos). Andere Cookies können zur Auswertung des Nutzerverhaltens oder zu Werbezwecken verwendet werden.
|
||||
|
||||
Cookies, die zur Durchführung des elektronischen Kommunikationsvorgangs, zur Bereitstellung bestimmter, von Ihnen erwünschter Funktionen (z. B. für die Warenkorbfunktion) oder zur Optimierung der Website (z. B. Cookies zur Messung des Webpublikums) erforderlich sind (notwendige Cookies), werden auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO gespeichert, sofern keine andere Rechtsgrundlage angegeben wird. Der Websitebetreiber hat ein berechtigtes Interesse an der Speicherung von notwendigen Cookies zur technisch fehlerfreien und optimierten Bereitstellung seiner Dienste. Sofern eine Einwilligung zur Speicherung von Cookies und vergleichbaren Wiedererkennungstechnologien abgefragt wurde, erfolgt die Verarbeitung ausschließlich auf Grundlage dieser Einwilligung (Art. 6 Abs. 1 lit. a DSGVO und § 25 Abs. 1 TDDDG); die Einwilligung ist jederzeit widerrufbar.
|
||||
|
||||
Sie können Ihren Browser so einstellen, dass Sie über das Setzen von Cookies informiert werden und Cookies nur im Einzelfall erlauben, die Annahme von Cookies für bestimmte Fälle oder generell ausschließen sowie das automatische Löschen der Cookies beim Schließen des Browsers aktivieren. Bei der Deaktivierung von Cookies kann die Funktionalität dieser Website eingeschränkt sein.
|
||||
|
||||
Welche Cookies und Dienste auf dieser Website eingesetzt werden, können Sie dieser Datenschutzerklärung entnehmen.
|
||||
|
||||
Einwilligung mit Usercentrics
|
||||
Diese Website nutzt die Consent-Technologie von Usercentrics, um Ihre Einwilligung zur Speicherung bestimmter Cookies auf Ihrem Endgerät oder zum Einsatz bestimmter Technologien einzuholen und diese datenschutzkonform zu dokumentieren. Anbieter dieser Technologie ist die Usercentrics GmbH, Sendlinger Straße 7, 80331 München, Website: https://usercentrics.com/de/ (im Folgenden „Usercentrics").
|
||||
|
||||
Wenn Sie unsere Website betreten, werden folgende personenbezogene Daten an Usercentrics übertragen: Ihre Einwilligung(en) bzw. der Widerruf Ihrer Einwilligung(en), Ihre IP-Adresse, Informationen über Ihren Browser, Informationen über Ihr Endgerät, Zeitpunkt Ihres Besuchs auf der Website, Geolocation.
|
||||
|
||||
Des Weiteren speichert Usercentrics ein Cookie in Ihrem Browser, um Ihnen die erteilten Einwilligungen bzw. deren Widerruf zuordnen zu können. Die so erfassten Daten werden gespeichert, bis Sie uns zur Löschung auffordern, das Usercentrics-Cookie selbst löschen oder der Zweck für die Datenspeicherung entfällt. Zwingende gesetzliche Aufbewahrungspflichten bleiben unberührt. Der Einsatz von Usercentrics erfolgt, um die gesetzlich vorgeschriebenen Einwilligungen für den Einsatz bestimmter Technologien einzuholen. Rechtsgrundlage hierfür ist Art. 6 Abs. 1 lit. c DSGVO.
|
||||
|
||||
Einwilligung mit Cookiebot
|
||||
Unsere Website nutzt die Consent-Technologie von Cookiebot. Anbieter dieser Technologie ist Cybot A/S, Havnegade 39, 1058 Kopenhagen, Dänemark (im Folgenden „Cookiebot"). Rechtsgrundlage hierfür ist Art. 6 Abs. 1 lit. c DSGVO.
|
||||
|
||||
Kontaktformular
|
||||
Wenn Sie uns per Kontaktformular Anfragen zukommen lassen, werden Ihre Angaben aus dem Anfrageformular inklusive der von Ihnen dort angegebenen Kontaktdaten zwecks Bearbeitung der Anfrage und für den Fall von Anschlussfragen bei uns gespeichert. Die Verarbeitung dieser Daten erfolgt auf Grundlage von Art. 6 Abs. 1 lit. b DSGVO, sofern Ihre Anfrage mit der Erfüllung eines Vertrags zusammenhängt oder zur Durchführung vorvertraglicher Maßnahmen erforderlich ist. In allen übrigen Fällen beruht die Verarbeitung auf unserem berechtigten Interesse (Art. 6 Abs. 1 lit. f DSGVO) oder auf Ihrer Einwilligung (Art. 6 Abs. 1 lit. a DSGVO).
|
||||
|
||||
Anfrage per E-Mail, Telefon oder Telefax
|
||||
Wenn Sie uns per E-Mail, Telefon oder Telefax kontaktieren, wird Ihre Anfrage inklusive aller daraus hervorgehenden personenbezogenen Daten (Name, Anfrage) zum Zwecke der Bearbeitung Ihres Anliegens bei uns gespeichert und verarbeitet.
|
||||
|
||||
Kommunikation via WhatsApp
|
||||
Für die Kommunikation nutzen wir den Instant-Messaging-Dienst WhatsApp. Anbieter ist die WhatsApp Ireland Limited, 4 Grand Canal Square, Grand Canal Harbour, Dublin 2, Irland. Das Unternehmen verfügt über eine Zertifizierung nach dem „EU-US Data Privacy Framework" (DPF).
|
||||
|
||||
Registrierung auf dieser Website
|
||||
Sie können sich auf dieser Website registrieren, um zusätzliche Funktionen auf der Seite zu nutzen. Die Verarbeitung der bei der Registrierung eingegebenen Daten erfolgt zum Zwecke der Durchführung des durch die Registrierung begründeten Nutzungsverhältnisses (Art. 6 Abs. 1 lit. b DSGVO).
|
||||
|
||||
5. Soziale Medien
|
||||
Facebook
|
||||
Auf dieser Website sind Elemente des sozialen Netzwerks Facebook integriert. Anbieter dieses Dienstes ist die Meta Platforms Ireland Limited, Merrion Road, Dublin 4, D04 X2K5, Irland. Soweit personenbezogene Daten erfasst und an Facebook weitergeleitet werden, sind wir und die Meta Platforms Ireland Limited gemeinsam für diese Datenverarbeitung verantwortlich (Art. 26 DSGVO). Die Datenübertragung in die USA wird auf die Standardvertragsklauseln der EU-Kommission gestützt. Das Unternehmen verfügt über eine Zertifizierung nach dem „EU-US Data Privacy Framework" (DPF).
|
||||
|
||||
X (ehemals Twitter)
|
||||
Auf dieser Website sind Funktionen des Dienstes X (ehemals Twitter) eingebunden. Anbieter ist die X Corp., 1355 Market Street, Suite 900, San Francisco, CA 94103, USA. Die Nutzung erfolgt auf Grundlage Ihrer Einwilligung nach Art. 6 Abs. 1 lit. a DSGVO und § 25 Abs. 1 TDDDG. Die Datenübertragung in die USA wird auf die Standardvertragsklauseln der EU-Kommission gestützt.
|
||||
|
||||
Instagram
|
||||
Auf dieser Website sind Funktionen des Dienstes Instagram eingebunden. Anbieter ist die Meta Platforms Ireland Limited. Die Datenübertragung in die USA wird auf die Standardvertragsklauseln der EU-Kommission gestützt. Das Unternehmen verfügt über eine Zertifizierung nach dem „EU-US Data Privacy Framework" (DPF).
|
||||
|
||||
LinkedIn
|
||||
Diese Website nutzt Elemente des Netzwerks LinkedIn. Anbieter ist die LinkedIn Ireland Unlimited Company, Wilton Plaza, Wilton Place, Dublin 2, Irland. Die Datenübertragung in die USA wird auf die Standardvertragsklauseln der EU-Kommission gestützt. Das Unternehmen verfügt über eine Zertifizierung nach dem „EU-US Data Privacy Framework" (DPF).
|
||||
|
||||
XING
|
||||
Diese Website nutzt Elemente des Netzwerks XING. Anbieter ist die New Work SE, Am Strandkai 1, 20457 Hamburg, Deutschland. Die Nutzung erfolgt auf Grundlage Ihrer Einwilligung nach Art. 6 Abs. 1 lit. a DSGVO und § 25 Abs. 1 TDDDG.
|
||||
|
||||
6. Analyse-Tools und Werbung
|
||||
Matomo
|
||||
Diese Website benutzt den Open Source Webanalysedienst Matomo. Die Nutzung dieses Analyse-Tools erfolgt auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO. Bei der Analyse mit Matomo setzen wir IP-Anonymisierung ein. Wir hosten Matomo bei PixelMechanics | grenzenlos digital, Bucher Str. 79a (Rilke Park), D-90419 Nuremberg, Germany. Wir haben einen Vertrag über Auftragsverarbeitung (AVV) geschlossen.
|
||||
|
||||
Google Ads
|
||||
Der Websitebetreiber verwendet Google Ads, ein Online-Werbeprogramm der Google Ireland Limited, Gordon House, Barrow Street, Dublin 4, Irland. Die Nutzung erfolgt auf Grundlage Ihrer Einwilligung nach Art. 6 Abs. 1 lit. a DSGVO und § 25 Abs. 1 TDDDG. Die Datenübertragung in die USA wird auf die Standardvertragsklauseln der EU-Kommission gestützt. Das Unternehmen verfügt über eine Zertifizierung nach dem „EU-US Data Privacy Framework" (DPF).
|
||||
|
||||
Google Ads Remarketing
|
||||
Diese Website nutzt die Funktionen von Google Ads Remarketing. Anbieter ist die Google Ireland Limited. Die Nutzung erfolgt auf Grundlage Ihrer Einwilligung nach Art. 6 Abs. 1 lit. a DSGVO und § 25 Abs. 1 TDDDG.
|
||||
|
||||
LinkedIn Insight Tag
|
||||
Diese Website nutzt das Insight-Tag von LinkedIn. Anbieter ist die LinkedIn Ireland Unlimited Company. Soweit eine Einwilligung eingeholt wurde, erfolgt der Einsatz auf Grundlage von Art. 6 Abs. 1 lit. a DSGVO und § 25 TDDDG; ansonsten auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO. Die Datenübertragung in die USA wird auf die Standardvertragsklauseln der EU-Kommission gestützt.
|
||||
|
||||
7. Plugins und Tools
|
||||
YouTube mit erweitertem Datenschutz
|
||||
Diese Website bindet Videos der Website YouTube ein. Betreiber ist die Google Ireland Limited. Die Nutzung von YouTube erfolgt im Interesse einer ansprechenden Darstellung unserer Online-Angebote (Art. 6 Abs. 1 lit. f DSGVO). Das Unternehmen verfügt über eine Zertifizierung nach dem „EU-US Data Privacy Framework" (DPF).
|
||||
|
||||
Font Awesome (lokales Hosting)
|
||||
Diese Seite nutzt zur einheitlichen Darstellung von Schriftarten Font Awesome. Font Awesome ist lokal installiert. Eine Verbindung zu Servern von Fonticons, Inc. findet dabei nicht statt.
|
||||
|
||||
Google Maps
|
||||
Diese Seite nutzt den Kartendienst Google Maps. Anbieter ist die Google Ireland Limited. Zur Nutzung ist es notwendig, Ihre IP-Adresse zu speichern. Diese Informationen werden in der Regel an einen Server von Google in den USA übertragen. Die Datenübertragung in die USA wird auf die Standardvertragsklauseln der EU-Kommission gestützt.
|
||||
|
||||
Google reCAPTCHA
|
||||
Wir nutzen „Google reCAPTCHA" auf dieser Website. Anbieter ist die Google Ireland Limited. Die Speicherung und Analyse der Daten erfolgt auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO.
|
||||
|
||||
8. Eigene Dienste
|
||||
Umgang mit Bewerberdaten
|
||||
Wir bieten Ihnen die Möglichkeit, sich bei uns zu bewerben (z. B. per E-Mail, postalisch oder via Online-Bewerberformular). Wenn Sie uns eine Bewerbung zukommen lassen, verarbeiten wir Ihre damit verbundenen personenbezogenen Daten, soweit dies zur Entscheidung über die Begründung eines Beschäftigungsverhältnisses erforderlich ist. Rechtsgrundlage hierfür ist § 26 BDSG nach deutschem Recht, Art. 6 Abs. 1 lit. b DSGVO und – sofern Sie eine Einwilligung erteilt haben – Art. 6 Abs. 1 lit. a DSGVO.
|
||||
|
||||
Aufbewahrungsdauer der Daten
|
||||
Sofern wir Ihnen kein Stellenangebot machen können, Sie ein Stellenangebot ablehnen oder Ihre Bewerbung zurückziehen, behalten wir uns das Recht vor, die von Ihnen übermittelten Daten auf Grundlage unserer berechtigten Interessen (Art. 6 Abs. 1 lit. f DSGVO) bis zu 6 Monate ab der Beendigung des Bewerbungsverfahrens bei uns aufzubewahren. Anschließend werden die Daten gelöscht.
|
||||
|
||||
Aufnahme in den Bewerber-Pool
|
||||
Sofern wir Ihnen kein Stellenangebot machen, besteht ggf. die Möglichkeit, Sie in unseren Bewerber-Pool aufzunehmen. Die Aufnahme geschieht ausschließlich auf Grundlage Ihrer ausdrücklichen Einwilligung (Art. 6 Abs. 1 lit. a DSGVO). Die Daten aus dem Bewerber-Pool werden spätestens zwei Jahre nach Erteilung der Einwilligung unwiderruflich gelöscht.
|
||||
@@ -0,0 +1,287 @@
|
||||
Verantwortlicher im Sinne der EU-Datenschutz-Grundverordnung (DSGVO) ist:
|
||||
|
||||
|
||||
|
||||
Mercedes-Benz AG
|
||||
Mercedesstraße 120
|
||||
70372 Stuttgart
|
||||
Deutschland
|
||||
E-Mail: dialog.mb@mercedes-benz.com
|
||||
|
||||
|
||||
|
||||
Datenschutzbeauftragter:
|
||||
|
||||
|
||||
|
||||
Mercedes-Benz Group AG
|
||||
Konzernbeauftragter für den Datenschutz
|
||||
HPC W079
|
||||
70546 Stuttgart
|
||||
Deutschland
|
||||
E-Mail: data.protection@mercedes-benz.com
|
||||
|
||||
|
||||
|
||||
Weitere Verantwortliche für Analyse- und Marketingaktivitäten (s. Ziffer 1.c. dieser Datenschutzhinweise) sind:
|
||||
|
||||
|
||||
|
||||
Mercedes-Benz AG
|
||||
Siemensstr. 7
|
||||
70469 Stuttgart
|
||||
Deutschland
|
||||
E-Mail: mobility@mercedes-benz.com
|
||||
|
||||
|
||||
|
||||
Datenschutzbeauftragter:
|
||||
|
||||
|
||||
|
||||
Mercedes-Benz Group AG
|
||||
Konzernbeauftragter für den Datenschutz
|
||||
HPC W079
|
||||
70546 Stuttgart
|
||||
Deutschland
|
||||
E-Mail: data.protection@mercedes-benz.com
|
||||
|
||||
|
||||
|
||||
1. Datenschutz
|
||||
|
||||
|
||||
|
||||
a. Wir freuen uns über Ihren Besuch auf unseren Webseiten und Ihr Interesse an unseren Angeboten. Der Schutz Ihrer personenbezogenen Daten ist für uns ein wichtiges Anliegen. In diesen Datenschutzhinweisen erklären wir, wie wir Ihre personenbezogenen Daten erheben, was wir damit tun, für welche Zwecke und auf welchen Rechtsgrundlagen dies geschieht, und welche Rechte und Ansprüche sich damit für Sie verbinden. Zusätzlich verweisen wir auf die Mercedes-Benz-Datenschutzrichtlinie:
|
||||
|
||||
|
||||
|
||||
Mercedes-Benz Datenschutzrichtlinie
|
||||
|
||||
|
||||
|
||||
b. Unsere Datenschutzhinweise für den Gebrauch unserer Webseiten und die Datenschutzrichtlinie von Mercedes-Benz gelten nicht für Ihre Aktivitäten auf den Webseiten von sozialen Netzwerken oder anderen Anbietern, die Sie über die Links auf unseren Webseiten erreichen können. Bitte informieren Sie sich auf den Webseiten dieser Anbieter über deren Datenschutzbestimmungen.
|
||||
|
||||
|
||||
|
||||
c. Im Rahmen der nachstehend näher beschriebenen Analyse- und Marketingaktivitäten auf diesen Webseiten arbeiten wir mit den oben angegebenen weiteren Verantwortlichen eng zusammen. Daten über Ihren Besuch auf unseren Webseiten verarbeiten wir gemeinsam mit diesen Verantwortlichen mittels folgender Technologien:
|
||||
|
||||
|
||||
|
||||
Für Analysezwecke:
|
||||
|
||||
GA4
|
||||
|
||||
|
||||
|
||||
Für Marketingzwecke:
|
||||
|
||||
Salesforce Marketing Cloud Personalization
|
||||
|
||||
|
||||
|
||||
Die Verarbeitung personenbezogener Daten zu den oben genannten Zwecken erfolgt nur, soweit Sie darin einwilligen, wie in Ziffer 5.c. dieser Datenschutzhinweise näher beschrieben.
|
||||
|
||||
|
||||
|
||||
Zwischen diesen Verantwortlichen wurden entsprechend den rechtlichen Vorgaben der Rahmen für die gemeinsame Verarbeitung personenbezogener Daten und die jeweiligen Verantwortlichkeiten vereinbart. Weitere Informationen zu den wesentlichen Inhalten dieser Vereinbarungen finden Sie als Anhang am Ende der Webseite.
|
||||
|
||||
|
||||
|
||||
2. Erhebung und Verarbeitung Ihrer personenbezogenen Daten
|
||||
|
||||
|
||||
|
||||
a. Wenn Sie unsere Webseiten besuchen, speichern wir bestimmte Angaben zu dem von Ihnen verwendeten Browser und Betriebssystem, das Datum und die Uhrzeit des Besuches, den Zugriffsstatus (z.B. ob Sie eine Webseite aufrufen konnten oder eine Fehlermeldung erhielten), die Nutzung von Funktionen der Webseite, die von Ihnen möglicherweise eingegebenen Suchbegriffe, die Häufigkeit, mit der Sie einzelne Webseiten aufrufen, die Bezeichnung abgerufener Dateien, die übertragenen Datenmenge, die Webseite, von der aus Sie auf unsere Webseiten gelangt sind, und die Webseite, die Sie von unseren Webseiten aus besuchen, sei es, indem Sie Links auf unseren Webseiten anklicken oder eine Domain direkt in das Eingabefeld derselben Registerkarte (bzw. desselben Fensters) Ihres Browsers eingeben, worin Sie unsere Webseiten geöffnet haben. Außerdem speichern wir aus Sicherheitsgründen, insbesondere zur Vorbeugung vor und Erkennung von Angriffen auf unsere Webseiten oder Betrugsversuchen, für die Dauer von sieben Tagen Ihre IP-Adresse und den Namen Ihres Internet Service Providers.
|
||||
|
||||
|
||||
|
||||
b. Andere personenbezogene Daten werden nur verarbeitet, falls Sie diese Daten z.B. im Rahmen einer Registrierung, eines Kontaktformulars, eines Chats, einer Umfrage, eines Preisausschreibens oder zur Durchführung eines Vertrages mitteilen, und auch in diesen Fällen nur, soweit uns dies aufgrund einer von Ihnen erteilten Einwilligung oder nach den geltenden Rechtsvorschriften (siehe Ziffer 7) gestattet ist.
|
||||
|
||||
|
||||
|
||||
c. Sie sind weder gesetzlich noch vertraglich verpflichtet, Ihre personenbezogenen Daten zu überlassen. Möglich ist aber, dass bestimmte Funktionen unserer Webseiten von der Überlassung personenbezogener Daten abhängen. Falls Sie in diesen Fällen personenbezogene Daten nicht überlassen, kann dies dazu führen, dass Funktionen nicht oder nur eingeschränkt zur Verfügung stehen.
|
||||
|
||||
|
||||
|
||||
3. Nutzungszwecke
|
||||
|
||||
|
||||
|
||||
a. Die bei einem Besuch unserer Webseiten erhobenen personenbezogenen Daten verwenden wir, um diese für Sie möglichst komfortabel zu betreiben sowie unsere IT-Systeme vor Angriffen und anderen rechtswidrigen Handlungen zu schützen.
|
||||
|
||||
|
||||
|
||||
b. Soweit Sie uns weitere personenbezogene Daten z.B. im Rahmen einer Registrierung, eines Chats, eines Kontaktformulars, einer Umfrage, eines Preisausschreibens oder zur Durchführung eines Vertrages mitteilen, nutzen wir diese Daten zu den genannten Zwecken, zu Zwecken der Kundenverwaltung, Marktsteuerung, Geschäfts- und Wirtschaftlichkeitsanalyse und – soweit erforderlich – zu Zwecken der Abwicklung und Abrechnung etwaiger Geschäftsvorgänge, jeweils in dem dafür erforderlichen Umfang.
|
||||
|
||||
|
||||
|
||||
c. Für weitere Zwecke (z. B. Anzeige von personalisierter Inhalten oder Werbung auf der Basis Ihres Nutzungsverhaltens) nutzen wir und ggf. ausgewählte Dritte Ihre Daten, soweit Sie dazu im Rahmen unseres Consent Management Systems Ihre Einwilligung (= Zustimmung) geben. Weitere Informationen und Entscheidungsmöglichkeiten erhalten Sie unter „Einstellungen“ im Footer ganz unten auf der Website.
|
||||
|
||||
|
||||
|
||||
d. Außerdem nutzen wir personenbezogene Daten, soweit wir dazu rechtlich verpflichtet sind (z. B. Speicherung zur Erfüllung handels- oder steuerrechtlicher Aufbewahrungspflichten, Herausgabe gem. behördlicher oder gerichtlicher Anordnung, z. B. an eine Strafverfolgungsbehörde).
|
||||
|
||||
|
||||
|
||||
4. Einbeziehung weiterer Beteiligter
|
||||
|
||||
|
||||
|
||||
a. Unsere Webseiten können auch Angebote Dritter enthalten. Wenn Sie ein solches Angebot anklicken, übertragen wir im erforderlichen Umfang Daten an den jeweiligen Anbieter (z. B. die Angabe, dass Sie dieses Angebot bei uns gefunden haben und ggf. weitere Informationen, die Sie hierfür auf unseren Webseiten bereits angegeben haben).
|
||||
|
||||
|
||||
|
||||
b. Wenn wir auf unseren Webseiten sogenannte „Social Plug-ins“ sozialer Netzwerke wie Facebook und Twitter einsetzen, binden wir diese wie folgt ein:
|
||||
|
||||
|
||||
|
||||
Wenn Sie unsere Webseiten besuchen, sind die Social Plug-ins deaktiviert, d.h. es findet keine Übertragung irgendwelcher Daten an die Betreiber dieser Netzwerke statt. Falls Sie eines der Netzwerke nutzen möchten, klicken Sie auf das jeweilige Social Plug-in, um eine direkte Verbindung mit dem Server des jeweiligen Netzwerks aufzubauen.Falls Sie bei dem Netzwerk ein Nutzerkonto haben und im Moment des Aktivierens des Social Plug-ins dort eingeloggt sind, kann das Netzwerk Ihren Besuch der unserer Webseiten Ihrem Nutzerkonto zuordnen. Wenn Sie das vermeiden möchten, loggen Sie sich bitte vor der Aktivierung des Social Plug-ins aus dem Netzwerk aus. Den Besuch anderer Mercedes-Benz-Webseiten kann ein soziales Netzwerk nicht zuordnen, bevor Sie nicht auch ein dort vorhandenes Social Plug-in aktiviert haben.
|
||||
|
||||
|
||||
|
||||
Wenn Sie ein Social Plug-in aktivieren, überträgt das Netzwerk die dadurch verfügbar werdenden Inhalte direkt an Ihren Browser, der sie in unsere Webseiten einbindet. In dieser Situation können auch Datenübertragungen stattfinden, die vom jeweiligen sozialen Netzwerk initiiert und gesteuert werden. Für Ihre Verbindung zu einem sozialen Netzwerk, die zwischen dem Netzwerk und Ihrem System stattfindenden Datenübertragungen und für Ihre Interaktionen auf dieser Plattform gelten ausschließlich die Datenschutzbestimmungen des jeweiligen Netzwerks.
|
||||
|
||||
|
||||
|
||||
Das Social Plug-in bleibt aktiv, bis Sie es deaktivieren oder Ihre Cookies löschen (siehe Ziffer 5.d).
|
||||
|
||||
|
||||
|
||||
c. Wenn Sie den Link zu einem Angebot anklicken oder ein Social Plug-in aktivieren, kann es sein, dass personenbezogene Daten zu Anbietern in Ländern außerhalb des Europäischen Wirtschaftsraums gelangen, die aus der Sicht der Europäischen Union („EU“) kein den EU-Standards entsprechendes „angemessenes Schutzniveau“ für die Verarbeitung personenbezogener Daten gewährleisten. Bitte denken Sie an diesen Umstand, bevor Sie einen Link anklicken oder ein Social Plug-in aktivieren und damit eine Übertragung Ihrer Daten auslösen.
|
||||
|
||||
|
||||
|
||||
d. Für Betrieb, Optimierung und Absicherung unserer Webseiten setzen wir außerdem qualifizierte Dienstleister (z. B. IT-Dienstleister, Marketing-Agenturen[A1] ) ein. Personenbezogene Daten geben wir an diese nur weiter, soweit dies erforderlich ist für die Bereitstellung und Nutzung der Webseiten und deren Funktionalitäten, zur Verfolgung berechtigter Interessen, zur Erfüllung rechtlicher Verpflichtungen oder soweit Sie darin eingewilligt haben (siehe Ziffer 7). Nähere Angaben zu den Empfängern finden Sie in unserem Consent Management System unter „Einstellungen“ im Footer ganz unten auf der Website.
|
||||
|
||||
|
||||
|
||||
e. Lead Ads sind Werbeanzeigen auf Webseiten oder in Sozialen Netzwerken Dritter (z. B. Meta Plattformen (Facebook, Instagram) oder Google), die es Ihnen ermöglichen, eine spezifische Anfrage (z. B. für ein(e) Probefahrt, Beratung, Angebot) an uns zu senden. Die Anfrage enthält bereits für die Erledigung Ihrer Anfrage erforderliche Daten (z. B. Kontaktdaten wie Name, Telefonnummer, E-Mail-Adresse, Ort, Fahrzeugmodell, für das Sie sich interessieren). Dafür geben wir Ihre Anfrage (zusammen mit diesen Daten) ggf. an ein anderes Mercedes-Benz Konzernunternehmen oder einen Mercedes-Benz Vertriebspartner weiter.
|
||||
|
||||
|
||||
|
||||
5. Cookies
|
||||
|
||||
|
||||
|
||||
a. Beim Besuch unserer Webseiten können Cookies zum Einsatz kommen. Technisch gesehen handelt es sich um sog. HTML-Cookies und ähnliche Softwaretools wie Web/DOM Storage oder Local Shared Objects (sog. „Flash-Cookies“), die wir zusammen als Cookies bezeichnen.
|
||||
|
||||
|
||||
|
||||
b. Cookies sind kleine Dateien, die während des Besuchs einer Webseite auf Ihrem Desktop-, Notebook- oder Mobilgerät abgelegt und später ausgelesen werden. Daraus kann man z. B. erkennen, ob es zwischen dem Gerät und den Webseiten schon eine Verbindung gegeben hat, Ihre bevorzugte Sprache oder andere Einstellungen berücksichtigen, Ihnen bestimmte Funktionalitäten (z. B. Online-Shop, Fahrzeugkonfigurator) anbieten oder nutzungsbasiert Ihre Interessen erkennen. Cookies können auch personenbezogene Daten enthalten.
|
||||
|
||||
|
||||
|
||||
c. Ob und welche Cookies bei Ihrem Besuch unserer Webseiten zum Einsatz kommen, hängt davon ab, welche Bereiche und Funktionen unserer Webseiten Sie nutzen und ob Sie dem Einsatz von Cookies, die nicht unbedingt, d. h. typischerweise aus technischen Gründen, erforderlich sind, in unserem Consent Management System zustimmen. Weitere Informationen und Entscheidungsmöglichkeiten erhalten Sie unter „Einstellungen“ im Footer ganz unten auf der Website.
|
||||
|
||||
|
||||
|
||||
d. Der Einsatz von Cookies hängt außerdem von den Einstellungen des von Ihnen verwendeten Web-Browsers (z. B. Microsoft Edge, Google Chrome, Apple Safari, Mozilla Firefox) ab. Die meisten Web-Browser sind so voreingestellt, dass sie bestimmte Arten von Cookies automatisch akzeptieren; diese Einstellung können Sie jedoch meistens ändern. Vorhandene Cookies können Sie jederzeit löschen. Web/DOM-Storage und Local Shared Objects können Sie separat löschen. Wie das in dem von Ihnen verwendeten Browser bzw. Gerät funktioniert, erfahren Sie in der Anleitung des Herstellers.
|
||||
|
||||
|
||||
|
||||
e. Die Einwilligung (= Zustimmung) zu sowie Ablehnung oder Löschung von Cookies sind an das verwendete Gerät und zudem an den jeweils verwendeten Web-Browser gebunden. Wenn Sie mehrere Geräte bzw. Web-Browser verwenden, können Sie die Entscheidungen bzw. Einstellungen jeweils unterschiedlich vornehmen.
|
||||
|
||||
|
||||
|
||||
f. Wenn Sie sich gegen den Einsatz von Cookies entscheiden oder diese löschen, kann es sein, dass Ihnen nicht alle Funktionen unserer Webseiten oder einzelne Funktionen nur eingeschränkt zur Verfügung stehen.
|
||||
|
||||
|
||||
|
||||
6. Sicherheit
|
||||
|
||||
|
||||
|
||||
Wir setzen technische und organisatorische Sicherheitsmaßnahmen ein, um Ihre durch uns verwalteten Daten gegen Manipulationen, Verlust, Zerstörung und gegen den Zugriff unberechtigter Personen zu schützen. Wir verbessern unsere Sicherheitsmaßnahmen fortlaufend entsprechend der technologischen Entwicklung.
|
||||
|
||||
|
||||
|
||||
7. Rechtsgrundlagen für Datenverarbeitung und Cookies
|
||||
|
||||
|
||||
|
||||
a. Soweit Sie uns für die Verarbeitung Ihrer personenbezogenen Daten eine Einwilligung erteilt haben, stellte diese die Rechtsgrundlage für die Verarbeitung dar (Art. 6 Abs. 1 Buchst. a DSGVO).
|
||||
|
||||
|
||||
|
||||
b. Für eine Verarbeitung personenbezogener Daten für die Zwecke der Anbahnung oder der Erfüllung eines Vertrages mit Ihnen ist Art. 6 Abs. 1 Buchst. b DSGVO die Rechtsgrundlage.
|
||||
|
||||
|
||||
|
||||
c. Soweit die Verarbeitung Ihrer personenbezogenen Daten zur Erfüllung unserer rechtlichen Verpflichtungen (z.B. zur Aufbewahrung von Daten) erforderlich ist, sind wir dazu gemäß Art. 6 Abs. 1 Buchst. c DSGVO befugt.
|
||||
|
||||
|
||||
|
||||
d. Außerdem verarbeiten wir personenbezogene Daten zur Wahrnehmung unserer berechtigten Interessen sowie berechtigter Interessen Dritter gemäß Art. 6 Abs. 1 Buchst. f DSGVO. Dazu gehören z. B. die Erhaltung der Funktionsfähigkeit unserer IT-Systeme, die (Direkt-)Vermarktung eigener und fremder Produkte und Dienstleistungen (soweit diese nicht mit Ihrer Einwilligung erfolgt), das Handeln auf Ihre Anfrage (z. B. über ein Kontaktformular oder Lead Ad) und die rechtlich gebotene Dokumentation von Geschäftskontakten. Wir berücksichtigen im Rahmen der jeweils erforderlichen Interessenabwägung insbesondere die Art der personenbezogenen Daten, den Verarbeitungszweck, die Verarbeitungsumstände und Ihr Interesse an der Vertraulichkeit Ihrer personenbezogenen Daten.
|
||||
|
||||
|
||||
|
||||
e. Das Ablegen und Auslesen von Cookies gemäß Ziffer 5.c. erfolgt auf der Basis von § 25 TTDSG (in Deutschland) bzw. § 165 Abs. 3 TKG (in Österreich).
|
||||
|
||||
|
||||
|
||||
8. Löschung Ihrer personenbezogenen Daten
|
||||
|
||||
|
||||
|
||||
Ihre IP-Adresse und den Namen Ihres Internet Service Providers, die wir aus Sicherheitsgründen speichern, löschen wir nach sieben Tagen. Im Übrigen löschen wir Ihre personenbezogenen Daten, sobald der Zweck, zu dem wir die Daten erhoben und verarbeitet haben, entfällt. Über diesen Zeitpunkt hinaus findet eine Speicherung nur statt, soweit dies gemäß den Gesetzen, Verordnungen oder sonstigen Rechtsvorschriften, denen wir unterliegen, in der EU oder nach Rechtsvorschriften in Drittstaaten, wenn dort jeweils ein angemessenes Datenschutzniveau gegeben ist, erforderlich ist. Soweit eine Löschung im Einzelfall nicht möglich ist, werden die entsprechenden personenbezogenen Daten mit dem Ziel markiert, ihre künftige Verarbeitung einzuschränken.
|
||||
|
||||
|
||||
|
||||
9. Betroffenenrechte
|
||||
|
||||
|
||||
|
||||
a. Als von der Datenverarbeitung betroffene Person haben Sie das Recht auf Auskunft (Art. 15 DSGVO), Berichtigung (Art. 16 DSGVO), Datenlöschung (Art. 17 DSGVO), Einschränkung der Verarbeitung (Art. 18 DSGVO) sowie Datenübertragbarkeit (Art. 20 DSGVO).
|
||||
|
||||
|
||||
|
||||
b. Haben Sie in die Verarbeitung Ihrer personenbezogenen Daten durch uns eingewilligt, haben Sie das Recht, die Einwilligung jederzeit zu widerrufen. Die Rechtmäßigkeit der Verarbeitung Ihrer personenbezogenen Daten bis zu einem Widerruf wird durch den Widerruf nicht berührt. Ebenso unberührt bleibt eine weitere Verarbeitung dieser Daten aufgrund einer anderen Rechtsgrundlage, etwa zur Erfüllung rechtlicher Verpflichtungen (vgl. Abschnitt „Rechtsgrundlagen der Verarbeitung“).
|
||||
|
||||
|
||||
|
||||
c. Widerspruchsrecht
|
||||
|
||||
Sie haben das Recht, aus Gründen, die sich aus Ihrer besonderen Situation ergeben, jederzeit gegen die Verarbeitung Sie betreffender personenbezogener Daten, die aufgrund von Art. 6 Abs. 1 e) DSGVO (Datenverarbeitung im öffentlichen Interesse) oder Art. 6 Abs. 1 f) DSGVO (Datenverarbeitung auf der Grundlage einer Interessenabwägung) erfolgt, Widerspruch einzulegen. Legen Sie Widerspruch ein, werden wir Ihre personenbezogenen Daten nur weiter verarbeiten, soweit wir dafür zwingende berechtigte Gründe nachweisen können, die Ihre Interessen, Rechte und Freiheiten überwiegen, oder soweit die Verarbeitung der Geltendmachung, Ausübung oder Verteidigung von Rechtsansprüchen dient. Sofern wir Ihre personenbezogenen Daten verarbeiten, um Direktwerbung zur Wahrnehmung berechtigter Interessen auf der Grundlage einer Interessenabwägung zu betreiben, haben Sie zudem das Recht, hiergegen jederzeit ohne Nennung von Gründen Widerspruch einzulegen.
|
||||
|
||||
|
||||
|
||||
d. Wir bitten Sie, Ihre Betroffenenrechte gegenüber der Mercedes-Benz AG nach Möglichkeit im folgenden Formular geltend zu machen.
|
||||
|
||||
Online-Formular Betroffenenrechte
|
||||
|
||||
|
||||
|
||||
e. Sind Sie der Ansicht, die Verarbeitung Ihrer personenbezogenen Daten verstoße gegen gesetzliche Vorgaben, haben Sie das Recht zur Beschwerde bei einer zuständigen Datenschutzaufsichtsbehörde (Art. 77 DSGVO).
|
||||
|
||||
|
||||
|
||||
10. Newsletter
|
||||
|
||||
|
||||
|
||||
Wenn Sie einen auf unserer Webseite angebotenen Newsletter abonnieren, werden die bei der Newsletter-Anmeldung angegebenen Daten nur für den Versand des Newsletters verwendet, soweit Sie nicht einer weitergehenden Nutzung zustimmen. Sie können das Abonnement jederzeit über die im Newsletter vorgesehene Abmeldemöglichkeit beenden.
|
||||
|
||||
|
||||
|
||||
11. Zentraler Zugangsservice der Mercedes-Benz Group AG
|
||||
|
||||
|
||||
|
||||
Mit dem zentralen Zugangsservice der Mercedes-Benz Group AG können Sie sich bei allen an diesen Service angeschlossenen Webseiten und Applikationen der Mercedes-Benz-Gruppe und ihrer Marken anmelden. Die dafür geltenden Nutzungsbedingungen enthalten spezielle Datenschutzregelungen. Sie können diese Nutzungsbedingungen auf den jeweiligen Anmeldeseiten der angeschlossenen Webseiten und Applikationen abrufen.
|
||||
|
||||
|
||||
|
||||
12. Datenübertragung an Empfänger außerhalb des Europäischen Wirtschaftsraums
|
||||
|
||||
|
||||
|
||||
a. Beim Einsatz von Dienstleistern (siehe Ziffer 4. d.) und der Weitergabe von Daten mit Ihrer Einwilligung (= Zustimmung) an Dritte (siehe Ziffer 3.c) können personenbezogene Daten an Empfänger in Ländern außerhalb der Europäischen Union („EU“), Islands, Liechtensteins und Norwegens (= Europäischer Wirtschaftsraum) übertragen und dort verarbeitet werden, insbesondere USA, Indien.
|
||||
|
||||
|
||||
|
||||
b. In den folgenden Ländern besteht aus der Sicht der EU ein den EU-Standards entsprechendes angemessenes Schutzniveau für die Verarbeitung personenbezogener Daten (sog. Angemessenheitsbeschluss): Andorra, Argentinien, Kanada (eingeschränkt), Färöer-Inseln, Guernsey, Israel, Isle of Man, Japan, Jersey, Neuseeland, Schweiz, Südkorea, Uruguay, Vereinigtes Königreich (UK), Vereinigte Staaten von Amerika (USA, eingeschränkt). Mit anderen Empfängern vereinbaren wir die Anwendung von EU-Standardvertragsklauseln, von verbindlichen Unternehmensregelungen oder andere zulässige Mechanismen, um entsprechend den gesetzlichen Anforderungen ein „angemessenes Schutzniveau“ zu schaffen. Informationen hierzu stellen wir Ihnen gerne über die in vorstehender Ziffer 9.d. genannten Kontaktdaten zur Verfügung.
|
||||
|
||||
|
||||
|
||||
Stand: August 2025
|
||||
@@ -0,0 +1,24 @@
|
||||
Datenschutz
|
||||
|
||||
Sofern innerhalb des Internetangebots der SafetyKon GmbH die Möglichkeit zur Eingabe persönlicher oder geschäftlicher Daten besteht, erfolgt diese Angabe der Daten durch den Nutzer auf ausdrücklich freiwilliger Basis. Die SafetyKon GmbH erklärt, dass diese Daten nur zu dem angegebenen Zweck verwendet und nicht an Dritte weitergegeben werden. Alle Angaben werden gemäß den geltenden datenschutzrechtlichen Bestimmungen vertraulich behandelt.
|
||||
|
||||
Ferner können Sie Ihre Zustimmung zur Verwendung der erhoben personenbezogenen Daten jederzeit mit Wirkung für die Zukunft widerrufen. Bitte teilen Sie uns Ihren Widerruf per E-Mail mit.
|
||||
|
||||
Verwendung von Cookies
|
||||
|
||||
Wir verwenden so genannte Cookies, um unser Angebot besser auf Ihre Wünsche ausrichten und um statistische Daten über die Nutzung unserer Website erheben zu können. Bei "Cookies" handelt es sich um kleine Textdateien, die von einer Website lokal im Speicher Ihres Internet-Browsers auf dem von Ihnen genutzten Rechner abgelegt werden können. Cookies ermöglichen insbesondere die Wiedererkennung des Internet-Browsers. Die Cookies unserer Website erheben keine persönlichen Daten über Sie oder Ihre Nutzung. Einmal gesetzte Cookies können Sie jederzeit selbst löschen, indem Sie den entsprechenden Menüpunkt in Ihrem Internet-Browser aufrufen oder die Cookies auf Ihrer Festplatte löschen. Einzelheiten hierzu finden Sie im Hilfemenü Ihres Internet-Browsers.
|
||||
Bitte beachten Sie unsere Hinweise zur Nutzung des Dienstes Google Analytics, der ebenfalls Cookies verwendet.
|
||||
|
||||
Nutzung unserer Website ohne Cookies
|
||||
|
||||
Selbstverständlich können Sie unsere Website auch nutzen, ohne dass Cookies verwendet werden. Sie können hierzu die Verwendung von Cookies jederzeit über die Einstellungen Ihres Internet-Browsers generell ablehnen ("deaktivieren") oder sich das Setzen von Cookies anzeigen lassen und dann im Einzelfall entscheiden, ob Sie Cookies akzeptieren ("Cookie-Warnung"). Die hierfür notwendigen Einstellungen Ihres Internet-Browsers können Sie unter dem Menü-Punkt "Extras/Internetoptionen" beim Internet-Explorer von Microsoft oder dem entsprechenden Menüpunkt bei anderen Browsern vornehmen. Einzelheiten hierzu finden Sie im Hilfemenü Ihres Internet-Browsers. Das Ablehnen oder Anzeigen von Cookies kann sich auf die Funktionalität unserer Website auswirken.
|
||||
|
||||
Nutzung von Google Analytics
|
||||
|
||||
Diese Website benutzt Google Analytics, einen Webanalysedienst der Google Inc. ("Google"). Google Analytics verwendet sog. "Cookies", Textdateien, die auf Ihrem Computer gespeichert werden und die eine Analyse der Benutzung der Website durch Sie ermöglichen. Die durch das Cookie erzeugten Informationen über Ihre Benutzung dieser Website werden in der Regel an einen Server von Google in den USA übertragen und dort gespeichert. Im Falle der Aktivierung der IP-Anonymisierung auf dieser Website, wird Ihre IP-Adresse von Google jedoch innerhalb von Mitgliedstaaten der Europäischen Union oder in anderen Vertragsstaaten des Abkommens über den Europäischen Wirtschaftsraum zuvor gekürzt. Nur in Ausnahmefällen wird die volle IP-Adresse an einen Server von Google in den USA übertragen und dort gekürzt. Im Auftrag des Betreibers dieser Website wird Google diese Informationen benutzen, um Ihre Nutzung der Website auszuwerten, um Reports über die Websiteaktivitäten zusammenzustellen und um weitere mit der Websitenutzung und der Internetnutzung verbundene Dienstleistungen gegenüber dem Websitebetreiber zu erbringen. Die im Rahmen von Google Analytics von Ihrem Browser übermittelte IP-Adresse wird nicht mit anderen Daten von Google zusammengeführt. Sie können die Speicherung der Cookies durch eine entsprechende Einstellung Ihrer Browser-Software verhindern; wir weisen Sie jedoch darauf hin, dass Sie in diesem Fall gegebenenfalls nicht sämtliche Funktionen dieser Website vollumfänglich werden nutzen können. Sie können darüber hinaus die Erfassung der durch das Cookie erzeugten und auf Ihre Nutzung der Website bezogenen Daten (inkl. Ihrer IP-Adresse) an Google sowie die Verarbeitung dieser Daten durch Google verhindern, indem Sie das unter dem folgenden Link (http://tools.google.com/dlpage/gaoptout?hl=de) verfügbare Browser-Plugin herunterladen und installieren.
|
||||
|
||||
Sie können die Erfassung durch Google Analytics verhindern, indem Sie auf folgenden Link klicken. Es wird ein Opt-Out-Cookie gesetzt, das die zukünftige Erfassung Ihrer Daten beim Besuch dieser Website verhindert:
|
||||
|
||||
Nähere Informationen zu Nutzungsbedingungen und Datenschutz finden Sie unter www.google.com/analytics/terms/de.html bzw. unter https://www.google.de/intl/de/policies/. Wir weisen Sie darauf hin, dass auf dieser Website Google Analytics um den Code "gat._anonymizeIp();" erweitert wurde, um eine anonymisierte Erfassung von IP-Adressen (sog. IP-Masking) zu gewährleisten.
|
||||
|
||||
Quelle: www.datenschutzbeauftragter.info.de
|
||||
Reference in New Issue
Block a user