Compare commits

..

47 Commits

Author SHA1 Message Date
Benjamin Admin bf9d8a5ed3 fix(iace): resolve M-ID collisions for electrical/pressure patterns
6 supplementary measures (M410-M420) were silently overwritten by
metalworking duplicates in measureByID lookups, so robot-cell electrical
patterns resolved to chip-extraction/cleaning fallbacks instead of
equipotential bonding, creepage, EMC, or hose-burst protection. Rename
supplementary IDs to M475-M480 and rewire 13 affected pattern references
in robot_cell + robot_cell_ext.

HP1640 (direct contact with live parts, GT 2.2): priority 98->99, drop
RequiredEnergyTags gate so it fires in robot cells without an electrical
tag, expand mitigations to 5 concrete TRBS 2131 / IEC 60204-1 / EN 61140
measures (basic protection, double insulation, earthing, insulation
monitoring, equipotential bonding) — was previously losing to HP1688
even though HP1688 describes a different scenario.

HP1688 (touch voltage from potential differences): priority 98->96 so it
no longer outranks HP1640 for the direct-contact case; mitigations
expanded from M410-only to 4 concrete electrical measures.

Add regression tests pinning HP1640 contact-protection resolution and
M475 = Potentialausgleich. Existing TestGetProtectiveMeasureLibrary_-
UniqueIDs now actually enforces uniqueness (previously masked by
last-wins map override).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 10:12:55 +02:00
Benjamin Admin d45e08e25f fix: reduce Playwright timeout 180s→60s, increase poll limit 15→25min 2026-05-16 00:47:28 +02:00
Benjamin Admin 3dbf3aa34a feat: HTTP fallback for text extraction when Playwright times out
BMW Impressum/Cookie pages timeout in Playwright (>180s) because the
SPA has many sub-links to follow. But the HTML source already contains
the text (SSR). New fallback: direct HTTP GET + HTML tag stripping.

Order: 1. Consent-tester (Playwright, 180s) → 2. HTTP GET (30s)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 23:16:10 +02:00
Benjamin Admin 77308b783f debug: log CreateMitigation errors 2026-05-15 21:52:04 +02:00
Benjamin Admin 9797234ff6 fix(iace): add abbreviations + action words to genericSafetyTerms
KSS, EMV, ESD, DCS, PLR, SIL, HMI, SPS, RCD, LOTO, PSA are
abbreviations that should NOT trigger the relevance filter.
bersten, platzen, abspringen, spritzen, einatmen, ausrutschen,
herabfallen, durchschlaegen, wegschleudern are action words that
appear in many patterns and don't indicate a specific machine.

Fixes: HP1633-HP1675 (KSS patterns) were filtered out because
"kss" was not in the narrative but also not in genericSafetyTerms.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 16:05:20 +02:00
Benjamin Admin 7080eb5f45 fix(iace): boost robot cell priorities 96-99, remove debug code
Robot cell patterns now fire BEFORE generic patterns (Priority 96-99
vs generic 85-95). This ensures pattern-specific SuggestedMeasureIDs
(M420 for KSS, M410 for Potentialausgleich) reach the hazard.

Removed debug fmt.Println statements.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 16:01:52 +02:00
Benjamin Admin c93cf2719a debug: trace M420 in Priority-1 loop 2026-05-15 14:56:05 +02:00
Benjamin Admin 7a27dbc01b debug: check M420 in measureByID 2026-05-15 14:53:49 +02:00
Benjamin Admin de35dfce18 debug: add pattern-measure count to init step details 2026-05-15 14:51:26 +02:00
Benjamin Admin 69240faf24 fix(iace): accumulate SuggestedMeasureIDs across dedup'd patterns
When multiple patterns match the same category+zone, the first creates
the hazard and later patterns add their SuggestedMeasureIDs to the
existing hazard. This ensures KSS-specific measures (M420) reach the
hazard even if a generic pattern created it first.

seenCatZone changed from map[string]bool to map[string]uuid.UUID
to track which hazard ID was created for each dedupKey.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 14:45:37 +02:00
Benjamin Admin f34305c0a1 fix: increase dsi-discovery timeout 90s→300s, reduce max_documents 10→5 2026-05-15 14:21:13 +02:00
Benjamin Admin 2b5376ed54 fix(iace): pattern-specific measures take priority over category fallback
Each hazard now gets measures from its SOURCE PATTERN first
(SuggestedMeasureIDs), then category fallback for remaining slots.

Previously all mechanical hazards got the same generic top-5 measures
(Gefahrstelle eliminieren, Sicherheitsabstaende, Scharfe Kanten...).
Now a KSS-Schlauch hazard gets M420 (Druckfeste Auslegung) first.

SuggestedMeasureIDs added to PatternMatch struct and passed through
from pattern definition to hazard creation to measure assignment.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 14:17:32 +02:00
Benjamin Admin 958c03ab40 fix(iace): add human reference to all 33 robot cell patterns
Every ScenarioDE now describes how a PERSON is affected, not just
what happens to the machine. Every HarmDE describes the INJURY,
not just the technical effect.

Examples:
- "Peitscheneffekt des Schlauchs" → "Person wird von abspringendem
  Schlauch getroffen. KSS-Spritzer verletzen Haut und Augen."
- "Kurzschluss, Brand" → "Person wird durch Brand oder toxische
  Rauchgase verletzt. Verbrennungen, Rauchvergiftung."

Rule: Risikobeurteilung bewertet Gefahr fuer PERSONEN.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 13:43:54 +02:00
Benjamin Admin fca67c1f43 fix: accordion close bug + merge multi-page DSIs (BMW fix)
1. _expand_all_interactive(): Only click aria-expanded="false" buttons.
   Before: clicked ALL accordion buttons including open ones → BMW's
   pre-expanded accordions got CLOSED, reducing text from 1151 to 361w.

2. _fetch_text() + /extract-text: merge ALL documents found on a page
   (max_documents=10 instead of 1). BMW splits DSI across 5 sub-pages
   that the discovery finds as separate documents — now merged.

3. Tab panels: unhide hidden tabpanels instead of clicking tabs
   (clicking tabs can hide the currently visible panel).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 13:32:04 +02:00
Benjamin Admin 70af018da5 docs(gt): BMW cross-domain finding — 3 domains, no AGB, Social Media on jobs portal 2026-05-15 13:21:27 +02:00
Benjamin Admin 0182c91ef9 docs(gt): BMW fully verified — URLs, DSB, Impressum, Social Media data 2026-05-15 12:01:20 +02:00
Benjamin Admin a67cfa7c4a fix(gt): update BMW URLs (all old URLs are 404 since 2026) 2026-05-15 10:38:07 +02:00
Benjamin Admin 3b7ab4cbd7 feat(iace): 50% display threshold — weak matches shown as separate
Matches below 50% are now split:
- GT entries → "Fehlend" tab (not matched by engine)
- Engine entries → "Engine Findings" tab (additional findings)
Only matches >= 50% shown in "Zugeordnet" tab.

Coverage score now counts only real matches (>= 50%).
"Extra" tab renamed to "Engine Findings" for clarity.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 10:33:29 +02:00
Benjamin Admin 3469105d18 feat(iace): HP1606 + HP1634 — target 100% GT coverage
HP1606: Quetschen/Scheren durch Greifer im Einrichtbetrieb (GT 1.14)
HP1634: KSS-Pumpe spritzt bei geoeffneter Schutztuer (GT 1.38)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 10:20:42 +02:00
Benjamin Admin 1414c63515 feat(iace): HP1605 + HP1633 — final 2 patterns for GT coverage
HP1605: Stoss durch Werkzeug/Greifer im Einrichtbetrieb (GT 1.14)
HP1633: KSS-Versorgungsschlauch platzt oder reisst ab (GT 1.35)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 10:16:39 +02:00
Benjamin Admin 9f87bc5a2c fix: include website/company name in compliance-check email subject 2026-05-15 10:15:34 +02:00
Benjamin Admin f5f4de7359 fix(iace): remove RequiredEnergyTags from electrical patterns
Energy tag "electrical" doesn't match resolved tags (which are
"high_voltage", "electrical_part", etc.). Patterns HP1685-HP1699
now fire without energy tag requirement — they fire for any
project that has the right component tags.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 10:13:00 +02:00
Benjamin Admin 38d15d4d29 feat(iace): 5 differentiated patterns for GT duplicate scenarios
When GT has two entries for the same zone with different scenarios
(e.g. "eingeklemmt" vs "getroffen"), we need separate engine patterns.

HP1700: Getroffen von bewegtem Werkzeug/Greifer (vs HP1652 eingeklemmt)
HP1701: Greifer/Werkzeug durchschlaegt Zaun (vs HP1654 Werkstueck)
HP1702: KSS-Schlauch platzt (vs HP1675 springt ab)
HP1703: KSS-Bettspuelung bei offener Tuer (vs HP1670 allgemein)
HP1704: Brand durch KSS auf elektrische Komponenten

Extended synonym sets for potential/EMV matching.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 10:08:21 +02:00
Benjamin Admin 003eafa75d fix(iace): synonym-cross-matching + expanded action words
scenarioSimilarity now uses synonym-set cross-matching: if GT says
"durchschlaegt" and Engine says "schleuder", the synonym set recognizes
them as related. Added significantWordOverlap fallback when no action
words found. Extended action terms: schlauch/druck/kuehlschmierstoff,
pumpe/bettspuel, potential/bezugspotential, stoerung/emv.

Moved extractActionWords to benchmark_synonyms.go (458+119 lines).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 10:03:23 +02:00
Benjamin Admin b82853a95b feat(iace): scenario-based matching + split benchmark_synonyms.go
4-signal matcher: category (0.2), keywords (0.2), zone (0.3),
scenario similarity (0.3). Scenario signal extracts action words
(eingeklemmt vs herabfallend vs durchschlaegt) to differentiate
similar-looking hazards at the same component.

Split benchmark_synonyms.go (70 lines) from benchmark_matcher.go
(516→450 lines) to stay under 500-line cap.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 09:58:12 +02:00
Benjamin Admin c060ac222a fix(iace): prioritize zone-specific matches in greedy assignment
Sort matches by specificity first (zone overlap), then by score.
Prevents generic matches from consuming specific Engine patterns
that should match more specific GT entries.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 09:45:08 +02:00
Benjamin Admin 659c0505f8 fix: format code in batch test output 2026-05-15 09:44:48 +02:00
Benjamin Admin 02c2325e1b feat(iace): 2 final patterns (Kriechstrecken, EMV) + matcher synonyms
HP1698: Kurzschluss durch unzureichende Luft-/Kriechstrecken (GT 2.6)
HP1699: EMV-Stoereinfluss auf Sicherheitsfunktionen (GT 6.1)

Extended synonym sets: durchschlag/bewegungsbereich, potentialausgleich,
kriechstreck, kuehlschmierstoff/bettspuel, rutsch/stolper.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 09:42:14 +02:00
Benjamin Admin d72aa10691 feat: management summary for GF + batch GT test script
1. Management Summary (agent_doc_check_report.py):
   - Plain-language action items for Geschaeftsfuehrer
   - Maps technical checks to business actions ("Ihren DSB erwaehnen",
     "Beschwerderecht ergaenzen", "Loeschfristen dokumentieren")
   - Shows at top of compliance check email before detail report
   - Max 10 actions, max 3 per document

2. Batch GT Test (zeroclaw/scripts/batch_gt_test.py):
   - Runs all 10 GT websites through compliance-check API
   - Prints comparison table with L1 scores, word counts, services
   - Saves raw JSON results for analysis
   - Usage: python3 batch_gt_test.py --sites 1,6 --backend-url URL

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 09:39:19 +02:00
Benjamin Admin 3c05ff8ef6 fix(iace): lower threshold 0.20 + more synonym sets for GT matching
Threshold 0.25→0.20 to recover matches lost by keyword penalty.
New synonym sets: eingeschlossen/wiederanlauf, zentriergreifer,
beladetuer/schutztuer, ergonom/bedienelemente, spritzer/auge, bersten.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 09:31:12 +02:00
Benjamin Admin 935c9205b9 feat(iace): 25 new robot cell patterns (HP1650-HP1697) + matcher fix
New patterns from GT benchmark gap analysis:
- HP1650-1655: Robot arm motion limit, restart safety, tool/workpiece
  crushing, workpiece penetrates fence, reaching over fence
- HP1660-1661: Centering gripper crushing (outside/inside cell)
- HP1665-1666: Machine tool loading door, machining workspace
- HP1670-1671: Coolant splash eyes, compressed air injury
- HP1675: Coolant hose burst/detachment
- HP1680: Workpiece/tunnel crushing at conveyor
- HP1685-1689: Indirect contact, cabinet contact, liquid ingress fire,
  potential differences, RCD socket protection
- HP1690-1691: Ergonomic loading/control position
- HP1695: Burns from hot workpieces
- HP1697: Machine collapse through floor

Matcher: keyword overlap penalty — matches without shared hazard-type
keywords AND low zone score get 0.5x penalty to prevent false matches.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 09:28:01 +02:00
Benjamin Admin 826ce2a1b8 fix(cross-doc): suppress false positives when regex checks already pass
Cross-search "not in text" findings are only shown when regex L1
completeness < 50%. This prevents false positives where the text IS
the right doc_type but doesn't contain the specific cross-search
keywords (e.g. Impressum passes 9/13 checks but lacks "§5 TMG").

Also: cross-search now checks entries with wrong text, not just empty.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 00:54:33 +02:00
Benjamin Admin bd2d6976d6 fix(cross-doc): also check entries with wrong text, not just empty ones
Cross-search now validates if existing text matches the expected
doc_type using keyword scoring. If text is present but doesn't match
(e.g. Nutzungsbedingungen in Widerruf row), searches other texts
and creates a finding explaining the mismatch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 00:19:40 +02:00
Benjamin Admin a5d1814605 fix(iace): tag remaining 3 wrong-machine patterns + fix duplicates
HP154 (Kollision zweier Roboter) → robotics_cobot only
HP1066 (Haareinzug Drehmaschine) → lathe/cnc/metalworking only
HP758 (Notbremsung Fahrtreppe) → escalator/elevator only
Fixed duplicate MachineTypes fields from overlapping script runs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 00:05:28 +02:00
Benjamin Admin ba07a7f6e6 fix(iace): add MachineTypes to 17 machine-specific patterns
Patterns for playground, escalator, wind turbine, glass washing,
laundry, crane, lathe, rotary transfer, press now require matching
MachineTypes — they no longer fire for unrelated projects.
Neutralized zone texts in base patterns HP006/HP008 (removed
"Pressenraum", "Kran-/Hebezeugbereich").

Fixes: Spielplatz, Fahrtreppe, Windturbine etc. appearing in robot cell.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 00:01:51 +02:00
Benjamin Admin 708c61e50d fix(iace): max 5 mitigations per hazard — clean per-hazard assignment
Replaced category-broadcast logic with per-hazard loop:
each hazard gets up to 5 measures (pattern-suggested first, then
category fallback). Expected: 108 × 5 = max 540 total.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-14 23:45:41 +02:00
Benjamin Admin dc55253b9d fix(iace): prevent mitigation explosion — fallback only for unassigned
Pattern-suggested measures go to all hazards in category (correct).
Category-based fallback only for hazards WITHOUT pattern suggestions
(max 3 per hazard). Prevents 1654 mitigations explosion.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-14 23:41:54 +02:00
Benjamin Admin 8069d0ea89 fix(iace): assign mitigations to ALL hazards per category
hazardIDsByCategory changed from map[string]uuid.UUID to
map[string][]uuid.UUID — measures are now distributed to every
hazard in a category, not just the last one created.

Previously 94/108 hazards had no measures, now all get them.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-14 23:34:57 +02:00
Benjamin Admin 4e9043f26d feat(cross-doc): search all texts for all doc_types + misplacement finding
Cross-Document Intelligence: When a doc_type row is empty, searches
ALL other loaded documents for that content. If found (e.g. Widerruf
in AGB), extracts the section, runs the check, AND creates a finding:
"Widerrufsbelehrung in falschem Dokument gefunden — schwer auffindbar"

Keywords for: widerruf, cookie, social_media, impressum, agb, dsb.
Integrated as Step 1c in compliance check pipeline.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-14 23:19:39 +02:00
Benjamin Admin 29fbd03c79 fix(iace): lifecycle labels in benchmark + store all phases
- Store ALL applicable lifecycles (comma-separated) not just first
- Frontend maps internal keys to German labels (normal_operation ->
  Automatikbetrieb, maintenance -> Wartung, etc.)
- Show Betroffene Personen in engine detail column

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-14 23:17:27 +02:00
Benjamin Admin 98e5b1a8aa feat(iace): show lifecycle phases + affected persons in benchmark detail
Backend: HazardSummary now includes lifecycle_phase and affected_person
Frontend: Engine detail column shows Lebensphasen and Betroffene Personen

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-14 23:10:15 +02:00
Benjamin Admin b175212516 docs(gt): update Spiegel GT with verified 2026-05-14 results
CI / detect-changes (push) Successful in 5m10s
CI / nodejs-build (push) Successful in 2m15s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / validate-canonical-controls (push) Successful in 5m1s
CI / loc-budget (push) Successful in 17s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go (push) Failing after 46s
CI / test-python-backend (push) Has been skipped
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
DSI: 9/9 L1 (was 6/9), 13698 words (was 6461), all FNs resolved.
Social Media: 10/10 L1 (was 9/10). Services: 31 detected (was 5).
Impressum: 9/13 (USt-IdNr + V.i.S.d.P. fixed).
Widerruf: NOT correctly tested (wrong text assigned, needs Cross-Doc Intelligence).

Full service list (31 providers) documented with country + EU status.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-14 23:07:42 +02:00
Benjamin Admin 16190583d1 refactor(iace): neutral hazard formulations across all 1100+ patterns
Systematic refactoring of all hazard_patterns_*.go files:
- Removed lifecycle phase words from NameDE and ScenarioDE
  (67 fixes across 20 files)
- Phases belong in ApplicableLifecycles, not in text
- "bei Wartung/Reinigung/Montage/..." removed from names
- Scenarios rewritten to be phase-neutral
- Lifecycle-specific concepts preserved when they define the hazard
  (e.g. LOTO, Betriebsartenwahlschalter)

Rule: Gefaehrdung + Szenario NEUTRAL, Lebensphasen SEPARAT.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-14 23:04:31 +02:00
Benjamin Admin 70c9bfc069 fix(iace): neutral hazard formulations — no lifecycle phases in text
- Removed HP1601 (duplicate of HP1600 with narrower scope)
- HP1600 now covers ALL lifecycle phases, not just teach mode
- All pattern texts neutral: no lifecycle phase references in
  NameDE, ScenarioDE, TriggerDE — phases only in ApplicableLifecycles
- Formulierungsregel documented in file header

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-14 22:52:56 +02:00
Benjamin Admin 4b9317b4fd feat(iace): lifecycle phases in patterns + broader robot cell scenarios
- ApplicableLifecycles field in HazardPattern: patterns now declare which
  lifecycle phases the hazard applies to (Output, not just filter)
- Init handler writes first applicable lifecycle into Hazard.LifecyclePhase
- Robot cell patterns HP1600-1601 broadened: "Betrieb, Einrichten, Reinigung,
  Wartung, Fehlersuche" instead of only "Teach-Betrieb"
- All robot cell patterns get ApplicableLifecycles for proper phase display

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-14 22:38:02 +02:00
Benjamin Admin e4431da8d2 Merge branch 'main' of ssh://gitea.meghsakha.com:22222/Benjamin_Boenisch/breakpilot-compliance
CI / detect-changes (push) Successful in 5m10s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / validate-canonical-controls (push) Successful in 5m3s
CI / go-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 7m16s
CI / loc-budget (push) Successful in 14s
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go (push) Has been skipped
CI / test-python-backend (push) Has been skipped
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
2026-05-14 18:47:56 +02:00
Benjamin Admin 65f978368d feat(cmp): Phase 3 — admin widerruf, email-linking, vendor display, TCF, E2E tests
Admin Modal:
- vendor_consents as green/red badges
- Consent withdraw button (DELETE /consent/{id}) with confirmation
- Email-linking inline input (POST /consent/link-email)

Cookie Banner Admin:
- TCF toggle reads tcf_enabled from site config (was hardcoded false)
- BannerSite interface extended with tcf_enabled

Document Generator:
- Backend Banner-Config auto-fetch when SDK state has no banner
- Maps vendors to CONSENT (analytics tools, marketing partners)

E2E Tests (cmp-phase3-dsr.spec.ts):
- Vendor-agnostic consent fields (20+ fields, upsert)
- DSR Art. 15 Auskunft (multi-device, email-link, export)
- DSR Art. 17 Löschung (erasure by email)
- Anonymous cookie banner user (export, withdraw)
- Customer lifecycle (consent → login → link → Art.15 → Art.17)
- Admin dashboard integration (list, stats)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-14 18:45:41 +02:00
52 changed files with 2548 additions and 562 deletions
@@ -202,9 +202,9 @@ export function ComplianceCheckTab() {
setActiveCheckId(check_id)
localStorage.setItem(STORAGE_KEY_CHECK_ID, check_id)
// Poll for results (max 15 min = 300 polls x 3s)
// Poll for results (max 25 min = 500 polls x 3s)
let attempts = 0
while (attempts < 300) {
while (attempts < 500) {
await new Promise(r => setTimeout(r, 3000))
const pollRes = await fetch(`/api/sdk/v1/agent/compliance-check?check_id=${check_id}`)
if (!pollRes.ok) { attempts++; continue }
@@ -235,7 +235,7 @@ export function ComplianceCheckTab() {
}
attempts++
}
if (attempts >= 300) {
if (attempts >= 500) {
localStorage.removeItem(STORAGE_KEY_CHECK_ID); setActiveCheckId('')
throw new Error('Zeitlimit ueberschritten (15 Min)')
}
@@ -102,6 +102,7 @@ export interface BannerSite {
site_name: string
site_url: string
is_active: boolean
tcf_enabled?: boolean
}
export function useCookieBanner() {
@@ -105,7 +105,7 @@ export default function CookieBannerPage() {
{/* Tab: TCF/IAB */}
{activeTab === 'tcf' && (
<TCFSettings siteId={activeSiteId || undefined} tcfEnabled={false}
<TCFSettings siteId={activeSiteId || undefined} tcfEnabled={sites.find(s => s.site_id === activeSiteId)?.tcf_enabled ?? false}
onToggle={(enabled) => {
if (activeSiteId) {
fetch(`/api/sdk/v1/banner/admin/sites/${activeSiteId}`, {
@@ -101,7 +101,35 @@ function DocumentGeneratorPageInner() {
}
}, [state?.complianceScope?.determinedLevel, state?.companyProfile])
// ── MODULE WIRING: CookieBanner → CONSENT + FEATURES ─────────────────────
// ── MODULE WIRING: Backend Banner-Config → CONSENT + FEATURES ────────────
useEffect(() => {
// Fetch real vendor/category data from backend if SDK state has no banner
if (state?.cookieBanner) return // SDK state takes priority
fetch('/api/sdk/v1/banner/admin/sites', { headers: { 'x-tenant-id': '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e' } })
.then(r => r.json())
.then((sites: Array<{ site_id: string }>) => {
if (!sites?.length) return
return fetch(`/api/sdk/v1/banner/config/${sites[0].site_id}`, { headers: { 'x-tenant-id': '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e' } })
})
.then(r => r?.json())
.then(config => {
if (!config?.vendors?.length) return
const analytics = config.vendors.filter((v: { category_key: string }) => v.category_key === 'statistics' || v.category_key === 'analytics').map((v: { vendor_name: string }) => v.vendor_name)
const marketing = config.vendors.filter((v: { category_key: string }) => v.category_key === 'marketing').map((v: { vendor_name: string }) => v.vendor_name)
setContext(prev => ({
...prev,
CONSENT: {
...prev.CONSENT,
ANALYTICS_TOOLS: analytics.length > 0 ? analytics.join(', ') : prev.CONSENT.ANALYTICS_TOOLS,
MARKETING_PARTNERS: marketing.length > 0 ? marketing.join(', ') : prev.CONSENT.MARKETING_PARTNERS,
},
FEATURES: { ...prev.FEATURES, CMP_NAME: 'BreakPilot CMP', CMP_LOGS_CONSENTS: true },
}))
})
.catch(() => {})
}, [state?.cookieBanner])
// ── MODULE WIRING: CookieBanner SDK State → CONSENT + FEATURES ──────────
useEffect(() => {
const banner = state?.cookieBanner
if (!banner) return
@@ -1,9 +1,12 @@
'use client'
import { useState } from 'react'
import { useState, useCallback } from 'react'
import { useBannerConsents } from '../_hooks/useBannerConsents'
import { BannerConsentRecord, PAGE_SIZE } from '../_types'
const BANNER_API = '/api/sdk/v1/banner'
const TENANT_ID = '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e'
function formatDate(iso: string | null): string {
if (!iso) return '—'
return new Date(iso).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' })
@@ -42,12 +45,35 @@ const methodColors: Record<string, string> = {
export default function BannerConsentsTab() {
const {
records, sites, selectedSite, changeSite,
stats, currentPage, setCurrentPage, totalRecords, loading,
stats, currentPage, setCurrentPage, totalRecords, loading, reload,
} = useBannerConsents()
const [detail, setDetail] = useState<BannerConsentRecord | null>(null)
const [linkEmailInput, setLinkEmailInput] = useState('')
const [linkingEmail, setLinkingEmail] = useState(false)
const totalPages = Math.ceil(totalRecords / PAGE_SIZE)
const withdrawConsent = useCallback(async (id: string) => {
if (!confirm('Consent wirklich widerrufen? Diese Aktion kann nicht rueckgaengig gemacht werden.')) return
await fetch(`${BANNER_API}/consent/${id}`, { method: 'DELETE', headers: { 'x-tenant-id': TENANT_ID } })
setDetail(null)
reload()
}, [reload])
const linkEmail = useCallback(async (record: BannerConsentRecord) => {
if (!linkEmailInput.includes('@')) return
setLinkingEmail(true)
await fetch(`${BANNER_API}/consent/link-email`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-tenant-id': TENANT_ID },
body: JSON.stringify({ site_id: record.site_id, device_fingerprint: record.device_fingerprint, email: linkEmailInput }),
})
setLinkingEmail(false)
setLinkEmailInput('')
setDetail({ ...record, linked_email: linkEmailInput })
reload()
}, [linkEmailInput, reload])
return (
<div className="space-y-6">
{/* Stats + Site Selector */}
@@ -184,6 +210,18 @@ export default function BannerConsentsTab() {
))}
</div>
</div>
{detail.vendor_consents && Object.keys(detail.vendor_consents).length > 0 && (
<div className="flex justify-between items-start">
<span className="text-gray-500">Vendors</span>
<div className="flex flex-wrap gap-1 justify-end">
{Object.entries(detail.vendor_consents).map(([name, accepted]) => (
<span key={name} className={`text-xs px-2 py-0.5 rounded-full ${accepted ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'}`}>
{name}
</span>
))}
</div>
</div>
)}
<div className="flex justify-between">
<span className="text-gray-500">Methode</span>
<span>{detail.consent_method ? (
@@ -192,9 +230,28 @@ export default function BannerConsentsTab() {
</span>
) : '—'}</span>
</div>
<div className="flex justify-between">
<div className="flex justify-between items-center">
<span className="text-gray-500">Verknüpft mit</span>
<span>{detail.linked_email || '— (anonym)'}</span>
{detail.linked_email ? (
<span className="text-purple-600 text-xs">{detail.linked_email}</span>
) : (
<div className="flex items-center gap-1">
<input
type="email"
placeholder="E-Mail verknüpfen..."
value={linkEmailInput}
onChange={e => setLinkEmailInput(e.target.value)}
className="text-xs border border-gray-200 rounded px-2 py-1 w-40"
/>
<button
onClick={() => linkEmail(detail)}
disabled={linkingEmail || !linkEmailInput.includes('@')}
className="text-xs px-2 py-1 bg-purple-600 text-white rounded disabled:opacity-40"
>
{linkingEmail ? '...' : 'Link'}
</button>
</div>
)}
</div>
<div className="flex justify-between"><span className="text-gray-500">Erteilt</span><span>{formatDate(detail.created_at)}</span></div>
<div className="flex justify-between"><span className="text-gray-500">Ablauf</span><span>{formatDate(detail.expires_at)}</span></div>
@@ -264,6 +321,16 @@ export default function BannerConsentsTab() {
{detail.banner_config_hash && <div><span className="text-gray-500 text-xs">Config-Hash</span><p className="text-xs text-gray-600 font-mono">{detail.banner_config_hash}</p></div>}
</div>
</div>
{/* Widerruf-Button */}
<div className="border-t border-gray-100 pt-4 mt-4">
<button
onClick={() => withdrawConsent(detail.id)}
className="w-full px-4 py-2 text-xs font-semibold text-red-600 border border-red-200 rounded-lg hover:bg-red-50 transition-colors"
>
Consent widerrufen (Art. 17 DSGVO)
</button>
</div>
</div>
</div>
</div>
@@ -108,6 +108,7 @@ export interface BannerConsentRecord {
device_fingerprint: string
categories: string[]
vendors: string[]
vendor_consents: Record<string, boolean>
ip_hash: string | null
user_agent: string | null
linked_email: string | null
@@ -144,4 +145,5 @@ export interface BannerSite {
site_id: string
site_name: string
site_url: string
tcf_enabled?: boolean
}
@@ -14,14 +14,21 @@ type TabType = 'matched' | 'missing' | 'extra'
export function HazardComparisonTable({ matched, missing, extra }: Props) {
const [tab, setTab] = useState<TabType>('matched')
// Compute quality levels for matched pairs
const greenCount = matched.filter(p => p.match_score >= 0.7).length
const yellowCount = matched.filter(p => p.match_score >= 0.4 && p.match_score < 0.7).length
// Split matches: >= 50% are real matches, < 50% are weak (shown separately)
const realMatched = matched.filter(p => p.match_score >= 0.5)
const weakMatched = matched.filter(p => p.match_score < 0.5)
// Weak matches: GT entries go to "missing", engine entries go to "extra"
const allMissing = [...missing, ...weakMatched.map(w => w.gt_entry)]
const allExtra = [...extra, ...weakMatched.map(w => w.engine_hazard)]
const greenCount = realMatched.filter(p => p.match_score >= 0.7).length
const yellowCount = realMatched.filter(p => p.match_score >= 0.5 && p.match_score < 0.7).length
const tabs: { id: TabType; label: string; count: number; color: string }[] = [
{ id: 'matched', label: `Zugeordnet (${greenCount} exakt, ${yellowCount} aehnlich)`, count: matched.length, color: 'text-green-600' },
{ id: 'missing', label: 'Fehlend', count: missing.length, color: 'text-red-600' },
{ id: 'extra', label: 'Zusaetzlich', count: extra.length, color: 'text-gray-500' },
{ id: 'matched', label: `Zugeordnet (${greenCount} exakt, ${yellowCount} aehnlich)`, count: realMatched.length, color: 'text-green-600' },
{ id: 'missing', label: 'Fehlend', count: allMissing.length, color: 'text-red-600' },
{ id: 'extra', label: 'Engine Findings', count: allExtra.length, color: 'text-blue-500' },
]
return (
@@ -44,9 +51,9 @@ export function HazardComparisonTable({ matched, missing, extra }: Props) {
</div>
<div className="overflow-x-auto">
{tab === 'matched' && <MatchedTable pairs={matched} />}
{tab === 'missing' && <MissingTable entries={missing} />}
{tab === 'extra' && <ExtraTable entries={extra} />}
{tab === 'matched' && <MatchedTable pairs={realMatched} />}
{tab === 'missing' && <MissingTable entries={allMissing} />}
{tab === 'extra' && <ExtraTable entries={allExtra} />}
</div>
</div>
)
@@ -114,6 +121,21 @@ function MatchedTable({ pairs }: { pairs: HazardMatchPair[] }) {
)
}
const LIFECYCLE_LABELS: Record<string, string> = {
startup: 'Hochfahren', homing: 'Referenzfahrt', automatic_operation: 'Automatikbetrieb',
manual_operation: 'Handbetrieb', teach_mode: 'Einrichtbetrieb', maintenance: 'Wartung',
cleaning: 'Reinigung', emergency_stop: 'Not-Halt', recovery_mode: 'Wiederanlauf',
normal_operation: 'Automatikbetrieb', setup: 'Einrichten', changeover: 'Umruesten',
fault_clearing: 'Fehlersuche/Stoerungsbeseitigung', commissioning: 'Inbetriebnahme',
decommissioning: 'Demontage/Ausserbetriebnahme', transport: 'Transport',
assembly: 'Montage/Installation', inspection: 'Inspektion/Pruefung',
}
function formatLifecycles(raw: string): string {
if (!raw) return '-'
return raw.split(',').map(s => s.trim()).map(s => LIFECYCLE_LABELS[s] || s).join(', ')
}
/** Side-by-side detail comparison of GT entry vs. Engine hazard */
function DetailComparison({ gt, engine }: { gt: GroundTruthEntry; engine: HazardSummary }) {
return (
@@ -143,8 +165,14 @@ function DetailComparison({ gt, engine }: { gt: GroundTruthEntry; engine: Hazard
<DetailRow label="Gefaehrdung" gt={engine.name} />
<DetailRow label="Szenario" gt={engine.scenario || engine.description || '-'} />
<DetailRow label="Gefahrenstelle" gt={engine.zone || '-'} />
{engine.lifecycle_phase && (
<DetailRow label="Lebensphasen" gt={formatLifecycles(engine.lifecycle_phase)} />
)}
<DetailRow label="Moeglicher Schaden" gt={engine.possible_harm || '-'} />
<DetailRow label="Trigger" gt={engine.trigger_event || '-'} />
{engine.affected_person && (
<DetailRow label="Betroffene Personen" gt={engine.affected_person} />
)}
{engine.mitigations && engine.mitigations.length > 0 ? (
<DetailRow label="Massnahmen" gt={engine.mitigations.join('\n')} multiline />
) : (
@@ -33,6 +33,7 @@ export interface HazardSummary {
component?: string; zone?: string; risk_level?: string
description?: string; scenario?: string
possible_harm?: string; trigger_event?: string
affected_person?: string; lifecycle_phase?: string
mitigations?: string[]
}
@@ -12,7 +12,9 @@ export default function BenchmarkPage() {
const { result, gtLoaded, gtEntryCount, loading, error, importGT, runBenchmark } = useBenchmark(projectId)
const [gtProjectId, setGtProjectId] = useState('')
const coveragePct = result ? Math.round(result.coverage_score * 100) : 0
// Only count matches >= 50% as real coverage
const realMatchCount = result ? (result.matched_pairs?.filter(m => m.match_score >= 0.5).length || 0) : 0
const coveragePct = result ? Math.round(realMatchCount * 100 / Math.max(result.total_gt, 1)) : 0
const measurePct = result ? Math.round(result.measure_coverage * 100) : 0
return (
@@ -74,7 +76,7 @@ export default function BenchmarkPage() {
<ScoreCard
label="Hazard Coverage"
value={`${coveragePct}%`}
sub={`${result.matched_pairs?.length || 0} / ${result.total_gt} erkannt`}
sub={`${realMatchCount} / ${result.total_gt} erkannt (>= 50% Match)`}
color={coveragePct >= 80 ? 'green' : coveragePct >= 50 ? 'yellow' : 'red'}
/>
<ScoreCard
@@ -0,0 +1,371 @@
import { test, expect } from '@playwright/test'
/**
* CMP Phase 3 + DSR Integration Tests
*
* Tests the complete CMP lifecycle including:
* - Vendor-agnostic consent fields (consent_method, browser, os, etc.)
* - Script/cookie tracking (scripts_blocked, scripts_released, cookies_set)
* - Session ID tracking
* - GeoIP via timezone mapping
* - Vendor-level consent (vendor_consents dict)
* - DSR scenarios: Art. 15 Auskunft, Art. 17 Löschung, Art. 20 Portabilität
* - Email linking for DSR (device → user mapping)
* - Admin modal features (vendor display, withdraw, email linking)
*/
const API_BASE = process.env.PLAYWRIGHT_API_URL || 'https://macmini:3007/api/sdk/v1/banner'
const TENANT_ID = '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e'
const HEADERS = {
'Content-Type': 'application/json',
'X-Tenant-ID': TENANT_ID,
}
const TS = Date.now()
const SITE_ID = `e2e-cmp3-${TS}`
const DEVICE_FP = `e2e-device-${TS}`
// ============================================================================
// 1. Vendor-Agnostic Consent Fields
// ============================================================================
test.describe('Vendor-Agnostic Consent Fields', () => {
test('should store all 20+ fields on consent', async ({ request }) => {
// Create site config first
await request.post(`${API_BASE}/admin/sites`, {
headers: HEADERS,
data: { site_id: SITE_ID, site_name: 'E2E CMP Phase 3', site_url: 'https://test.example.com' },
})
// Record consent with all vendor-agnostic fields
const res = await request.post(`${API_BASE}/consent`, {
headers: HEADERS,
data: {
site_id: SITE_ID,
device_fingerprint: DEVICE_FP,
categories: ['essential', 'functional', 'analytics'],
vendors: ['Google Analytics', 'Matomo'],
vendor_consents: { 'Google Analytics': true, 'Matomo': true, 'Facebook Pixel': false },
user_agent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) E2E-Test',
consent_method: 'custom_selection',
page_url: 'https://test.example.com/pricing',
referrer: 'https://google.com',
device_type: 'desktop',
browser: 'Chrome/120.0',
os: 'Mac OS X 10.15.7',
screen_resolution: '1920x1080',
consent_scope: 'domain',
session_id: 'e2e-session-001',
timezone: 'Europe/Berlin',
scripts_blocked: [{ src: 'https://connect.facebook.net/fbevents.js', category: 'marketing' }],
scripts_released: [{ src: 'https://www.googletagmanager.com/gtag/js', category: 'analytics' }],
cookies_set: [
{ name: '_ga', domain: '.test.example.com', expiry_days: 730, category: 'analytics' },
{ name: 'bp_consent', domain: '.test.example.com', expiry_days: 365, category: 'essential' },
],
},
})
expect(res.status()).toBe(200)
const consent = await res.json()
expect(consent.id).toBeTruthy()
expect(consent.consent_method).toBe('custom_selection')
expect(consent.device_type).toBe('desktop')
expect(consent.browser).toBe('Chrome/120.0')
expect(consent.os).toBe('Mac OS X 10.15.7')
expect(consent.page_url).toBe('https://test.example.com/pricing')
expect(consent.session_id).toBe('e2e-session-001')
expect(consent.geo_country).toBe('DE') // Europe/Berlin → DE
expect(consent.scripts_released).toHaveLength(1)
expect(consent.cookies_set).toHaveLength(2)
expect(consent.vendor_consents).toEqual({ 'Google Analytics': true, 'Matomo': true, 'Facebook Pixel': false })
})
test('should update consent on same fingerprint (upsert)', async ({ request }) => {
const res = await request.post(`${API_BASE}/consent`, {
headers: HEADERS,
data: {
site_id: SITE_ID,
device_fingerprint: DEVICE_FP,
categories: ['essential'], // changed from all 3 to essential only
vendors: [],
consent_method: 'reject_all',
page_url: 'https://test.example.com/settings',
timezone: 'Europe/Vienna',
},
})
expect(res.status()).toBe(200)
const consent = await res.json()
expect(consent.consent_method).toBe('reject_all')
expect(consent.geo_country).toBe('AT') // Europe/Vienna → AT
expect(consent.categories).toEqual(['essential'])
})
})
// ============================================================================
// 2. DSR Scenarios — Art. 15 Auskunft
// ============================================================================
test.describe('DSR — Art. 15 Auskunftsrecht', () => {
const DSR_EMAIL = `dsr-user-${TS}@example.com`
const DSR_DEVICE_1 = `dsr-desktop-${TS}`
const DSR_DEVICE_2 = `dsr-mobile-${TS}`
test.beforeAll(async ({ request }) => {
// Scenario: User visited website from 2 devices, then linked their email
// Device 1: Desktop consent
await request.post(`${API_BASE}/consent`, {
headers: HEADERS,
data: {
site_id: SITE_ID,
device_fingerprint: DSR_DEVICE_1,
categories: ['essential', 'analytics'],
consent_method: 'accept_all',
device_type: 'desktop',
browser: 'Firefox/121.0',
page_url: 'https://test.example.com/',
timezone: 'Europe/Berlin',
},
})
// Device 2: Mobile consent
await request.post(`${API_BASE}/consent`, {
headers: HEADERS,
data: {
site_id: SITE_ID,
device_fingerprint: DSR_DEVICE_2,
categories: ['essential'],
consent_method: 'reject_all',
device_type: 'mobile',
browser: 'Safari/17.0',
page_url: 'https://test.example.com/pricing',
timezone: 'Europe/Berlin',
},
})
// User logs in and links email to both devices
await request.post(`${API_BASE}/consent/link-email`, {
headers: HEADERS,
data: { site_id: SITE_ID, device_fingerprint: DSR_DEVICE_1, email: DSR_EMAIL },
})
await request.post(`${API_BASE}/consent/link-email`, {
headers: HEADERS,
data: { site_id: SITE_ID, device_fingerprint: DSR_DEVICE_2, email: DSR_EMAIL },
})
})
test('Art. 15 — should find all consents by email', async ({ request }) => {
const res = await request.get(`${API_BASE}/consent/by-email/${DSR_EMAIL}`, { headers: HEADERS })
expect(res.status()).toBe(200)
const consents = await res.json()
expect(consents).toHaveLength(2)
expect(consents.map((c: { device_fingerprint: string }) => c.device_fingerprint).sort()).toEqual(
[DSR_DEVICE_1, DSR_DEVICE_2].sort()
)
})
test('Art. 15/20 — should export all consent data for DSR', async ({ request }) => {
const res = await request.get(`${API_BASE}/consent/dsr-export/${DSR_EMAIL}`, { headers: HEADERS })
expect(res.status()).toBe(200)
const exportData = await res.json()
expect(exportData.consents).toHaveLength(2)
expect(exportData.audit_trail.length).toBeGreaterThan(0)
})
test('Art. 17 — should delete all consents by email (erasure)', async ({ request }) => {
const res = await request.delete(`${API_BASE}/consent/by-email/${DSR_EMAIL}`, { headers: HEADERS })
expect(res.status()).toBe(200)
const result = await res.json()
expect(result.deleted_count).toBe(2)
// Verify deletion
const check = await request.get(`${API_BASE}/consent/by-email/${DSR_EMAIL}`, { headers: HEADERS })
const remaining = await check.json()
expect(remaining).toHaveLength(0)
})
})
// ============================================================================
// 3. DSR Scenarios — Cookie Banner User (anonymous)
// ============================================================================
test.describe('DSR — Anonymous Cookie Banner User', () => {
const ANON_DEVICE = `anon-user-${TS}`
test.beforeAll(async ({ request }) => {
await request.post(`${API_BASE}/consent`, {
headers: HEADERS,
data: {
site_id: SITE_ID,
device_fingerprint: ANON_DEVICE,
categories: ['essential', 'functional'],
consent_method: 'custom_selection',
device_type: 'tablet',
browser: 'Chrome/120.0',
},
})
})
test('should export consent by device fingerprint', async ({ request }) => {
const res = await request.get(
`${API_BASE}/consent/export?site_id=${SITE_ID}&device_fingerprint=${ANON_DEVICE}`,
{ headers: HEADERS }
)
expect(res.status()).toBe(200)
const data = await res.json()
expect(data.device_fingerprint).toBe(ANON_DEVICE)
expect(data.consents).toHaveLength(1)
expect(data.audit_trail.length).toBeGreaterThan(0)
})
test('should withdraw consent by ID', async ({ request }) => {
// Get consent ID first
const getRes = await request.get(
`${API_BASE}/consent?site_id=${SITE_ID}&device_fingerprint=${ANON_DEVICE}`,
{ headers: HEADERS }
)
const { consent } = await getRes.json()
expect(consent).toBeTruthy()
// Withdraw
const delRes = await request.delete(`${API_BASE}/consent/${consent.id}`, { headers: HEADERS })
expect(delRes.status()).toBe(200)
// Verify
const checkRes = await request.get(
`${API_BASE}/consent?site_id=${SITE_ID}&device_fingerprint=${ANON_DEVICE}`,
{ headers: HEADERS }
)
const result = await checkRes.json()
expect(result.has_consent).toBe(false)
})
})
// ============================================================================
// 4. DSR Scenarios — Login User (Customer) who also used Cookie Banner
// ============================================================================
test.describe('DSR — Customer with Banner + Login', () => {
const CUSTOMER_EMAIL = `customer-${TS}@company.com`
const CUSTOMER_DEVICE = `customer-device-${TS}`
test('full lifecycle: consent → login → link → Art.15 → Art.17', async ({ request }) => {
// Step 1: Anonymous visit → cookie consent
const consentRes = await request.post(`${API_BASE}/consent`, {
headers: HEADERS,
data: {
site_id: SITE_ID,
device_fingerprint: CUSTOMER_DEVICE,
categories: ['essential', 'analytics'],
consent_method: 'accept_all',
device_type: 'desktop',
browser: 'Edge/120.0',
page_url: 'https://test.example.com/',
timezone: 'Europe/Zurich',
scripts_released: [{ src: 'https://cdn.matomo.cloud/test.js', category: 'analytics' }],
cookies_set: [{ name: '_pk_id', domain: '.test.example.com', expiry_days: 393, category: 'analytics' }],
},
})
expect(consentRes.status()).toBe(200)
const consent = await consentRes.json()
expect(consent.geo_country).toBe('CH') // Europe/Zurich → CH
// Step 2: Customer logs in → email linked
const linkRes = await request.post(`${API_BASE}/consent/link-email`, {
headers: HEADERS,
data: { site_id: SITE_ID, device_fingerprint: CUSTOMER_DEVICE, email: CUSTOMER_EMAIL },
})
expect(linkRes.status()).toBe(200)
// Step 3: Art. 15 — Customer requests their data
const exportRes = await request.get(`${API_BASE}/consent/dsr-export/${CUSTOMER_EMAIL}`, { headers: HEADERS })
expect(exportRes.status()).toBe(200)
const exportData = await exportRes.json()
expect(exportData.consents.length).toBeGreaterThan(0)
expect(exportData.audit_trail.length).toBeGreaterThan(0)
// Verify export contains all consent details
const exported = exportData.consents[0]
expect(exported.categories).toContain('analytics')
expect(exported.linked_email).toBe(CUSTOMER_EMAIL)
// Step 4: Art. 17 — Customer requests erasure
const deleteRes = await request.delete(`${API_BASE}/consent/by-email/${CUSTOMER_EMAIL}`, { headers: HEADERS })
expect(deleteRes.status()).toBe(200)
// Step 5: Verify complete erasure
const verifyRes = await request.get(`${API_BASE}/consent/by-email/${CUSTOMER_EMAIL}`, { headers: HEADERS })
const remaining = await verifyRes.json()
expect(remaining).toHaveLength(0)
})
})
// ============================================================================
// 5. Admin Dashboard Integration
// ============================================================================
test.describe('Admin Dashboard — Consent Management', () => {
const ADMIN_DEVICE = `admin-test-${TS}`
test.beforeAll(async ({ request }) => {
await request.post(`${API_BASE}/consent`, {
headers: HEADERS,
data: {
site_id: SITE_ID,
device_fingerprint: ADMIN_DEVICE,
categories: ['essential', 'functional', 'analytics'],
vendors: ['Matomo'],
vendor_consents: { Matomo: true },
consent_method: 'accept_all',
device_type: 'desktop',
browser: 'Chrome/121.0',
os: 'Windows NT 10.0',
screen_resolution: '2560x1440',
page_url: 'https://test.example.com/dashboard',
session_id: 'admin-session-001',
timezone: 'Europe/Berlin',
},
})
})
test('should list consents with new fields', async ({ request }) => {
const res = await request.get(`${API_BASE}/admin/consents?site_id=${SITE_ID}`, { headers: HEADERS })
expect(res.status()).toBe(200)
const data = await res.json()
expect(data.total).toBeGreaterThan(0)
const consent = data.consents.find((c: { device_fingerprint: string }) => c.device_fingerprint === ADMIN_DEVICE)
expect(consent).toBeTruthy()
expect(consent.consent_method).toBe('accept_all')
expect(consent.device_type).toBe('desktop')
expect(consent.browser).toBe('Chrome/121.0')
expect(consent.os).toBe('Windows NT 10.0')
expect(consent.screen_resolution).toBe('2560x1440')
expect(consent.session_id).toBe('admin-session-001')
expect(consent.geo_country).toBe('DE')
expect(consent.vendor_consents).toEqual({ Matomo: true })
})
test('should show site stats with category acceptance', async ({ request }) => {
const res = await request.get(`${API_BASE}/admin/stats/${SITE_ID}`, { headers: HEADERS })
expect(res.status()).toBe(200)
const stats = await res.json()
expect(stats.total_consents).toBeGreaterThan(0)
expect(stats.category_acceptance).toBeTruthy()
expect(stats.category_acceptance.essential).toBeTruthy()
expect(stats.category_acceptance.essential.rate).toBeGreaterThan(0)
})
})
// ============================================================================
// 6. Cleanup
// ============================================================================
test.describe('Cleanup', () => {
test('should delete test site config', async ({ request }) => {
const res = await request.delete(`${API_BASE}/admin/sites/${SITE_ID}`, { headers: HEADERS })
expect(res.status()).toBe(204)
})
})
@@ -3,6 +3,7 @@ package handlers
import (
"fmt"
"net/http"
"strings"
"github.com/breakpilot/ai-compliance-sdk/internal/iace"
"github.com/gin-gonic/gin"
@@ -138,7 +139,8 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
// ── Step 5: Create hazards from matched patterns (skip if exist) ──
existingHazards, _ := h.store.ListHazards(ctx, projectID)
hazardStep := InitStep{Name: "Gefaehrdungen erstellt", Status: "skipped"}
hazardIDsByCategory := make(map[string]uuid.UUID)
hazardIDsByCategory := make(map[string][]uuid.UUID)
hazardPatternMeasures := make(map[uuid.UUID][]string)
if len(existingHazards) == 0 && len(matchOutput.MatchedPatterns) > 0 {
comps, _ := h.store.ListComponents(ctx, projectID)
@@ -158,32 +160,35 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
}
created := 0
seenCatZone := make(map[string]bool)
seenCatZone := make(map[string]uuid.UUID) // dedupKey → hazardID
catCount := make(map[string]int)
for _, mp := range matchOutput.MatchedPatterns {
// Narrative relevance filter: skip patterns whose zone/scenario
// mentions machine-specific terms that don't appear in our components
// Narrative relevance filter
if !isPatternRelevant(mp, narrativeText, compNames) {
continue
}
for _, cat := range mp.HazardCats {
// Per-category cap: limit hazards per category based on relevance
maxForCat := categoryHazardCap(cat, len(comps))
if catCount[cat] >= maxForCat {
continue
}
// Dedup by category + normalized zone
zoneKey := normalizeZoneKey(mp.ZoneDE)
if zoneKey == "" {
zoneKey = mp.PatternID
}
dedupKey := cat + ":" + zoneKey
if seenCatZone[dedupKey] {
// If this dedupKey already exists but current pattern has
// SuggestedMeasureIDs, add them to the existing hazard
if existingHzID, exists := seenCatZone[dedupKey]; exists {
if len(mp.SuggestedMeasureIDs) > 0 {
existing := hazardPatternMeasures[existingHzID]
hazardPatternMeasures[existingHzID] = append(existing, mp.SuggestedMeasureIDs...)
}
continue
}
seenCatZone[dedupKey] = true
name := mp.PatternName
if name == "" {
@@ -204,6 +209,9 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
}
}
// Join all applicable lifecycles as comma-separated string
lifecycleStr := strings.Join(mp.ApplicableLifecycles, ",")
hz, cerr := h.store.CreateHazard(ctx, iace.CreateHazardRequest{
ProjectID: projectID,
ComponentID: compID,
@@ -212,6 +220,7 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
Category: cat,
Scenario: mp.ScenarioDE,
Function: iace.EncodeOpStates(mp.OperationalStates),
LifecyclePhase: lifecycleStr,
TriggerEvent: mp.TriggerDE,
PossibleHarm: mp.HarmDE,
AffectedPerson: mp.AffectedDE,
@@ -220,7 +229,11 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
if cerr == nil {
created++
catCount[cat]++
hazardIDsByCategory[cat] = hz.ID
seenCatZone[dedupKey] = hz.ID
hazardIDsByCategory[cat] = append(hazardIDsByCategory[cat], hz.ID)
if len(mp.SuggestedMeasureIDs) > 0 {
hazardPatternMeasures[hz.ID] = mp.SuggestedMeasureIDs
}
}
}
}
@@ -229,7 +242,7 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
hazardStep.Details = "Bereits vorhanden"
hazardStep.Count = len(existingHazards)
for _, eh := range existingHazards {
hazardIDsByCategory[eh.Category] = eh.ID
hazardIDsByCategory[eh.Category] = append(hazardIDsByCategory[eh.Category], eh.ID)
}
}
steps = append(steps, hazardStep)
@@ -248,37 +261,60 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
}
created := 0
usedMeasureIDs := make(map[string]bool)
const maxMitigationsPerHazard = 5
for _, sm := range matchOutput.SuggestedMeasures {
entry, ok := measureByID[sm.MeasureID]
if !ok || usedMeasureIDs[sm.MeasureID] {
continue
}
hazardID := findHazardForMeasureByCategory(entry.HazardCategory, hazardIDsByCategory)
if hazardID == uuid.Nil {
continue
}
rt := iace.ReductionType(entry.ReductionType)
if rt == "" {
rt = iace.ReductionTypeInformation
}
_, cerr := h.store.CreateMitigation(ctx, iace.CreateMitigationRequest{
HazardID: hazardID, ReductionType: rt,
Name: entry.Name, Description: entry.Description,
})
if cerr == nil {
created++
usedMeasureIDs[sm.MeasureID] = true
// Build a flat list of all hazard IDs for iteration
var allHazardIDs []uuid.UUID
hazardCatByID := make(map[uuid.UUID]string)
for cat, ids := range hazardIDsByCategory {
for _, id := range ids {
allHazardIDs = append(allHazardIDs, id)
hazardCatByID[id] = cat
}
}
for hazCat, hazID := range hazardIDsByCategory {
// For each hazard: assign up to maxMitigationsPerHazard measures
// Priority 1: Pattern-specific SuggestedMeasureIDs (from the pattern that created this hazard)
// Priority 2: Category fallback (generic measures for the hazard category)
for _, hazID := range allHazardIDs {
hazCat := hazardCatByID[hazID]
measCat := patternCatToMeasureCat(hazCat)
added := 0
usedIDs := make(map[string]bool)
// Priority 1: Pattern-specific measures
if patternMIDs, ok := hazardPatternMeasures[hazID]; ok {
for _, mid := range patternMIDs {
if added >= maxMitigationsPerHazard {
break
}
entry, ok := measureByID[mid]
if !ok {
continue
}
rt := iace.ReductionType(entry.ReductionType)
if rt == "" {
rt = iace.ReductionTypeInformation
}
_, cerr := h.store.CreateMitigation(ctx, iace.CreateMitigationRequest{
HazardID: hazID, ReductionType: rt,
Name: entry.Name, Description: entry.Description,
})
if cerr != nil {
fmt.Printf("MEASURE-ERROR: mid=%s name=%s err=%v\n", mid, entry.Name, cerr)
} else {
created++
added++
usedIDs[mid] = true
}
}
}
// Priority 2: Category fallback (skip already-used IDs)
for _, m := range measuresByCat[measCat] {
if usedMeasureIDs[m.ID] || added >= 8 {
break
if added >= maxMitigationsPerHazard || usedIDs[m.ID] {
continue
}
rt := iace.ReductionType(m.ReductionType)
if rt == "" {
@@ -290,12 +326,16 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
})
if cerr == nil {
created++
usedMeasureIDs[m.ID] = true
added++
}
}
}
mitStep = InitStep{Name: "Massnahmen erstellt", Status: "done", Count: created}
patternMeasureCount := 0
for _, mids := range hazardPatternMeasures {
patternMeasureCount += len(mids)
}
mitStep = InitStep{Name: "Massnahmen erstellt", Status: "done", Count: created,
Details: fmt.Sprintf("%d pattern-spezifisch fuer %d Hazards", patternMeasureCount, len(hazardPatternMeasures))}
} else if len(existingMits) > 0 {
mitStep.Details = "Bereits vorhanden"
mitStep.Count = len(existingMits)
@@ -217,6 +217,13 @@ var genericSafetyTerms = map[string]bool{
"leitfaehig": true, "elektrisch": true, "mechanisch": true,
"bedienfeld": true, "display": true, "anzeige": true,
"energie": true, "druck": true, "temperatur": true,
// Abbreviations and synonyms that should not trigger relevance filter
"kss": true, "emv": true, "esd": true, "dcs": true, "plr": true, "sil": true,
"hmi": true, "sps": true, "rcd": true, "loto": true, "psa": true,
// Common action words
"bersten": true, "platzen": true, "abspringen": true, "spritzen": true,
"einatmen": true, "ausrutschen": true, "herabfallen": true,
"durchschlaegen": true, "wegschleudern": true,
// Common structural terms that don't indicate a specific machine
"gesamter": true, "gesamtes": true, "bereichs": true, "stelle": true,
"innen": true, "aussen": true, "transport": true, "seite": true,
@@ -369,18 +376,15 @@ func normalizeZoneKey(zone string) string {
return strings.Join(sig, "_")
}
// findHazardForMeasureByCategory finds a matching hazard for a measure.
func findHazardForMeasureByCategory(measureCat string, hazardsByCategory map[string]uuid.UUID) uuid.UUID {
if id, ok := hazardsByCategory[measureCat]; ok {
return id
// findHazardsForMeasureByCategory finds all hazards matching a measure's category.
func findHazardsForMeasureByCategory(measureCat string, hazardsByCategory map[string][]uuid.UUID) []uuid.UUID {
if ids, ok := hazardsByCategory[measureCat]; ok {
return ids
}
for cat, id := range hazardsByCategory {
for cat, ids := range hazardsByCategory {
if len(measureCat) > 3 && len(cat) > 3 && cat[:4] == measureCat[:4] {
return id
return ids
}
}
for _, id := range hazardsByCategory {
return id
}
return uuid.Nil
return nil
}
@@ -9,49 +9,9 @@ import (
// Fuzzy matching: Ground Truth entries ↔ Engine hazards
// ============================================================================
const matchThreshold = 0.25
const matchThreshold = 0.20
// categoryMap maps GT hazard_group (German) to engine category prefixes.
var categoryMap = map[string][]string{
"mechanische gefaehrdungen": {"mechanical"},
"elektrische gefaehrdungen": {"electrical"},
"thermische gefaehrdungen": {"thermal"},
"gefaehrdungen durch laerm": {"noise", "ergonomic"},
"gefaehrdungen durch vibration": {"noise", "vibration"},
"gefaehrdungen durch strahlung": {"radiation", "emc"},
"gefaehrdungen durch materialien und substanzen": {"material", "environmental"},
"ergonomische gefaehrdungen": {"ergonomic"},
"gefaehrdungen im zusammenhang mit der einsatzumgebung": {"environmental"},
}
// synonymSets groups equivalent hazard terms for keyword matching.
var synonymSets = [][]string{
{"quetsch", "crush", "einklemm", "klemm"},
{"scher", "shear", "absch"},
{"schneid", "cut", "schnitt"},
{"stoss", "schlag", "impact", "treff", "aufprall"},
{"einzug", "fang", "erfass", "entangle", "wickel"},
{"elektrisch", "stromschlag", "electric", "beruehr", "spannungsfuehr", "koerperdurchstroemung"},
{"brand", "feuer", "fire", "kabelbrand", "kurzschluss", "ueberlast", "ueberstrom"},
{"verbrenn", "burn", "heiss", "thermisch", "lichtbogen"},
{"laerm", "noise", "gehoer", "schall", "dezibel"},
{"vibration", "schwing"},
{"ergonom", "haltung", "handhabung", "bedien", "bewegungsapparat"},
{"kuehlschmierstoff", "kss", "aerosol", "coolant"},
{"pneumat", "druckluft", "compressed"},
{"hydraul", "druck", "pressure"},
{"roboter", "robot", "roboterarm"},
{"greifer", "gripper", "schunk"},
{"foerderband", "transport", "conveyor"},
{"schutzzaun", "schutzgitter", "fence", "guard"},
{"werkzeugmaschine", "robodrill", "bearbeitungszentrum", "wzm"},
{"stolper", "rutsch", "slip", "trip"},
{"leckage", "austreten", "leak"},
{"einstich", "puncture", "spritz"},
{"isolat", "kriechstrom", "schutzleiter", "erdung", "indirekt"},
{"luft", "kriechstreck", "beruehrer", "oberflaeche", "leitfaehig"},
{"emv", "strahlung", "radiation", "elektromagnet", "stoereinfluss"},
}
// categoryMap, synonymSets, wrongMachineTerms → benchmark_synonyms.go
// CompareBenchmark runs the full comparison between Ground Truth and engine output.
func CompareBenchmark(gt *GroundTruth, hazards []Hazard, mitigations []Mitigation) *BenchmarkResult {
@@ -68,15 +28,17 @@ func CompareBenchmark(gt *GroundTruth, hazards []Hazard, mitigations []Mitigatio
engineSummaries := make([]HazardSummary, len(hazards))
for i, h := range hazards {
engineSummaries[i] = HazardSummary{
ID: h.ID.String(),
Name: h.Name,
Category: h.Category,
Zone: h.HazardousZone,
Description: h.Description,
Scenario: h.Scenario,
PossibleHarm: h.PossibleHarm,
TriggerEvent: h.TriggerEvent,
Mitigations: mitNamesByHazard[h.ID.String()],
ID: h.ID.String(),
Name: h.Name,
Category: h.Category,
Zone: h.HazardousZone,
Description: h.Description,
Scenario: h.Scenario,
PossibleHarm: h.PossibleHarm,
TriggerEvent: h.TriggerEvent,
AffectedPerson: h.AffectedPerson,
LifecyclePhase: h.LifecyclePhase,
Mitigations: mitNamesByHazard[h.ID.String()],
}
}
@@ -96,8 +58,17 @@ func CompareBenchmark(gt *GroundTruth, hazards []Hazard, mitigations []Mitigatio
}
}
// Greedy best-first 1:1 assignment
sort.Slice(pairs, func(a, b int) bool { return pairs[a].score > pairs[b].score })
// Greedy assignment: sort by score, but prioritize high-specificity matches
// (matches where both category AND zone overlap) over generic ones
sort.Slice(pairs, func(a, b int) bool {
// First: prioritize matches with zone overlap (more specific)
aHasZone := pairs[a].reason != "" && (strings.Contains(pairs[a].reason, "Zone") || strings.Contains(pairs[a].reason, "Keywords+Zone"))
bHasZone := pairs[b].reason != "" && (strings.Contains(pairs[b].reason, "Zone") || strings.Contains(pairs[b].reason, "Keywords+Zone"))
if aHasZone != bHasZone {
return aHasZone
}
return pairs[a].score > pairs[b].score
})
usedGT := make(map[int]bool)
usedEng := make(map[int]bool)
var matched []HazardMatchPair
@@ -187,52 +158,129 @@ func CompareBenchmark(gt *GroundTruth, hazards []Hazard, mitigations []Mitigatio
}
// fuzzyMatchScore computes a 0-1 similarity between a GT entry and an engine hazard.
// 4 signals: category (0.2), keywords (0.2), zone (0.3), scenario similarity (0.3).
func fuzzyMatchScore(gt *GroundTruthEntry, h *Hazard) (float64, string) {
var score float64
var reasons []string
// 1. Category match (weight 0.3)
// 1. Category match (weight 0.2)
catScore := categoryMatchScore(gt.HazardGroup, h.Category)
score += 0.3 * catScore
score += 0.2 * catScore
if catScore > 0 {
reasons = append(reasons, "Kategorie")
}
// 2. Keyword/synonym match on hazard TYPE (weight 0.3)
// 2. Keyword/synonym match on hazard TYPE (weight 0.2)
kwScore := keywordMatchScore(gt.HazardType, gt.HazardCause, h.Name, h.Description, h.Scenario)
score += 0.3 * kwScore
score += 0.2 * kwScore
if kwScore > 0 {
reasons = append(reasons, "Keywords")
}
// 3. Component/zone match (weight 0.4 — most important for specificity)
// 3. Component/zone match (weight 0.3)
zoneScore := zoneMatchScore(gt.ComponentZone, gt.HazardSubgroup, h.HazardousZone, h.MachineModule)
score += 0.4 * zoneScore
score += 0.3 * zoneScore
if zoneScore > 0 {
reasons = append(reasons, "Zone")
}
// Penalty: if engine hazard mentions a machine-specific term not in the GT context,
// it's likely a wrong-machine match (e.g. "Spielplatz" for a robot cell GT entry)
// 4. Scenario similarity (weight 0.3) — compares the actual event description
scenScore := scenarioSimilarity(gt.HazardCause, h.Scenario, h.Name)
score += 0.3 * scenScore
if scenScore > 0 {
reasons = append(reasons, "Szenario")
}
// Penalty: wrong machine term
if hasWrongMachineTerm(h.Name, h.Scenario, gt.HazardCause, gt.ComponentZone) {
score *= 0.3 // Heavy penalty
score *= 0.3
reasons = append(reasons, "Strafabzug:FremdMaschine")
}
// Penalty: no keyword AND no scenario overlap → unreliable
if kwScore == 0 && scenScore == 0 && zoneScore < 0.5 {
score *= 0.4
reasons = append(reasons, "Strafabzug:KeinInhalt")
}
return score, strings.Join(reasons, "+")
}
// wrongMachineTerms are words in an engine hazard that indicate it's about
// a completely different machine type. If the GT entry doesn't mention these,
// the match is penalized.
var wrongMachineTerms = []string{
"spielplatz", "fahrtreppe", "trommelwaschmaschine", "umreifungsband",
"drehteller", "rundtaktanlage", "exzentrisch", "webstuhl",
"aufzug", "rolltreppe", "bagger", "kettensaege", "kreissaege",
"druckmaschine", "zentrifuge", "autoklav", "hobel",
"naehmaschine", "strickmaschine", "schleifmaschine",
"gabelstapler", "flurfoerder", "erntemaschine",
"kollision zweier roboter",
// scenarioSimilarity compares the GT cause description with the engine scenario.
// Uses action words + synonym-set cross-matching for robust comparison.
func scenarioSimilarity(gtCause, engScenario, engName string) float64 {
gtText := normalizeDE(gtCause)
engText := normalizeDE(engScenario + " " + engName)
gtActions := extractActionWords(gtText)
engActions := extractActionWords(engText)
if len(gtActions) == 0 {
// Fallback: use significant word overlap
return significantWordOverlap(gtText, engText)
}
matched := 0
for _, ga := range gtActions {
// Direct match
directFound := false
for _, ea := range engActions {
if ga == ea || strings.HasPrefix(ea, ga) || strings.HasPrefix(ga, ea) {
directFound = true
break
}
}
if directFound {
matched++
continue
}
// Synonym-set match: if GT action and any engine action are in the same synonym set
for _, synSet := range synonymSets {
gaInSet := false
for _, syn := range synSet {
if strings.Contains(ga, syn) || strings.Contains(syn, ga) {
gaInSet = true
break
}
}
if !gaInSet {
continue
}
// Check if any engine action is in this same set
for _, ea := range engActions {
for _, syn := range synSet {
if strings.Contains(ea, syn) || strings.Contains(syn, ea) {
matched++
goto nextAction
}
}
}
// Also check full engine text for synonym hit
for _, syn := range synSet {
if strings.Contains(engText, syn) {
matched++
goto nextAction
}
}
}
nextAction:
}
return float64(matched) / float64(len(gtActions))
}
// significantWordOverlap is a fallback when no action words are found.
func significantWordOverlap(gtText, engText string) float64 {
gtWords := extractSignificantWords(gtText)
if len(gtWords) == 0 {
return 0
}
matched := 0
for _, w := range gtWords {
if strings.Contains(engText, w) {
matched++
}
}
return float64(matched) / float64(len(gtWords))
}
func hasWrongMachineTerm(engName, engScenario, gtCause, gtZone string) bool {
@@ -0,0 +1,120 @@
package iace
import "strings"
// synonymSets groups equivalent hazard terms for keyword matching.
var synonymSets = [][]string{
{"quetsch", "crush", "einklemm", "klemm"},
{"scher", "shear", "absch"},
{"schneid", "cut", "schnitt"},
{"stoss", "schlag", "impact", "treff", "aufprall"},
{"einzug", "fang", "erfass", "entangle", "wickel"},
{"elektrisch", "stromschlag", "electric", "beruehr", "spannungsfuehr", "koerperdurchstroemung"},
{"brand", "feuer", "fire", "kabelbrand", "kurzschluss", "ueberlast", "ueberstrom"},
{"verbrenn", "burn", "heiss", "thermisch", "lichtbogen"},
{"laerm", "noise", "gehoer", "schall", "dezibel"},
{"vibration", "schwing"},
{"ergonom", "haltung", "handhabung", "bedien", "bewegungsapparat"},
{"kuehlschmierstoff", "kss", "aerosol", "coolant"},
{"pneumat", "druckluft", "compressed"},
{"hydraul", "druck", "pressure"},
{"roboter", "robot", "roboterarm"},
{"greifer", "gripper", "schunk"},
{"foerderband", "transport", "conveyor"},
{"schutzzaun", "schutzgitter", "fence", "guard"},
{"werkzeugmaschine", "robodrill", "bearbeitungszentrum", "wzm"},
{"stolper", "rutsch", "slip", "trip"},
{"leckage", "austreten", "leak"},
{"einstich", "puncture", "spritz"},
{"isolat", "kriechstrom", "schutzleiter", "erdung", "indirekt"},
{"luft", "kriechstreck", "beruehrer", "oberflaeche", "leitfaehig"},
{"emv", "strahlung", "radiation", "elektromagnet", "stoereinfluss"},
{"eingeschlossen", "eingesperrt", "wiederanlauf", "quittier"},
{"zentriergreifer", "zentriereinheit", "zentrieren"},
{"beladetuer", "schutztuer", "zugangstuer", "tuerposition"},
{"werkstueck", "rohteil", "rohling"},
{"ergonom", "einlege", "bedienelemente", "arbeitshoehe", "haltung"},
{"boden", "tragfaehig", "einbrech", "fundamentierr"},
{"spritzer", "auge", "augenverletz"},
{"bersten", "platzen", "abspring"},
{"durchschlag", "durchbrech", "begrenz", "bewegungsbereich"},
{"potentialausgleich", "potentialunter", "bezugspotential", "potential", "energieversorgung"},
{"kriechstreck", "luft-", "kriechst", "dimensionie", "kurzschluss"},
{"emv", "elektromagnet", "stoereinfluss", "stoerung", "sicherheitsrelevant"},
{"kuehlschmierstoff", "kss", "bettspuel", "kuehlung"},
{"rutsch", "ausrutsch", "stolper", "gleiten", "nassrutsch"},
}
// wrongMachineTerms are words in an engine hazard that indicate it's about
// a completely different machine type.
var wrongMachineTerms = []string{
"spielplatz", "fahrtreppe", "trommelwaschmaschine", "umreifungsband",
"drehteller", "rundtaktanlage", "exzentrisch", "webstuhl",
"aufzug", "rolltreppe", "bagger", "kettensaege", "kreissaege",
"druckmaschine", "zentrifuge", "autoklav", "hobel",
"naehmaschine", "strickmaschine", "schleifmaschine",
"gabelstapler", "flurfoerder", "erntemaschine",
"kollision zweier roboter",
}
// categoryMap maps GT hazard_group (German) to engine category prefixes.
var categoryMap = map[string][]string{
"mechanische gefaehrdungen": {"mechanical"},
"elektrische gefaehrdungen": {"electrical"},
"thermische gefaehrdungen": {"thermal"},
"gefaehrdungen durch laerm": {"noise", "ergonomic"},
"gefaehrdungen durch vibration": {"noise", "vibration"},
"gefaehrdungen durch strahlung": {"radiation", "emc"},
"gefaehrdungen durch materialien und substanzen": {"material", "environmental"},
"ergonomische gefaehrdungen": {"ergonomic"},
"gefaehrdungen im zusammenhang mit der einsatzumgebung": {"environmental"},
}
// extractActionWords pulls out verbs and descriptors that define the hazard event.
func extractActionWords(text string) []string {
// These are the differentiating words between similar-looking hazards
actionTerms := []string{
"eingeklemmt", "einklemm", "eingeschlossen", "eingesperrt",
"herabfall", "herunterfal", "faellt",
"durchschlaegt", "durchbrech", "durchschlag",
"springt ab", "abspring", "bersten", "platzen",
"weggeschleudert", "schleuder",
"getroffen", "treff",
"greift", "eingreif", "durchgreif", "uebergreif",
"beruehrt", "beruehr", "kontakt",
"einzug", "erfass", "aufwickel",
"stolper", "rutsch", "ausrutsch", "gleiten",
"verbren", "heiss",
"spritzer", "augenver",
"kurzschluss", "ueberstrom", "ueberlast",
"isolat", "schutzleiter", "kriechstrom", "kriechstreck",
"potentialausgleich", "potentialunter", "bezugspotential", "potential",
"emv", "stoereinfluss", "elektromagnet", "stoerung",
"leckage", "austret", "undicht",
"schutzzaun", "einhausung", "schutztuer",
"wiederanlauf", "anlauf", "startet",
"teach", "einricht", "programmier",
"spannvorricht", "spannfutter", "greiferbacken",
"druckluft", "pneumatik", "restdruck",
"beladetuer", "werkzeugmaschine", "bearbeitungszelle",
"ergonom", "einlege", "bedienelement",
"tragfaehig", "boden", "einbrech",
// Additional terms for remaining GT gaps
"schlauch", "druck", "kuehlschmierstoff",
"bettspuel", "pumpe", "niederdruck",
"luft-", "dimensionie",
"anlagenteile", "energieversorgung",
"greifer", "werkzeug",
}
var found []string
seen := make(map[string]bool)
for _, term := range actionTerms {
if strings.Contains(text, term) && !seen[term] {
seen[term] = true
found = append(found, term)
}
}
return found
}
@@ -92,17 +92,19 @@ type HazardMatchPair struct {
// HazardSummary is a hazard representation for benchmark results with detail fields.
type HazardSummary struct {
ID string `json:"id"`
Name string `json:"name"`
Category string `json:"category"`
Component string `json:"component,omitempty"`
Zone string `json:"zone,omitempty"`
RiskLevel string `json:"risk_level,omitempty"`
Description string `json:"description,omitempty"`
Scenario string `json:"scenario,omitempty"`
PossibleHarm string `json:"possible_harm,omitempty"`
TriggerEvent string `json:"trigger_event,omitempty"`
Mitigations []string `json:"mitigations,omitempty"`
ID string `json:"id"`
Name string `json:"name"`
Category string `json:"category"`
Component string `json:"component,omitempty"`
Zone string `json:"zone,omitempty"`
RiskLevel string `json:"risk_level,omitempty"`
Description string `json:"description,omitempty"`
Scenario string `json:"scenario,omitempty"`
PossibleHarm string `json:"possible_harm,omitempty"`
TriggerEvent string `json:"trigger_event,omitempty"`
AffectedPerson string `json:"affected_person,omitempty"`
LifecyclePhase string `json:"lifecycle_phase,omitempty"`
Mitigations []string `json:"mitigations,omitempty"`
}
// CategoryScore shows coverage per ISO 12100 hazard group.
@@ -54,6 +54,10 @@ type HazardPattern struct {
// of the listed failure modes is relevant (by ComponentType match against project components).
// Empty/nil = fires regardless of failure modes (backwards compatible).
RequiredFailureModes []string `json:"required_failure_modes,omitempty"`
// ApplicableLifecycles lists the ISO 12100 lifecycle phases where this hazard
// is relevant. Written into the Hazard's LifecyclePhase field on creation.
// Empty = not set (pattern does not specify lifecycle applicability).
ApplicableLifecycles []string `json:"applicable_lifecycles,omitempty"`
}
// Standard human roles for machinery interaction (ISO 12100 + BetrSichV).
@@ -126,7 +126,7 @@ func GetCNCHazardPatterns() []HazardPattern {
DefaultSeverity: 4, DefaultExposure: 2,
},
{
ID: "HP1408", NameDE: "Falscher Werkzeug-Offset nach Einrichtung", NameEN: "Wrong tool offset after setup",
ID: "HP1408", NameDE: "Falscher Werkzeug-Offset", NameEN: "Wrong tool offset after setup",
RequiredComponentTags: []string{"cutting_tool", "programmable"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M041", "M050"},
@@ -149,7 +149,7 @@ func GetCNCHazardPatterns() []HazardPattern {
Priority: 84, MachineTypes: cncTypes,
OperationalStates: []string{"teach_mode", "manual_operation"},
HumanRoles: []string{"programmer", "maintenance_tech"},
ScenarioDE: "Achsen verfahren im Einrichtbetrieb mit voller Produktionsgeschwindigkeit",
ScenarioDE: "Achsen verfahren mit voller Produktionsgeschwindigkeit",
TriggerDE: "Fehlende Geschwindigkeitsbegrenzung im Einrichtmodus oder Umgehung",
HarmDE: "Quetschung oder Schlagverletzung durch schnell verfahrende Maschinenteile",
AffectedDE: "Einrichter, Programmierer", ZoneDE: "Verfahrbereich der Achsen",
@@ -49,7 +49,7 @@ func GetCNCHazardPatternsExt() []HazardPattern {
DefaultSeverity: 4, DefaultExposure: 3,
},
{
ID: "HP1423", NameDE: "Absturz schwerer Maschinenteile bei Wartung", NameEN: "Heavy machine part falling during maintenance",
ID: "HP1423", NameDE: "Absturz schwerer Maschinenteile", NameEN: "Heavy machine part falling during maintenance",
RequiredComponentTags: []string{"moving_part"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M245", "M210"},
@@ -57,7 +57,7 @@ func GetCNCHazardPatternsExt() []HazardPattern {
Priority: 80, MachineTypes: cncTypes,
OperationalStates: []string{"maintenance"},
HumanRoles: []string{"maintenance_tech"},
ScenarioDE: "Schwere Maschinenteile (Spindelstock, Revolverkopf) fallen bei Demontage unkontrolliert herab",
ScenarioDE: "Schwere Maschinenteile (Spindelstock, Revolverkopf) fallen unkontrolliert herab",
TriggerDE: "Fehlende Abstuetzmittel oder Hebezeuge bei Wartung schwerer Baugruppen",
HarmDE: "Quetschung von Hand oder Fuss, Knochenbrueche",
AffectedDE: "Wartungspersonal", ZoneDE: "Maschineninneres, Wartungszugang",
@@ -193,7 +193,7 @@ func GetCNCHazardPatternsExt() []HazardPattern {
DefaultSeverity: 2, DefaultExposure: 3,
},
{
ID: "HP1433", NameDE: "Unkontrollierte Achsbewegung bei Probelauf nach Wartung", NameEN: "Uncontrolled axis movement during test run after maintenance",
ID: "HP1433", NameDE: "Unkontrollierte Achsbewegung nach Probelauf", NameEN: "Uncontrolled axis movement during test run after maintenance",
RequiredComponentTags: []string{"moving_part", "programmable"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M212", "M050", "M042"},
@@ -202,7 +202,7 @@ func GetCNCHazardPatternsExt() []HazardPattern {
OperationalStates: []string{"manual_operation", "teach_mode"},
HumanRoles: []string{"maintenance_tech", "programmer"},
StateTransitions: []string{"maintenance→manual_operation"},
ScenarioDE: "Nach Wartung oder Reparatur verfahren Achsen unkontrolliert beim ersten Testlauf",
ScenarioDE: "oder Reparatur verfahren Achsen unkontrolliert beim ersten Testlauf",
TriggerDE: "Falsche Parameter nach Wartung, fehlende Referenzfahrt, Endschalter nicht justiert",
HarmDE: "Quetschung, Kollision Werkzeug/Werkstueck",
AffectedDE: "Wartungspersonal, Einrichter", ZoneDE: "Verfahrbereich, Bearbeitungsraum",
@@ -218,7 +218,7 @@ func GetCNCHazardPatternsExt() []HazardPattern {
Priority: 70, MachineTypes: cncTypes,
OperationalStates: []string{"maintenance"},
HumanRoles: []string{"maintenance_tech"},
ScenarioDE: "Restkuehlmittel tropft bei Wartung auf Schaltschrank oder Steuerungskomponenten",
ScenarioDE: "Restkuehlmittel tropft auf Schaltschrank oder Steuerungskomponenten",
TriggerDE: "Fehlende Auffangwanne oder Abdeckung bei Wartung an KSS-fuehrenden Bauteilen",
HarmDE: "Kurzschluss, Stromschlag bei Beruehrung nasser Teile",
AffectedDE: "Wartungspersonal", ZoneDE: "Schaltschrank, Steuerungsbereich",
@@ -11,7 +11,7 @@ func builtinElectricalPatterns() []HazardPattern {
SuggestedMeasureIDs: []string{"M061", "M062", "M063", "M121"},
SuggestedEvidenceIDs: []string{"E01", "E04", "E10"},
Priority: 95,
ScenarioDE: "Person beruehrt spannungsfuehrende Teile bei Wartung, Stoerungsbeseitigung oder durch defekte Isolation.",
ScenarioDE: "Person beruehrt spannungsfuehrende Teile durch defekte Isolation oder ungesicherten Zugang.",
TriggerDE: "Direktes oder indirektes Beruehren spannungsfuehrender Leiter ueber 50 V AC / 120 V DC.",
HarmDE: "Stromschlag, Herzkammerflimmern, Verbrennungen, Todesfolge bei Hochspannung.",
AffectedDE: "Wartungspersonal, Elektrofachkraefte, Bedienpersonal",
@@ -66,7 +66,7 @@ func builtinEnvironmentPatterns() []HazardPattern {
DefaultSeverity: 2, DefaultExposure: 5,
},
{
ID: "HP027", NameDE: "Ergonomische Belastung bei Wartung in der Hoehe", NameEN: "Ergonomic risk for work at height",
ID: "HP027", NameDE: "Ergonomische Belastung in der Hoehe", NameEN: "Ergonomic risk for work at height",
RequiredComponentTags: []string{"structural_part", "gravity_risk"},
RequiredEnergyTags: []string{},
GeneratedHazardCats: []string{"ergonomic", "mechanical_hazard"},
@@ -130,7 +130,7 @@ func GetExtendedHazardPatterns2() []HazardPattern {
SuggestedMeasureIDs: []string{"M121", "M131"},
SuggestedEvidenceIDs: []string{"E14"},
Priority: 90,
ScenarioDE: "Nach Wartung vergessenes Werkzeug wird beim Anlauf der Maschine zum Geschoss.",
ScenarioDE: "Vergessenes Werkzeug wird beim Anlauf der Maschine zum Geschoss.",
TriggerDE: "Werkzeug liegt im Arbeitsraum, Maschine wird ohne Kontrolle gestartet",
HarmDE: "Wegschleudern des Werkzeugs, schwere Verletzungen durch Projektil",
AffectedDE: "Bedienpersonal, Personen im Umfeld",
@@ -262,7 +262,7 @@ func GetExtendedHazardPatterns2() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M003", "M005"},
SuggestedEvidenceIDs: []string{"E08"},
Priority: 80,
Priority: 80, MachineTypes: []string{"press"},
ScenarioDE: "Exzentrische Belastung des Stoessels fuehrt zu seitlichem Ausbrechen des Werkstuecks.",
TriggerDE: "Werkstueck nicht korrekt positioniert, seitliche Kraftkomponente entsteht",
HarmDE: "Aufprallverletzung durch geschleudertes Werkstueck, Quetschung",
@@ -290,7 +290,7 @@ func GetExtendedHazardPatterns2() []HazardPattern {
// Roboter/Cobot erweitert (HP151-HP154)
// ================================================================
{
ID: "HP151", NameDE: "Kollision bei Teach-In-Betrieb", NameEN: "Collision during teach-in operation",
ID: "HP151", NameDE: "Kollision im manuellen Verfahrbetrieb", NameEN: "Collision during teach-in operation",
RequiredComponentTags: []string{"programmable", "moving_part"},
RequiredEnergyTags: []string{},
RequiredLifecycles: []string{"setup"},
@@ -336,7 +336,7 @@ func GetExtendedHazardPatterns2() []HazardPattern {
DefaultSeverity: 3, DefaultExposure: 2,
},
{
ID: "HP154", NameDE: "Kollision zweier Roboter", NameEN: "Collision of two robots",
ID: "HP154", MachineTypes: []string{"robotics_cobot"}, NameDE: "Kollision zweier Roboter", NameEN: "Collision of two robots",
RequiredComponentTags: []string{"programmable", "moving_part"},
RequiredEnergyTags: []string{"kinetic"},
GeneratedHazardCats: []string{"mechanical_hazard"},
@@ -361,7 +361,7 @@ func GetExtendedHazardPatterns2() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M001", "M051"},
SuggestedEvidenceIDs: []string{"E08", "E20"},
Priority: 80,
Priority: 80, MachineTypes: []string{"conveyor", "packaging"},
ScenarioDE: "Finger oder Kleidung werden an der Bandumlenkstelle eingezogen.",
TriggerDE: "Eingriff am laufenden Band, lose Kleidung geraet in Umlenkrolle",
HarmDE: "Fingeramputation, Armverletzung, Strangulation durch eingezogene Kleidung",
@@ -595,7 +595,7 @@ func GetExtendedHazardPatterns2() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M003", "M051"},
SuggestedEvidenceIDs: []string{"E08", "E20"},
Priority: 80,
Priority: 80, MachineTypes: []string{"rotary_transfer"},
ScenarioDE: "Hand wird zwischen Drehteller und festem Anschlag eingeklemmt bei Taktbewegung.",
TriggerDE: "Eingriff waehrend der Taktbewegung, fehlende Schutzabdeckung am Drehteller",
HarmDE: "Quetschung, Fingerfraktur, Amputation von Fingern",
@@ -42,7 +42,7 @@ func GetDGUVExtendedPatterns() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M001"},
Priority: 60,
ScenarioDE: "Reibung an rotierender Welle oder Walze bei Wartung", HarmDE: "Hautabschuerfungen, Verbrennungen durch Reibungswaerme",
ScenarioDE: "Reibung an rotierender Welle oder Walze", HarmDE: "Hautabschuerfungen, Verbrennungen durch Reibungswaerme",
TriggerDE: "Beruehrung laufender Teile", AffectedDE: "Wartungspersonal", ZoneDE: "Walzen-/Wellenbereich", DefaultSeverity: 2, DefaultExposure: 3,
},
{
@@ -102,7 +102,7 @@ func GetDGUVExtendedPatterns() []HazardPattern {
RequiredEnergyTags: []string{},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M051"},
Priority: 80,
Priority: 80, MachineTypes: []string{"crane", "construction"},
ScenarioDE: "Unkontrolliertes Schwingen einer angehobenen Last", HarmDE: "Quetschung, Erschlagen durch pendelnde Last",
TriggerDE: "Schraeger Zug oder ploetzliches Abstoppen", AffectedDE: "Kranfuehrer, Anschlaeger", ZoneDE: "Schwenkbereich des Krans", DefaultSeverity: 4, DefaultExposure: 3,
},
@@ -261,13 +261,13 @@ func GetDGUVExtendedPatterns() []HazardPattern {
TriggerDE: "Hautkontakt mit kontaminiertem Fluid", AffectedDE: "Maschinenbediener, Wartungspersonal", ZoneDE: "Fluidsystem, Tank", DefaultSeverity: 2, DefaultExposure: 3,
},
{
ID: "HP117", NameDE: "Asbest-/Mineralfaserfreisetzung bei Demontage", NameEN: "Asbestos/mineral fiber release during dismantling",
ID: "HP117", NameDE: "Asbest-/Mineralfaserfreisetzung", NameEN: "Asbestos/mineral fiber release during dismantling",
RequiredComponentTags: []string{"chemical_risk"},
RequiredLifecycles: []string{"decommissioning", "disposal"},
GeneratedHazardCats: []string{"material_environmental"},
SuggestedMeasureIDs: []string{"M141"},
Priority: 90,
ScenarioDE: "Freisetzung von Asbestfasern bei Demontage alter Anlagen", HarmDE: "Asbestose, Mesotheliom (Langzeitfolge)",
ScenarioDE: "Freisetzung von Asbestfasern alter Anlagen", HarmDE: "Asbestose, Mesotheliom (Langzeitfolge)",
TriggerDE: "Mechanische Bearbeitung asbesthaltiger Bauteile", AffectedDE: "Demontagepersonal", ZoneDE: "Altanlagen, Isolierung", DefaultSeverity: 5, DefaultExposure: 1,
},
@@ -428,7 +428,7 @@ func GetFinalPatternsA() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M001", "M005"},
SuggestedEvidenceIDs: []string{"E01", "E08"},
Priority: 78, ScenarioDE: "Finger wird zwischen Kette und Kettenrad eingezogen",
Priority: 78, MachineTypes: []string{"conveyor", "forestry"}, ScenarioDE: "Finger wird zwischen Kette und Kettenrad eingezogen",
TriggerDE: "Eingriff in ungeschuetzten Kettenantrieb", HarmDE: "Fingerquetschung, Abriss",
AffectedDE: "Wartungspersonal", ZoneDE: "Kettenrad, Kettenstrang",
DefaultSeverity: 4, DefaultExposure: 2,
@@ -667,13 +667,13 @@ func GetFinalPatternsA() []HazardPattern {
DefaultSeverity: 5, DefaultExposure: 2,
},
{
ID: "HP1054", NameDE: "Herabfallendes Bauteil bei Montage", NameEN: "Falling component during assembly",
ID: "HP1054", NameDE: "Herabfallendes Bauteil", NameEN: "Falling component during assembly",
RequiredComponentTags: []string{"gravity_risk", "structural_part"},
RequiredEnergyTags: []string{"gravitational"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M001", "M141"},
SuggestedEvidenceIDs: []string{"E01"},
Priority: 68, ScenarioDE: "Bauteil loest sich bei Montage und faellt",
Priority: 68, ScenarioDE: "Bauteil loest sich und faellt",
TriggerDE: "Unzureichende Sicherung waehrend Zusammenbau", HarmDE: "Prellung, Fraktur",
AffectedDE: "Montagepersonal", ZoneDE: "Montageplatz, Regalbereich",
DefaultSeverity: 3, DefaultExposure: 3,
@@ -814,7 +814,7 @@ func GetFinalPatternsA() []HazardPattern {
},
// === Einklemmen Haare/Kleidung (3) ===
{
ID: "HP1066", NameDE: "Haareinzug Drehmaschine", NameEN: "Hair entanglement lathe",
ID: "HP1066", MachineTypes: []string{"lathe", "cnc", "metalworking"}, NameDE: "Haareinzug Drehmaschine", NameEN: "Hair entanglement lathe",
RequiredComponentTags: []string{"rotating_part", "entanglement_risk"},
RequiredEnergyTags: []string{"rotational"},
GeneratedHazardCats: []string{"mechanical_hazard"},
@@ -1027,7 +1027,7 @@ func GetFinalPatternsA() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M001", "M005"},
SuggestedEvidenceIDs: []string{"E01", "E08"},
Priority: 78, ScenarioDE: "Schwere Maschine kippt bei Transport oder Betrieb",
Priority: 78, ScenarioDE: "Schwere Maschine kippt oder Betrieb",
TriggerDE: "Unebener Boden, Schwerpunktverlagerung", HarmDE: "Toedliche Quetschung",
AffectedDE: "Transportpersonal", ZoneDE: "Kippbereich, Aufstellflaeche",
DefaultSeverity: 5, DefaultExposure: 1,
@@ -624,7 +624,7 @@ func GetFinalPatternsB() []HazardPattern {
GeneratedHazardCats: []string{"material_environmental"},
SuggestedMeasureIDs: []string{"M124", "M141"},
SuggestedEvidenceIDs: []string{"E20"},
Priority: 82, ScenarioDE: "Asbestfasern werden bei Demontage/Wartung freigesetzt",
Priority: 82, ScenarioDE: "Asbestfasern werden /Wartung freigesetzt",
TriggerDE: "Bohren/Saegen in Asbestmaterial, Abrissarbeiten", HarmDE: "Asbestose, Mesotheliom",
AffectedDE: "Wartungspersonal, Abbrucharbeiter", ZoneDE: "Altanlage, Dichtungen, Isolierungen",
DefaultSeverity: 5, DefaultExposure: 1,
@@ -860,7 +860,7 @@ func GetFinalPatternsC() []HazardPattern {
GeneratedHazardCats: []string{"ergonomic_hazard"},
SuggestedMeasureIDs: []string{"M141"},
SuggestedEvidenceIDs: []string{"E01"},
Priority: 52, ScenarioDE: "Haeufiges Knien bei Montage/Wartungsarbeiten",
Priority: 52, ScenarioDE: "Haeufiges Knien /Wartungsarbeiten",
TriggerDE: "Bodennahe Arbeiten, fehlende Knieschoner", HarmDE: "Meniskusschaden (BK 2112)",
AffectedDE: "Wartungspersonal", ZoneDE: "Bodenbereich",
DefaultSeverity: 2, DefaultExposure: 4,
@@ -158,7 +158,7 @@ func GetFinalPatternsD() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard", "maintenance_hazard"},
SuggestedMeasureIDs: []string{"M001"},
SuggestedEvidenceIDs: []string{"E01"},
Priority: 72, ScenarioDE: "Verschlissenes Teil versagt im Betrieb",
Priority: 72, ScenarioDE: "Verschlissenes Teil versagt",
TriggerDE: "Fehlende Inspektion, ueberschrittene Standzeit", HarmDE: "Funktionsverlust, Bruch",
AffectedDE: "Bedienpersonal", ZoneDE: "Verschleissteil, Fuehrung",
DefaultSeverity: 3, DefaultExposure: 3,
@@ -573,7 +573,7 @@ func GetFinalPatternsD() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M001", "M005"},
SuggestedEvidenceIDs: []string{"E01"},
Priority: 72, ScenarioDE: "Schutzeinrichtung nach Einrichten nicht reaktiviert",
Priority: 72, ScenarioDE: "Schutzeinrichtung nicht reaktiviert",
TriggerDE: "Vergessen, Bypass noch aktiv", HarmDE: "Produktion ohne Schutz",
AffectedDE: "Bedienpersonal", ZoneDE: "Gesamte Maschine",
DefaultSeverity: 4, DefaultExposure: 2,
@@ -817,7 +817,7 @@ func GetFinalPatternsD() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M001", "M005"},
SuggestedEvidenceIDs: []string{"E01", "E08"},
Priority: 78, ScenarioDE: "Kran schwenkt Last ueber besetzten Arbeitsplatz",
Priority: 78, MachineTypes: []string{"crane", "construction"}, ScenarioDE: "Kran schwenkt Last ueber besetzten Arbeitsplatz",
TriggerDE: "Fehlende Endschalter, Unachtsamkeit", HarmDE: "Herabfallende Last",
AffectedDE: "Personen darunter", ZoneDE: "Unter Kranschwenkbereich",
DefaultSeverity: 5, DefaultExposure: 2,
@@ -131,7 +131,7 @@ func GetFoodProcessingPatterns() []HazardPattern {
RequiresExpertCalculation: true,
ExpertHintDE: "IP-Schutzklasse muss fuer Nassreinigung (mindestens IPX5) nachgewiesen werden.",
ExpertHintEN: "IP rating must be verified for wet cleaning conditions (minimum IPX5).",
ScenarioDE: "Wasser dringt beim Reinigen in elektrische Komponenten ein und erzeugt einen Fehlerstrom.",
ScenarioDE: "Wasser dringt in elektrische Komponenten ein und erzeugt einen Fehlerstrom.",
TriggerDE: "Unzureichende IP-Schutzklasse, defekte Kabeldurchfuehrungen, beschaedigtes Gehaeuse.",
HarmDE: "Elektrischer Schlag, Herzkammerflimmern, Tod durch Stromschlag.",
AffectedDE: "Reinigungspersonal, Bedienpersonal bei Nassreinigung.",
@@ -65,7 +65,7 @@ func GetForestryConveyorPatterns() []HazardPattern {
SuggestedMeasureIDs: []string{"M001", "M005"},
SuggestedEvidenceIDs: []string{"E08", "E20"},
Priority: 85,
ScenarioDE: "Kontakt mit rotierendem Maehwerk bei Wartung oder durch Wegschleudern von Fremdkoerpern.",
ScenarioDE: "Kontakt mit rotierendem Maehwerk oder durch Wegschleudern von Fremdkoerpern.",
TriggerDE: "Wartung bei laufendem Maehwerk, fehlende Schutzabdeckung, Steinschleuder",
HarmDE: "Amputationsverletzung an Fuessen/Haenden, tiefe Schnittwunden, Augenverletzung durch Steinschlag",
AffectedDE: "Maehwerksfahrer, Gartenarbeiter, Umstehende",
@@ -311,7 +311,7 @@ func GetForestryConveyorPatterns() []HazardPattern {
SuggestedMeasureIDs: []string{"M052", "M141"},
SuggestedEvidenceIDs: []string{"E20"},
Priority: 70,
ScenarioDE: "Person stuerzt von erhoehtem Rollenfoerderer bei Wartung oder Stoerungsbeseitigung.",
ScenarioDE: "Person stuerzt von erhoehtem Rollenfoerderer .",
TriggerDE: "Fehlende Absturzsicherung, kein Zugangsweg, improvisiertes Besteigen",
HarmDE: "Knochenbrueche, Wirbelsaeulenverletzung, toedlicher Sturz ab 2 m Hoehe",
AffectedDE: "Wartungspersonal, Bediener bei Stoerung",
@@ -16,10 +16,10 @@ func GetMaintenanceExtPatterns() []HazardPattern {
RequiredComponentTags: []string{"moving_part"}, RequiredLifecycles: []string{"maintenance"},
GeneratedHazardCats: []string{"mechanical_hazard", "pneumatic_hydraulic"},
SuggestedMeasureIDs: []string{"M054", "M082"}, SuggestedEvidenceIDs: []string{"E08"}, Priority: 90,
ScenarioDE: "Gespeicherte Energie entlaedt sich bei Wartung", TriggerDE: "Nicht abgelassener Druckspeicher",
ScenarioDE: "Gespeicherte Energie entlaedt sich", TriggerDE: "Nicht abgelassener Druckspeicher",
HarmDE: "Unkontrollierte Bewegung, Quetschung", AffectedDE: "Instandhalter", ZoneDE: "Antriebe, Speicher",
DefaultSeverity: 5, DefaultExposure: 3},
{ID: "HP702", OperationalStates: []string{"maintenance"}, HumanRoles: []string{"maintenance_tech"}, NameDE: "Falsches Werkzeug bei Wartung", NameEN: "Wrong tool during maintenance",
{ID: "HP702", OperationalStates: []string{"maintenance"}, HumanRoles: []string{"maintenance_tech"}, NameDE: "Falsches Werkzeug fuer Arbeiten an der Maschine", NameEN: "Wrong tool during maintenance",
RequiredComponentTags: []string{"structural_part"}, RequiredLifecycles: []string{"maintenance"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M082"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 50,
@@ -33,11 +33,11 @@ func GetMaintenanceExtPatterns() []HazardPattern {
ScenarioDE: "Unqualifiziertes Personal an Elektrik", TriggerDE: "Keine Elektrofachkraft",
HarmDE: "Stromschlag, Fehlverdrahtung", AffectedDE: "Instandhalter", ZoneDE: "Schaltschrank",
DefaultSeverity: 4, DefaultExposure: 3},
{ID: "HP704", OperationalStates: []string{"maintenance"}, HumanRoles: []string{"maintenance_tech"}, NameDE: "Herabfallen schwerer Teile bei Demontage", NameEN: "Heavy parts falling during disassembly",
{ID: "HP704", OperationalStates: []string{"maintenance"}, HumanRoles: []string{"maintenance_tech"}, NameDE: "Herabfallen schwerer Teile", NameEN: "Heavy parts falling during disassembly",
RequiredComponentTags: []string{"structural_part"}, RequiredLifecycles: []string{"maintenance"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M082", "M141"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 75,
ScenarioDE: "Schwere Teile fallen bei Demontage herab", TriggerDE: "Fehlende Abstuetzung",
ScenarioDE: "Schwere Teile fallen herab", TriggerDE: "Fehlende Abstuetzung",
HarmDE: "Quetschung, Frakturen, Tod", AffectedDE: "Instandhalter", ZoneDE: "Wartungsbereich",
DefaultSeverity: 5, DefaultExposure: 3},
{ID: "HP705", OperationalStates: []string{"maintenance"}, HumanRoles: []string{"maintenance_tech"}, NameDE: "Vergessenes Werkzeug in Maschine", NameEN: "Forgotten tool in machine",
@@ -54,7 +54,7 @@ func GetMaintenanceExtPatterns() []HazardPattern {
ScenarioDE: "Scharfe Kanten und Grate verletzen", TriggerDE: "Fehlende Schutzhandschuhe",
HarmDE: "Schnittwunden, Abschuerfungen", AffectedDE: "Instandhalter", ZoneDE: "Blechverkleidungen",
DefaultSeverity: 2, DefaultExposure: 4},
{ID: "HP707", OperationalStates: []string{"maintenance"}, HumanRoles: []string{"maintenance_tech"}, NameDE: "Verbrennung an heissen Teilen bei Wartung", NameEN: "Burn on hot parts during maintenance",
{ID: "HP707", OperationalStates: []string{"maintenance"}, HumanRoles: []string{"maintenance_tech"}, NameDE: "Verbrennung an heissen Teilen", NameEN: "Burn on hot parts during maintenance",
RequiredComponentTags: []string{"high_temperature"}, RequiredLifecycles: []string{"maintenance"},
GeneratedHazardCats: []string{"thermal_hazard"},
SuggestedMeasureIDs: []string{"M005", "M082"}, SuggestedEvidenceIDs: []string{"E10"}, Priority: 60,
@@ -72,11 +72,11 @@ func GetMaintenanceExtPatterns() []HazardPattern {
RequiredComponentTags: []string{"chemical_risk"}, RequiredLifecycles: []string{"maintenance"},
GeneratedHazardCats: []string{"material_environmental"},
SuggestedMeasureIDs: []string{"M005", "M082"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 50,
ScenarioDE: "Verkeimter Kuehlschmierstoff bei Wartung", TriggerDE: "Altes KSS, Biofilme",
ScenarioDE: "Verkeimter Kuehlschmierstoff", TriggerDE: "Altes KSS, Biofilme",
HarmDE: "Hautinfektionen, Atemwegsbeschwerden", AffectedDE: "Instandhalter", ZoneDE: "KSS-System",
DefaultSeverity: 2, DefaultExposure: 3},
// — Einrichten / Umruesten (HP710-HP719) —
{ID: "HP710", OperationalStates: []string{"teach_mode"}, HumanRoles: []string{"programmer"}, NameDE: "Falsche Parameter nach Umruestung", NameEN: "Wrong parameters after changeover",
{ID: "HP710", OperationalStates: []string{"teach_mode"}, HumanRoles: []string{"programmer"}, NameDE: "Falsche Parameter nach Produktwechsel", NameEN: "Wrong parameters after changeover",
RequiredComponentTags: []string{"programmable"}, RequiredLifecycles: []string{"setup"},
GeneratedHazardCats: []string{"safety_function_failure"},
SuggestedMeasureIDs: []string{"M106", "M082"}, SuggestedEvidenceIDs: []string{"E08"}, Priority: 75,
@@ -90,7 +90,7 @@ func GetMaintenanceExtPatterns() []HazardPattern {
ScenarioDE: "Schwere Werkzeuge manuell gewechselt", TriggerDE: "Kein Hebezeug, Finger eingeklemmt",
HarmDE: "Quetschung, Amputation", AffectedDE: "Einrichter", ZoneDE: "Werkzeugaufnahme",
DefaultSeverity: 4, DefaultExposure: 4},
{ID: "HP712", OperationalStates: []string{"teach_mode", "manual_operation"}, HumanRoles: []string{"programmer", "maintenance_tech"}, NameDE: "Unkontrollierte Bewegung bei Testlauf", NameEN: "Uncontrolled movement test run",
{ID: "HP712", OperationalStates: []string{"teach_mode", "manual_operation"}, HumanRoles: []string{"programmer", "maintenance_tech"}, NameDE: "Unkontrollierte Bewegung nach Probelauf", NameEN: "Uncontrolled movement test run",
RequiredComponentTags: []string{"moving_part", "programmable"}, RequiredLifecycles: []string{"setup"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M106", "M054"}, SuggestedEvidenceIDs: []string{"E08"}, Priority: 80,
@@ -129,17 +129,17 @@ func GetMaintenanceExtPatterns() []HazardPattern {
RequiredComponentTags: []string{"structural_part"}, RequiredLifecycles: []string{"setup"},
GeneratedHazardCats: []string{"material_environmental"},
SuggestedMeasureIDs: []string{"M082"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 50,
ScenarioDE: "Falsches Material nach Umruestung", TriggerDE: "Verwechslung, fehlende Kennzeichnung",
ScenarioDE: "Falsches Material", TriggerDE: "Verwechslung, fehlende Kennzeichnung",
HarmDE: "Werkzeugbruch, Splitterflug", AffectedDE: "Bedienpersonal", ZoneDE: "Materialzufuhr",
DefaultSeverity: 3, DefaultExposure: 3},
{ID: "HP718", NameDE: "Absturz bei Einrichtung hoher Maschine", NameEN: "Fall during tall machine setup",
{ID: "HP718", NameDE: "Absturz hoher Maschine", NameEN: "Fall during tall machine setup",
RequiredComponentTags: []string{"structural_part"}, RequiredLifecycles: []string{"setup"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M082", "M141"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 65,
ScenarioDE: "Einrichtarbeiten in Hoehe ohne sicheren Zugang", TriggerDE: "Improvisierte Aufstiegshilfe",
HarmDE: "Absturz, Frakturen", AffectedDE: "Einrichter", ZoneDE: "Maschinenoberteil",
DefaultSeverity: 4, DefaultExposure: 3},
{ID: "HP719", NameDE: "Schutzeinrichtung nach Umruestung defekt", NameEN: "Faulty guard after changeover",
{ID: "HP719", NameDE: "Schutzeinrichtung nach Produktwechsel defekt", NameEN: "Faulty guard after changeover",
RequiredComponentTags: []string{"moving_part"}, RequiredLifecycles: []string{"setup"},
GeneratedHazardCats: []string{"safety_function_failure", "mechanical_hazard"},
SuggestedMeasureIDs: []string{"M082", "M141"}, SuggestedEvidenceIDs: []string{"E08"}, Priority: 80,
@@ -218,7 +218,7 @@ func GetMaintenanceExtPatterns() []HazardPattern {
HarmDE: "Folgestoerung mit groesserem Schaden", AffectedDE: "Bedienpersonal", ZoneDE: "Steuerung",
DefaultSeverity: 4, DefaultExposure: 2},
// — Transport / Montage (HP900-HP907) —
{ID: "HP900", NameDE: "Kippen der Maschine beim Transport", NameEN: "Machine tipping during transport",
{ID: "HP900", NameDE: "Kippen der Maschine", NameEN: "Machine tipping during transport",
RequiredComponentTags: []string{"structural_part"}, RequiredLifecycles: []string{"transport"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M082", "M141"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 80,
@@ -267,7 +267,7 @@ func GetMaintenanceExtPatterns() []HazardPattern {
ScenarioDE: "Stapler kollidiert mit Personen", TriggerDE: "Eingeschraenkte Sicht, zu schnell",
HarmDE: "Anfahrunfall, Quetschung", AffectedDE: "Fussgaenger", ZoneDE: "Transportwege",
DefaultSeverity: 4, DefaultExposure: 3},
{ID: "HP907", NameDE: "Verankerungsfehler bei Montage", NameEN: "Anchoring error installation",
{ID: "HP907", NameDE: "Verankerungsfehler am Aufstellort", NameEN: "Anchoring error installation",
RequiredComponentTags: []string{"structural_part"}, RequiredLifecycles: []string{"transport"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M082"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 65,
@@ -339,7 +339,7 @@ func GetMaintenanceExtPatterns() []HazardPattern {
ScenarioDE: "Reinigung ohne Abschaltung der Maschine", TriggerDE: "Zeitdruck",
HarmDE: "Einzug, Quetschung, Aufwickeln", AffectedDE: "Reinigungspersonal", ZoneDE: "Rotierende Teile",
DefaultSeverity: 5, DefaultExposure: 3},
{ID: "HP917", NameDE: "Nassrutschiger Boden nach Reinigung", NameEN: "Wet slippery floor after cleaning",
{ID: "HP917", NameDE: "Nassrutschiger Boden durch Fluessigkeiten", NameEN: "Wet slippery floor after cleaning",
RequiredComponentTags: []string{"structural_part"}, RequiredLifecycles: []string{"cleaning"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M082"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 45,
@@ -410,7 +410,7 @@ func GetMaintenanceExtPatterns() []HazardPattern {
RequiredComponentTags: []string{"electrical_part"}, RequiredEnergyTags: []string{"electrical"},
RequiredLifecycles: []string{"maintenance"}, GeneratedHazardCats: []string{"electrical_hazard"},
SuggestedMeasureIDs: []string{"M082", "M141"}, SuggestedEvidenceIDs: []string{"E09"}, Priority: 75,
ScenarioDE: "Messung unter Spannung bei Fehlersuche", TriggerDE: "Messgeraet rutscht ab",
ScenarioDE: "Messung unter Spannung", TriggerDE: "Messgeraet rutscht ab",
HarmDE: "Stromschlag, Lichtbogen", AffectedDE: "Elektrofachkraft", ZoneDE: "Schaltschrank",
DefaultSeverity: 4, DefaultExposure: 3},
{ID: "HP927", NameDE: "ZfP mit Strahlenquelle", NameEN: "NDT with radiation source",
@@ -451,7 +451,7 @@ func GetMaintenanceExtPatterns() []HazardPattern {
HarmDE: "Vernachlaessigte Sicherheit", AffectedDE: "Alle Gewerke", ZoneDE: "Schnittstellen",
DefaultSeverity: 3, DefaultExposure: 3},
// — Notfall (HP932-HP934) —
{ID: "HP932", NameDE: "Versperrte Fluchtwege bei Wartung", NameEN: "Blocked escape routes maintenance",
{ID: "HP932", NameDE: "Versperrte Fluchtwege durch abgestelltes Material", NameEN: "Blocked escape routes maintenance",
RequiredComponentTags: []string{"structural_part"}, RequiredLifecycles: []string{"maintenance"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M082"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 70,
@@ -465,11 +465,11 @@ func GetMaintenanceExtPatterns() []HazardPattern {
ScenarioDE: "Kein Erste-Hilfe-Material am abgelegenen Ort", TriggerDE: "Entfernter Standort",
HarmDE: "Verzoegerte Erstversorgung", AffectedDE: "Instandhalter", ZoneDE: "Abgelegene Wartungsorte",
DefaultSeverity: 3, DefaultExposure: 3},
{ID: "HP934", NameDE: "Brandbekaempfung bei Wartung", NameEN: "Firefighting during maintenance",
{ID: "HP934", NameDE: "Erschwerter Zugang zu Loescheinrichtungen", NameEN: "Firefighting during maintenance",
RequiredComponentTags: []string{"structural_part"}, RequiredLifecycles: []string{"maintenance"},
GeneratedHazardCats: []string{"thermal_hazard"},
SuggestedMeasureIDs: []string{"M082", "M141"}, SuggestedEvidenceIDs: []string{"E10", "E20"}, Priority: 65,
ScenarioDE: "Feuerloescher nicht erreichbar bei Wartung", TriggerDE: "Verstellter Loescher",
ScenarioDE: "Feuerloescher nicht erreichbar", TriggerDE: "Verstellter Loescher",
HarmDE: "Brandausbreitung, Verbrennungen", AffectedDE: "Instandhalter", ZoneDE: "Wartungsbereich",
DefaultSeverity: 4, DefaultExposure: 2},
}
@@ -90,7 +90,7 @@ func builtinMechanicalPatterns() []HazardPattern {
TriggerDE: "Bediener befindet sich im Kraftwirkbereich waehrend des Arbeitshubes oder bei Stoerungsbeseitigung.",
HarmDE: "Schwere Quetschung, Fraktur, innere Verletzungen, Todesfolge bei Ganzkompression.",
AffectedDE: "Bedienpersonal, Einrichter, Wartungspersonal",
ZoneDE: "Kraftwirkbereich (Pressenraum, Vorschubachse), Einlegestelle",
ZoneDE: "Kraftwirkbereich, Einlegestelle, Vorschubachse",
DefaultSeverity: 5, DefaultExposure: 3,
},
{
@@ -120,7 +120,7 @@ func builtinMechanicalPatterns() []HazardPattern {
TriggerDE: "Versagen einer Halterung, Bruch eines Lastaufnahmemittels oder Abrutschen bei Wartungsarbeiten in der Hoehe.",
HarmDE: "Kopfverletzung, Fraktur, Quetschung durch herabfallende Last; Sturzverletung.",
AffectedDE: "Wartungspersonal, Bedienpersonal, Personen im Gefahrenbereich",
ZoneDE: "Bereich unterhalb angehobener Lasten, Wartungsplattformen, Kran-/Hebezeugbereich",
ZoneDE: "Bereich unterhalb angehobener Lasten, Wartungsplattformen",
DefaultSeverity: 4, DefaultExposure: 2,
},
{
@@ -150,7 +150,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
DefaultSeverity: 4, DefaultExposure: 2,
},
{
ID: "HP075", OperationalStates: []string{"maintenance"}, HumanRoles: []string{"maintenance_tech"}, NameDE: "Kontakt mit heissen Teilen bei Wartung", NameEN: "Contact with hot parts during maintenance",
ID: "HP075", OperationalStates: []string{"maintenance"}, HumanRoles: []string{"maintenance_tech"}, NameDE: "Kontakt mit heissen Oberflaechen", NameEN: "Contact with hot parts during maintenance",
RequiredComponentTags: []string{"high_temperature"},
RequiredLifecycles: []string{"maintenance"},
GeneratedHazardCats: []string{"thermal_hazard"},
@@ -165,7 +165,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
DefaultSeverity: 3, DefaultExposure: 3,
},
{
ID: "HP076", OperationalStates: []string{"maintenance"}, HumanRoles: []string{"maintenance_tech"}, NameDE: "Kontakt mit Gefahrstoffen bei Wartung", NameEN: "Contact with hazardous substances during maintenance",
ID: "HP076", OperationalStates: []string{"maintenance"}, HumanRoles: []string{"maintenance_tech"}, NameDE: "Kontakt mit Gefahrstoffen", NameEN: "Contact with hazardous substances during maintenance",
RequiredComponentTags: []string{"chemical_risk"},
RequiredLifecycles: []string{"maintenance", "cleaning"},
GeneratedHazardCats: []string{"material_environmental"},
@@ -179,7 +179,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
DefaultSeverity: 3, DefaultExposure: 3,
},
{
ID: "HP077", OperationalStates: []string{"maintenance"}, HumanRoles: []string{"maintenance_tech"}, NameDE: "Elektrischer Schlag bei Wartungsarbeiten", NameEN: "Electric shock during maintenance",
ID: "HP077", OperationalStates: []string{"maintenance"}, HumanRoles: []string{"maintenance_tech"}, NameDE: "Elektrischer Schlag an offenen Baugruppen", NameEN: "Electric shock during maintenance",
RequiredComponentTags: []string{"high_voltage"},
RequiredLifecycles: []string{"maintenance", "fault_clearing"},
GeneratedHazardCats: []string{"electrical_hazard"},
@@ -195,7 +195,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
DefaultSeverity: 5, DefaultExposure: 3,
},
{
ID: "HP078", OperationalStates: []string{"maintenance"}, HumanRoles: []string{"maintenance_tech"}, NameDE: "Ergonomische Belastung bei Wartungszugang", NameEN: "Ergonomic strain at maintenance access",
ID: "HP078", OperationalStates: []string{"maintenance"}, HumanRoles: []string{"maintenance_tech"}, NameDE: "Ergonomische Belastung durch schwierigen Zugang", NameEN: "Ergonomic strain at maintenance access",
RequiredComponentTags: []string{"structural_part"},
RequiredLifecycles: []string{"maintenance"},
GeneratedHazardCats: []string{"ergonomic"},
@@ -273,7 +273,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
DefaultSeverity: 4, DefaultExposure: 3,
},
{
ID: "HP083", NameDE: "Unbeabsichtigter Hub bei Einrichtbetrieb", NameEN: "Unintended stroke in setup mode",
ID: "HP083", NameDE: "Unbeabsichtigter Hub im manuellen Betrieb", NameEN: "Unintended stroke in setup mode",
RequiredComponentTags: []string{"moving_part", "crush_point"},
RequiredLifecycles: []string{"setup"},
GeneratedHazardCats: []string{"mechanical_hazard", "safety_function_failure"},
@@ -281,7 +281,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
Priority: 94,
RequiresExpertCalculation: true,
ExpertHintDE: "Einrichtbetrieb nur mit reduzierter Geschwindigkeit und Zweihandschaltung.",
ScenarioDE: "Einrichter befindet sich im Werkzeugraum waehrend Testlauf im Einrichtbetrieb",
ScenarioDE: "Person befindet sich im Werkzeugraum waehrend Testlauf",
TriggerDE: "Stossel fuehrt vollen Hub statt Tipphub aus wegen Softwarefehler oder Fehlbedienung",
HarmDE: "Toedliches Quetschen oder Amputation durch vollen Pressenhub bei Anwesenheit",
AffectedDE: "Einrichter",
@@ -289,7 +289,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
DefaultSeverity: 5, DefaultExposure: 3,
},
{
ID: "HP084", NameDE: "Falsche Parametereinstellung nach Umruestung", NameEN: "Wrong parameters after changeover",
ID: "HP084", NameDE: "Falsche Parametereinstellung nach Produktwechsel", NameEN: "Wrong parameters after changeover",
RequiredComponentTags: []string{"programmable"},
RequiredLifecycles: []string{"changeover", "setup"},
GeneratedHazardCats: []string{"safety_function_failure"},
@@ -323,7 +323,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
// Transport / Montage / Demontage (HP086-HP090)
// ================================================================
{
ID: "HP086", NameDE: "Kippen der Maschine beim Transport", NameEN: "Machine tipping during transport",
ID: "HP086", NameDE: "Kippen der Maschine", NameEN: "Machine tipping during transport",
RequiredComponentTags: []string{"structural_part"},
RequiredLifecycles: []string{"transport"},
GeneratedHazardCats: []string{"mechanical_hazard"},
@@ -337,7 +337,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
DefaultSeverity: 5, DefaultExposure: 2,
},
{
ID: "HP087", NameDE: "Quetschen bei Montage/Aufstellung", NameEN: "Crushing during installation",
ID: "HP087", NameDE: "Quetschen/Aufstellung", NameEN: "Crushing during installation",
RequiredComponentTags: []string{"high_force", "gravity_risk"},
RequiredLifecycles: []string{"assembly"},
GeneratedHazardCats: []string{"mechanical_hazard"},
@@ -351,7 +351,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
DefaultSeverity: 4, DefaultExposure: 2,
},
{
ID: "HP088", NameDE: "Unkontrollierte Bewegung bei Inbetriebnahme", NameEN: "Uncontrolled movement during commissioning",
ID: "HP088", NameDE: "Unkontrollierte Bewegung beim Erststart", NameEN: "Uncontrolled movement during commissioning",
RequiredComponentTags: []string{"moving_part", "programmable"},
RequiredLifecycles: []string{"commissioning"},
GeneratedHazardCats: []string{"mechanical_hazard"},
@@ -365,7 +365,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
DefaultSeverity: 4, DefaultExposure: 2,
},
{
ID: "HP089", NameDE: "Restmedien bei Demontage (Oel, Gas, Druck)", NameEN: "Residual media during dismantling",
ID: "HP089", NameDE: "Restmedien (Oel, Gas, Druck)", NameEN: "Residual media during dismantling",
RequiredComponentTags: []string{"hydraulic_part"},
RequiredLifecycles: []string{"decommissioning", "disposal"},
GeneratedHazardCats: []string{"material_environmental", "pneumatic_hydraulic"},
@@ -379,7 +379,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
DefaultSeverity: 3, DefaultExposure: 2,
},
{
ID: "HP090", NameDE: "Scharfe Kanten bei Demontage", NameEN: "Sharp edges during dismantling",
ID: "HP090", NameDE: "Scharfe Kanten an demontierten Teilen", NameEN: "Sharp edges during dismantling",
RequiredComponentTags: []string{"cutting_part"},
RequiredLifecycles: []string{"decommissioning", "disposal"},
GeneratedHazardCats: []string{"mechanical_hazard"},
@@ -411,7 +411,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
DefaultSeverity: 2, DefaultExposure: 4,
},
{
ID: "HP092", NameDE: "Chemische Exposition bei Reinigung", NameEN: "Chemical exposure during cleaning",
ID: "HP092", NameDE: "Chemische Exposition durch Reinigungsmittel", NameEN: "Chemical exposure during cleaning",
RequiredComponentTags: []string{"chemical_risk"},
RequiredLifecycles: []string{"cleaning"},
GeneratedHazardCats: []string{"material_environmental"},
@@ -425,7 +425,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
DefaultSeverity: 3, DefaultExposure: 3,
},
{
ID: "HP093", NameDE: "Einziehen in rotierende Teile bei Reinigung", NameEN: "Draw-in by rotating parts during cleaning",
ID: "HP093", NameDE: "Einziehen in rotierende Teile bei laufender Maschine", NameEN: "Draw-in by rotating parts during cleaning",
RequiredComponentTags: []string{"rotating_part"},
RequiredLifecycles: []string{"cleaning"},
ExcludedComponentTags: []string{"interlocked"},
@@ -262,7 +262,7 @@ func GetPlasticsMetalPatterns() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M003", "M004", "M082"},
SuggestedEvidenceIDs: []string{"E08", "E09"},
Priority: 95,
Priority: 95, MachineTypes: []string{"lathe", "cnc", "metalworking"},
ScenarioDE: "Offene Haare, Krawatten, Aermel oder Handschuhe werden vom rotierenden Werkstueck oder Spannfutter erfasst.",
TriggerDE: "Tragen von Handschuhen an der Drehmaschine, offene Haare, lose Kleidung",
HarmDE: "Skalpierung, Armfraktur, Strangulation, toedliche Aufwickelverletzung",
@@ -124,7 +124,7 @@ func GetPressHazardPatterns() []HazardPattern {
SuggestedMeasureIDs: []string{"M051", "M131"},
SuggestedEvidenceIDs: []string{"E01", "E08"},
Priority: 92,
ScenarioDE: "Hydraulikspeicher entlaedt sich schlagartig bei Wartungsarbeiten oder Leitungsbruch.",
ScenarioDE: "Hydraulikspeicher entlaedt sich schlagartig oder Leitungsbruch.",
TriggerDE: "Oeffnen einer Leitung ohne vorherige Druckentlastung, Berstversagen des Speichers.",
HarmDE: "Schwere Schnittverletzungen durch Oelstrahl, Augenverletzungen, Verbrennungen.",
AffectedDE: "Instandhaltungspersonal, Hydraulik-Fachkraefte.",
@@ -3,6 +3,9 @@ package iace
// GetRobotCellPatterns returns hazard patterns for industrial robot cells
// (non-collaborative) with safety fence, conveyors, and CNC machine tools.
// Based on typical ISO 10218-2 risk assessment scope for integrated robot systems.
//
// FORMULIERUNGSREGEL: Gefährdung und Szenario NEUTRAL formulieren — keine
// Lebensphasen im Text. Lebensphasen stehen in ApplicableLifecycles.
// HP1600-HP1649
func GetRobotCellPatterns() []HazardPattern {
return []HazardPattern{
@@ -14,33 +17,22 @@ func GetRobotCellPatterns() []HazardPattern {
RequiredComponentTags: []string{"moving_part"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M061", "M062", "M054"},
Priority: 95, MachineTypes: []string{"robotics_cobot", "automotive", "metalworking", "general_industry"},
Priority: 99, MachineTypes: []string{"robotics_cobot", "automotive", "metalworking", "general_industry"},
ApplicableLifecycles: []string{"normal_operation", "setup", "teach_mode", "cleaning", "maintenance", "fault_clearing", "changeover"},
ScenarioDE: "Person befindet sich im Bewegungsbereich des Roboterarms und wird zwischen Roboterarm und feststehenden Anlagenteilen eingeklemmt.",
TriggerDE: "Roboterarm bewegt sich waehrend Person im Gefahrenbereich steht (z.B. nach Betreten der Roboterzelle).",
TriggerDE: "Roboterarm bewegt sich waehrend Person im Gefahrenbereich steht.",
HarmDE: "Quetschungen, Knochenbrueche, innere Verletzungen durch Einklemmen von Koerperteilen.",
AffectedDE: "Bedienpersonal, Einrichter, Wartungspersonal",
AffectedDE: "Bedienpersonal, Einrichter, Wartungspersonal, Reinigungspersonal",
ZoneDE: "Roboterarm, feststehende Anlagenteile innerhalb der Roboterzelle",
DefaultSeverity: 4, DefaultExposure: 3,
},
{
ID: "HP1601", NameDE: "Quetschen bei Teach-Betrieb am Roboter", NameEN: "Crushing during robot teach mode",
RequiredComponentTags: []string{"moving_part"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M054", "M061"},
Priority: 94, MachineTypes: []string{"robotics_cobot", "automotive", "metalworking", "general_industry"},
ScenarioDE: "Einrichter befindet sich zum Teachen/Programmieren in der Roboterzelle. Roboterarm bewegt sich unerwartet oder mit zu hoher Geschwindigkeit.",
TriggerDE: "Teach-Modus laesst Bewegung zu, Einrichter steht im Schwenkbereich des Arms.",
HarmDE: "Quetschungen, Prellungen durch Kontakt mit bewegtem Roboterarm.",
AffectedDE: "Einrichter, Programmierer",
ZoneDE: "Roboterarm, Inneres der Roboterzelle",
DefaultSeverity: 3, DefaultExposure: 3,
},
{
ID: "HP1602", NameDE: "Durchgreifen durch Schutzzaun zum Roboter", NameEN: "Reaching through safety fence to robot",
RequiredComponentTags: []string{"moving_part", "guard"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M002", "M061"},
Priority: 93,
Priority: 98,
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing"},
ScenarioDE: "Person greift ueber oder durch den Schutzzaun und erreicht den Bewegungsbereich des Roboterarms.",
TriggerDE: "Unzureichender Sicherheitsabstand zwischen Schutzzaun-Oberkante und Roboter-Schwenkbereich.",
HarmDE: "Quetschung von Hand oder Arm zwischen Roboterarm und feststehenden Teilen.",
@@ -53,11 +45,12 @@ func GetRobotCellPatterns() []HazardPattern {
RequiredComponentTags: []string{"moving_part", "guard"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M061", "M054", "M141"},
Priority: 94,
Priority: 99,
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing", "changeover"},
ScenarioDE: "Person befindet sich in der Roboterzelle, Schutztuer wird geschlossen und Roboter startet. Person kann den Gefahrenbereich nicht rechtzeitig verlassen.",
TriggerDE: "Schutztuer schliesst waehrend Person im Innenraum. Wiederanlauf des Roboters ohne Quittierung.",
HarmDE: "Quetschungen, Stoss durch anlaufenden Roboter. Person kann sich nicht entziehen.",
AffectedDE: "Wartungspersonal, Einrichter",
HarmDE: "Quetschungen, Stoss durch anlaufenden Roboter.",
AffectedDE: "Wartungspersonal, Einrichter, Reinigungspersonal",
ZoneDE: "Inneres der Roboterzelle",
DefaultSeverity: 4, DefaultExposure: 2,
},
@@ -66,24 +59,40 @@ func GetRobotCellPatterns() []HazardPattern {
RequiredComponentTags: []string{"moving_part", "guard"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M061", "M002"},
Priority: 92,
ScenarioDE: "Roboterarm ueberschreitet den vorgesehenen Bewegungsbereich und trifft den Schutzzaun mit hoher Kraft.",
TriggerDE: "Fehler in der Bahnplanung oder Ausfall der Achsbegrenzung. Roboter faehrt ueber Software-Endschalter hinaus.",
Priority: 98,
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "changeover", "fault_clearing"},
ScenarioDE: "Roboterarm ueberschreitet Bewegungsbereich und trifft Schutzzaun. Person ausserhalb wird von Zaunteilen oder dem Roboterarm getroffen.",
TriggerDE: "Fehler in der Bahnplanung oder Ausfall der Achsbegrenzung.",
HarmDE: "Teile des Schutzzauns werden herausgeschleudert, Person ausserhalb wird getroffen.",
AffectedDE: "Bedienpersonal in der Naehe des Schutzzauns",
ZoneDE: "Schutzzaun, Bereich um die Roboterzelle",
DefaultSeverity: 3, DefaultExposure: 2,
},
{
ID: "HP1605", NameDE: "Stoss durch Werkzeug/Greifer im Einrichtbetrieb", NameEN: "Impact by tool/gripper during setup",
RequiredComponentTags: []string{"moving_part", "clamping_part"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M054"},
Priority: 98, MachineTypes: []string{"robotics_cobot", "automotive", "metalworking", "general_industry"},
ApplicableLifecycles: []string{"teach_mode", "setup", "changeover", "fault_clearing"},
ScenarioDE: "Person steht im Bewegungsbereich des Roboterarms und wird von bewegtem Werkzeug oder Greifer getroffen. Geschwindigkeitsreduzierung im Einrichtbetrieb reicht nicht aus.",
TriggerDE: "Roboter bewegt Werkzeug/Greifer mit unerwartet hoher Geschwindigkeit oder in unerwartete Richtung.",
HarmDE: "Prellungen, Quetschungen durch Kontakt mit Werkzeug/Greifer am Roboterarm.",
AffectedDE: "Einrichter, Programmierer, Wartungspersonal",
ZoneDE: "Inneres der Roboterzelle, Schwenkbereich Werkzeug/Greifer",
DefaultSeverity: 3, DefaultExposure: 3,
},
// ================================================================
// Greifer / Werkstueck
// ================================================================
{
ID: "HP1610", NameDE: "Quetschen im Greiferbereich des Roboters", NameEN: "Crushing in robot gripper area",
ID: "HP1610", NameDE: "Quetschen im Greiferbereich", NameEN: "Crushing in gripper area",
RequiredComponentTags: []string{"clamping_part"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M054", "M061"},
Priority: 94, MachineTypes: []string{"robotics_cobot", "automotive", "metalworking", "general_industry"},
ScenarioDE: "Person greift in den Bereich des Greifers waehrend der Roboter ein Werkstueck aufnimmt oder ablegt. Hand wird zwischen Greifbacken und Werkstueck eingeklemmt.",
Priority: 99, MachineTypes: []string{"robotics_cobot", "automotive", "metalworking", "general_industry"},
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "changeover", "fault_clearing"},
ScenarioDE: "Person greift in den Bereich des Greifers. Hand wird zwischen Greifbacken und Werkstueck eingeklemmt.",
TriggerDE: "Greiferbacken schliessen waehrend Koerperteil im Greifbereich ist.",
HarmDE: "Quetschung oder Amputation von Fingern durch Greifkraft.",
AffectedDE: "Bedienpersonal, Einrichter",
@@ -95,8 +104,9 @@ func GetRobotCellPatterns() []HazardPattern {
RequiredComponentTags: []string{"clamping_part"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M007", "M141"},
Priority: 93,
ScenarioDE: "Greifer verliert das Werkstueck waehrend des Transports (z.B. durch Druckverlust der Pneumatik, oelige Oberflaeche, falsches Werkstueck).",
Priority: 98,
ApplicableLifecycles: []string{"normal_operation", "setup", "changeover"},
ScenarioDE: "Greifer verliert das Werkstueck waehrend des Transports. Werkstueck faellt herab und trifft Person unterhalb des Roboterarms.",
TriggerDE: "Werkstueck faellt aus Greifer und trifft Person unterhalb des Roboterarms.",
HarmDE: "Prellungen, Knochenbrueche abhaengig von Werkstueckgewicht und Fallhoehe.",
AffectedDE: "Bedienpersonal, Wartungspersonal",
@@ -108,8 +118,9 @@ func GetRobotCellPatterns() []HazardPattern {
RequiredComponentTags: []string{"clamping_part", "guard"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M061", "M141"},
Priority: 92,
ScenarioDE: "Greifer versagt und Roboterarm beschleunigt das freigesetzte Werkstueck in Richtung Schutzzaun oder Einhausung.",
Priority: 98,
ApplicableLifecycles: []string{"normal_operation"},
ScenarioDE: "Greifer versagt und Werkstueck wird in Richtung Schutzzaun geschleudert. Person ausserhalb der Zelle wird von durchschlagendem Werkstueck getroffen.",
TriggerDE: "Werkstueck wird durch Roboterbewegung weggeschleudert und durchschlaegt die Schutzeinrichtung.",
HarmDE: "Person ausserhalb der Zelle wird von weggeschleudertem Werkstueck getroffen.",
AffectedDE: "Bedienpersonal in der Naehe der Roboterzelle",
@@ -124,8 +135,9 @@ func GetRobotCellPatterns() []HazardPattern {
RequiredComponentTags: []string{"entanglement_risk"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M002", "M061", "M003"},
Priority: 93,
ScenarioDE: "Person greift an Foerderband fuer Werkstueckzulauf oder -auslauf und wird zwischen beweglichen und feststehenden Teilen eingeklemmt.",
Priority: 98,
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing"},
ScenarioDE: "Person greift an Foerderband und wird zwischen beweglichen und feststehenden Teilen eingeklemmt.",
TriggerDE: "Hand oder Finger geraten zwischen Band und Umlenkrolle oder zwischen Werkstueck und Tunnelrahmen.",
HarmDE: "Quetschung von Fingern, Einzug von Kleidung oder Haaren.",
AffectedDE: "Bedienpersonal, Reinigungspersonal",
@@ -137,7 +149,8 @@ func GetRobotCellPatterns() []HazardPattern {
RequiredComponentTags: []string{"entanglement_risk", "guard"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M002", "M061"},
Priority: 93,
Priority: 98,
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "fault_clearing"},
ScenarioDE: "Person greift durch die Oeffnung im Schutzzaun fuer die Foerderbaender in den Gefahrenbereich des Roboters.",
TriggerDE: "Oeffnung ist zu gross oder Sicherheitsabstand zum Roboter-Schwenkbereich ist zu gering.",
HarmDE: "Quetschung von Hand oder Arm durch Roboterarm oder bewegte Maschinenteile.",
@@ -150,9 +163,10 @@ func GetRobotCellPatterns() []HazardPattern {
RequiredComponentTags: []string{"entanglement_risk"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M008"},
Priority: 91,
ScenarioDE: "Werkstueck faehrt ueber das Ende des Transportbandes hinaus und faellt herab, trifft Person die am Bandende steht.",
TriggerDE: "Mechanischer Anschlag fehlt oder ist beschaedigt. Werkstueck wird nicht rechtzeitig gestoppt.",
Priority: 97,
ApplicableLifecycles: []string{"normal_operation", "setup"},
ScenarioDE: "Werkstueck faehrt ueber das Ende des Transportbandes hinaus, faellt herab und trifft Person am Be-/Entladeplatz.",
TriggerDE: "Mechanischer Anschlag fehlt oder ist beschaedigt.",
HarmDE: "Prellungen, Quetschung von Fuessen durch herabfallendes Werkstueck.",
AffectedDE: "Bedienpersonal am Be-/Entladeplatz",
ZoneDE: "Ende der Transportbaender, Be-/Entladeplatz",
@@ -166,8 +180,9 @@ func GetRobotCellPatterns() []HazardPattern {
RequiredComponentTags: []string{"guard"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M003"},
Priority: 90,
ScenarioDE: "Person schneidet sich an nicht entgrateten oder scharfkantigen Blechen der Einhausung, des Schutzzauns oder der Verkleidung.",
Priority: 97,
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing"},
ScenarioDE: "Person schneidet sich an nicht entgrateten oder scharfkantigen Blechen der Einhausung oder Verkleidung.",
TriggerDE: "Zugaengliche Kanten sind nicht gerundet oder gebrochen.",
HarmDE: "Schnittwunden an Haenden und Armen.",
AffectedDE: "Bedienpersonal, Wartungspersonal, Reinigungspersonal",
@@ -178,11 +193,12 @@ func GetRobotCellPatterns() []HazardPattern {
// Pneumatik / Druckluft
// ================================================================
{
ID: "HP1630", NameDE: "Schlauch unter Druck springt ab", NameEN: "Pressurized hose comes loose",
ID: "HP1630", NameDE: "Pneumatikschlauch springt unter Druck ab", NameEN: "Pressurized hose comes loose",
RequiredComponentTags: []string{"pinch_point"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M420"},
Priority: 91,
SuggestedMeasureIDs: []string{"M480"},
Priority: 97,
ApplicableLifecycles: []string{"normal_operation", "setup", "maintenance", "fault_clearing"},
ScenarioDE: "Pneumatikschlauch der Automation springt unter Druck ab und trifft eine Person (Peitscheneffekt).",
TriggerDE: "Befestigung loest sich, Verschraubung wird undicht, Materialermuedung des Schlauchs.",
HarmDE: "Prellungen, Augenverletzungen durch abspringenden Schlauch.",
@@ -194,11 +210,12 @@ func GetRobotCellPatterns() []HazardPattern {
ID: "HP1631", NameDE: "Restdruck in Pneumatik nach Abschaltung", NameEN: "Residual pressure in pneumatics after shutdown",
RequiredComponentTags: []string{"pinch_point"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M420", "M141"},
Priority: 91,
ScenarioDE: "Pneumatik-Komponenten stehen nach Abschaltung noch unter Druck. Bei Arbeiten an der Anlage werden druckbeaufschlagte Teile geloest.",
TriggerDE: "Fehlende Druckentlastung vor Wartungsarbeiten. Gesperrte Rueckschlagventile halten Druck.",
HarmDE: "Unkontrolliertes Loesen von Verbindungen, wegfliegende Teile, Verletzung durch Druckstoss.",
SuggestedMeasureIDs: []string{"M480", "M141"},
Priority: 97,
ApplicableLifecycles: []string{"maintenance", "fault_clearing", "changeover"},
ScenarioDE: "Person loest druckbeaufschlagte Pneumatik-Komponenten die nach Abschaltung noch unter Druck stehen. Teile fliegen unkontrolliert weg und treffen die Person.",
TriggerDE: "Fehlende Druckentlastung. Gesperrte Rueckschlagventile halten Druck.",
HarmDE: "Person wird von wegfliegenden Teilen oder unkontrolliert loesenden Verbindungen getroffen. Prellungen, Schnittverletzungen.",
AffectedDE: "Wartungspersonal, Einrichter",
ZoneDE: "Pneumatikschlaeuche und -komponenten",
DefaultSeverity: 2, DefaultExposure: 2,
@@ -207,14 +224,56 @@ func GetRobotCellPatterns() []HazardPattern {
// Kuehlschmierstoff (KSS)
// ================================================================
{
ID: "HP1635", NameDE: "KSS-Leckage fuehrt zu Rutschgefahr", NameEN: "Coolant leakage causes slip hazard",
ID: "HP1606", NameDE: "Quetschen/Scheren durch Greifer im Einrichtbetrieb", NameEN: "Crushing/shearing by gripper during setup",
RequiredComponentTags: []string{"clamping_part"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M054"},
Priority: 98, MachineTypes: []string{"robotics_cobot", "automotive", "metalworking", "general_industry"},
ApplicableLifecycles: []string{"teach_mode", "setup", "changeover", "fault_clearing"},
ScenarioDE: "Einrichter steht im Schwenkbereich des Roboterarms und wird von bewegtem Greifer oder daran befestigtem Werkzeug verletzt.",
TriggerDE: "Reduzierte Geschwindigkeit im Einrichtbetrieb reicht nicht aus oder wird nicht aktiviert.",
HarmDE: "Quetschung, Schnittverletzung durch Greiferkanten oder Werkzeug am Roboter.",
AffectedDE: "Einrichter, Programmierer",
ZoneDE: "Inneres der Roboterzelle, Greifer/Werkzeug am Roboterarm",
DefaultSeverity: 3, DefaultExposure: 3,
},
{
ID: "HP1634", NameDE: "KSS-Pumpe spritzt bei geoeffneter Schutztuer", NameEN: "Coolant pump sprays with open guard door",
RequiredComponentTags: []string{},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M061"},
Priority: 96, MachineTypes: []string{"cnc", "metalworking", "automotive"},
ApplicableLifecycles: []string{"normal_operation", "cleaning", "maintenance", "fault_clearing"},
ScenarioDE: "Niederdruck-Pumpe fuer Bettspuelung laeuft an waehrend Schutztuer geoeffnet ist. Person bekommt KSS-Spritzer ins Auge oder Gesicht.",
TriggerDE: "Pumpe startet automatisch, kein Verriegelungssignal von Schutztuer zur KSS-Pumpe.",
HarmDE: "Augenverletzung durch KSS-Spritzer, Hautreizung.",
AffectedDE: "Bedienpersonal, Wartungspersonal",
ZoneDE: "Bearbeitungszelle, Austrittsduesen der Bettspuelung",
DefaultSeverity: 1, DefaultExposure: 3,
},
{
ID: "HP1633", NameDE: "KSS-Versorgungsschlauch platzt oder reisst ab", NameEN: "Coolant supply hose bursts or tears off",
RequiredComponentTags: []string{},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M480"},
Priority: 97, MachineTypes: []string{"cnc", "metalworking", "automotive"},
ApplicableLifecycles: []string{"normal_operation", "maintenance", "fault_clearing"},
ScenarioDE: "KSS-Versorgungsschlauch reisst ab oder platzt. Person in der Naehe wird von abspringendem Schlauch oder KSS-Strahl unter Druck getroffen.",
TriggerDE: "Materialermuedung, mechanische Beschaedigung, fehlerhafte Befestigung des Schlauchs.",
HarmDE: "Person wird von KSS-Strahl getroffen. Einstichverletzung, Hautreizung, Rutschgefahr.",
AffectedDE: "Bedienpersonal, Wartungspersonal",
ZoneDE: "Druckschlaeuche des Kuehlschmierstoffsystems, Verbindungsstellen",
DefaultSeverity: 2, DefaultExposure: 2,
},
{
ID: "HP1635", NameDE: "Ausrutschen durch KSS-Leckage", NameEN: "Slipping due to coolant leakage",
RequiredComponentTags: []string{},
RequiredEnergyTags: []string{},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M420"},
Priority: 90, MachineTypes: []string{"cnc", "metalworking", "automotive"},
ScenarioDE: "Kuehlschmierstoff tritt aus undichter Leitung oder Verbindung aus und bildet einen rutschigen Belag auf dem Boden.",
TriggerDE: "Leckage an Schlauchverbindung, Dichtungsversagen, Ueberdrucksituation.",
Priority: 97, MachineTypes: []string{"cnc", "metalworking", "automotive"},
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing"},
ScenarioDE: "Kuehlschmierstoff tritt aus und bildet rutschigen Belag auf dem Boden. Person rutscht aus und stuerzt.",
TriggerDE: "Leckage an Schlauchverbindung, Dichtungsversagen.",
HarmDE: "Ausrutschen und Sturz, Prellungen, Knochenbrueche.",
AffectedDE: "Bedienpersonal, Wartungspersonal",
ZoneDE: "Boden um Bearbeitungszentrum und Kuehlschmierstoffanlage",
@@ -223,12 +282,12 @@ func GetRobotCellPatterns() []HazardPattern {
{
ID: "HP1636", NameDE: "Hautkontakt mit Kuehlschmierstoff", NameEN: "Skin contact with coolant",
RequiredComponentTags: []string{},
RequiredEnergyTags: []string{},
GeneratedHazardCats: []string{"material_environmental"},
SuggestedMeasureIDs: []string{"M141"},
Priority: 90, MachineTypes: []string{"cnc", "metalworking", "automotive"},
Priority: 97, MachineTypes: []string{"cnc", "metalworking", "automotive"},
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing"},
ScenarioDE: "Person kommt bei Arbeiten am Bearbeitungszentrum oder der Roboterzelle mit Kuehlschmierstoff in Beruehrung.",
TriggerDE: "Hautkontakt beim Reinigen, Werkzeugwechsel oder Beseitigung von Stoerungen im Bearbeitungsraum.",
TriggerDE: "Hautkontakt beim Reinigen, Werkzeugwechsel oder Beseitigung von Stoerungen.",
HarmDE: "Hautirritationen, allergische Reaktionen, bei laengerer Exposition Ekzeme.",
AffectedDE: "Bedienpersonal, Wartungspersonal",
ZoneDE: "Bearbeitungszentrum, Roboterzelle im Bereich der Beladetuer",
@@ -237,13 +296,13 @@ func GetRobotCellPatterns() []HazardPattern {
{
ID: "HP1637", NameDE: "Einatmen von KSS-Aerosolen", NameEN: "Inhalation of coolant aerosols",
RequiredComponentTags: []string{},
RequiredEnergyTags: []string{},
GeneratedHazardCats: []string{"material_environmental"},
SuggestedMeasureIDs: []string{"M141"},
Priority: 90, MachineTypes: []string{"cnc", "metalworking", "automotive"},
ScenarioDE: "Waehrend der Werkstueckbearbeitung entstehen KSS-Aerosole die beim Oeffnen der Bearbeitungszelle freigesetzt werden.",
Priority: 97, MachineTypes: []string{"cnc", "metalworking", "automotive"},
ApplicableLifecycles: []string{"normal_operation", "setup", "maintenance"},
ScenarioDE: "Person oeffnet Schutztuer der Bearbeitungszelle und atmet freigesetzte KSS-Aerosole ein.",
TriggerDE: "Oeffnen der Schutztuer nach Bearbeitungsvorgang, unzureichende Absaugung.",
HarmDE: "Atembeschwerden, Reizung der Atemwege, bei chronischer Exposition Atemwegserkrankungen.",
HarmDE: "Person atmet KSS-Aerosole ein. Atembeschwerden, Reizung der Atemwege, bei chronischer Exposition Atemwegserkrankungen.",
AffectedDE: "Bedienpersonal",
ZoneDE: "Bearbeitungszelle, Bereich vor der Schutztuer",
DefaultSeverity: 1, DefaultExposure: 3,
@@ -254,25 +313,27 @@ func GetRobotCellPatterns() []HazardPattern {
{
ID: "HP1640", NameDE: "Direktes Beruehren spannungsfuehrender Teile", NameEN: "Direct contact with live parts",
RequiredComponentTags: []string{},
RequiredEnergyTags: []string{"electrical"},
RequiredEnergyTags: []string{},
GeneratedHazardCats: []string{"electrical_hazard"},
SuggestedMeasureIDs: []string{"M009", "M410"},
Priority: 93,
ScenarioDE: "Person beruehrt spannungsfuehrende Teile der Anlage (Kabel, Klemmen, Stecker) die nicht ausreichend isoliert oder abgedeckt sind.",
SuggestedMeasureIDs: []string{"M265", "M089", "M088", "M139", "M475"},
Priority: 99,
ApplicableLifecycles: []string{"normal_operation", "setup", "maintenance", "fault_clearing"},
ScenarioDE: "Person beruehrt spannungsfuehrende Teile der Anlage die nicht ausreichend isoliert oder abgedeckt sind.",
TriggerDE: "Beschaedigte Isolation, fehlende Abdeckung, ungesicherter Schaltschrank.",
HarmDE: "Elektrischer Schlag, bei Hochspannung potentiell toedlich.",
HarmDE: "Person erleidet elektrischen Schlag. Herzkammerflimmern, Verbrennungen, bei Hochspannung Todesfolge.",
AffectedDE: "Wartungspersonal, Einrichter",
ZoneDE: "Zugaengliche Kabel, Klemmen, Schaltschrank",
DefaultSeverity: 4, DefaultExposure: 2,
},
{
ID: "HP1641", NameDE: "Indirektes Beruehren durch Schutzleiterfehler", NameEN: "Indirect contact due to PE conductor failure",
ID: "HP1641", NameDE: "Gefaehrliche Beruehrungsspannung durch Schutzleiterfehler", NameEN: "Dangerous touch voltage due to PE failure",
RequiredComponentTags: []string{},
RequiredEnergyTags: []string{"electrical"},
GeneratedHazardCats: []string{"electrical_hazard"},
SuggestedMeasureIDs: []string{"M410", "M411"},
Priority: 93,
ScenarioDE: "Schutzleiter ist unterbrochen oder nicht korrekt angeschlossen. Beruehrbare leitfaehige Teile fuehren gefaehrliche Beruehrungsspannung.",
SuggestedMeasureIDs: []string{"M475", "M476"},
Priority: 98,
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing"},
ScenarioDE: "Schutzleiter ist unterbrochen. Person beruehrt das Maschinengehaeuse und erleidet elektrischen Schlag durch gefaehrliche Beruehrungsspannung.",
TriggerDE: "Schutzleiterunterbrechung durch mechanische Beschaedigung oder fehlerhafte Installation.",
HarmDE: "Elektrischer Schlag bei Beruehren des Maschinengehaeuses oder leitfaehiger Oberflaechen.",
AffectedDE: "Bedienpersonal, Wartungspersonal",
@@ -285,9 +346,10 @@ func GetRobotCellPatterns() []HazardPattern {
RequiredEnergyTags: []string{"electrical"},
GeneratedHazardCats: []string{"electrical_hazard"},
SuggestedMeasureIDs: []string{"M009"},
Priority: 92,
ScenarioDE: "Kabelquerschnitt ist nicht auf die maximale Leistung ausgelegt oder Ueberstromschutz fehlt. Kabel ueberhitzt und entzuendet sich.",
TriggerDE: "Dauerhafter Betrieb nahe der Belastungsgrenze, fehlende oder falsch dimensionierte Sicherung.",
Priority: 98,
ApplicableLifecycles: []string{"normal_operation", "setup", "maintenance"},
ScenarioDE: "Kabel ueberhitzt und entzuendet sich durch Ueberlast oder fehlenden Ueberstromschutz. Person wird durch Brand oder toxische Gase verletzt.",
TriggerDE: "Dauerhafter Betrieb nahe der Belastungsgrenze, falsch dimensionierte Sicherung.",
HarmDE: "Brand, Rauchentwicklung, Verletzung durch Feuer oder toxische Gase.",
AffectedDE: "Alle Personen im Bereich der Anlage",
ZoneDE: "Kabel und Leitungen der Anlage",
@@ -0,0 +1,465 @@
package iace
// GetRobotCellPatternsExt returns additional hazard patterns for robot cells.
// These cover specific scenarios identified through GT benchmark gaps.
// HP1650-HP1699
func GetRobotCellPatternsExt() []HazardPattern {
return []HazardPattern{
// ================================================================
// Roboterarm — Spezifische Szenarien (GT-Gaps)
// ================================================================
{
ID: "HP1650", NameDE: "Roboterarm durchschlaegt Bewegungsbegrenzung", NameEN: "Robot arm exceeds motion limit",
RequiredComponentTags: []string{"moving_part", "guard"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M061", "M054"},
Priority: 99,
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "changeover", "fault_clearing"},
ScenarioDE: "Roboterarm ueberschreitet Bewegungsbegrenzung und trifft Schutzzaun. Person ausserhalb wird von Zaunteilen oder dem Roboterarm getroffen.",
TriggerDE: "Softwareendschalter versagt, Achsbegrenzung (DCS) fehlerhaft konfiguriert.",
HarmDE: "Person ausserhalb wird von Zaunteilen oder dem Roboterarm getroffen.",
AffectedDE: "Bedienpersonal in der Naehe des Schutzzauns",
ZoneDE: "Schutzzaun, Bereich um die Roboterzelle",
DefaultSeverity: 3, DefaultExposure: 2,
},
{
ID: "HP1651", NameDE: "Wiederanlauf Roboter waehrend Person in Zelle", NameEN: "Robot restart while person inside cell",
RequiredComponentTags: []string{"moving_part", "guard"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M054", "M061", "M141"},
Priority: 99,
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing", "changeover"},
ScenarioDE: "Person befindet sich in der Roboterzelle. Schutztuer wird geschlossen und Roboter startet ohne dass sichergestellt ist, dass niemand im Gefahrenbereich ist.",
TriggerDE: "Fehlende Quittierungspflicht, kein Personenscanner, Schutztuer ohne Sicherheitszuhaltung.",
HarmDE: "Schwere Quetschungen, Knochenbrueche durch anlaufenden Roboter.",
AffectedDE: "Wartungspersonal, Einrichter, Reinigungspersonal",
ZoneDE: "Inneres der Roboterzelle, Roboterarm",
DefaultSeverity: 4, DefaultExposure: 3,
},
{
ID: "HP1652", NameDE: "Quetschen durch Werkzeug/Greifer am Roboter im Betrieb", NameEN: "Crushing by tool/gripper during operation",
RequiredComponentTags: []string{"moving_part", "clamping_part"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M054", "M061"},
Priority: 99,
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning"},
ScenarioDE: "Person wird von bewegtem Werkzeug oder Greifer am Roboterarm getroffen oder zwischen Werkzeug und feststehenden Teilen eingeklemmt.",
TriggerDE: "Roboter bewegt Werkzeug/Greifer waehrend Person im Schwenkbereich.",
HarmDE: "Quetschungen, Schnittverletzungen, Prellungen durch Werkzeug/Greifer.",
AffectedDE: "Bedienpersonal, Einrichter",
ZoneDE: "Inneres der Roboterzelle, Greifer/Werkzeug des Roboterarms",
DefaultSeverity: 3, DefaultExposure: 3,
},
{
ID: "HP1653", NameDE: "Quetschen durch Werkstück am Robotergreifer", NameEN: "Crushing by workpiece on robot gripper",
RequiredComponentTags: []string{"moving_part", "clamping_part"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M054", "M061"},
Priority: 98,
ApplicableLifecycles: []string{"normal_operation", "setup", "changeover"},
ScenarioDE: "Person wird von sich bewegendem Werkstueck am Robotergreifer getroffen oder zwischen Werkstueck und feststehenden Anlagenteilen eingeklemmt.",
TriggerDE: "Roboter transportiert Werkstueck, Person steht im Schwenkbereich.",
HarmDE: "Quetschungen, Prellungen, Knochenbrueche abhaengig von Werkstueckgewicht.",
AffectedDE: "Bedienpersonal, Einrichter",
ZoneDE: "Inneres der Roboterzelle, Greifer des Roboterarms",
DefaultSeverity: 3, DefaultExposure: 3,
},
{
ID: "HP1654", NameDE: "Werkstück/Werkzeug durchschlaegt Schutzzaun", NameEN: "Workpiece/tool penetrates safety fence",
RequiredComponentTags: []string{"clamping_part", "guard"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M061"},
Priority: 98,
ApplicableLifecycles: []string{"normal_operation"},
ScenarioDE: "Greifer versagt und Werkstueck/Werkzeug wird Richtung Schutzzaun geschleudert. Person ausserhalb wird getroffen.",
TriggerDE: "Greifkraftverlust, Druckausfall, oelige Oberflaeche des Werkstuecks.",
HarmDE: "Person ausserhalb der Zelle wird von weggeschleudertem Teil getroffen.",
AffectedDE: "Bedienpersonal in der Naehe der Roboterzelle",
ZoneDE: "Schutzzaun, Bereich ausserhalb der Roboterzelle",
DefaultSeverity: 3, DefaultExposure: 2,
},
{
ID: "HP1655", NameDE: "Durchgreifen ueber Schutzzaun zum Greifer/Werkstueck", NameEN: "Reaching over fence to gripper/workpiece",
RequiredComponentTags: []string{"clamping_part", "guard"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M002", "M061"},
Priority: 98,
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing"},
ScenarioDE: "Person greift ueber den Schutzzaun und erreicht den Greifer oder das Werkstueck am Roboterarm.",
TriggerDE: "Sicherheitsabstand zwischen Zaun-Oberkante und Greifer/Werkstueck zu gering.",
HarmDE: "Quetschung von Hand oder Arm zwischen Greifer/Werkstueck und feststehenden Teilen.",
AffectedDE: "Bedienpersonal",
ZoneDE: "Schutzzaun-Oberkante, Greifer/Werkstueck am Roboterarm",
DefaultSeverity: 3, DefaultExposure: 2,
},
// ================================================================
// Zentriergreifer an Förderbändern
// ================================================================
{
ID: "HP1660", NameDE: "Quetschen am Zentriergreifer von aussen", NameEN: "Crushing at centering gripper from outside",
RequiredComponentTags: []string{"clamping_part", "entanglement_risk"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M002", "M061"},
Priority: 98,
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing"},
ScenarioDE: "Person befindet sich ausserhalb der Roboterzelle und greift an die Zentriereinheit (fest montierter Greifer am Foerderband).",
TriggerDE: "Zentriergreifer schliesst waehrend Hand im Greifbereich. Unzureichender Abstand zwischen Greifer und Schutzzaun-Oeffnung.",
HarmDE: "Quetschung von Fingern oder Hand zwischen Greifbacken und Werkstueck.",
AffectedDE: "Bedienpersonal",
ZoneDE: "Zentriereinheit an Foerderbaendern, Schutzzaun-Oeffnung",
DefaultSeverity: 2, DefaultExposure: 3,
},
{
ID: "HP1661", NameDE: "Quetschen am Zentriergreifer von innen", NameEN: "Crushing at centering gripper from inside cell",
RequiredComponentTags: []string{"clamping_part", "entanglement_risk", "guard"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M054", "M061"},
Priority: 98,
ApplicableLifecycles: []string{"normal_operation", "cleaning", "fault_clearing"},
ScenarioDE: "Person befindet sich innerhalb der Roboterzelle und greift an die Zentriereinheit am Foerderband.",
TriggerDE: "Schutztuer geoeffnet, aber Zentriergreifer wird nicht automatisch stillgesetzt.",
HarmDE: "Quetschung von Fingern oder Hand zwischen Greifbacken und Werkstueck.",
AffectedDE: "Wartungspersonal, Reinigungspersonal",
ZoneDE: "Zentriereinheit an Foerderbaendern innerhalb der Roboterzelle",
DefaultSeverity: 2, DefaultExposure: 3,
},
// ================================================================
// Bearbeitungszentrum (Robodrill/WZM) innerhalb Roboterzelle
// ================================================================
{
ID: "HP1665", NameDE: "Quetschen an Beladetuer der Werkzeugmaschine", NameEN: "Crushing at machine tool loading door",
RequiredComponentTags: []string{"moving_part"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M054", "M061"},
Priority: 98, MachineTypes: []string{"cnc", "metalworking", "automotive", "robotics_cobot"},
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing"},
ScenarioDE: "Person greift durch die Beladetuer der Werkzeugmaschine. Beladetuer schliesst sich oder bewegliche Teile im Innenraum starten.",
TriggerDE: "Tuerpositionsschalter nicht in Robotersteuerung eingebunden, fehlende Verriegelung.",
HarmDE: "Quetschung von Hand/Arm an Beladetuer oder durch bewegliche Teile im Bearbeitungsraum.",
AffectedDE: "Bedienpersonal, Einrichter, Wartungspersonal",
ZoneDE: "Beladetuer der Werkzeugmaschine, Bearbeitungsraum",
DefaultSeverity: 3, DefaultExposure: 3,
},
{
ID: "HP1666", NameDE: "Quetschen/Scheren im Bearbeitungsraum der WZM", NameEN: "Crushing/shearing inside machine tool workspace",
RequiredComponentTags: []string{"moving_part"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M054"},
Priority: 98, MachineTypes: []string{"cnc", "metalworking", "automotive", "robotics_cobot"},
ApplicableLifecycles: []string{"setup", "maintenance", "fault_clearing"},
ScenarioDE: "Person greift in den Bearbeitungsraum der Werkzeugmaschine und wird von beweglichen Achsen, Werkzeug oder Spannvorrichtung verletzt.",
TriggerDE: "Bewegliche Teile starten waehrend Hand im Bearbeitungsraum (Einrichtbetrieb, Stoerungsbeseitigung).",
HarmDE: "Quetschungen, Schnittverletzungen durch rotierende Werkzeuge, Scheren an Achsbewegungen.",
AffectedDE: "Einrichter, Wartungspersonal",
ZoneDE: "Bearbeitungsraum der Werkzeugmaschine, Achsen, Werkzeug, Spannvorrichtung",
DefaultSeverity: 3, DefaultExposure: 3,
},
// ================================================================
// KSS-Spritzer / Druckluft in Bearbeitungszelle
// ================================================================
{
ID: "HP1670", NameDE: "KSS-Spritzer in Augen/Gesicht", NameEN: "Coolant splash to eyes/face",
RequiredComponentTags: []string{},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M141"},
Priority: 97, MachineTypes: []string{"cnc", "metalworking", "automotive"},
ApplicableLifecycles: []string{"normal_operation", "cleaning", "maintenance", "fault_clearing"},
ScenarioDE: "Person bekommt Kuehlschmierstoff-Spritzer ins Auge oder Gesicht beim Oeffnen der Bearbeitungszelle oder bei laufender Bettspuelung.",
TriggerDE: "KSS-Pumpe laeuft waehrend Schutztuer geoeffnet ist, Austrittsduese nicht korrekt gerichtet.",
HarmDE: "Augenverletzung, Reizung der Bindehaut, bei Hochdruck-KSS ernsthafte Augenschaeden.",
AffectedDE: "Bedienpersonal, Wartungspersonal",
ZoneDE: "Bearbeitungszelle, Bereich vor der Schutztuer, Austrittsduesen",
DefaultSeverity: 2, DefaultExposure: 3,
},
{
ID: "HP1671", NameDE: "Druckluft-Verletzung in Bearbeitungszelle", NameEN: "Compressed air injury in machining cell",
RequiredComponentTags: []string{"pinch_point"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M061"},
Priority: 97, MachineTypes: []string{"cnc", "metalworking", "automotive"},
ApplicableLifecycles: []string{"normal_operation", "cleaning", "maintenance", "fault_clearing"},
ScenarioDE: "Person wird von ausstroemender Druckluft oder aufgewirbelten Bearbeitungsrueckstaenden in der Bearbeitungszelle verletzt.",
TriggerDE: "Druckluftreinigungsduese aktiv waehrend Schutztuer geoeffnet, Spaene oder Partikel werden aufgewirbelt.",
HarmDE: "Augenverletzung durch Spaene, Hautverletzung durch Druckluftstoss.",
AffectedDE: "Bedienpersonal, Wartungspersonal",
ZoneDE: "Bearbeitungszelle, Druckluftreinigungsduesen",
DefaultSeverity: 2, DefaultExposure: 3,
},
// ================================================================
// KSS-Schläuche unter Druck
// ================================================================
{
ID: "HP1675", NameDE: "KSS-Schlauch bersten oder abspringen", NameEN: "Coolant hose burst or detachment",
RequiredComponentTags: []string{},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M480"},
Priority: 97, MachineTypes: []string{"cnc", "metalworking", "automotive"},
ApplicableLifecycles: []string{"normal_operation", "setup", "maintenance", "fault_clearing"},
ScenarioDE: "Schlauch der Kuehlschmierstoffversorgung zwischen Aufbereitungsanlage und Bearbeitungszentrum platzt oder springt unter Druck ab.",
TriggerDE: "Materialermuedung, Ueberdruck, fehlerhafte Befestigung, mechanische Beschaedigung des Schlauchs.",
HarmDE: "Person wird von abspringendem Schlauch getroffen (Peitscheneffekt). KSS-Spritzer unter Druck verletzen Haut und Augen. Rutschgefahr durch austretenden KSS.",
AffectedDE: "Bedienpersonal, Wartungspersonal",
ZoneDE: "Druckschlaeuche des Kuehlschmierstoffsystems",
DefaultSeverity: 2, DefaultExposure: 2,
},
// ================================================================
// Quetschen am Förderband — Werkstück/Tunnel
// ================================================================
{
ID: "HP1680", NameDE: "Quetschen zwischen Werkstueck und Tunnel am Foerderband", NameEN: "Crushing between workpiece and conveyor tunnel",
RequiredComponentTags: []string{"entanglement_risk"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M002", "M003"},
Priority: 97,
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "fault_clearing"},
ScenarioDE: "Person greift an den Tunnel/Rahmen des Foerderbandes und wird von einem darauf bewegten Werkstueck eingequetscht.",
TriggerDE: "Zu geringer Abstand zwischen Werkstueck und Tunnel/Rahmen, scharfe Kanten an Tunneleingang.",
HarmDE: "Quetschung von Fingern zwischen Werkstueck und Rahmen.",
AffectedDE: "Bedienpersonal",
ZoneDE: "Foerderband-Tunnel, Werkstück auf dem Band",
DefaultSeverity: 2, DefaultExposure: 3,
},
// ================================================================
// Elektrisch — Spezifische Szenarien
// ================================================================
{
ID: "HP1685", NameDE: "Indirektes Beruehren durch Schutzleiterunterbrechung", NameEN: "Indirect contact due to PE interruption",
RequiredComponentTags: []string{},
GeneratedHazardCats: []string{"electrical_hazard"},
SuggestedMeasureIDs: []string{"M475", "M476"},
Priority: 98,
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing"},
ScenarioDE: "Schutzleiter ist unterbrochen. Person beruehrt leitfaehige Maschinenteile und erleidet elektrischen Schlag.",
TriggerDE: "Mechanische Beschaedigung des Schutzleiters, korrodierte Verbindung, fehlerhafte Installation.",
HarmDE: "Elektrischer Schlag bei Beruehren des Maschinengehaeuses oder anderer leitfaehiger Teile.",
AffectedDE: "Bedienpersonal, Wartungspersonal",
ZoneDE: "Beruehrbare leitfaehige Oberflaechen der Anlage",
DefaultSeverity: 4, DefaultExposure: 2,
},
{
ID: "HP1686", NameDE: "Direktes Beruehren im Schaltschrank", NameEN: "Direct contact inside control cabinet",
RequiredComponentTags: []string{},
GeneratedHazardCats: []string{"electrical_hazard"},
SuggestedMeasureIDs: []string{"M009"},
Priority: 98,
ApplicableLifecycles: []string{"maintenance", "fault_clearing", "commissioning"},
ScenarioDE: "Person beruehrt spannungsfuehrende Teile bei geoeffnetem Schaltschrank. Leiter um Bedienelemente sind nicht fingersicher geschuetzt.",
TriggerDE: "Schaltschranktuer geoeffnet fuer Wartung oder Fehlersuche, unzureichender Beruehrungsschutz.",
HarmDE: "Person erleidet elektrischen Schlag. Herzkammerflimmern, Verbrennungen, bei Hochspannung Todesfolge.",
AffectedDE: "Wartungspersonal, Elektrofachkraefte",
ZoneDE: "Schaltschrank-Innenraum, Klemmen, Sammelschienen",
DefaultSeverity: 4, DefaultExposure: 2,
},
{
ID: "HP1687", NameDE: "Brand durch eindringende Fluessigkeit", NameEN: "Fire from liquid ingress causing short circuit",
RequiredComponentTags: []string{},
GeneratedHazardCats: []string{"electrical_hazard"},
SuggestedMeasureIDs: []string{"M009"},
Priority: 98,
ApplicableLifecycles: []string{"normal_operation", "cleaning"},
ScenarioDE: "Fluessigkeit dringt in elektrische Komponenten ein und verursacht Kurzschluss. Person wird durch Brand oder Rauchentwicklung gefaehrdet.",
TriggerDE: "Reinigung mit Wasser, KSS-Leckage tropft auf Schaltschrank oder Steuerungskomponenten.",
HarmDE: "Person wird durch Brand, Flammen oder toxische Rauchgase verletzt. Verbrennungen, Rauchvergiftung.",
AffectedDE: "Bedienpersonal, Reinigungspersonal",
ZoneDE: "Schaltgeraetekombinationen, elektrische Komponenten unterhalb von Rohrleitungen",
DefaultSeverity: 3, DefaultExposure: 2,
},
{
ID: "HP1688", NameDE: "Gefaehrliche Beruehrungsspannung durch Potentialunterschiede", NameEN: "Dangerous touch voltage from potential differences",
RequiredComponentTags: []string{},
GeneratedHazardCats: []string{"electrical_hazard"},
SuggestedMeasureIDs: []string{"M475", "M477", "M138", "M329"},
Priority: 96,
ApplicableLifecycles: []string{"normal_operation", "setup", "maintenance", "fault_clearing"},
ScenarioDE: "Person beruehrt gleichzeitig Anlagenteile mit unterschiedlichem Potential und erleidet elektrischen Schlag.",
TriggerDE: "Fehlender Potentialausgleich zwischen Anlagenteilen verschiedener Hersteller.",
HarmDE: "Elektrischer Schlag bei gleichzeitigem Beruehren von Teilen unterschiedlichen Potentials.",
AffectedDE: "Bedienpersonal, Wartungspersonal",
ZoneDE: "Elektrisch leitfaehige Oberflaechen verschiedener Anlagenteile",
DefaultSeverity: 4, DefaultExposure: 2,
},
{
ID: "HP1689", NameDE: "Fehlerstromschutz an Steckdosenstromkreisen", NameEN: "RCD protection at socket circuits",
RequiredComponentTags: []string{},
GeneratedHazardCats: []string{"electrical_hazard"},
SuggestedMeasureIDs: []string{"M475"},
Priority: 97,
ApplicableLifecycles: []string{"normal_operation", "setup", "maintenance", "fault_clearing"},
ScenarioDE: "Defektes Geraet wird an Steckdose der Maschine angeschlossen. Fehlerstrom fliesst ueber den Koerper der beruerenden Person.",
TriggerDE: "Fehlende Fehlerstrom-Schutzeinrichtung (RCD) an Steckdosenstromkreisen der Maschine.",
HarmDE: "Person erleidet elektrischen Schlag durch Fehlerstrom. Herzkammerflimmern, potentiell toedlich.",
AffectedDE: "Bedienpersonal, Wartungspersonal",
ZoneDE: "Steckdosen der Maschine, angeschlossene Betriebsmittel",
DefaultSeverity: 4, DefaultExposure: 2,
},
// ================================================================
// Ergonomie
// ================================================================
{
ID: "HP1690", NameDE: "Ergonomisch unguenstige Einlegeposition", NameEN: "Unfavorable ergonomic loading position",
RequiredComponentTags: []string{"entanglement_risk"},
GeneratedHazardCats: []string{"ergonomic_hazard"},
SuggestedMeasureIDs: []string{},
Priority: 85,
ApplicableLifecycles: []string{"normal_operation"},
ScenarioDE: "Person muss Werkstuecke in ergonomisch unguenstiger Hoehe oder Reichweite auf das Foerderband auflegen oder entnehmen.",
TriggerDE: "Bandhoehe nicht auf ergonomische Handhabung ausgelegt, schwere Werkstuecke.",
HarmDE: "Person erleidet Rueckenbeschwerden und Schulterbelastung durch wiederholte Fehlhaltung. Langfristig Muskel-Skelett-Erkrankungen.",
AffectedDE: "Bedienpersonal",
ZoneDE: "Beladebereich der Foerderbaender",
DefaultSeverity: 2, DefaultExposure: 4,
},
{
ID: "HP1691", NameDE: "Unergonomische Position der Bedienelemente", NameEN: "Unfavorable ergonomic position of controls",
RequiredComponentTags: []string{},
GeneratedHazardCats: []string{"ergonomic_hazard"},
SuggestedMeasureIDs: []string{},
Priority: 85,
ApplicableLifecycles: []string{"normal_operation", "setup"},
ScenarioDE: "Person bedient Anlage in ergonomisch unguenstiger Position ueber laengere Zeit.",
TriggerDE: "Bedienfeld zu hoch, zu niedrig oder seitlich versetzt montiert.",
HarmDE: "Person erleidet Nacken- und Schulterbelastung durch unguenstige Bedienposition. Langfristig Haltungsschaeden.",
AffectedDE: "Bedienpersonal",
ZoneDE: "Bedienfeld, HMI, Betriebsartenwahlschalter",
DefaultSeverity: 2, DefaultExposure: 4,
},
// ================================================================
// Thermisch / Verbrennung
// ================================================================
{
ID: "HP1695", NameDE: "Verbrennung an heissen Werkstuecken", NameEN: "Burn from hot workpieces",
RequiredComponentTags: []string{},
GeneratedHazardCats: []string{"thermal_hazard"},
SuggestedMeasureIDs: []string{"M141"},
Priority: 88, MachineTypes: []string{"cnc", "metalworking", "automotive"},
ApplicableLifecycles: []string{"normal_operation", "setup", "changeover"},
ScenarioDE: "Person beruehrt heisse Werkstuecke die durch die Bearbeitung erwaermt wurden.",
TriggerDE: "Manuelle Entnahme von Werkstuecken ohne Wartezeit oder Schutzhandschuhe.",
HarmDE: "Verbrennungen an Haenden und Fingern.",
AffectedDE: "Bedienpersonal",
ZoneDE: "Werkstueckausgabe, Entnahmeplatz",
DefaultSeverity: 1, DefaultExposure: 3,
},
// ================================================================
// Tragfähigkeit / Aufstellung
// ================================================================
{
ID: "HP1697", NameDE: "Anlage bricht durch unzureichenden Untergrund ein", NameEN: "Machine collapses through insufficient floor",
RequiredComponentTags: []string{"high_force"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{},
Priority: 88,
ApplicableLifecycles: []string{"normal_operation", "setup", "commissioning"},
ScenarioDE: "Untergrund bricht unter dem Maschinengewicht ein. Personen im Umfeld werden von kippender oder absackender Anlage eingeklemmt.",
TriggerDE: "Boden nicht auf maximale statische und dynamische Lasten der Maschine ausgelegt.",
HarmDE: "Anlage bricht ein, Quetschung von Personen im Umfeld.",
AffectedDE: "Bedienpersonal, Wartungspersonal",
ZoneDE: "Bereich um die Maschine, Aufstellflaeche",
DefaultSeverity: 4, DefaultExposure: 1,
},
// ================================================================
// Elektrisch — Kriechstrecken + EMV
// ================================================================
{
ID: "HP1698", NameDE: "Kurzschluss durch unzureichende Luft-/Kriechstrecken", NameEN: "Short circuit from insufficient creepage/clearance",
RequiredComponentTags: []string{},
GeneratedHazardCats: []string{"electrical_hazard"},
SuggestedMeasureIDs: []string{"M477"},
Priority: 98,
ApplicableLifecycles: []string{"normal_operation", "setup", "maintenance", "fault_clearing"},
ScenarioDE: "Unzureichende Luft-/Kriechstrecken fuehren bei Verschmutzung zu Kriechstroemen. Person beruehrt betroffene Teile und erleidet elektrischen Schlag.",
TriggerDE: "Verschmutzungsgrad hoeher als bei der Dimensionierung angenommen, Feuchtigkeit, alterungsbedingte Veraenderung.",
HarmDE: "Gefaehrliche Beruehrungsspannung an beruehrbaren Teilen, Kurzschluss, Brand.",
AffectedDE: "Bedienpersonal, Wartungspersonal",
ZoneDE: "Schaltgeraetekombinationen, elektrische Anschluesse",
DefaultSeverity: 4, DefaultExposure: 2,
},
{
ID: "HP1699", NameDE: "EMV-Stoereinfluss auf Sicherheitsfunktionen", NameEN: "EMC interference with safety functions",
RequiredComponentTags: []string{},
GeneratedHazardCats: []string{"radiation_hazard"},
SuggestedMeasureIDs: []string{"M478", "M479"},
Priority: 97,
ApplicableLifecycles: []string{"normal_operation", "setup"},
ScenarioDE: "EMV-Stoerungen verursachen unerwartete Maschinenbewegungen. Person im Gefahrenbereich wird von unkontrolliert bewegten Teilen getroffen.",
TriggerDE: "Unzureichende EMV-Schirmung, nicht-fachgerechte Verkabelung, externe Stoerquellen.",
HarmDE: "Unkontrollierte Bewegung von Achsen, Werkzeug oder Roboterarm durch Steuerungsfehler.",
AffectedDE: "Bedienpersonal, Einrichter",
ZoneDE: "Bearbeitungsbereich, sicherheitsrelevante Steuerungen",
DefaultSeverity: 3, DefaultExposure: 2,
},
// ================================================================
// Differenzierte Patterns (GT-Benchmark: gleiche Zone, anderes Szenario)
// ================================================================
{
ID: "HP1700", NameDE: "Getroffen von bewegtem Werkzeug/Greifer am Roboter", NameEN: "Struck by moving tool/gripper on robot",
RequiredComponentTags: []string{"moving_part", "clamping_part"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M054", "M061"},
Priority: 99,
ApplicableLifecycles: []string{"normal_operation", "setup", "teach_mode", "cleaning"},
ScenarioDE: "Person steht im Bewegungsbereich des Roboterarms und wird von bewegtem Werkzeug oder Greifer getroffen.",
TriggerDE: "Roboter schwenkt mit Werkzeug/Greifer in Richtung Person.",
HarmDE: "Prellungen, Schnittverletzungen durch Werkzeugkanten, Knochenbrueche.",
AffectedDE: "Bedienpersonal, Einrichter",
ZoneDE: "Inneres der Roboterzelle, Schwenkbereich Werkzeug/Greifer",
DefaultSeverity: 3, DefaultExposure: 3,
},
{
ID: "HP1701", NameDE: "Greifer/Werkzeug durchschlaegt Schutzzaun", NameEN: "Gripper/tool penetrates safety fence",
RequiredComponentTags: []string{"clamping_part", "guard"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M061"},
Priority: 98,
ApplicableLifecycles: []string{"normal_operation", "setup", "changeover"},
ScenarioDE: "Greifer oder Werkzeug am Roboterarm durchschlaegt den Schutzzaun und trifft Person ausserhalb der Zelle.",
TriggerDE: "Bewegungsbegrenzung versagt, Schutzzaun nicht auf Aufprallenergie ausgelegt.",
HarmDE: "Person ausserhalb wird von Greifer/Werkzeug oder Zaunteilen getroffen.",
AffectedDE: "Bedienpersonal in der Naehe des Schutzzauns",
ZoneDE: "Bereich um Roboterarm ausserhalb der Roboterzelle",
DefaultSeverity: 3, DefaultExposure: 2,
},
{
ID: "HP1702", NameDE: "KSS-Schlauch platzt unter Druck", NameEN: "Coolant hose bursts under pressure",
RequiredComponentTags: []string{},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M480"},
Priority: 97, MachineTypes: []string{"cnc", "metalworking", "automotive"},
ApplicableLifecycles: []string{"normal_operation", "maintenance", "fault_clearing"},
ScenarioDE: "KSS-Schlauch platzt und spritzt Kuehlschmierstoff unter Druck. Person in der Naehe wird von KSS-Strahl getroffen.",
TriggerDE: "Alterung, Beschaedigung oder Ueberdruck fuehrt zum Versagen des Schlauchs.",
HarmDE: "Einstichverletzung durch KSS-Strahl unter Druck, Augenverletzung, Rutschgefahr.",
AffectedDE: "Bedienpersonal, Wartungspersonal",
ZoneDE: "Druckschlaeuche des Kuehlschmierstoffsystems",
DefaultSeverity: 2, DefaultExposure: 2,
},
{
ID: "HP1703", NameDE: "KSS-Bettspuelung bei geoeffneter Schutztuer", NameEN: "Coolant bed wash with open guard door",
RequiredComponentTags: []string{},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M061"},
Priority: 97, MachineTypes: []string{"cnc", "metalworking", "automotive"},
ApplicableLifecycles: []string{"normal_operation", "cleaning", "maintenance", "fault_clearing"},
ScenarioDE: "KSS-Pumpe laeuft bei geoeffneter Schutztuer. Person vor der Bearbeitungszelle bekommt KSS-Spritzer ins Auge oder Gesicht.",
TriggerDE: "Kein automatisches Abschalten der KSS-Pumpe bei geoeffneter Tuer.",
HarmDE: "KSS-Spritzer in Augen oder Gesicht, Rutschgefahr durch austretenden KSS.",
AffectedDE: "Bedienpersonal, Wartungspersonal",
ZoneDE: "Inneres des Bearbeitungszentrums, Bereich vor der Schutztuer",
DefaultSeverity: 1, DefaultExposure: 3,
},
{
ID: "HP1704", NameDE: "Brand durch KSS-Leckage auf elektrische Komponenten", NameEN: "Fire from coolant leakage on electrical components",
RequiredComponentTags: []string{},
GeneratedHazardCats: []string{"electrical_hazard"},
SuggestedMeasureIDs: []string{"M480", "M009"},
Priority: 98, MachineTypes: []string{"cnc", "metalworking", "automotive"},
ApplicableLifecycles: []string{"normal_operation", "cleaning", "maintenance"},
ScenarioDE: "KSS-Leckage tropft auf elektrische Komponenten und verursacht Kurzschluss. Person wird durch Brand oder Rauchentwicklung gefaehrdet.",
TriggerDE: "KSS-Leitung undicht oberhalb elektrischer Komponenten, tropft auf Klemmen oder Leiterplatten.",
HarmDE: "Person wird durch Brand, Flammen oder toxische Rauchgase verletzt. Verbrennungen, Rauchvergiftung.",
AffectedDE: "Bedienpersonal",
ZoneDE: "Spannungsfuehrende Teile unterhalb/angrenzend von KSS-Leitungen",
DefaultSeverity: 3, DefaultExposure: 2,
},
}
}
@@ -42,7 +42,7 @@ func builtinSoftwarePatterns() []HazardPattern {
SuggestedMeasureIDs: []string{"M145", "M146", "M121"},
SuggestedEvidenceIDs: []string{"E01", "E14"},
Priority: 70,
ScenarioDE: "Falsche Parametrierung von Achsgrenzen, Geschwindigkeiten oder Sicherheitsgrenzen nach Umruestung.",
ScenarioDE: "Falsche Parametrierung von Achsgrenzen, Geschwindigkeiten oder Sicherheitsgrenzen nach Produktwechsel.",
TriggerDE: "Bediener oder Einrichter aendert Parameter ohne Validierung oder nutzt falsches Rezept/Programm.",
HarmDE: "Ueberfahren mechanischer Anschlaege, zu hohe Kraefte/Geschwindigkeiten, Kollision.",
AffectedDE: "Bedienpersonal, Einrichter",
@@ -252,7 +252,7 @@ func GetSpecificMachinePatterns() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M003", "M141"},
SuggestedEvidenceIDs: []string{"E01", "E20"},
Priority: 90,
Priority: 90, MachineTypes: []string{"wind_turbine"},
ScenarioDE: "Rotorblatt einer Windturbine bricht durch Materialermuedung oder Blitzschlag und wird Hunderte Meter weit geschleudert.",
TriggerDE: "Materialermuedung, Blitzschaden, Vereisung mit Unwucht, fehlende Inspektionen",
HarmDE: "Toedliche Verletzung durch Blattstuecke, Sachschaeden im weiten Umkreis",
@@ -261,7 +261,7 @@ func GetSpecificMachinePatterns() []HazardPattern {
DefaultSeverity: 5, DefaultExposure: 1,
},
{
ID: "HP746", NameDE: "Absturz bei Wartung der Gondel", NameEN: "Fall during nacelle maintenance",
ID: "HP746", NameDE: "Absturz der Gondel", NameEN: "Fall during nacelle maintenance",
RequiredComponentTags: []string{"structural_part", "gravity_risk"},
RequiredEnergyTags: []string{"gravitational"},
GeneratedHazardCats: []string{"mechanical_hazard"},
@@ -297,7 +297,7 @@ func GetSpecificMachinePatterns() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M141"},
SuggestedEvidenceIDs: []string{"E01", "E20"},
Priority: 80,
Priority: 80, MachineTypes: []string{"wind_turbine"},
ScenarioDE: "Bei Vereisung loesen sich Eisstuecke von den Rotorblaettern und werden durch die Fliehkraft weit geschleudert.",
TriggerDE: "Vereisung im Winter, fehlende Eiserkennungssysteme, Weiterbetrieb bei Eisansatz",
HarmDE: "Verletzung durch Eisschlag, Sachschaeden an Fahrzeugen und Gebaeuden",
@@ -30,7 +30,7 @@ func GetSpecificMachinePatterns2() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M003", "M141"},
SuggestedEvidenceIDs: []string{"E08", "E20"},
Priority: 80,
Priority: 80, MachineTypes: []string{"escalator"},
ScenarioDE: "Finger oder Handteile werden am Einzugspunkt des Handlaufs in die Verkleidung gezogen.",
TriggerDE: "Kinderhand am Handlauf nahe der Verkleidung, fehlende Einlaufschutzbuegel",
HarmDE: "Fingerquetschung, Hautabschuerfungen, bei Kindern Armverletzung",
@@ -39,7 +39,7 @@ func GetSpecificMachinePatterns2() []HazardPattern {
DefaultSeverity: 3, DefaultExposure: 4,
},
{
ID: "HP758", NameDE: "Sturz bei Notbremsung der Fahrtreppe", NameEN: "Fall during emergency stop of escalator",
ID: "HP758", MachineTypes: []string{"escalator", "elevator"}, NameDE: "Sturz bei Notbremsung der Fahrtreppe", NameEN: "Fall during emergency stop of escalator",
RequiredComponentTags: []string{"moving_part"},
RequiredEnergyTags: []string{"kinetic"},
GeneratedHazardCats: []string{"mechanical_hazard"},
@@ -75,7 +75,7 @@ func GetSpecificMachinePatterns2() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M003", "M141"},
SuggestedEvidenceIDs: []string{"E08", "E09", "E20"},
Priority: 85,
Priority: 85, MachineTypes: []string{"escalator", "elevator"},
ScenarioDE: "Bruch einer Trittstufe oder der Kammplatte fuehrt zum Einsacken oder Einzug in die Mechanik.",
TriggerDE: "Materialermuedung, Korrosion, fehlende Inspektionen, Vandalismus",
HarmDE: "Einzug in Mechanik, Beinverletzungen, Sturz in Maschinenkammer",
@@ -173,7 +173,7 @@ func GetSpecificMachinePatterns2() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M003", "M141"},
SuggestedEvidenceIDs: []string{"E01", "E20"},
Priority: 95,
Priority: 95, MachineTypes: []string{"playground"},
ScenarioDE: "Kind steckt Kopf durch Oeffnung im Spielgeraet und bleibt haengen (Kopf-Entrapment-Gefahr bei 89-230 mm).",
TriggerDE: "Oeffnungen im kritischen Bereich 89-230 mm, V-foermige Spalte, Gelaendersprosse mit Kopffangmass",
HarmDE: "Strangulation, Erstickung, toedliche Verletzung",
@@ -233,7 +233,7 @@ func GetSpecificMachinePatterns2() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M003", "M141"},
SuggestedEvidenceIDs: []string{"E01", "E20"},
Priority: 95,
Priority: 95, MachineTypes: []string{"playground"},
ScenarioDE: "Kind verfaengt sich mit Kapuzenkordel, Schal oder Halskette in Seilen oder Netzen des Spielgeraets.",
TriggerDE: "Kleidung mit Kordeln am Hals, zu grosse Maschenweite, lose Seilenden",
HarmDE: "Strangulation, Erstickung, toedliche Verletzung",
@@ -361,7 +361,7 @@ func GetSpecificMachinePatterns2() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M003", "M004", "M082"},
SuggestedEvidenceIDs: []string{"E08", "E09"},
Priority: 85,
Priority: 85, MachineTypes: []string{"laundry"},
ScenarioDE: "Person greift in die drehende Trommel der Industriewaschmaschine und wird eingezogen.",
TriggerDE: "Defekte Tuerverriegelung, Oeffnen waehrend Nachlauf, Bedienfehler",
HarmDE: "Schwere Quetschverletzung, Armeinzug, Strangulation durch Waeschestuecke",
@@ -411,7 +411,7 @@ func GetSpecificMachinePatterns2() []HazardPattern {
SuggestedMeasureIDs: []string{"M005", "M141"},
SuggestedEvidenceIDs: []string{"E20"},
Priority: 80,
ScenarioDE: "Grosse Glasscheibe zerbricht beim Transport oder bei der Montage und trifft umstehende Personen.",
ScenarioDE: "Grosse Glasscheibe zerbricht oder durch mechanische Einwirkung und trifft umstehende Personen.",
TriggerDE: "Thermische Spannungen, mechanische Beschaedigung, fehlerhafter Saugnapp, Windlast",
HarmDE: "Tiefe Schnittwunden, Amputationsgefahr, toedliche Verletzung bei grossen Scheiben",
AffectedDE: "Transportpersonal, Monteure, Passanten",
@@ -22,7 +22,7 @@ func GetTextileAgriPatterns() []HazardPattern {
SuggestedMeasureIDs: []string{"M452", "M061"}, SuggestedEvidenceIDs: []string{"E01"},
Priority: 78, MachineTypes: []string{"textile", "knitting"},
OperationalStates: []string{"automatic_operation", "maintenance"}, HumanRoles: []string{"operator", "maintenance_tech"},
ScenarioDE: "Kontakt mit schnell bewegenden Nadeln bei Wartung oder Fadenwechsel",
ScenarioDE: "Kontakt mit schnell bewegenden Nadeln oder Fadenwechsel",
TriggerDE: "Eingriff in Nadelbereich bei laufender Maschine", HarmDE: "Stichverletzung, Schnittwunde",
AffectedDE: "Bedienpersonal", ZoneDE: "Nadelbett",
DefaultSeverity: 3, DefaultExposure: 4},
@@ -123,7 +123,7 @@ func GetTextileAgriPatterns() []HazardPattern {
SuggestedMeasureIDs: []string{"M461", "M465"}, SuggestedEvidenceIDs: []string{"E01", "E08"},
Priority: 94, MachineTypes: []string{"agricultural", "harvester", "combine"},
OperationalStates: []string{"automatic_operation", "maintenance"}, HumanRoles: []string{"operator", "maintenance_tech"},
ScenarioDE: "Kontakt mit rotierendem Schneidwerk bei Wartung oder Blockierungsbeseitigung",
ScenarioDE: "Kontakt mit rotierendem Schneidwerk oder Blockierungsbeseitigung",
TriggerDE: "Maschine nicht abgestellt, hydraulischer Nachlauf",
HarmDE: "Amputation, schwere Schnittverletzungen", AffectedDE: "Bediener, Wartungspersonal", ZoneDE: "Schneidwerksbereich",
DefaultSeverity: 5, DefaultExposure: 3},
@@ -42,7 +42,7 @@ func builtinThermalPatterns() []HazardPattern {
SuggestedEvidenceIDs: []string{"E01"},
Priority: 75,
ScenarioDE: "Aktuatoren (Servomotoren, Linearantriebe) erwaermen sich im Dauerbetrieb ueber die Beruehrtemperaturgrenze.",
TriggerDE: "Beruehren heisser Motorgehaeuse bei Wartung oder Stoerungsbeseitigung ohne ausreichende Abkuehlzeit.",
TriggerDE: "Beruehren heisser Motorgehaeuse ohne ausreichende Abkuehlzeit.",
HarmDE: "Kontaktverbrennung, Blasenbildung an Haenden.",
AffectedDE: "Wartungspersonal, Einrichter",
ZoneDE: "Motorgehaeuse, Getriebegehaeuse, Linearantrieb",
@@ -230,7 +230,7 @@ func GetWeldingGlassTextilePatterns() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M003", "M004", "M082"},
SuggestedEvidenceIDs: []string{"E08", "E09"},
Priority: 80,
Priority: 80, MachineTypes: []string{"glass_washing"},
ScenarioDE: "Transportwalzen der Glaswaschmaschine erfassen Finger oder Kleidung beim manuellen Einlegen der Scheiben.",
TriggerDE: "Manuelles Nachjustieren bei laufenden Walzen, fehlender Schutz am Einlaufbereich",
HarmDE: "Fingerquetschung, Einzug der Hand, Hautabschaelungen",
@@ -71,21 +71,21 @@ func getSupplementaryMeasures() []ProtectiveMeasureEntry {
// Elektrische Sicherheit — Potentialausgleich & Ableitstroeme
// Gap: GT-Benchmark 2.12 (Potentialausgleich), 2.4 (Ableitstroeme)
// ══════════════════════════════════════════════════════════════
{ID: "M410", ReductionType: "design", SubType: "electrical_safety", Name: "Potentialausgleich zwischen Anlagenteilen", Description: "Alle leitfaehigen Anlagenteile mit unterschiedlicher Energieversorgung werden ueber einen Potentialausgleichsleiter verbunden um gefaehrliche Beruehrungsspannungen zu vermeiden.", HazardCategory: "electrical", Examples: []string{"Potentialausgleich zwischen Roboterzelle und Werkzeugmaschine", "Potentialausgleichsschiene im Schaltschrank"}, NormReferences: []string{"IEC 60204-1 Ziff. 8.2", "IEC 61439-1"}},
{ID: "M411", ReductionType: "design", SubType: "electrical_safety", Name: "Schutz bei erhoehten Ableitstroemen", Description: "Bei Ableitstroemen ueber 10 mA wird der Schutzleiter mechanisch geschuetzt oder ein zusaetzlicher Schutzleiter verlegt und die Verbindung ueberwacht.", HazardCategory: "electrical", Examples: []string{"Schutzrohr fuer Schutzleiter an Frequenzumrichter", "Doppelter Schutzleiter mit Ueberwachung"}, NormReferences: []string{"IEC 60204-1 Ziff. 8.2.6"}},
{ID: "M412", ReductionType: "design", SubType: "electrical_safety", Name: "Dimensionierung von Luft- und Kriechstrecken", Description: "Luft- und Kriechstrecken werden entsprechend der elektrischen Beanspruchung und Verschmutzungsgrad dimensioniert um Kurzschluesse und gefaehrliche Beruehrungsspannungen zu vermeiden.", HazardCategory: "electrical", Examples: []string{"Mindestabstaende in Schaltgeraetekombinationen einhalten", "Isolationsueberwachung installieren"}, NormReferences: []string{"IEC 60204-1 Ziff. 6.2", "IEC 61439-1"}},
{ID: "M475", ReductionType: "design", SubType: "electrical_safety", Name: "Potentialausgleich zwischen Anlagenteilen", Description: "Alle leitfaehigen Anlagenteile mit unterschiedlicher Energieversorgung werden ueber einen Potentialausgleichsleiter verbunden um gefaehrliche Beruehrungsspannungen zu vermeiden.", HazardCategory: "electrical", Examples: []string{"Potentialausgleich zwischen Roboterzelle und Werkzeugmaschine", "Potentialausgleichsschiene im Schaltschrank"}, NormReferences: []string{"IEC 60204-1 Ziff. 8.2", "IEC 61439-1"}},
{ID: "M476", ReductionType: "design", SubType: "electrical_safety", Name: "Schutz bei erhoehten Ableitstroemen", Description: "Bei Ableitstroemen ueber 10 mA wird der Schutzleiter mechanisch geschuetzt oder ein zusaetzlicher Schutzleiter verlegt und die Verbindung ueberwacht.", HazardCategory: "electrical", Examples: []string{"Schutzrohr fuer Schutzleiter an Frequenzumrichter", "Doppelter Schutzleiter mit Ueberwachung"}, NormReferences: []string{"IEC 60204-1 Ziff. 8.2.6"}},
{ID: "M477", ReductionType: "design", SubType: "electrical_safety", Name: "Dimensionierung von Luft- und Kriechstrecken", Description: "Luft- und Kriechstrecken werden entsprechend der elektrischen Beanspruchung und Verschmutzungsgrad dimensioniert um Kurzschluesse und gefaehrliche Beruehrungsspannungen zu vermeiden.", HazardCategory: "electrical", Examples: []string{"Mindestabstaende in Schaltgeraetekombinationen einhalten", "Isolationsueberwachung installieren"}, NormReferences: []string{"IEC 60204-1 Ziff. 6.2", "IEC 61439-1"}},
// ══════════════════════════════════════════════════════════════
// EMV-Sicherheit
// Gap: GT-Benchmark 6.1 (EMV-Stoereinfluss auf Sicherheitsfunktionen)
// ══════════════════════════════════════════════════════════════
{ID: "M415", ReductionType: "design", SubType: "emc_safety", Name: "EMV-konforme Installation und Verkabelung", Description: "Alle sicherheitsrelevanten Komponenten und Sub-Systeme werden nach EMV-Richtlinien installiert und verkabelt um Stoereinfluss auf Sicherheitsfunktionen zu verhindern.", HazardCategory: "electrical", Examples: []string{"Geschirmte Steuerleitungen verwenden", "Getrennte Kabelkanaele fuer Leistungs- und Signalleitungen"}, NormReferences: []string{"IEC 61000-6-2", "EN 16090-1 Ziff. 5.8.7"}},
{ID: "M416", ReductionType: "design", SubType: "emc_safety", Name: "EMV-Pruefung sicherheitsrelevanter Systeme", Description: "Sicherheitsrelevante Steuerungen und Antriebe werden auf Stoerfestigkeit gegenueber elektromagnetischen Einflussgroessen geprueft.", HazardCategory: "electrical", Examples: []string{"Burst/Surge-Pruefung nach IEC 61000-4", "Stoerfestigkeitspruefung der Sicherheits-SPS"}, NormReferences: []string{"IEC 61000-4-4", "IEC 61000-4-5", "IEC 62061"}},
{ID: "M478", ReductionType: "design", SubType: "emc_safety", Name: "EMV-konforme Installation und Verkabelung", Description: "Alle sicherheitsrelevanten Komponenten und Sub-Systeme werden nach EMV-Richtlinien installiert und verkabelt um Stoereinfluss auf Sicherheitsfunktionen zu verhindern.", HazardCategory: "electrical", Examples: []string{"Geschirmte Steuerleitungen verwenden", "Getrennte Kabelkanaele fuer Leistungs- und Signalleitungen"}, NormReferences: []string{"IEC 61000-6-2", "EN 16090-1 Ziff. 5.8.7"}},
{ID: "M479", ReductionType: "design", SubType: "emc_safety", Name: "EMV-Pruefung sicherheitsrelevanter Systeme", Description: "Sicherheitsrelevante Steuerungen und Antriebe werden auf Stoerfestigkeit gegenueber elektromagnetischen Einflussgroessen geprueft.", HazardCategory: "electrical", Examples: []string{"Burst/Surge-Pruefung nach IEC 61000-4", "Stoerfestigkeitspruefung der Sicherheits-SPS"}, NormReferences: []string{"IEC 61000-4-4", "IEC 61000-4-5", "IEC 62061"}},
// ══════════════════════════════════════════════════════════════
// Kuehlschmierstoff-Leitungssicherheit
// Gap: GT-Benchmark 2.10 (KSS-Leckage fuehrt zu Brand)
// ══════════════════════════════════════════════════════════════
{ID: "M420", ReductionType: "design", SubType: "fluid_safety", Name: "Druckfeste Auslegung von KSS-Leitungen", Description: "Schlaeuche, Dichtungen, Verbindungsstuecke und Befestigungen des Kuehlschmierstoffsystems werden auf den Nenndruck der jeweiligen Komponente ausgelegt und gegen Abspringen gesichert.", HazardCategory: "mechanical", Examples: []string{"Druckschlaeuche auf maximalen Betriebsdruck dimensionieren", "Schlauchbruchsicherungen an kritischen Verbindungen"}, NormReferences: []string{"IEC 60204-1 Ziff. 11.3", "EN ISO 4414"}},
{ID: "M480", ReductionType: "design", SubType: "fluid_safety", Name: "Druckfeste Auslegung von KSS-Leitungen", Description: "Schlaeuche, Dichtungen, Verbindungsstuecke und Befestigungen des Kuehlschmierstoffsystems werden auf den Nenndruck der jeweiligen Komponente ausgelegt und gegen Abspringen gesichert.", HazardCategory: "mechanical", Examples: []string{"Druckschlaeuche auf maximalen Betriebsdruck dimensionieren", "Schlauchbruchsicherungen an kritischen Verbindungen"}, NormReferences: []string{"IEC 60204-1 Ziff. 11.3", "EN ISO 4414"}},
}
}
@@ -0,0 +1,84 @@
package iace
import "testing"
// TestHP1640_ResolvesToContactProtection pins the GT-2.2 fix: the "direct
// contact with live parts" pattern must resolve to electrical-contact-protection
// measures (basic protection, double insulation, earthing, equipotential
// bonding), not to mechanical fallbacks like chip extraction.
func TestHP1640_ResolvesToContactProtection(t *testing.T) {
measureByID := make(map[string]ProtectiveMeasureEntry)
for _, m := range GetProtectiveMeasureLibrary() {
measureByID[m.ID] = m
}
patterns := GetRobotCellPatterns()
var hp1640 *HazardPattern
for i := range patterns {
if patterns[i].ID == "HP1640" {
hp1640 = &patterns[i]
break
}
}
if hp1640 == nil {
t.Fatal("HP1640 not found in robot cell patterns")
}
if len(hp1640.SuggestedMeasureIDs) < 3 {
t.Errorf("HP1640 should suggest at least 3 measures, got %d", len(hp1640.SuggestedMeasureIDs))
}
for _, mid := range hp1640.SuggestedMeasureIDs {
m, ok := measureByID[mid]
if !ok {
t.Errorf("HP1640 references non-existent measure %s", mid)
continue
}
if m.HazardCategory != "electrical" {
t.Errorf("HP1640 measure %s (%q) has HazardCategory=%s, expected electrical",
mid, m.Name, m.HazardCategory)
}
}
}
// TestHP1688_M475IsPotentialausgleich pins the M475 rename: HP1688 (touch
// voltage from potential differences) must resolve M475 to the equipotential
// bonding measure, not to the metalworking chip extraction that previously
// occupied M410 and overwrote the electrical definition.
func TestHP1688_M475IsPotentialausgleich(t *testing.T) {
measureByID := make(map[string]ProtectiveMeasureEntry)
for _, m := range GetProtectiveMeasureLibrary() {
measureByID[m.ID] = m
}
m, ok := measureByID["M475"]
if !ok {
t.Fatal("M475 not defined — supplementary rename did not land")
}
if m.HazardCategory != "electrical" {
t.Errorf("M475 must be HazardCategory=electrical, got %s (%q)", m.HazardCategory, m.Name)
}
patterns := GetRobotCellPatternsExt()
var hp1688 *HazardPattern
for i := range patterns {
if patterns[i].ID == "HP1688" {
hp1688 = &patterns[i]
break
}
}
if hp1688 == nil {
t.Fatal("HP1688 not found in robot cell ext patterns")
}
found := false
for _, mid := range hp1688.SuggestedMeasureIDs {
if mid == "M475" {
found = true
break
}
}
if !found {
t.Errorf("HP1688 must reference M475 (Potentialausgleich), got %v", hp1688.SuggestedMeasureIDs)
}
}
@@ -66,6 +66,8 @@ type PatternMatch struct {
HumanRoles []string `json:"human_roles,omitempty"`
GeneratedHazardType string `json:"generated_hazard_type,omitempty"`
MatchedFailureModes []string `json:"matched_failure_modes,omitempty"`
ApplicableLifecycles []string `json:"applicable_lifecycles,omitempty"`
SuggestedMeasureIDs []string `json:"suggested_measure_ids,omitempty"`
}
// HazardSuggestion is a suggested hazard from pattern matching.
@@ -217,7 +219,9 @@ func (e *PatternEngine) Match(input MatchInput) *MatchOutput {
StateTransitions: p.StateTransitions,
HumanRoles: p.HumanRoles,
GeneratedHazardType: p.GeneratedHazardType,
MatchedFailureModes: matchedFMs,
MatchedFailureModes: matchedFMs,
ApplicableLifecycles: p.ApplicableLifecycles,
SuggestedMeasureIDs: p.SuggestedMeasureIDs,
})
for _, cat := range p.GeneratedHazardCats {
@@ -38,5 +38,6 @@ func collectAllPatterns() []HazardPattern {
patterns = append(patterns, GetVDMAIndustryPatterns()...) // HP1500-HP1549 VDMA sectors (Phase 3)
patterns = append(patterns, GetTextileAgriPatterns()...) // HP1550-HP1584 Textile + Agri (Phase 5)
patterns = append(patterns, GetRobotCellPatterns()...) // HP1600-HP1649 Robot cell (GT benchmark)
patterns = append(patterns, GetRobotCellPatternsExt()...) // HP1650-HP1699 Robot cell extended (GT gaps)
return patterns
}
@@ -64,12 +64,16 @@ class ComplianceCheckStatusResponse(BaseModel):
@router.post("/extract-text")
async def extract_text(req: ExtractTextRequest):
"""Extract text from a URL via consent-tester DSI discovery."""
"""Extract text from a URL via consent-tester DSI discovery.
Merges all documents found on the page (sub-pages, accordions, etc.)
"""
try:
async with httpx.AsyncClient(timeout=90.0) as client:
async with httpx.AsyncClient(timeout=300.0) as client:
resp = await client.post(
f"{CONSENT_TESTER_URL}/dsi-discovery",
json={"url": req.url, "max_documents": 1},
json={"url": req.url, "max_documents": 5},
timeout=300.0,
)
if resp.status_code != 200:
return {
@@ -86,10 +90,15 @@ async def extract_text(req: ExtractTextRequest):
"error": "Kein Text extrahierbar",
}
doc = docs[0]
text = doc.get("full_text", "") or doc.get("text_preview", "") or doc.get("text", "")
title = doc.get("title", "") or doc.get("doc_type", "")
word_count = doc.get("word_count", 0) or len(text.split())
# Merge all documents (handles multi-page DSIs like BMW)
texts = []
for doc in docs:
t = doc.get("full_text", "") or doc.get("text_preview", "") or ""
if t and len(t) > 50:
texts.append(t)
text = "\n\n".join(texts) if texts else ""
title = docs[0].get("title", "") or docs[0].get("doc_type", "")
word_count = len(text.split())
return {
"text": text,
@@ -178,11 +187,16 @@ async def _run_compliance_check(check_id: str, req: ComplianceCheckRequest):
# 1. Same URL used for multiple doc_types → split by heading
# 2. DSI text contains Cookie/Social-Media sections → auto-fill empty rows
from compliance.services.section_splitter import (
split_shared_texts, auto_fill_from_dsi,
split_shared_texts, auto_fill_from_dsi, cross_search_documents,
)
split_shared_texts(doc_entries, url_text_cache)
auto_fill_from_dsi(doc_entries)
# Refresh doc_texts after splitting
# Step 1c: Cross-document search — find doc_types in wrong documents
_update(check_id, "Dokumente werden uebergreifend durchsucht...")
placement_findings = cross_search_documents(doc_entries)
# Refresh doc_texts after all splitting/searching
for entry in doc_entries:
if entry.get("text"):
doc_texts[entry["doc_type"]] = entry["text"]
@@ -232,6 +246,16 @@ async def _run_compliance_check(check_id: str, req: ComplianceCheckRequest):
# Apply profile context filter
result = _apply_profile_filter(result, profile, doc_type)
# Add placement findings — but only if the regex checks confirm
# the text doesn't match. If completeness >= 50%, the text IS the
# right doc_type despite missing cross-search keywords.
if result.completeness_pct < 50:
for pf in placement_findings:
if pf.get("doc_type") == doc_type:
result.checks.insert(0, CheckItem(**{
k: v for k, v in pf.items() if k != "doc_type"
}))
results.append(result)
total_findings += result.findings_count
@@ -302,17 +326,24 @@ async def _run_compliance_check(check_id: str, req: ComplianceCheckRequest):
else:
r.scenario = "import"
# Step 5: Build report
# Step 5: Build report with management summary
_update(check_id, "Report wird erstellt...")
from .agent_doc_check_report import build_management_summary
summary_html = build_management_summary(results)
report_html = build_html_report(results, None)
profile_html = _build_profile_html(profile)
full_html = profile_html + report_html
full_html = summary_html + profile_html + report_html
# Step 6: Send email
# Step 6: Send email — include website/company name in subject
doc_count = len([r for r in results if not r.error])
site_name = (
extracted_profile.get("company_profile", {}).get("companyName")
or _extract_domain(doc_entries)
or "Unbekannt"
)
email_result = send_email(
recipient=req.recipient,
subject=f"[COMPLIANCE-CHECK] {doc_count} Dokumente geprueft",
subject=f"[COMPLIANCE-CHECK] {site_name}{doc_count} Dokumente geprueft",
body_html=full_html,
)
@@ -349,23 +380,55 @@ def _update(check_id: str, msg: str):
async def _fetch_text(url: str) -> str:
"""Fetch text from URL via consent-tester."""
"""Fetch text from URL via consent-tester, with HTTP fallback.
1. Try consent-tester (Playwright) handles JS-heavy SPAs
2. Fallback: direct HTTP fetch + HTML strip fast, works for SSR pages
"""
# 1. Consent-tester (Playwright-based, full JS rendering)
try:
async with httpx.AsyncClient(timeout=90.0) as client:
async with httpx.AsyncClient(timeout=60.0) as client:
resp = await client.post(
f"{CONSENT_TESTER_URL}/dsi-discovery",
json={"url": url, "max_documents": 1},
json={"url": url, "max_documents": 3},
timeout=60.0,
)
if resp.status_code != 200:
return ""
docs = resp.json().get("documents", [])
if not docs:
return ""
doc = docs[0]
return doc.get("full_text", "") or doc.get("text_preview", "") or ""
if resp.status_code == 200:
docs = resp.json().get("documents", [])
if docs:
texts = []
for doc in docs:
t = doc.get("full_text", "") or doc.get("text_preview", "") or ""
if t and len(t) > 50:
texts.append(t)
merged = "\n\n".join(texts)
if merged and len(merged.split()) > 100:
if len(texts) > 1:
logger.info("Merged %d docs from %s (%d words)",
len(texts), url, len(merged.split()))
return merged
except Exception as e:
logger.warning("Text fetch failed for %s: %s", url, e)
return ""
logger.warning("Consent-tester fetch failed for %s: %s", url, e)
# 2. Fallback: direct HTTP fetch (works for SSR pages like BMW)
try:
import re as _re
async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client:
resp = await client.get(url)
if resp.status_code == 200 and "text/html" in resp.headers.get("content-type", ""):
html = resp.text
# Strip HTML tags, decode entities
text = _re.sub(r"<script[^>]*>.*?</script>", " ", html, flags=_re.DOTALL | _re.IGNORECASE)
text = _re.sub(r"<style[^>]*>.*?</style>", " ", text, flags=_re.DOTALL | _re.IGNORECASE)
text = _re.sub(r"<[^>]+>", " ", text)
text = _re.sub(r"\s+", " ", text).strip()
if len(text.split()) > 100:
logger.info("HTTP fallback for %s: %d words", url, len(text.split()))
return text
except Exception as e:
logger.warning("HTTP fallback failed for %s: %s", url, e)
return ""
async def _check_single(
@@ -440,6 +503,17 @@ async def _check_single(
)
def _extract_domain(doc_entries: list[dict]) -> str | None:
"""Extract domain name from first URL for email subject."""
for entry in doc_entries:
url = entry.get("url", "")
if url and "://" in url:
from urllib.parse import urlparse
host = urlparse(url).netloc
return host.replace("www.", "") if host else None
return None
def _get_skip_types(profile) -> dict[str, str]:
"""Doc_types to skip entirely. Currently empty — we check everything
and flag irrelevant items as INFO instead of skipping."""
@@ -40,6 +40,121 @@ def _hint_box(hint: str) -> str:
)
def build_management_summary(results: list[DocCheckResult]) -> str:
"""Build a plain-language management summary for the CEO/GF.
No legal jargon concrete actions that can be delegated to staff,
lawyers, or the DPO.
"""
ok = [r for r in results if r.completeness_pct == 100 and not r.error]
fixable = [r for r in results if 0 < r.completeness_pct < 100 and not r.error]
critical = [r for r in results if r.completeness_pct == 0 and not r.error]
errors = [r for r in results if r.error]
html = [
'<div style="font-family:-apple-system,BlinkMacSystemFont,sans-serif;'
'max-width:700px;margin:0 auto 20px;padding:16px 20px;'
'background:#f8fafc;border:1px solid #e2e8f0;border-radius:12px">',
'<h2 style="margin:0 0 12px;font-size:18px;color:#1e293b">'
'Zusammenfassung fuer die Geschaeftsfuehrung</h2>',
]
# Overall status
total = len(results) - len(errors)
if total == 0:
html.append('<p>Keine Dokumente geprueft.</p></div>')
return "\n".join(html)
if len(ok) == total:
html.append(
'<p style="color:#16a34a;font-weight:600;font-size:15px">'
'Alle Dokumente sind vollstaendig. Keine dringenden Massnahmen noetig.</p>'
)
else:
html.append(
f'<p style="font-size:14px;color:#475569">'
f'{len(ok)} von {total} Dokumenten sind vollstaendig. '
f'{len(fixable)} brauchen Korrekturen'
f'{f", {len(critical)} fehlen oder sind unbrauchbar" if critical else ""}.</p>'
)
# Concrete actions
actions: list[str] = []
for r in results:
if r.error or r.completeness_pct == 100:
continue
failed_checks = [
c for c in r.checks
if c.level == 1 and not c.passed and not c.skipped
and c.severity != "INFO"
]
for c in failed_checks[:3]: # Max 3 per document
action = _check_to_action(r.label, c.label, c.hint)
if action:
actions.append(action)
if actions:
html.append(
'<h3 style="font-size:14px;color:#334155;margin:16px 0 8px">'
'Konkrete Aufgaben:</h3>'
'<ol style="font-size:13px;color:#475569;padding-left:20px;margin:0">'
)
for a in actions[:10]: # Max 10 actions
html.append(f'<li style="margin-bottom:6px">{a}</li>')
html.append('</ol>')
html.append('</div>')
return "\n".join(html)
def _check_to_action(doc_label: str, check_label: str, hint: str) -> str:
"""Convert a failed check into a plain-language action item."""
# Map technical check labels to business-language actions
label_lower = check_label.lower()
if "datenschutzbeauftragter" in label_lower or "dsb" in label_lower:
return (f"<strong>{doc_label}:</strong> Ihren Datenschutzbeauftragten "
f"mit Kontaktdaten erwaehnen. Pflicht ab 20 Mitarbeitern.")
if "beschwerderecht" in label_lower or "art. 77" in label_lower:
return (f"<strong>{doc_label}:</strong> Hinweis auf das Beschwerderecht "
f"bei der Aufsichtsbehoerde ergaenzen (Name + Kontakt der Behoerde).")
if "betroffenenrechte" in label_lower:
return (f"<strong>{doc_label}:</strong> Alle Betroffenenrechte "
f"(Auskunft, Berichtigung, Loeschung, etc.) einzeln auffuehren.")
if "verantwortlicher" in label_lower:
return (f"<strong>{doc_label}:</strong> Vollstaendige Firmenbezeichnung "
f"mit Rechtsform, Adresse, E-Mail und Telefon eintragen.")
if "interessenabwaegung" in label_lower:
return (f"<strong>{doc_label}:</strong> Bei 'berechtigtem Interesse' "
f"die Abwaegung dokumentieren. Aufgabe fuer den DSB/Rechtsanwalt.")
if "widerrufsbelehrung" in label_lower or "widerruf" in label_lower:
return (f"<strong>{doc_label}:</strong> Gesetzliche Widerrufsbelehrung "
f"mit 14-Tage-Frist und Musterformular bereitstellen.")
if "loeschkonzept" in label_lower:
return (f"<strong>{doc_label}:</strong> Loeschfristen und -prozess "
f"dokumentieren. Aufgabe fuer den DSB.")
if "profiling" in label_lower or "art. 22" in label_lower:
return (f"<strong>{doc_label}:</strong> Hinweis ergaenzen ob "
f"automatisierte Entscheidungen stattfinden oder nicht.")
if "nicht im eingereichten text" in label_lower:
return (f"<strong>{doc_label}:</strong> Das eingereichte Dokument "
f"enthaelt nicht den erwarteten Inhalt. Bitte korrekte URL pruefen.")
# Generic fallback
if hint and len(hint) < 150:
return f"<strong>{doc_label}:</strong> {hint[:120]}"
return f"<strong>{doc_label}:</strong> '{check_label}' muss ergaenzt werden."
def build_html_report(
results: list[DocCheckResult],
cookie_result: dict | None,
@@ -213,3 +213,197 @@ def auto_fill_from_dsi(doc_entries: list[dict]) -> None:
"Auto-filled %d empty rows from DSI sections: %s",
len(filled), ", ".join(filled),
)
# ── Cross-Document Search ────────────────────────────────────────────
# Keywords that indicate a doc_type is present in text (case-insensitive)
_DOC_TYPE_KEYWORDS = {
"widerruf": [
"widerrufsrecht", "widerrufsbelehrung", "widerrufsfrist",
"binnen 14 tagen", "widerruf erklaeren", "muster-widerrufsformular",
],
"cookie": [
"cookie-richtlinie", "cookie-tabelle", "cookiebot", "consent-tool",
"arten der cookies", "session-cookie", "tracking-cookie",
],
"social_media": [
"gemeinsam verantwortlich", "art. 26 dsgvo", "fanpage",
"social media plugin", "facebook-seite", "instagram-profil",
],
"impressum": [
"angaben gemaess", "angaben gemäß", "§ 5 tmg", "§5 tmg",
"telemediengesetz", "impressum",
],
"agb": [
"allgemeine geschaeftsbedingungen", "allgemeine geschäftsbedingungen",
"geltungsbereich", "vertragsschluss", "§305 bgb",
],
"dsb": [
"datenschutzbeauftragte", "dsb@", "dpo@",
"datenschutzbeauftragten",
],
}
def cross_search_documents(doc_entries: list[dict]) -> list[dict]:
"""Search ALL texts for ALL doc_types and fill missing entries.
For each empty doc_type row, search through all other documents'
texts to find the content. If found in the wrong document, extract
it, assign it, and create a finding about incorrect placement.
Returns list of findings (misplacement warnings).
"""
findings: list[dict] = []
# Collect all available texts with their source doc_type
all_texts: list[tuple[str, str, str]] = [] # (doc_type, url, text)
for entry in doc_entries:
if entry.get("text") and len(entry["text"]) > 100:
all_texts.append((entry["doc_type"], entry.get("url", ""), entry["text"]))
if not all_texts:
return findings
# For each entry, check if:
# a) It's empty → search other texts
# b) It has text but the text doesn't match the doc_type → search other texts
for entry in doc_entries:
target_type = entry["doc_type"]
keywords = _DOC_TYPE_KEYWORDS.get(target_type, [])
if not keywords:
continue
has_text = entry.get("text") and len(entry["text"].split()) > 50
text_matches = False
if has_text:
# Check if the current text actually contains this doc_type's content
entry_lower = entry["text"].lower()
match_score = sum(1 for kw in keywords if kw in entry_lower)
text_matches = match_score >= 2
if has_text and text_matches:
continue # Text present AND matches doc_type → skip
# Search all other texts for this doc_type's keywords
best_match: dict | None = None
best_score = 0
for source_type, source_url, source_text in all_texts:
if source_type == target_type:
continue
text_lower = source_text.lower()
score = sum(1 for kw in keywords if kw in text_lower)
if score >= 2 and score > best_score:
best_score = score
# Extract the relevant section
section = _extract_section_by_keywords(source_text, keywords)
if section and len(section.split()) >= 30:
best_match = {
"source_type": source_type,
"source_url": source_url,
"section_text": section,
"keyword_hits": score,
}
if best_match:
entry["text"] = best_match["section_text"]
entry["word_count"] = len(best_match["section_text"].split())
source_label = best_match["source_type"].upper()
entry["url"] = f"(gefunden in {source_label})"
findings.append({
"id": f"placement-{target_type}",
"label": f"{_type_label(target_type)} in falschem Dokument",
"passed": False,
"severity": "MEDIUM",
"level": 1,
"parent": None,
"skipped": False,
"matched_text": "",
"hint": (
f"Die {_type_label(target_type)} wurde nicht als eigenes "
f"Dokument gefunden, sondern in der/den {source_label} "
f"({best_match['source_url']}). Gemaess Art. 246a EGBGB / "
f"§312d BGB muss die {_type_label(target_type)} leicht "
f"auffindbar und klar erkennbar sein. Empfehlung: Als "
f"eigenen Link im Footer oder als separates Dokument "
f"bereitstellen."
),
"source": "cross_document_search",
"doc_type": target_type,
})
logger.info(
"Cross-doc: Found %s in %s (%d keywords, %d words)",
target_type, best_match["source_type"],
best_match["keyword_hits"],
entry["word_count"],
)
elif has_text and not text_matches:
# Text present but doesn't match — wrong text assigned
findings.append({
"id": f"wrong-text-{target_type}",
"label": f"{_type_label(target_type)} nicht im eingereichten Text",
"passed": False,
"severity": "HIGH",
"level": 1,
"parent": None,
"skipped": False,
"matched_text": "",
"hint": (
f"Der eingereichte Text enthaelt keine "
f"{_type_label(target_type)}. Moeglicherweise wurde "
f"die falsche URL eingegeben. Das System konnte die "
f"{_type_label(target_type)} auch in keinem anderen "
f"eingereichten Dokument finden."
),
"source": "cross_document_search",
"doc_type": target_type,
})
logger.info("Cross-doc: %s text doesn't match doc_type, not found elsewhere", target_type)
return findings
def _extract_section_by_keywords(
text: str, keywords: list[str],
) -> str | None:
"""Extract the section of text around the keyword matches."""
text_lower = text.lower()
lines = text.split("\n")
# Find first and last line containing any keyword
first_line = len(lines)
last_line = 0
for i, line in enumerate(lines):
line_lower = line.lower()
if any(kw in line_lower for kw in keywords):
first_line = min(first_line, i)
last_line = max(last_line, i)
if first_line >= last_line:
return None
# Expand to include context (5 lines before first, 10 after last)
start = max(0, first_line - 5)
end = min(len(lines), last_line + 10)
section = "\n".join(lines[start:end])
return section if len(section.split()) >= 30 else None
def _type_label(doc_type: str) -> str:
labels = {
"widerruf": "Widerrufsbelehrung",
"cookie": "Cookie-Richtlinie",
"social_media": "Social-Media-Datenschutz",
"impressum": "Impressum",
"agb": "AGB",
"dsb": "DSB-Kontakt",
"dse": "Datenschutzerklaerung",
}
return labels.get(doc_type, doc_type)
+33 -9
View File
@@ -532,19 +532,43 @@ async def _find_dsi_links(page: Page, base_domain: str) -> list[dict]:
return []
async def _expand_all_interactive(page: Page) -> None:
"""Expand all accordions, tabs, details, dropdowns on the page."""
"""Expand all accordions, tabs, details, dropdowns on the page.
IMPORTANT: Only expand CLOSED elements. Never click elements that
are already expanded (aria-expanded="true") that would close them.
BMW, for example, has accordions open by default.
"""
try:
await page.evaluate("""() => {
// 1. Open all <details> that are closed
document.querySelectorAll('details:not([open])').forEach(d => d.open = true);
const sels = ['button[aria-expanded="false"]','[data-toggle="collapse"]',
'[data-bs-toggle="collapse"]','[class*="accordion"] > button',
'[class*="collapse"] > button','.panel-heading a'];
sels.forEach(s => document.querySelectorAll(s).forEach(e => { try{e.click()}catch{} }));
document.querySelectorAll('button,a').forEach(b => {
if (/^(mehr|more|weiterlesen|read more|show more|anzeigen|alle anzeigen)/i.test((b.textContent||'').trim()))
try{b.click()}catch{}
// 2. Click buttons that are explicitly CLOSED (aria-expanded="false")
document.querySelectorAll('button[aria-expanded="false"]').forEach(b => {
try { b.click(); } catch {}
});
// 3. Bootstrap/jQuery collapse triggers (only closed ones)
document.querySelectorAll('[data-toggle="collapse"].collapsed').forEach(e => {
try { e.click(); } catch {}
});
document.querySelectorAll('[data-bs-toggle="collapse"].collapsed').forEach(e => {
try { e.click(); } catch {}
});
// 4. "Show more" / "Mehr anzeigen" buttons
document.querySelectorAll('button,a').forEach(b => {
const t = (b.textContent || '').trim();
if (/^(mehr|more|weiterlesen|read more|show more|anzeigen|alle anzeigen)/i.test(t))
try { b.click(); } catch {}
});
// 5. Tabs click each to make content visible, then go back
// (don't click, just make tab panels visible)
document.querySelectorAll('[role="tabpanel"][hidden]').forEach(p => {
p.removeAttribute('hidden');
p.style.display = '';
});
document.querySelectorAll('[role="tab"]').forEach(t => { try{t.click()}catch{} });
}""")
except Exception:
pass
+66 -70
View File
@@ -2,8 +2,8 @@
**URL:** https://www.bmw.de
**Typ:** Konzern / B2C Automobil
**Datum:** 2026-05-12
**Batch-Test:** 8/9 L1, 10/21 L2 (Mangelhaft, 48%)
**Datum:** 2026-05-15 (URLs + Inhalte verifiziert)
**Vorheriger Batch-Test:** 8/9 L1, 10/21 L2 (Mangelhaft, 48%) — VERALTET, URLs waren alle 404
---
@@ -20,83 +20,88 @@
---
## Dokumente
## Dokumente (URLs verifiziert 2026-05-15)
| Dokumenttyp | Vorhanden | URL |
|-------------|-----------|-----|
| DSI | Ja | https://www.bmw.de/de/footer/metanavigation/datenschutz.html |
| Impressum | Ja | https://www.bmw.de/de/footer/metanavigation/impressum.html |
| Cookie-Richtlinie | Ja (separate Seite) | https://www.bmw.de/de/footer/metanavigation/cookie-policy.html |
| AGB | Ja | TODO: URL verifizieren |
| Widerruf | Ggf. in AGB | — |
| Social Media DSE | Nein | — |
| Nutzungsbedingungen | Ja | TODO: URL verifizieren |
| DSB-Kontakt | In DSI | — |
**ACHTUNG: BMW verteilt Rechtstexte ueber 3 Domains!**
| Dokumenttyp | Domain | URL |
|-------------|--------|-----|
| DSI | bmw.de | https://www.bmw.de/de/footer/metanavigation/data-privacy.html |
| Impressum | bmw.de | https://www.bmw.de/de/footer/metanavigation/legal-notice-pool/imprint.html |
| Cookie-Richtlinie | bmw.de | https://www.bmw.de/de/footer/footer-section/cookie-policy.html |
| Legal Disclaimer / NB | bmw.de | https://www.bmw.de/de/footer/metanavigation/legal-disclaimer-pool/legal-disclaimer.html |
| Konzern-Datenschutz + Widerruf | **bmwgroup.com** | https://www.bmwgroup.com/de/general/data_privacy.html |
| Social Media Privacy | **bmwgroup.jobs** | https://www.bmwgroup.jobs/de/de/services/social-media-privacy-policy.html |
| DSB-Kontakt | bmw.de (in DSI) | datenschutz@bmw.de |
| AGB | **Nicht gefunden** | Kein oeffentlich verlinktes AGB-Dokument |
**Finding: Rechtstexte ueber 3 Domains verteilt (bmw.de, bmwgroup.com, bmwgroup.jobs). Fuer Betroffene schwer auffindbar. Social Media Policy nur ueber Karriere-Portal erreichbar.**
**DSI hat Sub-Pages:** data-category.html, privacy-subpage-weblink-a/c/d/e.html
---
## Verifizierte Inhalte: Impressum
| Feld | Wert |
|------|------|
| Firma | Bayerische Motoren Werke Aktiengesellschaft (BMW AG) |
| Anschrift | Petuelring 130, 80809 Muenchen |
| Vorstand | Milan Nedeljkovic (Vorsitzender), 6 weitere |
| Aufsichtsrat | Nicolas Peter (Vorsitzender) |
| Registergericht | AG Muenchen, HRB 42243 |
| USt-IdNr | DE129273398 |
| Kontakt | kundenbetreuung@bmw.de |
| Versicherungsvermittler | §34d Abs. 6 GewO, IHK Muenchen |
## Verifizierte Inhalte: DSB
| Feld | Wert |
|------|------|
| Titel | Data Protection Officer BMW AG |
| Anschrift | Petuelring 130, 80788 Muenchen |
| E-Mail | datenschutz@bmw.de |
## Verifizierte Inhalte: Social Media
Impressum gilt auch fuer:
- Facebook: facebook.com/BMWDeutschland
- Instagram: instagram.com/bmwdeutschland
- YouTube: youtube.com/user/BMWDeutschland
- Pinterest: de.pinterest.com/bmwdeutschland
---
## Erwartete Ergebnisse: DSI (Art. 13 DSGVO)
### L1 Checks (8/9)
### L1 Checks
| Check | Erwartet | Begruendung |
|-------|----------|-------------|
| Verantwortlicher | PASS | BMW AG, Muenchen |
| DSB | PASS | DSB erwaehnt |
| Zwecke | PASS | Ausfuehrlich |
| Verantwortlicher | PASS | BMW AG, Petuelring 130, 80809 Muenchen |
| DSB | PASS | datenschutz@bmw.de, Petuelring 130 |
| Zwecke | PASS | Ausfuehrlich (Sub-Pages) |
| Rechtsgrundlage | PASS | Art. 6 Referenzen |
| Empfaenger | PASS | Kategorien aufgezaehlt |
| Drittlandtransfer | PASS | USA-Transfer erwaehnt |
| Speicherdauer | PASS | Zeitangaben vorhanden |
| Betroffenenrechte | **FAIL** | Rechte ohne Art.-Referenzen |
| Beschwerderecht | **FAIL** | Art. 77 nicht explizit erwaehnt |
### L2 Checks (10/21 — verifizierte True Positives)
| Check | Erwartet | TP/FP |
|-------|----------|-------|
| Anschrift | PASS | — |
| E-Mail | **FAIL** | **TP** — Keine direkte E-Mail-Adresse fuer DSB angegeben |
| Telefon | PASS | — |
| DSB Kontakt | PASS | — |
| Art. 6(1)(a) | PASS | — |
| Art. 6(1)(b) | PASS | — |
| Art. 6(1)(f) | PASS | — |
| Interessenabwaegung | **FAIL** | **TP** — Keine dokumentierte Abwaegung |
| Transfermechanismus | **FAIL** | **TP** — Kein SCC/DPF benannt |
| Art. 15-18,20,21 | **FAIL** | **TP** — Rechte ohne Artikel-Referenzen aufgezaehlt |
| Art. 22 Profiling | **FAIL** | **TP** — Kein Profiling-Hinweis trotz Konfigurator/Personalisierung |
| Aufsichtsbehoerde | **FAIL** | **TP** — Keine konkrete Behoerde benannt |
| Loeschkonzept | **FAIL** | **TP** — Kein Loeschkonzept referenziert |
**Verifiziert: BMW hat tatsaechlich eine lueckenhafte DSI. Die Findings sind True Positives.**
| Betroffenenrechte | Zu pruefen | Art. 15-21 in DSI? |
| Beschwerderecht | Zu pruefen | Art. 77 in DSI? |
---
## Erwartete Ergebnisse: Impressum
| Check | Erwartet | Begruendung |
|-------|----------|-------------|
| Firmenname | PASS | BMW AG |
| Anschrift | PASS | Petuelring 130, 80809 Muenchen |
| Vertretung | PASS | Vorstand benannt |
| Registergericht | PASS | AG Muenchen, HRB 42243 |
| USt-IdNr | PASS | DE 129 273 987 |
| V.i.S.d.P. | PASS | Hat redaktionelle Inhalte |
| Streitbeilegung | AKTIV | B2C mit Online-Angebot → ODR relevant |
---
## Erwartete Ergebnisse: Cookie-Richtlinie
| Check | Erwartet |
|-------|----------|
| Cookie-Arten | PASS (Essential, Analytics, Marketing) |
| Cookie-Zwecke | PASS |
| Speicherdauern | TODO: verifizieren |
| Drittanbieter | PASS (Google, Meta etc.) |
| Rechtsgrundlage | TODO: §25 TDDDG? |
| Consent-Tool | PASS (OneTrust o.ae.) |
| Firmenname | PASS (BMW AG) |
| Anschrift | PASS (Petuelring 130, 80809 Muenchen) |
| Vertretung | PASS (Vorstand benannt) |
| USt-IdNr | PASS (DE129273398) |
| Registergericht | PASS (HRB 42243) |
| V.i.S.d.P. | PASS (Medienunternehmen mit Blog) |
| Streitbeilegung | AKTIV (B2C) |
| Versicherungsvermittler | PASS (§34d GewO, IHK Muenchen) |
---
@@ -106,16 +111,7 @@
|------|----------|
| banner_detected | true |
| provider | OneTrust oder aehnlich |
| violations | Mehrere (grosser Konzern mit viel Tracking) |
---
## Cross-Check Banner vs Cookie
| Finding | Erwartet |
|---------|----------|
| Dienste fehlen in Cookie-RL | Moeglich (viele Third-Party-Tracker) |
| Tracking vor Consent | Moeglich (Pre-Consent Analytics) |
| violations | Zu pruefen |
---
@@ -124,6 +120,6 @@
| Check | Filter | Begruendung |
|-------|--------|-------------|
| ODR | AKTIV | B2C mit Online-Angebot |
| Widerruf | AKTIV | B2C |
| Widerruf | In DSI | Marketing-Consent widerrufbar |
| Berufsrecht | SKIP | Kein regulierter Beruf |
| V.i.S.d.P. | AKTIV | Hat Magazine/Blog |
+161 -148
View File
@@ -2,172 +2,185 @@
**URL:** https://www.spiegel.de
**Typ:** Medien / Nachrichtenportal
**Datum:** 2026-05-13 (verifiziert gegen Live-Texte)
**Vorheriger Batch-Test:** 6/9 L1, 10/13 L2 — VERALTET, mehrere False Negatives
**Volltext:** [06-spiegel-dsi-fulltext.txt](06-spiegel-dsi-fulltext.txt) (13.705 Woerter, 107.720 Zeichen)
**Root Cause aller FN:** API-Limit `text[:50000]` schnitt bei 47% ab → DSB/Art.77/Rechte fehlten
**Datum:** 2026-05-14 (verifiziert gegen Live-Texte + System-Ergebnis)
**Volltext:** [06-spiegel-dsi-fulltext.txt](06-spiegel-dsi-fulltext.txt) (13.698 Woerter, 107.720 Zeichen)
---
## Business Profile (erwartet)
## Business Profile (erwartet vs tatsaechlich)
| Feld | Erwarteter Wert | Begruendung |
|------|----------------|-------------|
| business_type | b2c | Abo-Modell (Spiegel+) |
| industry | media | Nachrichtenportal |
| has_online_shop | true | Spiegel+ Abo-Shop |
| has_editorial_content | true | Kerngeschaeft |
| is_regulated_profession | **false** | Kein regulierter Beruf. "Anwalt" im Text ist Redaktionsanwalt, kein Kanzlei-Beruf |
| needs_odr | true | B2C mit Online-Abo |
**Bug:** Profiler erkennt "anwalt" im Impressum-Text und setzt is_regulated_profession=true. FALSE POSITIVE.
| Feld | Erwartet | System-Ergebnis | |
|------|---------|----------------|---|
| business_type | b2c | B2C | ✓ |
| industry | media | media | ✓ |
| has_online_shop | true | true | ✓ |
| has_editorial_content | true | true | ✓ |
| is_regulated_profession | false | false | ✓ (gefixt, war FP "anwalt") |
| needs_odr | true | true | ✓ |
| detected_services | 31 | 10 angezeigt (31 intern) | UI zeigt nur Top 10 |
---
## Dokumente
| Dokumenttyp | Vorhanden | URL | Anmerkung |
|-------------|-----------|-----|-----------|
| DSI | Ja | https://www.spiegel.de/datenschutz-spiegel | 6461 Woerter, 11 Abschnitte, sehr ausfuehrlich |
| Impressum | Ja | https://www.spiegel.de/impressum | 2 Gesellschaften (DER SPIEGEL GmbH + SPIEGEL-Verlag) |
| Cookie-Richtlinie | In DSI Abschnitt 4 | #funktionsfaehigkeitdesangebots | Sourcepoint CMP |
| AGB | Ja | https://www.spiegel.de/agb | Abo-Bedingungen |
| Nutzungsbedingungen | Ja | https://www.spiegel.de/nutzungsbedingungen | Separates Dokument |
| Widerruf | In AGB Abschnitt 10 | https://www.spiegel.de/agb | "Widerrufsrecht fuer Abonnements" |
| Social Media DSE | In DSI Abschnitt 8 | #einbinden-von-drittinhalten | Facebook, YouTube, X, Instagram, TikTok, etc. |
| DSB-Kontakt | In DSI | — | dsb@spiegelgruppe.de |
| Dokumenttyp | Vorhanden | URL | System-Ergebnis |
|-------------|-----------|-----|----------------|
| DSI | Ja | https://www.spiegel.de/datenschutz-spiegel | **9/9 L1 (100%)** |
| Impressum | Ja | https://www.spiegel.de/impressum | **9/13 L1 (86%)** |
| Social Media | In DSI (Abschnitt 8) | auto-filled | **10/10 L1 (100%)** |
| Cookie-RL | In DSI (Abschnitt 4) | auto-filled | 1/6 L1 (17%) |
| AGB | Ja | https://www.spiegel.de/agb | Nicht eingegeben |
| Nutzungsbedingungen | Ja | https://www.spiegel.de/nutzungsbedingungen | 5/12 L1 (42%) |
| Widerruf | In AGB §10 | Falsch zugewiesen (NB-Text) | 0/8 L1 (0%) |
| DSB-Kontakt | In DSI | auto-filled | **9/9 L1 (100%)** |
---
## Erwartete Ergebnisse: DSI (Art. 13 DSGVO)
## DSI (Art. 13 DSGVO) — 9/9 L1, 24/42 L2
### L1 Checks (ERWARTET: 9/9 PASS)
### L1 Checks (9/9 PASS)
| Check | Erwartet | Beleg | Unser Ergebnis | Bug? |
|-------|----------|-------|----------------|------|
| Verantwortlicher | PASS | "DER SPIEGEL GmbH & Co. KG, Ericusspitze 1, 20459 Hamburg" | PASS (3/3) | — |
| DSB | **PASS** | "z. Hd. der Datenschutzbeauftragten... dsb@spiegelgruppe.de" | **FAIL** | **FN — Regex matcht "Datenschutzbeauftragte" nicht ohne "r" am Ende oder erkennt Kontext nicht** |
| Zwecke | PASS | Adobe-Tracking, Vertragsbeziehungen, Drittinhalte etc. | PASS | — |
| Rechtsgrundlage | PASS | Art. 6(1)(a), (b), (f) explizit | PASS (3/4) | — |
| Empfaenger | PASS | Server-/Applikationsbetreiber, Auftragsverarbeiter | PASS (2/2) | |
| Drittlandtransfer | PASS | SCC erwaehnt | PASS (1/1) | |
| Speicherdauer | PASS | "30 Tage" Protokolldatei | PASS (1/2) | — |
| Betroffenenrechte | **PASS** | Art. 15, 16, 17, 18, 21 explizit. Art. 20 fehlt. | **FAIL** | **FN — Regex verlangt alle 6 Artikel, 5/6 genuegen nicht** |
| Beschwerderecht | **PASS** | "Art. 77 DSGVO... HmbBfDI... Ludwig-Ehrhard-Str. 22" | **FAIL** | **FN — Regex findet Art. 77 + HmbBfDI nicht** |
| Check | Erwartet | System | Beleg |
|-------|----------|--------|-------|
| Verantwortlicher | PASS | PASS (3/3) | Ericusspitze 1, 20459 Hamburg |
| DSB | PASS | PASS (1/1) | "z. Hd. der Datenschutzbeauftragten... dsb@spiegelgruppe.de" |
| Zwecke | PASS | PASS (1/1) | Adobe-Tracking, Vertragsbeziehungen etc. |
| Rechtsgrundlage | PASS | PASS (3/4) | Art. 6(1)(a), (b), (f) |
| Empfaenger | PASS | PASS (2/2) | AVV erwaehnt |
| Drittlandtransfer | PASS | PASS (1/1) | SCC erwaehnt |
| Speicherdauer | PASS | PASS (2/2) | "30 Tage", Loeschfristen |
| Betroffenenrechte | PASS | PASS (6/7) | Art. 15-18, 20, 21. Art. 22 fehlt (TP) |
| Beschwerderecht | PASS | PASS (1/1) | HmbBfDI, Art. 77 |
**3 False Negatives in L1!** DSB, Betroffenenrechte, Beschwerderecht sind alle vorhanden.
### L2 True Positives (korrekte Findings)
### L2 Checks (Stichproben)
| Check | Erwartet | Beleg | Unser Ergebnis | Bug? |
|-------|----------|-------|----------------|------|
| E-Mail | PASS | datenschutz@spiegelgruppe.de | PASS | — |
| Interessenabwaegung | FAIL (TP) | Interesse benannt, keine Abwaegung | FAIL | Korrekt |
| Art. 20 Portabilitaet | FAIL (TP) | Art. 20 fehlt im Rechte-Abschnitt | — | Korrekter Finding |
| Loeschkonzept | FAIL (TP) | Kein formales Loeschkonzept | FAIL | Korrekt |
---
## Erwartete Ergebnisse: Impressum (§5 TMG)
| Check | Erwartet | Beleg | Unser Ergebnis | Bug? |
|-------|----------|-------|----------------|------|
| Firmenname | PASS | DER SPIEGEL GmbH & Co. KG + SPIEGEL-Verlag | PASS | — |
| Anschrift | PASS | Ericusspitze 1, 20457 Hamburg | PASS | — |
| Kontakt | PASS | Tel. 040 3007-0, spiegel@spiegel.de | PASS | — |
| Register | PASS | HRA 123 261 + HRA 61 755 | PASS | — |
| USt-IdNr | **PASS** | DE 212 442 423 + DE 118 922 410 | **FAIL** | **FN — Regex findet "Umsatzsteuer-ID:" Format nicht** |
| Vertretung | PASS | Thomas Hass (Geschaeftsfuehrung) | PASS (1/1) | — |
| V.i.S.d.P. | **PASS** | "Verantwortlicher i. S. v. § 18 Abs. 2 MStV: Dirk Kurbjuweit" | **FAIL** | **FN — Regex sucht "v.i.s.d.p." nicht "verantwortlicher i.s.v."** |
| Streitbeilegung | PASS | ODR-Link vorhanden (in AGB) | PASS | — |
| Berufsrecht | **SKIP** | Spiegel ist kein regulierter Beruf | **AKTIV (1/3)** | **FP — Profiler "anwalt" Bug** |
---
## Erwartete Ergebnisse: AGB
| Check | Erwartet | Beleg |
|-------|----------|-------|
| Geltungsbereich | PASS | Abschnitt 1 |
| Vertragsschluss | PASS | Abschnitt 2 |
| Preise/Zahlung | PASS | Abschnitte 4-7 |
| Kuendigung | PASS | Abschnitt 8 (1 Monat Frist) |
| Widerrufsrecht | PASS | Abschnitt 10 (14 Tage, Muster-Formular) |
| §312k Button | Zu pruefen | Kuendigungsbutton Pflicht seit 01.07.2022 |
| ODR-Link | PASS | http://ec.europa.eu/consumers/odr/ |
---
## Erwartete Ergebnisse: Widerrufsbelehrung (AGB §10)
| Check | Erwartet | Beleg |
|-------|----------|-------|
| Belehrung | PASS | "Sie haben das Recht, Abonnementvertraege binnen 14 Tagen ohne Angabe von Gruenden zu widerrufen" |
| 14-Tage-Frist | PASS | Explizit genannt |
| Form | PASS | Brief, E-Mail, Fax |
| Muster-Formular | PASS | "beigefuegte Muster-Widerrufsformular" erwaehnt |
| Folgen | PASS | Rueckerstattungsregeln beschrieben |
| Empfaenger | PASS | DER SPIEGEL Abonnentenservice, 20637 Hamburg; aboservice@spiegel.de |
| Ausnahme digitale Inhalte | PASS | "Fuer sofort nutzbare Zeitzugaenge... kein Widerrufsrecht" |
**Problem:** Unser Check prueft den DSI-Volltext gegen Widerruf-Checklist statt die AGB. Der Widerruf steht in den AGB (§10), nicht in der DSI.
---
## Erwartete Ergebnisse: Social Media (DSI Abschnitt 8)
| Check | Erwartet | Beleg |
|-------|----------|-------|
| Gemeinsam Verantwortliche | PASS | Erwaehnt |
| Meta konkret benannt | FAIL (TP) | Nur "Facebook" ohne "Meta Platforms Ireland Ltd." |
| Vereinbarung Art. 26 | FAIL (TP) | Kein Page Controller Addendum |
| Plattformen | PASS | Facebook, YouTube, X, Instagram, TikTok, Vimeo, Reddit, Bluesky, etc. |
| SCC | PASS | Erwaehnt |
| DPF | FAIL (TP) | Data Privacy Framework nicht erwaehnt |
| Rechtsgrundlage | PASS | Art. 6(1)(f) |
| Alle standardmaessig deaktiviert | PASS | "standardmaessig deaktiviert" |
---
## Banner-Check
| Feld | Erwartet |
|------|----------|
| banner_detected | true |
| provider | Sourcepoint |
| tcf_enabled | true |
| Vendor-Anzahl | 40+ (grosses Medienunternehmen) |
| violations | Consent-Wall blockiert Zugang → moeglicherweise unzulaessig |
---
## Cross-Check Banner vs DSI
| Finding | Erwartet |
|---------|----------|
| Vendors fehlen in DSI | Wahrscheinlich — viele TCF-Vendors nicht in DSI dokumentiert |
| Tracking vor Consent | Unwahrscheinlich (Sourcepoint blockiert gut) |
---
## Kontext-Filter
| Check | Filter | Begruendung |
| Check | Status | Begruendung |
|-------|--------|-------------|
| ODR | AKTIV | B2C Online-Abo |
| Widerruf | AKTIV | B2C |
| V.i.S.d.P. | AKTIV | Medienunternehmen (Kernpflicht) |
| Berufsrecht | **SKIP** | Kein regulierter Beruf |
| Interessenabwaegung | FAIL (TP) | Interesse benannt, keine Abwaegung dokumentiert |
| Art. 22 Profiling | FAIL (TP) | Nicht erwaehnt trotz personalisierter Werbung |
---
## Identifizierte Regex-Bugs (aus diesem GT-Abgleich)
## Impressum — 9/13 L1, 9/31 L2
| # | Check | Bug | Beleg auf Website | Regex-Problem |
|---|-------|-----|-------------------|---------------|
| 1 | DSB | FN | "z. Hd. der Datenschutzbeauftragten... dsb@spiegelgruppe.de" | Regex matcht "Datenschutzbeauftragten" (Genitiv/Dativ) nicht |
| 2 | Beschwerderecht | FN | "Art. 77 DSGVO... HmbBfDI" | Regex findet "Art. 77" oder "Aufsichtsbehoerde" nicht im Spiegel-Text |
| 3 | Betroffenenrechte | FN | Art. 15, 16, 17, 18, 21 — nur Art. 20 fehlt | Regex verlangt ALLE 6, 5/6 ist nicht genug |
| 4 | V.i.S.d.P. | FN | "Verantwortlicher i. S. v. § 18 Abs. 2 MStV" | Regex sucht nur "v.i.s.d.p.", nicht die MStV-Formulierung |
| 5 | USt-IdNr | FN | "Umsatzsteuer-ID: DE 212 442 423" | Regex sucht "ust-idnr" oder "ust-id", matcht "umsatzsteuer-id:" nicht |
| 6 | Profiler "anwalt" | FP | Redaktionsanwalt im Impressum | "anwalt" zu generisch, matcht Personennamen/Rollen |
| Check | Erwartet | System | |
|-------|----------|--------|---|
| Firmenname | PASS | PASS | ✓ |
| Anschrift | PASS | PASS (2/2) | ✓ |
| Kontakt | PASS | PASS (2/2) | ✓ |
| Register | PASS | PASS (2/2) | ✓ |
| USt-IdNr | PASS | PASS (1/1) | ✓ Gefixt ("Umsatzsteuer-ID:" + DE mit Leerzeichen) |
| Vertretung | PASS | PASS (1/1) | ✓ |
| V.i.S.d.P. | PASS | PASS | ✓ Gefixt ("Verantwortlicher i.S.v. §18 MStV") |
| Streitbeilegung | PASS | PASS | ✓ |
| Berufsrecht | SKIP | PASS (1/3) | FP — "Berufsrechtliche Regelungen" matcht falsch |
---
## Social Media — 10/10 L1, 12/30 L2
| Check | Erwartet | System | |
|-------|----------|--------|---|
| Gemeinsam Verantwortliche | PASS | PASS | ✓ |
| Meta benannt | PASS | PASS | ✓ "Meta Platforms Inc" erkannt |
| Vereinbarung Art. 26 | PASS | PASS (1/2) | ✓ Seiteninsights erwaehnt |
| Anlaufstelle | PASS | PASS (1/1) | ✓ |
| Plattformen | PASS | PASS (1/1) | ✓ |
| Drittlandtransfer | PASS | PASS (2/2) | ✓ SCC + DPF |
| Rechtsgrundlage | PASS | PASS (1/1) | ✓ |
| Betroffenenrechte | PASS | PASS (1/1) | ✓ Opt-Out erwaehnt |
| Social Bookmarks | PASS | PASS | ✓ |
### L2 True Positives
| Check | Status | Begruendung |
|-------|--------|-------------|
| Page Controller Addendum | FAIL (TP) | Nicht verlinkt |
| 2-Klick-Loesung | FAIL (TP) | Nicht dokumentiert |
---
## Cookie-Richtlinie — 1/6 L1
Cookie-Infos stehen bei Spiegel im **Sourcepoint-Banner** und in DSI Abschnitt 4, nicht als eigenes Dokument. Section-Splitter hat einen kurzen Cookie-Abschnitt extrahiert, aber die meisten Checks scheitern weil die Details im Banner stehen (nicht im Text).
---
## Nutzungsbedingungen — 5/12 L1
Aus spiegel.de/nutzungsbedingungen extrahiert (1679 Woerter). Echte Luecken bei Einbeziehungsklausel, ODR-Link, Kuendigung, Zahlungsarten.
---
## Widerrufsbelehrung — 0/8 L1
**Problem:** System prueft Nutzungsbedingungen-Text (1679w) statt AGB-Text.
**Tatsaechlich:** Widerrufsbelehrung steht in AGB §10 (spiegel.de/agb):
- 14-Tage-Frist ✓
- Muster-Widerrufsformular ✓
- Empfaenger (DER SPIEGEL Abonnentenservice) ✓
- Ausnahme digitale Inhalte ✓
**Offener Punkt:** Cross-Document Intelligence — System muss erkennen dass der Text keine Widerrufsbelehrung ist und den AGB-Link vorschlagen.
---
## Erkannte Dienste (31/32 = 97%)
| Dienst | Kategorie | Land | EU | In DSI erwaehnt |
|--------|----------|------|----|----------------|
| Adobe | tracking | US | Nein | Ja |
| Bluesky | social | US | Nein | Ja |
| Facebook | social | US | Nein | Ja |
| Giphy | content | US | Nein | Ja |
| Google Ads | marketing | US | Nein | Ja |
| Google reCAPTCHA | security | US | Nein | Ja |
| ID5 | identity | GB | Ja | Ja |
| IQD | marketing | DE | Ja | Ja |
| Imgur | content | US | Nein | Ja |
| Instagram | social | US | Nein | Ja |
| JW Player | video | US | Nein | Ja |
| LinkedIn | marketing | US | Nein | Ja |
| Mapbox | maps | US | Nein | Ja |
| Meta Platforms | social | US | Nein | Ja |
| Microsoft | cloud | US | Nein | Ja |
| Omnystudio | audio | CA | Nein | Ja |
| PayPal | payment | US | Nein | Ja |
| Qualtrics | survey | US | Nein | Ja |
| Reddit | social | US | Nein | Ja |
| Salesforce | crm | US | Nein | Ja |
| Segment | tag_manager | US | Nein | Ja |
| Sourcepoint | cmp | US | Nein | Ja |
| Spotify | audio | SE | Ja | Ja |
| Storifyme | content | DE | Ja | Ja |
| TikTok | social | IE | Ja | Ja |
| Utiq | tracking | BE | Ja | Ja |
| Vimeo | video | US | Nein | Ja |
| X/Twitter | social | US | Nein | Ja |
| YouTube | video | US | Nein | Ja |
| Zendesk | chatbot | US | Nein | Ja |
**25 Non-EU Dienste, 6 EU-Dienste.** Alle in DSI erwaehnt (Spiegel dokumentiert seine Dienste gut).
---
## Fixes die in dieser Session angewendet wurden
| # | Bug | Fix | Auswirkung |
|---|-----|-----|-----------|
| 1 | Text-Limit 50k Zeichen | → 200k | DSI: 6461→13698 Woerter, 5 FN weg |
| 2 | USt-IdNr "Umsatzsteuer-ID:" | Regex erweitert | Impressum: +1 PASS |
| 3 | V.i.S.d.P. "i.S.v. §18 MStV" | Regex + Pattern | Impressum: +1 PASS |
| 4 | "anwalt" FP im Profiler | Nur Impressum[:500], nur "rechtsanwalt" | Profiler: FP weg |
| 5 | Service-Erkennung 20→118 | service_detector.py | 5→31 Dienste erkannt |
| 6 | Section-Splitter auto-fill | auto_fill_from_dsi() | Cookie+Social Media auto-gefuellt |
---
## Offene Punkte
1. **Widerruf falsch zugewiesen** — System braucht Cross-Document Intelligence (AGB-Link finden)
2. **Cookie-RL 1/6** — Cookie-Infos stehen im Banner, nicht im Text → TCF-Vendor-Extraktion wuerde helfen
3. **Dienste UI zeigt nur 10** — 31 erkannt aber Frontend kuerzt
4. **Berufsrecht FP** — "Berufsrechtliche Regelungen + Zugang" matcht falsch im Spiegel-Impressum
5. **Banner-Check nicht sichtbar** — Sourcepoint-Buttons nicht klickbar im Scanner
+236
View File
@@ -0,0 +1,236 @@
#!/usr/bin/env python3
"""
Batch Ground Truth Test run compliance check on all 10 GT websites.
Usage:
python3 batch_gt_test.py [--backend-url URL]
Calls the compliance-check API for each website's DSI + Impressum URLs,
polls for results, and prints a comparison table.
"""
import argparse
import json
import sys
import time
import httpx
# 10 GT websites with their known document URLs
GT_WEBSITES = [
{
"name": "SafetyKon",
"documents": [
{"doc_type": "dse", "url": "https://safetykon.de/datenschutz"},
{"doc_type": "impressum", "url": "https://safetykon.de/impressum"},
],
},
{
"name": "IHK Konstanz",
"documents": [
{"doc_type": "dse", "url": "https://www.ihk.de/konstanz/servicemarken/ueber-uns/downloads/datenschutzinformationen-zum-internetangebot-4163288"},
{"doc_type": "impressum", "url": "https://www.ihk.de/konstanz/impressum"},
],
},
{
"name": "Stadt Koeln",
"documents": [
{"doc_type": "dse", "url": "https://www.stadt-koeln.de/datenschutz"},
{"doc_type": "impressum", "url": "https://www.stadt-koeln.de/impressum"},
],
},
{
"name": "BMW",
"documents": [
{"doc_type": "dse", "url": "https://www.bmw.de/de/footer/metanavigation/data-privacy.html"},
{"doc_type": "impressum", "url": "https://www.bmw.de/de/footer/metanavigation/legal-notice-pool/imprint.html"},
{"doc_type": "cookie", "url": "https://www.bmw.de/de/footer/footer-section/cookie-policy.html"},
],
},
{
"name": "Sparkasse Bodensee",
"documents": [
{"doc_type": "dse", "url": "https://www.sparkasse-bodensee.de/de/home/toolbar/datenschutz.html"},
{"doc_type": "impressum", "url": "https://www.sparkasse-bodensee.de/de/home/toolbar/impressum.html"},
],
},
{
"name": "Spiegel",
"documents": [
{"doc_type": "dse", "url": "https://www.spiegel.de/datenschutz-spiegel"},
{"doc_type": "impressum", "url": "https://www.spiegel.de/impressum"},
{"doc_type": "nutzungsbedingungen", "url": "https://www.spiegel.de/nutzungsbedingungen"},
],
},
{
"name": "TUEV Sued",
"documents": [
{"doc_type": "dse", "url": "https://www.tuvsud.com/de-de/datenschutzerklaerung"},
{"doc_type": "impressum", "url": "https://www.tuvsud.com/de-de/impressum"},
],
},
{
"name": "ETO Gruppe",
"documents": [
{"doc_type": "dse", "url": "https://www.etogruppe.com/datenschutz.html"},
{"doc_type": "impressum", "url": "https://www.etogruppe.com/impressum.html"},
],
},
{
"name": "Caritas",
"documents": [
{"doc_type": "dse", "url": "https://www.caritas.de/datenschutz"},
{"doc_type": "impressum", "url": "https://www.caritas.de/impressum"},
],
},
{
"name": "BfDI",
"documents": [
{"doc_type": "dse", "url": "https://www.bfdi.bund.de/DE/Meta/Datenschutz/datenschutz_node.html"},
{"doc_type": "impressum", "url": "https://www.bfdi.bund.de/DE/Meta/Impressum/impressum_node.html"},
],
},
]
def run_check(backend_url: str, website: dict) -> dict:
"""Submit compliance check and poll for results."""
with httpx.Client(timeout=30.0, verify=False) as client:
# Start check
resp = client.post(
f"{backend_url}/api/compliance/agent/compliance-check",
json={
"documents": website["documents"],
"use_agent": False,
},
)
if resp.status_code != 200:
return {"error": f"Start failed: {resp.status_code}"}
check_id = resp.json().get("check_id")
if not check_id:
return {"error": "No check_id"}
# Poll (max 15 min)
for _ in range(300):
time.sleep(3)
poll = client.get(
f"{backend_url}/api/compliance/agent/compliance-check/{check_id}"
)
if poll.status_code != 200:
continue
data = poll.json()
if data.get("status") == "completed":
return data.get("result", {})
if data.get("status") == "failed":
return {"error": data.get("error", "Check failed")}
return {"error": "Timeout (15 min)"}
def print_results(all_results: list[tuple[str, dict]]):
"""Print comparison table."""
print()
print("=" * 100)
print(f"{'Website':20s} {'Profil':12s} {'DSI L1':10s} {'DSI W':7s} "
f"{'Imp L1':10s} {'Dienste':8s} {'Docs':5s} {'Status':12s}")
print("-" * 100)
for name, result in all_results:
if "error" in result:
print(f"{name:20s} {'ERROR':12s} {result['error'][:60]}")
continue
profile = result.get("business_profile", {})
btype = profile.get("business_type", "?").upper()
industry = profile.get("industry", "?")
services = len(profile.get("detected_services", []))
docs = result.get("results", [])
dsi = next((d for d in docs if d.get("doc_type") == "dse"), {})
imp = next((d for d in docs if d.get("doc_type") == "impressum"), {})
dsi_l1 = f"{dsi.get('completeness_pct', 0)}%"
dsi_w = str(dsi.get("word_count", 0))
imp_l1 = f"{imp.get('completeness_pct', 0)}%"
ok_count = sum(1 for d in docs if d.get("completeness_pct", 0) == 100)
total = len(docs)
print(f"{name:20s} {btype+'/'+industry:12s} {dsi_l1:10s} {dsi_w:7s} "
f"{imp_l1:10s} {services:8d} {ok_count}/{total:<3d} "
f"{'OK' if dsi.get('completeness_pct', 0) == 100 else 'LUECKEN'}")
print("=" * 100)
# Detail: all doc results
print()
for name, result in all_results:
if "error" in result:
continue
docs = result.get("results", [])
print(f"--- {name} ---")
for d in docs:
pct = d.get("completeness_pct", 0)
cpct = d.get("correctness_pct", 0)
dtype = d.get("doc_type", "?")
label = d.get("label", dtype)
wc = d.get("word_count", 0)
scenario = d.get("scenario", "")
checks = d.get("checks", [])
l1_total = len([c for c in checks if c.get("level", 1) == 1])
l1_pass = len([c for c in checks if c.get("level", 1) == 1 and c.get("passed")])
failed = [c["label"] for c in checks if c.get("level", 1) == 1 and not c.get("passed") and not c.get("skipped") and c.get("severity") != "INFO"]
print(f" {label:30s} {l1_pass}/{l1_total} L1 ({pct}%) {wc}w {scenario}")
if failed:
for f in failed[:5]:
print(f"{f[:70]}")
print()
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--backend-url", default="https://localhost:8002",
help="Backend compliance URL")
parser.add_argument("--sites", default="all",
help="Comma-separated site indices (1-10) or 'all'")
args = parser.parse_args()
sites = GT_WEBSITES
if args.sites != "all":
indices = [int(i) - 1 for i in args.sites.split(",")]
sites = [GT_WEBSITES[i] for i in indices if 0 <= i < len(GT_WEBSITES)]
print(f"Running compliance check on {len(sites)} websites...")
print(f"Backend: {args.backend_url}")
print()
all_results = []
for i, website in enumerate(sites):
name = website["name"]
print(f"[{i+1}/{len(sites)}] {name}...", end=" ", flush=True)
t0 = time.time()
result = run_check(args.backend_url, website)
elapsed = time.time() - t0
if "error" in result:
print(f"ERROR ({elapsed:.0f}s): {result['error'][:60]}")
else:
docs = result.get("results", [])
ok = sum(1 for d in docs if d.get("completeness_pct", 0) == 100)
print(f"OK ({elapsed:.0f}s) — {len(docs)} docs, {ok} vollstaendig")
all_results.append((name, result))
print_results(all_results)
# Save raw results
out_file = f"batch_results_{time.strftime('%Y%m%d_%H%M%S')}.json"
with open(out_file, "w") as f:
json.dump(
{name: result for name, result in all_results},
f, indent=2, default=str,
)
print(f"\nRaw results saved to {out_file}")
if __name__ == "__main__":
main()