Compare commits

..

41 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
30 changed files with 1800 additions and 315 deletions
@@ -202,9 +202,9 @@ export function ComplianceCheckTab() {
setActiveCheckId(check_id) setActiveCheckId(check_id)
localStorage.setItem(STORAGE_KEY_CHECK_ID, 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 let attempts = 0
while (attempts < 300) { while (attempts < 500) {
await new Promise(r => setTimeout(r, 3000)) await new Promise(r => setTimeout(r, 3000))
const pollRes = await fetch(`/api/sdk/v1/agent/compliance-check?check_id=${check_id}`) const pollRes = await fetch(`/api/sdk/v1/agent/compliance-check?check_id=${check_id}`)
if (!pollRes.ok) { attempts++; continue } if (!pollRes.ok) { attempts++; continue }
@@ -235,7 +235,7 @@ export function ComplianceCheckTab() {
} }
attempts++ attempts++
} }
if (attempts >= 300) { if (attempts >= 500) {
localStorage.removeItem(STORAGE_KEY_CHECK_ID); setActiveCheckId('') localStorage.removeItem(STORAGE_KEY_CHECK_ID); setActiveCheckId('')
throw new Error('Zeitlimit ueberschritten (15 Min)') throw new Error('Zeitlimit ueberschritten (15 Min)')
} }
@@ -14,14 +14,21 @@ type TabType = 'matched' | 'missing' | 'extra'
export function HazardComparisonTable({ matched, missing, extra }: Props) { export function HazardComparisonTable({ matched, missing, extra }: Props) {
const [tab, setTab] = useState<TabType>('matched') const [tab, setTab] = useState<TabType>('matched')
// Compute quality levels for matched pairs // Split matches: >= 50% are real matches, < 50% are weak (shown separately)
const greenCount = matched.filter(p => p.match_score >= 0.7).length const realMatched = matched.filter(p => p.match_score >= 0.5)
const yellowCount = matched.filter(p => p.match_score >= 0.4 && p.match_score < 0.7).length 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 }[] = [ 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: 'matched', label: `Zugeordnet (${greenCount} exakt, ${yellowCount} aehnlich)`, count: realMatched.length, color: 'text-green-600' },
{ id: 'missing', label: 'Fehlend', count: missing.length, color: 'text-red-600' }, { id: 'missing', label: 'Fehlend', count: allMissing.length, color: 'text-red-600' },
{ id: 'extra', label: 'Zusaetzlich', count: extra.length, color: 'text-gray-500' }, { id: 'extra', label: 'Engine Findings', count: allExtra.length, color: 'text-blue-500' },
] ]
return ( return (
@@ -44,9 +51,9 @@ export function HazardComparisonTable({ matched, missing, extra }: Props) {
</div> </div>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
{tab === 'matched' && <MatchedTable pairs={matched} />} {tab === 'matched' && <MatchedTable pairs={realMatched} />}
{tab === 'missing' && <MissingTable entries={missing} />} {tab === 'missing' && <MissingTable entries={allMissing} />}
{tab === 'extra' && <ExtraTable entries={extra} />} {tab === 'extra' && <ExtraTable entries={allExtra} />}
</div> </div>
</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 */ /** Side-by-side detail comparison of GT entry vs. Engine hazard */
function DetailComparison({ gt, engine }: { gt: GroundTruthEntry; engine: HazardSummary }) { function DetailComparison({ gt, engine }: { gt: GroundTruthEntry; engine: HazardSummary }) {
return ( return (
@@ -143,8 +165,14 @@ function DetailComparison({ gt, engine }: { gt: GroundTruthEntry; engine: Hazard
<DetailRow label="Gefaehrdung" gt={engine.name} /> <DetailRow label="Gefaehrdung" gt={engine.name} />
<DetailRow label="Szenario" gt={engine.scenario || engine.description || '-'} /> <DetailRow label="Szenario" gt={engine.scenario || engine.description || '-'} />
<DetailRow label="Gefahrenstelle" gt={engine.zone || '-'} /> <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="Moeglicher Schaden" gt={engine.possible_harm || '-'} />
<DetailRow label="Trigger" gt={engine.trigger_event || '-'} /> <DetailRow label="Trigger" gt={engine.trigger_event || '-'} />
{engine.affected_person && (
<DetailRow label="Betroffene Personen" gt={engine.affected_person} />
)}
{engine.mitigations && engine.mitigations.length > 0 ? ( {engine.mitigations && engine.mitigations.length > 0 ? (
<DetailRow label="Massnahmen" gt={engine.mitigations.join('\n')} multiline /> <DetailRow label="Massnahmen" gt={engine.mitigations.join('\n')} multiline />
) : ( ) : (
@@ -33,6 +33,7 @@ export interface HazardSummary {
component?: string; zone?: string; risk_level?: string component?: string; zone?: string; risk_level?: string
description?: string; scenario?: string description?: string; scenario?: string
possible_harm?: string; trigger_event?: string possible_harm?: string; trigger_event?: string
affected_person?: string; lifecycle_phase?: string
mitigations?: string[] mitigations?: string[]
} }
@@ -12,7 +12,9 @@ export default function BenchmarkPage() {
const { result, gtLoaded, gtEntryCount, loading, error, importGT, runBenchmark } = useBenchmark(projectId) const { result, gtLoaded, gtEntryCount, loading, error, importGT, runBenchmark } = useBenchmark(projectId)
const [gtProjectId, setGtProjectId] = useState('') 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 const measurePct = result ? Math.round(result.measure_coverage * 100) : 0
return ( return (
@@ -74,7 +76,7 @@ export default function BenchmarkPage() {
<ScoreCard <ScoreCard
label="Hazard Coverage" label="Hazard Coverage"
value={`${coveragePct}%`} 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'} color={coveragePct >= 80 ? 'green' : coveragePct >= 50 ? 'yellow' : 'red'}
/> />
<ScoreCard <ScoreCard
@@ -3,6 +3,7 @@ package handlers
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"strings"
"github.com/breakpilot/ai-compliance-sdk/internal/iace" "github.com/breakpilot/ai-compliance-sdk/internal/iace"
"github.com/gin-gonic/gin" "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) ── // ── Step 5: Create hazards from matched patterns (skip if exist) ──
existingHazards, _ := h.store.ListHazards(ctx, projectID) existingHazards, _ := h.store.ListHazards(ctx, projectID)
hazardStep := InitStep{Name: "Gefaehrdungen erstellt", Status: "skipped"} 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 { if len(existingHazards) == 0 && len(matchOutput.MatchedPatterns) > 0 {
comps, _ := h.store.ListComponents(ctx, projectID) comps, _ := h.store.ListComponents(ctx, projectID)
@@ -158,32 +160,35 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
} }
created := 0 created := 0
seenCatZone := make(map[string]bool) seenCatZone := make(map[string]uuid.UUID) // dedupKey → hazardID
catCount := make(map[string]int) catCount := make(map[string]int)
for _, mp := range matchOutput.MatchedPatterns { for _, mp := range matchOutput.MatchedPatterns {
// Narrative relevance filter: skip patterns whose zone/scenario // Narrative relevance filter
// mentions machine-specific terms that don't appear in our components
if !isPatternRelevant(mp, narrativeText, compNames) { if !isPatternRelevant(mp, narrativeText, compNames) {
continue continue
} }
for _, cat := range mp.HazardCats { for _, cat := range mp.HazardCats {
// Per-category cap: limit hazards per category based on relevance
maxForCat := categoryHazardCap(cat, len(comps)) maxForCat := categoryHazardCap(cat, len(comps))
if catCount[cat] >= maxForCat { if catCount[cat] >= maxForCat {
continue continue
} }
// Dedup by category + normalized zone
zoneKey := normalizeZoneKey(mp.ZoneDE) zoneKey := normalizeZoneKey(mp.ZoneDE)
if zoneKey == "" { if zoneKey == "" {
zoneKey = mp.PatternID zoneKey = mp.PatternID
} }
dedupKey := cat + ":" + zoneKey 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 continue
} }
seenCatZone[dedupKey] = true
name := mp.PatternName name := mp.PatternName
if name == "" { if name == "" {
@@ -204,11 +209,8 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
} }
} }
// Join applicable lifecycles for the LifecyclePhase field // Join all applicable lifecycles as comma-separated string
lifecycleStr := "" lifecycleStr := strings.Join(mp.ApplicableLifecycles, ",")
if len(mp.ApplicableLifecycles) > 0 {
lifecycleStr = mp.ApplicableLifecycles[0]
}
hz, cerr := h.store.CreateHazard(ctx, iace.CreateHazardRequest{ hz, cerr := h.store.CreateHazard(ctx, iace.CreateHazardRequest{
ProjectID: projectID, ProjectID: projectID,
@@ -227,7 +229,11 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
if cerr == nil { if cerr == nil {
created++ created++
catCount[cat]++ 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
}
} }
} }
} }
@@ -236,7 +242,7 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
hazardStep.Details = "Bereits vorhanden" hazardStep.Details = "Bereits vorhanden"
hazardStep.Count = len(existingHazards) hazardStep.Count = len(existingHazards)
for _, eh := range existingHazards { for _, eh := range existingHazards {
hazardIDsByCategory[eh.Category] = eh.ID hazardIDsByCategory[eh.Category] = append(hazardIDsByCategory[eh.Category], eh.ID)
} }
} }
steps = append(steps, hazardStep) steps = append(steps, hazardStep)
@@ -255,37 +261,60 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
} }
created := 0 created := 0
usedMeasureIDs := make(map[string]bool) const maxMitigationsPerHazard = 5
for _, sm := range matchOutput.SuggestedMeasures { // Build a flat list of all hazard IDs for iteration
entry, ok := measureByID[sm.MeasureID] var allHazardIDs []uuid.UUID
if !ok || usedMeasureIDs[sm.MeasureID] { hazardCatByID := make(map[uuid.UUID]string)
continue for cat, ids := range hazardIDsByCategory {
} for _, id := range ids {
hazardID := findHazardForMeasureByCategory(entry.HazardCategory, hazardIDsByCategory) allHazardIDs = append(allHazardIDs, id)
if hazardID == uuid.Nil { hazardCatByID[id] = cat
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
} }
} }
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) measCat := patternCatToMeasureCat(hazCat)
added := 0 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] { for _, m := range measuresByCat[measCat] {
if usedMeasureIDs[m.ID] || added >= 8 { if added >= maxMitigationsPerHazard || usedIDs[m.ID] {
break continue
} }
rt := iace.ReductionType(m.ReductionType) rt := iace.ReductionType(m.ReductionType)
if rt == "" { if rt == "" {
@@ -297,12 +326,16 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
}) })
if cerr == nil { if cerr == nil {
created++ created++
usedMeasureIDs[m.ID] = true
added++ 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 { } else if len(existingMits) > 0 {
mitStep.Details = "Bereits vorhanden" mitStep.Details = "Bereits vorhanden"
mitStep.Count = len(existingMits) mitStep.Count = len(existingMits)
@@ -217,6 +217,13 @@ var genericSafetyTerms = map[string]bool{
"leitfaehig": true, "elektrisch": true, "mechanisch": true, "leitfaehig": true, "elektrisch": true, "mechanisch": true,
"bedienfeld": true, "display": true, "anzeige": true, "bedienfeld": true, "display": true, "anzeige": true,
"energie": true, "druck": true, "temperatur": 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 // Common structural terms that don't indicate a specific machine
"gesamter": true, "gesamtes": true, "bereichs": true, "stelle": true, "gesamter": true, "gesamtes": true, "bereichs": true, "stelle": true,
"innen": true, "aussen": true, "transport": true, "seite": true, "innen": true, "aussen": true, "transport": true, "seite": true,
@@ -369,18 +376,15 @@ func normalizeZoneKey(zone string) string {
return strings.Join(sig, "_") return strings.Join(sig, "_")
} }
// findHazardForMeasureByCategory finds a matching hazard for a measure. // findHazardsForMeasureByCategory finds all hazards matching a measure's category.
func findHazardForMeasureByCategory(measureCat string, hazardsByCategory map[string]uuid.UUID) uuid.UUID { func findHazardsForMeasureByCategory(measureCat string, hazardsByCategory map[string][]uuid.UUID) []uuid.UUID {
if id, ok := hazardsByCategory[measureCat]; ok { if ids, ok := hazardsByCategory[measureCat]; ok {
return id return ids
} }
for cat, id := range hazardsByCategory { for cat, ids := range hazardsByCategory {
if len(measureCat) > 3 && len(cat) > 3 && cat[:4] == measureCat[:4] { if len(measureCat) > 3 && len(cat) > 3 && cat[:4] == measureCat[:4] {
return id return ids
} }
} }
for _, id := range hazardsByCategory { return nil
return id
}
return uuid.Nil
} }
@@ -9,49 +9,9 @@ import (
// Fuzzy matching: Ground Truth entries ↔ Engine hazards // 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. // categoryMap, synonymSets, wrongMachineTerms → benchmark_synonyms.go
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"},
}
// CompareBenchmark runs the full comparison between Ground Truth and engine output. // CompareBenchmark runs the full comparison between Ground Truth and engine output.
func CompareBenchmark(gt *GroundTruth, hazards []Hazard, mitigations []Mitigation) *BenchmarkResult { 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)) engineSummaries := make([]HazardSummary, len(hazards))
for i, h := range hazards { for i, h := range hazards {
engineSummaries[i] = HazardSummary{ engineSummaries[i] = HazardSummary{
ID: h.ID.String(), ID: h.ID.String(),
Name: h.Name, Name: h.Name,
Category: h.Category, Category: h.Category,
Zone: h.HazardousZone, Zone: h.HazardousZone,
Description: h.Description, Description: h.Description,
Scenario: h.Scenario, Scenario: h.Scenario,
PossibleHarm: h.PossibleHarm, PossibleHarm: h.PossibleHarm,
TriggerEvent: h.TriggerEvent, TriggerEvent: h.TriggerEvent,
Mitigations: mitNamesByHazard[h.ID.String()], 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 // Greedy assignment: sort by score, but prioritize high-specificity matches
sort.Slice(pairs, func(a, b int) bool { return pairs[a].score > pairs[b].score }) // (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) usedGT := make(map[int]bool)
usedEng := make(map[int]bool) usedEng := make(map[int]bool)
var matched []HazardMatchPair 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. // 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) { func fuzzyMatchScore(gt *GroundTruthEntry, h *Hazard) (float64, string) {
var score float64 var score float64
var reasons []string var reasons []string
// 1. Category match (weight 0.3) // 1. Category match (weight 0.2)
catScore := categoryMatchScore(gt.HazardGroup, h.Category) catScore := categoryMatchScore(gt.HazardGroup, h.Category)
score += 0.3 * catScore score += 0.2 * catScore
if catScore > 0 { if catScore > 0 {
reasons = append(reasons, "Kategorie") 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) kwScore := keywordMatchScore(gt.HazardType, gt.HazardCause, h.Name, h.Description, h.Scenario)
score += 0.3 * kwScore score += 0.2 * kwScore
if kwScore > 0 { if kwScore > 0 {
reasons = append(reasons, "Keywords") 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) zoneScore := zoneMatchScore(gt.ComponentZone, gt.HazardSubgroup, h.HazardousZone, h.MachineModule)
score += 0.4 * zoneScore score += 0.3 * zoneScore
if zoneScore > 0 { if zoneScore > 0 {
reasons = append(reasons, "Zone") reasons = append(reasons, "Zone")
} }
// Penalty: if engine hazard mentions a machine-specific term not in the GT context, // 4. Scenario similarity (weight 0.3) — compares the actual event description
// it's likely a wrong-machine match (e.g. "Spielplatz" for a robot cell GT entry) 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) { if hasWrongMachineTerm(h.Name, h.Scenario, gt.HazardCause, gt.ComponentZone) {
score *= 0.3 // Heavy penalty score *= 0.3
reasons = append(reasons, "Strafabzug:FremdMaschine") 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, "+") return score, strings.Join(reasons, "+")
} }
// wrongMachineTerms are words in an engine hazard that indicate it's about // scenarioSimilarity compares the GT cause description with the engine scenario.
// a completely different machine type. If the GT entry doesn't mention these, // Uses action words + synonym-set cross-matching for robust comparison.
// the match is penalized. func scenarioSimilarity(gtCause, engScenario, engName string) float64 {
var wrongMachineTerms = []string{ gtText := normalizeDE(gtCause)
"spielplatz", "fahrtreppe", "trommelwaschmaschine", "umreifungsband", engText := normalizeDE(engScenario + " " + engName)
"drehteller", "rundtaktanlage", "exzentrisch", "webstuhl",
"aufzug", "rolltreppe", "bagger", "kettensaege", "kreissaege", gtActions := extractActionWords(gtText)
"druckmaschine", "zentrifuge", "autoklav", "hobel", engActions := extractActionWords(engText)
"naehmaschine", "strickmaschine", "schleifmaschine",
"gabelstapler", "flurfoerder", "erntemaschine", if len(gtActions) == 0 {
"kollision zweier roboter", // 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 { 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. // HazardSummary is a hazard representation for benchmark results with detail fields.
type HazardSummary struct { type HazardSummary struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Category string `json:"category"` Category string `json:"category"`
Component string `json:"component,omitempty"` Component string `json:"component,omitempty"`
Zone string `json:"zone,omitempty"` Zone string `json:"zone,omitempty"`
RiskLevel string `json:"risk_level,omitempty"` RiskLevel string `json:"risk_level,omitempty"`
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
Scenario string `json:"scenario,omitempty"` Scenario string `json:"scenario,omitempty"`
PossibleHarm string `json:"possible_harm,omitempty"` PossibleHarm string `json:"possible_harm,omitempty"`
TriggerEvent string `json:"trigger_event,omitempty"` TriggerEvent string `json:"trigger_event,omitempty"`
Mitigations []string `json:"mitigations,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. // CategoryScore shows coverage per ISO 12100 hazard group.
@@ -262,7 +262,7 @@ func GetExtendedHazardPatterns2() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M003", "M005"}, SuggestedMeasureIDs: []string{"M003", "M005"},
SuggestedEvidenceIDs: []string{"E08"}, SuggestedEvidenceIDs: []string{"E08"},
Priority: 80, Priority: 80, MachineTypes: []string{"press"},
ScenarioDE: "Exzentrische Belastung des Stoessels fuehrt zu seitlichem Ausbrechen des Werkstuecks.", ScenarioDE: "Exzentrische Belastung des Stoessels fuehrt zu seitlichem Ausbrechen des Werkstuecks.",
TriggerDE: "Werkstueck nicht korrekt positioniert, seitliche Kraftkomponente entsteht", TriggerDE: "Werkstueck nicht korrekt positioniert, seitliche Kraftkomponente entsteht",
HarmDE: "Aufprallverletzung durch geschleudertes Werkstueck, Quetschung", HarmDE: "Aufprallverletzung durch geschleudertes Werkstueck, Quetschung",
@@ -336,7 +336,7 @@ func GetExtendedHazardPatterns2() []HazardPattern {
DefaultSeverity: 3, DefaultExposure: 2, 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"}, RequiredComponentTags: []string{"programmable", "moving_part"},
RequiredEnergyTags: []string{"kinetic"}, RequiredEnergyTags: []string{"kinetic"},
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
@@ -361,7 +361,7 @@ func GetExtendedHazardPatterns2() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M001", "M051"}, SuggestedMeasureIDs: []string{"M001", "M051"},
SuggestedEvidenceIDs: []string{"E08", "E20"}, SuggestedEvidenceIDs: []string{"E08", "E20"},
Priority: 80, Priority: 80, MachineTypes: []string{"conveyor", "packaging"},
ScenarioDE: "Finger oder Kleidung werden an der Bandumlenkstelle eingezogen.", ScenarioDE: "Finger oder Kleidung werden an der Bandumlenkstelle eingezogen.",
TriggerDE: "Eingriff am laufenden Band, lose Kleidung geraet in Umlenkrolle", TriggerDE: "Eingriff am laufenden Band, lose Kleidung geraet in Umlenkrolle",
HarmDE: "Fingeramputation, Armverletzung, Strangulation durch eingezogene Kleidung", HarmDE: "Fingeramputation, Armverletzung, Strangulation durch eingezogene Kleidung",
@@ -595,7 +595,7 @@ func GetExtendedHazardPatterns2() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M003", "M051"}, SuggestedMeasureIDs: []string{"M003", "M051"},
SuggestedEvidenceIDs: []string{"E08", "E20"}, SuggestedEvidenceIDs: []string{"E08", "E20"},
Priority: 80, Priority: 80, MachineTypes: []string{"rotary_transfer"},
ScenarioDE: "Hand wird zwischen Drehteller und festem Anschlag eingeklemmt bei Taktbewegung.", ScenarioDE: "Hand wird zwischen Drehteller und festem Anschlag eingeklemmt bei Taktbewegung.",
TriggerDE: "Eingriff waehrend der Taktbewegung, fehlende Schutzabdeckung am Drehteller", TriggerDE: "Eingriff waehrend der Taktbewegung, fehlende Schutzabdeckung am Drehteller",
HarmDE: "Quetschung, Fingerfraktur, Amputation von Fingern", HarmDE: "Quetschung, Fingerfraktur, Amputation von Fingern",
@@ -102,7 +102,7 @@ func GetDGUVExtendedPatterns() []HazardPattern {
RequiredEnergyTags: []string{}, RequiredEnergyTags: []string{},
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M051"}, SuggestedMeasureIDs: []string{"M051"},
Priority: 80, Priority: 80, MachineTypes: []string{"crane", "construction"},
ScenarioDE: "Unkontrolliertes Schwingen einer angehobenen Last", HarmDE: "Quetschung, Erschlagen durch pendelnde Last", 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, TriggerDE: "Schraeger Zug oder ploetzliches Abstoppen", AffectedDE: "Kranfuehrer, Anschlaeger", ZoneDE: "Schwenkbereich des Krans", DefaultSeverity: 4, DefaultExposure: 3,
}, },
@@ -428,7 +428,7 @@ func GetFinalPatternsA() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M001", "M005"}, SuggestedMeasureIDs: []string{"M001", "M005"},
SuggestedEvidenceIDs: []string{"E01", "E08"}, 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", TriggerDE: "Eingriff in ungeschuetzten Kettenantrieb", HarmDE: "Fingerquetschung, Abriss",
AffectedDE: "Wartungspersonal", ZoneDE: "Kettenrad, Kettenstrang", AffectedDE: "Wartungspersonal", ZoneDE: "Kettenrad, Kettenstrang",
DefaultSeverity: 4, DefaultExposure: 2, DefaultSeverity: 4, DefaultExposure: 2,
@@ -814,7 +814,7 @@ func GetFinalPatternsA() []HazardPattern {
}, },
// === Einklemmen Haare/Kleidung (3) === // === 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"}, RequiredComponentTags: []string{"rotating_part", "entanglement_risk"},
RequiredEnergyTags: []string{"rotational"}, RequiredEnergyTags: []string{"rotational"},
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
@@ -817,7 +817,7 @@ func GetFinalPatternsD() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M001", "M005"}, SuggestedMeasureIDs: []string{"M001", "M005"},
SuggestedEvidenceIDs: []string{"E01", "E08"}, 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", TriggerDE: "Fehlende Endschalter, Unachtsamkeit", HarmDE: "Herabfallende Last",
AffectedDE: "Personen darunter", ZoneDE: "Unter Kranschwenkbereich", AffectedDE: "Personen darunter", ZoneDE: "Unter Kranschwenkbereich",
DefaultSeverity: 5, DefaultExposure: 2, DefaultSeverity: 5, DefaultExposure: 2,
@@ -90,7 +90,7 @@ func builtinMechanicalPatterns() []HazardPattern {
TriggerDE: "Bediener befindet sich im Kraftwirkbereich waehrend des Arbeitshubes oder bei Stoerungsbeseitigung.", TriggerDE: "Bediener befindet sich im Kraftwirkbereich waehrend des Arbeitshubes oder bei Stoerungsbeseitigung.",
HarmDE: "Schwere Quetschung, Fraktur, innere Verletzungen, Todesfolge bei Ganzkompression.", HarmDE: "Schwere Quetschung, Fraktur, innere Verletzungen, Todesfolge bei Ganzkompression.",
AffectedDE: "Bedienpersonal, Einrichter, Wartungspersonal", AffectedDE: "Bedienpersonal, Einrichter, Wartungspersonal",
ZoneDE: "Kraftwirkbereich (Pressenraum, Vorschubachse), Einlegestelle", ZoneDE: "Kraftwirkbereich, Einlegestelle, Vorschubachse",
DefaultSeverity: 5, DefaultExposure: 3, 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.", TriggerDE: "Versagen einer Halterung, Bruch eines Lastaufnahmemittels oder Abrutschen bei Wartungsarbeiten in der Hoehe.",
HarmDE: "Kopfverletzung, Fraktur, Quetschung durch herabfallende Last; Sturzverletung.", HarmDE: "Kopfverletzung, Fraktur, Quetschung durch herabfallende Last; Sturzverletung.",
AffectedDE: "Wartungspersonal, Bedienpersonal, Personen im Gefahrenbereich", AffectedDE: "Wartungspersonal, Bedienpersonal, Personen im Gefahrenbereich",
ZoneDE: "Bereich unterhalb angehobener Lasten, Wartungsplattformen, Kran-/Hebezeugbereich", ZoneDE: "Bereich unterhalb angehobener Lasten, Wartungsplattformen",
DefaultSeverity: 4, DefaultExposure: 2, DefaultSeverity: 4, DefaultExposure: 2,
}, },
{ {
@@ -262,7 +262,7 @@ func GetPlasticsMetalPatterns() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M003", "M004", "M082"}, SuggestedMeasureIDs: []string{"M003", "M004", "M082"},
SuggestedEvidenceIDs: []string{"E08", "E09"}, 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.", 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", TriggerDE: "Tragen von Handschuhen an der Drehmaschine, offene Haare, lose Kleidung",
HarmDE: "Skalpierung, Armfraktur, Strangulation, toedliche Aufwickelverletzung", HarmDE: "Skalpierung, Armfraktur, Strangulation, toedliche Aufwickelverletzung",
@@ -17,7 +17,7 @@ func GetRobotCellPatterns() []HazardPattern {
RequiredComponentTags: []string{"moving_part"}, RequiredComponentTags: []string{"moving_part"},
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M061", "M062", "M054"}, 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"}, 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.", 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.", TriggerDE: "Roboterarm bewegt sich waehrend Person im Gefahrenbereich steht.",
@@ -31,7 +31,7 @@ func GetRobotCellPatterns() []HazardPattern {
RequiredComponentTags: []string{"moving_part", "guard"}, RequiredComponentTags: []string{"moving_part", "guard"},
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M002", "M061"}, SuggestedMeasureIDs: []string{"M002", "M061"},
Priority: 93, Priority: 98,
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing"}, ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing"},
ScenarioDE: "Person greift ueber oder durch den Schutzzaun und erreicht den Bewegungsbereich des Roboterarms.", ScenarioDE: "Person greift ueber oder durch den Schutzzaun und erreicht den Bewegungsbereich des Roboterarms.",
TriggerDE: "Unzureichender Sicherheitsabstand zwischen Schutzzaun-Oberkante und Roboter-Schwenkbereich.", TriggerDE: "Unzureichender Sicherheitsabstand zwischen Schutzzaun-Oberkante und Roboter-Schwenkbereich.",
@@ -45,7 +45,7 @@ func GetRobotCellPatterns() []HazardPattern {
RequiredComponentTags: []string{"moving_part", "guard"}, RequiredComponentTags: []string{"moving_part", "guard"},
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M061", "M054", "M141"}, SuggestedMeasureIDs: []string{"M061", "M054", "M141"},
Priority: 94, Priority: 99,
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing", "changeover"}, 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.", 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.", TriggerDE: "Schutztuer schliesst waehrend Person im Innenraum. Wiederanlauf des Roboters ohne Quittierung.",
@@ -59,15 +59,29 @@ func GetRobotCellPatterns() []HazardPattern {
RequiredComponentTags: []string{"moving_part", "guard"}, RequiredComponentTags: []string{"moving_part", "guard"},
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M061", "M002"}, SuggestedMeasureIDs: []string{"M061", "M002"},
Priority: 92, Priority: 98,
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "changeover", "fault_clearing"}, ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "changeover", "fault_clearing"},
ScenarioDE: "Roboterarm ueberschreitet den vorgesehenen Bewegungsbereich und trifft den Schutzzaun mit hoher Kraft.", 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.", TriggerDE: "Fehler in der Bahnplanung oder Ausfall der Achsbegrenzung.",
HarmDE: "Teile des Schutzzauns werden herausgeschleudert, Person ausserhalb wird getroffen.", HarmDE: "Teile des Schutzzauns werden herausgeschleudert, Person ausserhalb wird getroffen.",
AffectedDE: "Bedienpersonal in der Naehe des Schutzzauns", AffectedDE: "Bedienpersonal in der Naehe des Schutzzauns",
ZoneDE: "Schutzzaun, Bereich um die Roboterzelle", ZoneDE: "Schutzzaun, Bereich um die Roboterzelle",
DefaultSeverity: 3, DefaultExposure: 2, 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 // Greifer / Werkstueck
// ================================================================ // ================================================================
@@ -76,7 +90,7 @@ func GetRobotCellPatterns() []HazardPattern {
RequiredComponentTags: []string{"clamping_part"}, RequiredComponentTags: []string{"clamping_part"},
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M054", "M061"}, SuggestedMeasureIDs: []string{"M054", "M061"},
Priority: 94, MachineTypes: []string{"robotics_cobot", "automotive", "metalworking", "general_industry"}, Priority: 99, MachineTypes: []string{"robotics_cobot", "automotive", "metalworking", "general_industry"},
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "changeover", "fault_clearing"}, ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "changeover", "fault_clearing"},
ScenarioDE: "Person greift in den Bereich des Greifers. Hand wird zwischen Greifbacken und Werkstueck eingeklemmt.", ScenarioDE: "Person greift in den Bereich des Greifers. Hand wird zwischen Greifbacken und Werkstueck eingeklemmt.",
TriggerDE: "Greiferbacken schliessen waehrend Koerperteil im Greifbereich ist.", TriggerDE: "Greiferbacken schliessen waehrend Koerperteil im Greifbereich ist.",
@@ -90,9 +104,9 @@ func GetRobotCellPatterns() []HazardPattern {
RequiredComponentTags: []string{"clamping_part"}, RequiredComponentTags: []string{"clamping_part"},
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M007", "M141"}, SuggestedMeasureIDs: []string{"M007", "M141"},
Priority: 93, Priority: 98,
ApplicableLifecycles: []string{"normal_operation", "setup", "changeover"}, ApplicableLifecycles: []string{"normal_operation", "setup", "changeover"},
ScenarioDE: "Greifer verliert das Werkstueck waehrend des Transports (Druckverlust, oelige Oberflaeche, falsches Werkstueck).", 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.", TriggerDE: "Werkstueck faellt aus Greifer und trifft Person unterhalb des Roboterarms.",
HarmDE: "Prellungen, Knochenbrueche abhaengig von Werkstueckgewicht und Fallhoehe.", HarmDE: "Prellungen, Knochenbrueche abhaengig von Werkstueckgewicht und Fallhoehe.",
AffectedDE: "Bedienpersonal, Wartungspersonal", AffectedDE: "Bedienpersonal, Wartungspersonal",
@@ -104,9 +118,9 @@ func GetRobotCellPatterns() []HazardPattern {
RequiredComponentTags: []string{"clamping_part", "guard"}, RequiredComponentTags: []string{"clamping_part", "guard"},
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M061", "M141"}, SuggestedMeasureIDs: []string{"M061", "M141"},
Priority: 92, Priority: 98,
ApplicableLifecycles: []string{"normal_operation"}, ApplicableLifecycles: []string{"normal_operation"},
ScenarioDE: "Greifer versagt und Roboterarm beschleunigt das freigesetzte Werkstueck in Richtung Schutzzaun oder Einhausung.", 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.", TriggerDE: "Werkstueck wird durch Roboterbewegung weggeschleudert und durchschlaegt die Schutzeinrichtung.",
HarmDE: "Person ausserhalb der Zelle wird von weggeschleudertem Werkstueck getroffen.", HarmDE: "Person ausserhalb der Zelle wird von weggeschleudertem Werkstueck getroffen.",
AffectedDE: "Bedienpersonal in der Naehe der Roboterzelle", AffectedDE: "Bedienpersonal in der Naehe der Roboterzelle",
@@ -121,7 +135,7 @@ func GetRobotCellPatterns() []HazardPattern {
RequiredComponentTags: []string{"entanglement_risk"}, RequiredComponentTags: []string{"entanglement_risk"},
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M002", "M061", "M003"}, SuggestedMeasureIDs: []string{"M002", "M061", "M003"},
Priority: 93, Priority: 98,
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing"}, ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing"},
ScenarioDE: "Person greift an Foerderband und wird zwischen beweglichen und feststehenden Teilen eingeklemmt.", 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.", TriggerDE: "Hand oder Finger geraten zwischen Band und Umlenkrolle oder zwischen Werkstueck und Tunnelrahmen.",
@@ -135,7 +149,7 @@ func GetRobotCellPatterns() []HazardPattern {
RequiredComponentTags: []string{"entanglement_risk", "guard"}, RequiredComponentTags: []string{"entanglement_risk", "guard"},
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M002", "M061"}, SuggestedMeasureIDs: []string{"M002", "M061"},
Priority: 93, Priority: 98,
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "fault_clearing"}, ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "fault_clearing"},
ScenarioDE: "Person greift durch die Oeffnung im Schutzzaun fuer die Foerderbaender in den Gefahrenbereich des Roboters.", 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.", TriggerDE: "Oeffnung ist zu gross oder Sicherheitsabstand zum Roboter-Schwenkbereich ist zu gering.",
@@ -149,9 +163,9 @@ func GetRobotCellPatterns() []HazardPattern {
RequiredComponentTags: []string{"entanglement_risk"}, RequiredComponentTags: []string{"entanglement_risk"},
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M008"}, SuggestedMeasureIDs: []string{"M008"},
Priority: 91, Priority: 97,
ApplicableLifecycles: []string{"normal_operation", "setup"}, ApplicableLifecycles: []string{"normal_operation", "setup"},
ScenarioDE: "Werkstueck faehrt ueber das Ende des Transportbandes hinaus und faellt herab.", ScenarioDE: "Werkstueck faehrt ueber das Ende des Transportbandes hinaus, faellt herab und trifft Person am Be-/Entladeplatz.",
TriggerDE: "Mechanischer Anschlag fehlt oder ist beschaedigt.", TriggerDE: "Mechanischer Anschlag fehlt oder ist beschaedigt.",
HarmDE: "Prellungen, Quetschung von Fuessen durch herabfallendes Werkstueck.", HarmDE: "Prellungen, Quetschung von Fuessen durch herabfallendes Werkstueck.",
AffectedDE: "Bedienpersonal am Be-/Entladeplatz", AffectedDE: "Bedienpersonal am Be-/Entladeplatz",
@@ -166,7 +180,7 @@ func GetRobotCellPatterns() []HazardPattern {
RequiredComponentTags: []string{"guard"}, RequiredComponentTags: []string{"guard"},
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M003"}, SuggestedMeasureIDs: []string{"M003"},
Priority: 90, Priority: 97,
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing"}, ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing"},
ScenarioDE: "Person schneidet sich an nicht entgrateten oder scharfkantigen Blechen der Einhausung oder Verkleidung.", ScenarioDE: "Person schneidet sich an nicht entgrateten oder scharfkantigen Blechen der Einhausung oder Verkleidung.",
TriggerDE: "Zugaengliche Kanten sind nicht gerundet oder gebrochen.", TriggerDE: "Zugaengliche Kanten sind nicht gerundet oder gebrochen.",
@@ -182,8 +196,8 @@ func GetRobotCellPatterns() []HazardPattern {
ID: "HP1630", NameDE: "Pneumatikschlauch springt unter Druck ab", NameEN: "Pressurized hose comes loose", ID: "HP1630", NameDE: "Pneumatikschlauch springt unter Druck ab", NameEN: "Pressurized hose comes loose",
RequiredComponentTags: []string{"pinch_point"}, RequiredComponentTags: []string{"pinch_point"},
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M420"}, SuggestedMeasureIDs: []string{"M480"},
Priority: 91, Priority: 97,
ApplicableLifecycles: []string{"normal_operation", "setup", "maintenance", "fault_clearing"}, ApplicableLifecycles: []string{"normal_operation", "setup", "maintenance", "fault_clearing"},
ScenarioDE: "Pneumatikschlauch der Automation springt unter Druck ab und trifft eine Person (Peitscheneffekt).", ScenarioDE: "Pneumatikschlauch der Automation springt unter Druck ab und trifft eine Person (Peitscheneffekt).",
TriggerDE: "Befestigung loest sich, Verschraubung wird undicht, Materialermuedung des Schlauchs.", TriggerDE: "Befestigung loest sich, Verschraubung wird undicht, Materialermuedung des Schlauchs.",
@@ -196,12 +210,12 @@ func GetRobotCellPatterns() []HazardPattern {
ID: "HP1631", NameDE: "Restdruck in Pneumatik nach Abschaltung", NameEN: "Residual pressure in pneumatics after shutdown", ID: "HP1631", NameDE: "Restdruck in Pneumatik nach Abschaltung", NameEN: "Residual pressure in pneumatics after shutdown",
RequiredComponentTags: []string{"pinch_point"}, RequiredComponentTags: []string{"pinch_point"},
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M420", "M141"}, SuggestedMeasureIDs: []string{"M480", "M141"},
Priority: 91, Priority: 97,
ApplicableLifecycles: []string{"maintenance", "fault_clearing", "changeover"}, ApplicableLifecycles: []string{"maintenance", "fault_clearing", "changeover"},
ScenarioDE: "Pneumatik-Komponenten stehen nach Abschaltung noch unter Druck. Bei Arbeiten an der Anlage werden druckbeaufschlagte Teile geloest.", 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.", TriggerDE: "Fehlende Druckentlastung. Gesperrte Rueckschlagventile halten Druck.",
HarmDE: "Unkontrolliertes Loesen von Verbindungen, wegfliegende Teile.", HarmDE: "Person wird von wegfliegenden Teilen oder unkontrolliert loesenden Verbindungen getroffen. Prellungen, Schnittverletzungen.",
AffectedDE: "Wartungspersonal, Einrichter", AffectedDE: "Wartungspersonal, Einrichter",
ZoneDE: "Pneumatikschlaeuche und -komponenten", ZoneDE: "Pneumatikschlaeuche und -komponenten",
DefaultSeverity: 2, DefaultExposure: 2, DefaultSeverity: 2, DefaultExposure: 2,
@@ -209,14 +223,56 @@ func GetRobotCellPatterns() []HazardPattern {
// ================================================================ // ================================================================
// Kuehlschmierstoff (KSS) // Kuehlschmierstoff (KSS)
// ================================================================ // ================================================================
{
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", ID: "HP1635", NameDE: "Ausrutschen durch KSS-Leckage", NameEN: "Slipping due to coolant leakage",
RequiredComponentTags: []string{}, RequiredComponentTags: []string{},
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M420"}, SuggestedMeasureIDs: []string{"M420"},
Priority: 90, MachineTypes: []string{"cnc", "metalworking", "automotive"}, Priority: 97, MachineTypes: []string{"cnc", "metalworking", "automotive"},
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing"}, ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing"},
ScenarioDE: "Kuehlschmierstoff tritt aus undichter Leitung oder Verbindung aus und bildet einen rutschigen Belag auf dem Boden.", ScenarioDE: "Kuehlschmierstoff tritt aus und bildet rutschigen Belag auf dem Boden. Person rutscht aus und stuerzt.",
TriggerDE: "Leckage an Schlauchverbindung, Dichtungsversagen.", TriggerDE: "Leckage an Schlauchverbindung, Dichtungsversagen.",
HarmDE: "Ausrutschen und Sturz, Prellungen, Knochenbrueche.", HarmDE: "Ausrutschen und Sturz, Prellungen, Knochenbrueche.",
AffectedDE: "Bedienpersonal, Wartungspersonal", AffectedDE: "Bedienpersonal, Wartungspersonal",
@@ -228,7 +284,7 @@ func GetRobotCellPatterns() []HazardPattern {
RequiredComponentTags: []string{}, RequiredComponentTags: []string{},
GeneratedHazardCats: []string{"material_environmental"}, GeneratedHazardCats: []string{"material_environmental"},
SuggestedMeasureIDs: []string{"M141"}, 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"}, ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing"},
ScenarioDE: "Person kommt bei Arbeiten am Bearbeitungszentrum oder der Roboterzelle mit Kuehlschmierstoff in Beruehrung.", ScenarioDE: "Person kommt bei Arbeiten am Bearbeitungszentrum oder der Roboterzelle mit Kuehlschmierstoff in Beruehrung.",
TriggerDE: "Hautkontakt beim Reinigen, Werkzeugwechsel oder Beseitigung von Stoerungen.", TriggerDE: "Hautkontakt beim Reinigen, Werkzeugwechsel oder Beseitigung von Stoerungen.",
@@ -242,11 +298,11 @@ func GetRobotCellPatterns() []HazardPattern {
RequiredComponentTags: []string{}, RequiredComponentTags: []string{},
GeneratedHazardCats: []string{"material_environmental"}, GeneratedHazardCats: []string{"material_environmental"},
SuggestedMeasureIDs: []string{"M141"}, SuggestedMeasureIDs: []string{"M141"},
Priority: 90, MachineTypes: []string{"cnc", "metalworking", "automotive"}, Priority: 97, MachineTypes: []string{"cnc", "metalworking", "automotive"},
ApplicableLifecycles: []string{"normal_operation", "setup", "maintenance"}, ApplicableLifecycles: []string{"normal_operation", "setup", "maintenance"},
ScenarioDE: "Waehrend der Werkstueckbearbeitung entstehen KSS-Aerosole die beim Oeffnen der Bearbeitungszelle freigesetzt werden.", ScenarioDE: "Person oeffnet Schutztuer der Bearbeitungszelle und atmet freigesetzte KSS-Aerosole ein.",
TriggerDE: "Oeffnen der Schutztuer nach Bearbeitungsvorgang, unzureichende Absaugung.", TriggerDE: "Oeffnen der Schutztuer nach Bearbeitungsvorgang, unzureichende Absaugung.",
HarmDE: "Atembeschwerden, Reizung der Atemwege.", HarmDE: "Person atmet KSS-Aerosole ein. Atembeschwerden, Reizung der Atemwege, bei chronischer Exposition Atemwegserkrankungen.",
AffectedDE: "Bedienpersonal", AffectedDE: "Bedienpersonal",
ZoneDE: "Bearbeitungszelle, Bereich vor der Schutztuer", ZoneDE: "Bearbeitungszelle, Bereich vor der Schutztuer",
DefaultSeverity: 1, DefaultExposure: 3, DefaultSeverity: 1, DefaultExposure: 3,
@@ -257,14 +313,14 @@ func GetRobotCellPatterns() []HazardPattern {
{ {
ID: "HP1640", NameDE: "Direktes Beruehren spannungsfuehrender Teile", NameEN: "Direct contact with live parts", ID: "HP1640", NameDE: "Direktes Beruehren spannungsfuehrender Teile", NameEN: "Direct contact with live parts",
RequiredComponentTags: []string{}, RequiredComponentTags: []string{},
RequiredEnergyTags: []string{"electrical"}, RequiredEnergyTags: []string{},
GeneratedHazardCats: []string{"electrical_hazard"}, GeneratedHazardCats: []string{"electrical_hazard"},
SuggestedMeasureIDs: []string{"M009", "M410"}, SuggestedMeasureIDs: []string{"M265", "M089", "M088", "M139", "M475"},
Priority: 93, Priority: 99,
ApplicableLifecycles: []string{"normal_operation", "setup", "maintenance", "fault_clearing"}, ApplicableLifecycles: []string{"normal_operation", "setup", "maintenance", "fault_clearing"},
ScenarioDE: "Person beruehrt spannungsfuehrende Teile der Anlage die nicht ausreichend isoliert oder abgedeckt sind.", ScenarioDE: "Person beruehrt spannungsfuehrende Teile der Anlage die nicht ausreichend isoliert oder abgedeckt sind.",
TriggerDE: "Beschaedigte Isolation, fehlende Abdeckung, ungesicherter Schaltschrank.", 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", AffectedDE: "Wartungspersonal, Einrichter",
ZoneDE: "Zugaengliche Kabel, Klemmen, Schaltschrank", ZoneDE: "Zugaengliche Kabel, Klemmen, Schaltschrank",
DefaultSeverity: 4, DefaultExposure: 2, DefaultSeverity: 4, DefaultExposure: 2,
@@ -274,10 +330,10 @@ func GetRobotCellPatterns() []HazardPattern {
RequiredComponentTags: []string{}, RequiredComponentTags: []string{},
RequiredEnergyTags: []string{"electrical"}, RequiredEnergyTags: []string{"electrical"},
GeneratedHazardCats: []string{"electrical_hazard"}, GeneratedHazardCats: []string{"electrical_hazard"},
SuggestedMeasureIDs: []string{"M410", "M411"}, SuggestedMeasureIDs: []string{"M475", "M476"},
Priority: 93, Priority: 98,
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing"}, ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing"},
ScenarioDE: "Schutzleiter ist unterbrochen oder nicht korrekt angeschlossen. Beruehrbare leitfaehige Teile fuehren gefaehrliche Beruehrungsspannung.", ScenarioDE: "Schutzleiter ist unterbrochen. Person beruehrt das Maschinengehaeuse und erleidet elektrischen Schlag durch gefaehrliche Beruehrungsspannung.",
TriggerDE: "Schutzleiterunterbrechung durch mechanische Beschaedigung oder fehlerhafte Installation.", TriggerDE: "Schutzleiterunterbrechung durch mechanische Beschaedigung oder fehlerhafte Installation.",
HarmDE: "Elektrischer Schlag bei Beruehren des Maschinengehaeuses oder leitfaehiger Oberflaechen.", HarmDE: "Elektrischer Schlag bei Beruehren des Maschinengehaeuses oder leitfaehiger Oberflaechen.",
AffectedDE: "Bedienpersonal, Wartungspersonal", AffectedDE: "Bedienpersonal, Wartungspersonal",
@@ -290,9 +346,9 @@ func GetRobotCellPatterns() []HazardPattern {
RequiredEnergyTags: []string{"electrical"}, RequiredEnergyTags: []string{"electrical"},
GeneratedHazardCats: []string{"electrical_hazard"}, GeneratedHazardCats: []string{"electrical_hazard"},
SuggestedMeasureIDs: []string{"M009"}, SuggestedMeasureIDs: []string{"M009"},
Priority: 92, Priority: 98,
ApplicableLifecycles: []string{"normal_operation", "setup", "maintenance"}, ApplicableLifecycles: []string{"normal_operation", "setup", "maintenance"},
ScenarioDE: "Kabelquerschnitt ist nicht auf die maximale Leistung ausgelegt oder Ueberstromschutz fehlt. Kabel ueberhitzt und entzuendet sich.", 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.", TriggerDE: "Dauerhafter Betrieb nahe der Belastungsgrenze, falsch dimensionierte Sicherung.",
HarmDE: "Brand, Rauchentwicklung, Verletzung durch Feuer oder toxische Gase.", HarmDE: "Brand, Rauchentwicklung, Verletzung durch Feuer oder toxische Gase.",
AffectedDE: "Alle Personen im Bereich der Anlage", AffectedDE: "Alle Personen im Bereich 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,
},
}
}
@@ -252,7 +252,7 @@ func GetSpecificMachinePatterns() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M003", "M141"}, SuggestedMeasureIDs: []string{"M003", "M141"},
SuggestedEvidenceIDs: []string{"E01", "E20"}, 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.", ScenarioDE: "Rotorblatt einer Windturbine bricht durch Materialermuedung oder Blitzschlag und wird Hunderte Meter weit geschleudert.",
TriggerDE: "Materialermuedung, Blitzschaden, Vereisung mit Unwucht, fehlende Inspektionen", TriggerDE: "Materialermuedung, Blitzschaden, Vereisung mit Unwucht, fehlende Inspektionen",
HarmDE: "Toedliche Verletzung durch Blattstuecke, Sachschaeden im weiten Umkreis", HarmDE: "Toedliche Verletzung durch Blattstuecke, Sachschaeden im weiten Umkreis",
@@ -297,7 +297,7 @@ func GetSpecificMachinePatterns() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M141"}, SuggestedMeasureIDs: []string{"M141"},
SuggestedEvidenceIDs: []string{"E01", "E20"}, 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.", 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", TriggerDE: "Vereisung im Winter, fehlende Eiserkennungssysteme, Weiterbetrieb bei Eisansatz",
HarmDE: "Verletzung durch Eisschlag, Sachschaeden an Fahrzeugen und Gebaeuden", HarmDE: "Verletzung durch Eisschlag, Sachschaeden an Fahrzeugen und Gebaeuden",
@@ -30,7 +30,7 @@ func GetSpecificMachinePatterns2() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M003", "M141"}, SuggestedMeasureIDs: []string{"M003", "M141"},
SuggestedEvidenceIDs: []string{"E08", "E20"}, SuggestedEvidenceIDs: []string{"E08", "E20"},
Priority: 80, Priority: 80, MachineTypes: []string{"escalator"},
ScenarioDE: "Finger oder Handteile werden am Einzugspunkt des Handlaufs in die Verkleidung gezogen.", ScenarioDE: "Finger oder Handteile werden am Einzugspunkt des Handlaufs in die Verkleidung gezogen.",
TriggerDE: "Kinderhand am Handlauf nahe der Verkleidung, fehlende Einlaufschutzbuegel", TriggerDE: "Kinderhand am Handlauf nahe der Verkleidung, fehlende Einlaufschutzbuegel",
HarmDE: "Fingerquetschung, Hautabschuerfungen, bei Kindern Armverletzung", HarmDE: "Fingerquetschung, Hautabschuerfungen, bei Kindern Armverletzung",
@@ -39,7 +39,7 @@ func GetSpecificMachinePatterns2() []HazardPattern {
DefaultSeverity: 3, DefaultExposure: 4, 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"}, RequiredComponentTags: []string{"moving_part"},
RequiredEnergyTags: []string{"kinetic"}, RequiredEnergyTags: []string{"kinetic"},
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
@@ -75,7 +75,7 @@ func GetSpecificMachinePatterns2() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M003", "M141"}, SuggestedMeasureIDs: []string{"M003", "M141"},
SuggestedEvidenceIDs: []string{"E08", "E09", "E20"}, 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.", ScenarioDE: "Bruch einer Trittstufe oder der Kammplatte fuehrt zum Einsacken oder Einzug in die Mechanik.",
TriggerDE: "Materialermuedung, Korrosion, fehlende Inspektionen, Vandalismus", TriggerDE: "Materialermuedung, Korrosion, fehlende Inspektionen, Vandalismus",
HarmDE: "Einzug in Mechanik, Beinverletzungen, Sturz in Maschinenkammer", HarmDE: "Einzug in Mechanik, Beinverletzungen, Sturz in Maschinenkammer",
@@ -173,7 +173,7 @@ func GetSpecificMachinePatterns2() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M003", "M141"}, SuggestedMeasureIDs: []string{"M003", "M141"},
SuggestedEvidenceIDs: []string{"E01", "E20"}, 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).", 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", TriggerDE: "Oeffnungen im kritischen Bereich 89-230 mm, V-foermige Spalte, Gelaendersprosse mit Kopffangmass",
HarmDE: "Strangulation, Erstickung, toedliche Verletzung", HarmDE: "Strangulation, Erstickung, toedliche Verletzung",
@@ -233,7 +233,7 @@ func GetSpecificMachinePatterns2() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M003", "M141"}, SuggestedMeasureIDs: []string{"M003", "M141"},
SuggestedEvidenceIDs: []string{"E01", "E20"}, 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.", 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", TriggerDE: "Kleidung mit Kordeln am Hals, zu grosse Maschenweite, lose Seilenden",
HarmDE: "Strangulation, Erstickung, toedliche Verletzung", HarmDE: "Strangulation, Erstickung, toedliche Verletzung",
@@ -361,7 +361,7 @@ func GetSpecificMachinePatterns2() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M003", "M004", "M082"}, SuggestedMeasureIDs: []string{"M003", "M004", "M082"},
SuggestedEvidenceIDs: []string{"E08", "E09"}, SuggestedEvidenceIDs: []string{"E08", "E09"},
Priority: 85, Priority: 85, MachineTypes: []string{"laundry"},
ScenarioDE: "Person greift in die drehende Trommel der Industriewaschmaschine und wird eingezogen.", ScenarioDE: "Person greift in die drehende Trommel der Industriewaschmaschine und wird eingezogen.",
TriggerDE: "Defekte Tuerverriegelung, Oeffnen waehrend Nachlauf, Bedienfehler", TriggerDE: "Defekte Tuerverriegelung, Oeffnen waehrend Nachlauf, Bedienfehler",
HarmDE: "Schwere Quetschverletzung, Armeinzug, Strangulation durch Waeschestuecke", HarmDE: "Schwere Quetschverletzung, Armeinzug, Strangulation durch Waeschestuecke",
@@ -230,7 +230,7 @@ func GetWeldingGlassTextilePatterns() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M003", "M004", "M082"}, SuggestedMeasureIDs: []string{"M003", "M004", "M082"},
SuggestedEvidenceIDs: []string{"E08", "E09"}, 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.", ScenarioDE: "Transportwalzen der Glaswaschmaschine erfassen Finger oder Kleidung beim manuellen Einlegen der Scheiben.",
TriggerDE: "Manuelles Nachjustieren bei laufenden Walzen, fehlender Schutz am Einlaufbereich", TriggerDE: "Manuelles Nachjustieren bei laufenden Walzen, fehlender Schutz am Einlaufbereich",
HarmDE: "Fingerquetschung, Einzug der Hand, Hautabschaelungen", HarmDE: "Fingerquetschung, Einzug der Hand, Hautabschaelungen",
@@ -71,21 +71,21 @@ func getSupplementaryMeasures() []ProtectiveMeasureEntry {
// Elektrische Sicherheit — Potentialausgleich & Ableitstroeme // Elektrische Sicherheit — Potentialausgleich & Ableitstroeme
// Gap: GT-Benchmark 2.12 (Potentialausgleich), 2.4 (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: "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: "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: "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: "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: "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 // EMV-Sicherheit
// Gap: GT-Benchmark 6.1 (EMV-Stoereinfluss auf Sicherheitsfunktionen) // 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: "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: "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: "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 // Kuehlschmierstoff-Leitungssicherheit
// Gap: GT-Benchmark 2.10 (KSS-Leckage fuehrt zu Brand) // 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)
}
}
@@ -67,6 +67,7 @@ type PatternMatch struct {
GeneratedHazardType string `json:"generated_hazard_type,omitempty"` GeneratedHazardType string `json:"generated_hazard_type,omitempty"`
MatchedFailureModes []string `json:"matched_failure_modes,omitempty"` MatchedFailureModes []string `json:"matched_failure_modes,omitempty"`
ApplicableLifecycles []string `json:"applicable_lifecycles,omitempty"` ApplicableLifecycles []string `json:"applicable_lifecycles,omitempty"`
SuggestedMeasureIDs []string `json:"suggested_measure_ids,omitempty"`
} }
// HazardSuggestion is a suggested hazard from pattern matching. // HazardSuggestion is a suggested hazard from pattern matching.
@@ -220,6 +221,7 @@ func (e *PatternEngine) Match(input MatchInput) *MatchOutput {
GeneratedHazardType: p.GeneratedHazardType, GeneratedHazardType: p.GeneratedHazardType,
MatchedFailureModes: matchedFMs, MatchedFailureModes: matchedFMs,
ApplicableLifecycles: p.ApplicableLifecycles, ApplicableLifecycles: p.ApplicableLifecycles,
SuggestedMeasureIDs: p.SuggestedMeasureIDs,
}) })
for _, cat := range p.GeneratedHazardCats { 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, GetVDMAIndustryPatterns()...) // HP1500-HP1549 VDMA sectors (Phase 3)
patterns = append(patterns, GetTextileAgriPatterns()...) // HP1550-HP1584 Textile + Agri (Phase 5) patterns = append(patterns, GetTextileAgriPatterns()...) // HP1550-HP1584 Textile + Agri (Phase 5)
patterns = append(patterns, GetRobotCellPatterns()...) // HP1600-HP1649 Robot cell (GT benchmark) patterns = append(patterns, GetRobotCellPatterns()...) // HP1600-HP1649 Robot cell (GT benchmark)
patterns = append(patterns, GetRobotCellPatternsExt()...) // HP1650-HP1699 Robot cell extended (GT gaps)
return patterns return patterns
} }
@@ -64,12 +64,16 @@ class ComplianceCheckStatusResponse(BaseModel):
@router.post("/extract-text") @router.post("/extract-text")
async def extract_text(req: ExtractTextRequest): 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: try:
async with httpx.AsyncClient(timeout=90.0) as client: async with httpx.AsyncClient(timeout=300.0) as client:
resp = await client.post( resp = await client.post(
f"{CONSENT_TESTER_URL}/dsi-discovery", 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: if resp.status_code != 200:
return { return {
@@ -86,10 +90,15 @@ async def extract_text(req: ExtractTextRequest):
"error": "Kein Text extrahierbar", "error": "Kein Text extrahierbar",
} }
doc = docs[0] # Merge all documents (handles multi-page DSIs like BMW)
text = doc.get("full_text", "") or doc.get("text_preview", "") or doc.get("text", "") texts = []
title = doc.get("title", "") or doc.get("doc_type", "") for doc in docs:
word_count = doc.get("word_count", 0) or len(text.split()) 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 { return {
"text": text, "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 # 1. Same URL used for multiple doc_types → split by heading
# 2. DSI text contains Cookie/Social-Media sections → auto-fill empty rows # 2. DSI text contains Cookie/Social-Media sections → auto-fill empty rows
from compliance.services.section_splitter import ( 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) split_shared_texts(doc_entries, url_text_cache)
auto_fill_from_dsi(doc_entries) 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: for entry in doc_entries:
if entry.get("text"): if entry.get("text"):
doc_texts[entry["doc_type"]] = entry["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 # Apply profile context filter
result = _apply_profile_filter(result, profile, doc_type) 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) results.append(result)
total_findings += result.findings_count total_findings += result.findings_count
@@ -302,17 +326,24 @@ async def _run_compliance_check(check_id: str, req: ComplianceCheckRequest):
else: else:
r.scenario = "import" r.scenario = "import"
# Step 5: Build report # Step 5: Build report with management summary
_update(check_id, "Report wird erstellt...") _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) report_html = build_html_report(results, None)
profile_html = _build_profile_html(profile) 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]) 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( email_result = send_email(
recipient=req.recipient, 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, body_html=full_html,
) )
@@ -349,23 +380,55 @@ def _update(check_id: str, msg: str):
async def _fetch_text(url: str) -> 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: try:
async with httpx.AsyncClient(timeout=90.0) as client: async with httpx.AsyncClient(timeout=60.0) as client:
resp = await client.post( resp = await client.post(
f"{CONSENT_TESTER_URL}/dsi-discovery", 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: if resp.status_code == 200:
return "" docs = resp.json().get("documents", [])
docs = resp.json().get("documents", []) if docs:
if not docs: texts = []
return "" for doc in docs:
doc = docs[0] t = doc.get("full_text", "") or doc.get("text_preview", "") or ""
return 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: except Exception as e:
logger.warning("Text fetch failed for %s: %s", url, e) logger.warning("Consent-tester fetch failed for %s: %s", url, e)
return ""
# 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( 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]: def _get_skip_types(profile) -> dict[str, str]:
"""Doc_types to skip entirely. Currently empty — we check everything """Doc_types to skip entirely. Currently empty — we check everything
and flag irrelevant items as INFO instead of skipping.""" 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( def build_html_report(
results: list[DocCheckResult], results: list[DocCheckResult],
cookie_result: dict | None, 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", "Auto-filled %d empty rows from DSI sections: %s",
len(filled), ", ".join(filled), 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 [] return []
async def _expand_all_interactive(page: Page) -> None: 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: try:
await page.evaluate("""() => { await page.evaluate("""() => {
// 1. Open all <details> that are closed
document.querySelectorAll('details:not([open])').forEach(d => d.open = true); 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', // 2. Click buttons that are explicitly CLOSED (aria-expanded="false")
'[class*="collapse"] > button','.panel-heading a']; document.querySelectorAll('button[aria-expanded="false"]').forEach(b => {
sels.forEach(s => document.querySelectorAll(s).forEach(e => { try{e.click()}catch{} })); try { b.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{} // 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: except Exception:
pass pass
+66 -70
View File
@@ -2,8 +2,8 @@
**URL:** https://www.bmw.de **URL:** https://www.bmw.de
**Typ:** Konzern / B2C Automobil **Typ:** Konzern / B2C Automobil
**Datum:** 2026-05-12 **Datum:** 2026-05-15 (URLs + Inhalte verifiziert)
**Batch-Test:** 8/9 L1, 10/21 L2 (Mangelhaft, 48%) **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 | **ACHTUNG: BMW verteilt Rechtstexte ueber 3 Domains!**
|-------------|-----------|-----|
| DSI | Ja | https://www.bmw.de/de/footer/metanavigation/datenschutz.html | | Dokumenttyp | Domain | URL |
| 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 | | DSI | bmw.de | https://www.bmw.de/de/footer/metanavigation/data-privacy.html |
| AGB | Ja | TODO: URL verifizieren | | Impressum | bmw.de | https://www.bmw.de/de/footer/metanavigation/legal-notice-pool/imprint.html |
| Widerruf | Ggf. in AGB | — | | Cookie-Richtlinie | bmw.de | https://www.bmw.de/de/footer/footer-section/cookie-policy.html |
| Social Media DSE | Nein | — | | Legal Disclaimer / NB | bmw.de | https://www.bmw.de/de/footer/metanavigation/legal-disclaimer-pool/legal-disclaimer.html |
| Nutzungsbedingungen | Ja | TODO: URL verifizieren | | Konzern-Datenschutz + Widerruf | **bmwgroup.com** | https://www.bmwgroup.com/de/general/data_privacy.html |
| DSB-Kontakt | In DSI | — | | 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) ## Erwartete Ergebnisse: DSI (Art. 13 DSGVO)
### L1 Checks (8/9) ### L1 Checks
| Check | Erwartet | Begruendung | | Check | Erwartet | Begruendung |
|-------|----------|-------------| |-------|----------|-------------|
| Verantwortlicher | PASS | BMW AG, Muenchen | | Verantwortlicher | PASS | BMW AG, Petuelring 130, 80809 Muenchen |
| DSB | PASS | DSB erwaehnt | | DSB | PASS | datenschutz@bmw.de, Petuelring 130 |
| Zwecke | PASS | Ausfuehrlich | | Zwecke | PASS | Ausfuehrlich (Sub-Pages) |
| Rechtsgrundlage | PASS | Art. 6 Referenzen | | Rechtsgrundlage | PASS | Art. 6 Referenzen |
| Empfaenger | PASS | Kategorien aufgezaehlt | | Empfaenger | PASS | Kategorien aufgezaehlt |
| Drittlandtransfer | PASS | USA-Transfer erwaehnt | | Drittlandtransfer | PASS | USA-Transfer erwaehnt |
| Speicherdauer | PASS | Zeitangaben vorhanden | | Speicherdauer | PASS | Zeitangaben vorhanden |
| Betroffenenrechte | **FAIL** | Rechte ohne Art.-Referenzen | | Betroffenenrechte | Zu pruefen | Art. 15-21 in DSI? |
| Beschwerderecht | **FAIL** | Art. 77 nicht explizit erwaehnt | | Beschwerderecht | Zu pruefen | Art. 77 in DSI? |
### 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.**
--- ---
## Erwartete Ergebnisse: Impressum ## 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 | | Check | Erwartet |
|-------|----------| |-------|----------|
| Cookie-Arten | PASS (Essential, Analytics, Marketing) | | Firmenname | PASS (BMW AG) |
| Cookie-Zwecke | PASS | | Anschrift | PASS (Petuelring 130, 80809 Muenchen) |
| Speicherdauern | TODO: verifizieren | | Vertretung | PASS (Vorstand benannt) |
| Drittanbieter | PASS (Google, Meta etc.) | | USt-IdNr | PASS (DE129273398) |
| Rechtsgrundlage | TODO: §25 TDDDG? | | Registergericht | PASS (HRB 42243) |
| Consent-Tool | PASS (OneTrust o.ae.) | | 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 | | banner_detected | true |
| provider | OneTrust oder aehnlich | | provider | OneTrust oder aehnlich |
| violations | Mehrere (grosser Konzern mit viel Tracking) | | violations | Zu pruefen |
---
## Cross-Check Banner vs Cookie
| Finding | Erwartet |
|---------|----------|
| Dienste fehlen in Cookie-RL | Moeglich (viele Third-Party-Tracker) |
| Tracking vor Consent | Moeglich (Pre-Consent Analytics) |
--- ---
@@ -124,6 +120,6 @@
| Check | Filter | Begruendung | | Check | Filter | Begruendung |
|-------|--------|-------------| |-------|--------|-------------|
| ODR | AKTIV | B2C mit Online-Angebot | | ODR | AKTIV | B2C mit Online-Angebot |
| Widerruf | AKTIV | B2C | | Widerruf | In DSI | Marketing-Consent widerrufbar |
| Berufsrecht | SKIP | Kein regulierter Beruf | | Berufsrecht | SKIP | Kein regulierter Beruf |
| V.i.S.d.P. | AKTIV | Hat Magazine/Blog | | V.i.S.d.P. | AKTIV | Hat Magazine/Blog |
+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()