Compare commits

..

1 Commits

Author SHA1 Message Date
Benjamin Admin 576063515b feat(ai-sdk): searchControls — deep dense pull recalls control sources on implementation questions
CI / detect-changes (pull_request) Successful in 8s
CI / branch-name (pull_request) Successful in 2s
CI / guardrail-integrity (pull_request) Successful in 6s
CI / secret-scan (pull_request) Successful in 8s
CI / dep-audit (pull_request) Failing after 55s
CI / sbom-scan (pull_request) Failing after 1m1s
CI / build-sha-integrity (pull_request) Successful in 11s
CI / validate-canonical-controls (pull_request) Successful in 5s
CI / loc-budget (pull_request) Successful in 16s
CI / go-lint (pull_request) Successful in 50s
CI / python-lint (pull_request) Failing after 15s
CI / nodejs-lint (pull_request) Failing after 1m8s
CI / nodejs-build (pull_request) Successful in 3m1s
CI / test-go (pull_request) Successful in 59s
CI / iace-gt-coverage (pull_request) Successful in 15s
CI / test-python-backend (pull_request) Successful in 27s
CI / test-python-document-crawler (pull_request) Successful in 13s
CI / test-python-dsms-gateway (pull_request) Successful in 10s
Measured (raw dense, top-500, "Welche Controls passen zu Security Updates?"):
NIST at dense rank 9 (115 chunks), CRA Annex at rank 8 — both shallow, just below
the client's small top-K, so the rank layer (#38) never saw them. OWASP: absent from
the corpus (separate ingest).

Add searchControls: on an explicit implementation question (queryWantsControls) pull a
deep dense pool (depth 60, no filter), classify each hit's role in code, and keep only
the four control-pool roles (operational/procedural requirement, control standard,
implementation guidance) — no source_role tagging of the corpus. Merge-dedup into the
pool; the existing rerank + applyControlRoles then order them (op_req > procedural >
standard > guidance). So CRA Annex I (operational_requirement) lands Top-1 and NIST
(control_standard) enters Top-3/5, while ENISA stays visible. Norm questions (no control
intent) are untouched.

Tested: isControlPoolRole, controlRoleOf payload classification (NIST/CRA-Annex/DORA).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-24 14:03:45 +02:00
21 changed files with 4 additions and 1258 deletions
@@ -211,13 +211,6 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
}
for _, cat := range mp.HazardCats {
// Native cyber/AI categories (frontend groups I+J) belong to the
// CRA module, not the traditional CE (ISO 12100) hazard log.
// Enforced centrally here so it holds for EVERY project.
if isCyberSecurityCategory(cat) {
fmt.Printf("CYBER-SKIP: cat=%s pattern=%s — routed to CRA module\n", cat, mp.PatternID)
continue
}
maxForCat := categoryHazardCap(cat, len(comps))
if catCount[cat] >= maxForCat {
continue
@@ -298,10 +291,6 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
if len(mp.SuggestedMeasureIDs) > 0 {
hazardPatternMeasures[hz.ID] = mp.SuggestedMeasureIDs
}
// E1: one hazard per pattern — keep only the primary (first
// eligible) category; a secondary category would be the same
// scenario+zone under a different label (cross-category duplicate).
break
}
}
}
@@ -1,45 +0,0 @@
package handlers
// Safety/Security separation for the IACE hazard log.
//
// The traditional CE risk assessment (Maschinenrichtlinie / EN ISO 12100) and
// the cybersecurity assessment (Cyber Resilience Act) are two distinct steps.
// IACE owns the traditional, physical + functional-safety hazards; the CRA
// module (/sdk/iace/{id}/cra) owns the native cyber/AI topics and re-examines
// which safety functions a cyber attack can re-open (see iace-safety-bridge).
//
// The split is by the NATURE of the hazard, not by the component: a control
// fault, bus failure or botched update is FUNCTIONAL safety (random/systematic
// fault) and stays in CE — independent of whether the controller is a bought-in
// CE-marked PLC or the manufacturer's own embedded control. Only the security
// PROPERTIES against malicious actors (access control, firmware/update
// integrity, SBOM, vulnerability handling, default passwords) are CRA.
//
// Functional-safety control categories (software_control, software_fault,
// safety_function_failure, configuration_error, communication_failure,
// update_failure, sensor_fault, …) therefore intentionally STAY in IACE — they
// are the safety functions whose loss the CRA bridge re-examines.
//
// Enforced centrally in InitializeProject so it holds for EVERY project.
var nativeCyberSecurityCategories = map[string]bool{
// I. Cyber / Netzwerk — security against malicious actors
"unauthorized_access": true,
"firmware_corruption": true,
"cyber_resilience": true,
"logging_audit_failure": true,
"cyber_network": true,
"sensor_spoofing": true,
// J. KI-spezifisch
"ai_specific": true,
"ai_misclassification": true,
"false_classification": true,
"model_drift": true,
"data_poisoning": true,
"unintended_bias": true,
}
// isCyberSecurityCategory reports whether a hazard category is a native cyber/AI
// topic that belongs to the CRA module rather than the traditional CE hazard log.
func isCyberSecurityCategory(category string) bool {
return nativeCyberSecurityCategories[category]
}
@@ -1,37 +0,0 @@
package handlers
import "testing"
func TestIsCyberSecurityCategory_RoutedToCRA(t *testing.T) {
cyber := []string{
"unauthorized_access", "firmware_corruption", "cyber_resilience",
"logging_audit_failure", "cyber_network", "sensor_spoofing",
"ai_specific", "ai_misclassification", "false_classification",
"model_drift", "data_poisoning", "unintended_bias",
}
for _, c := range cyber {
if !isCyberSecurityCategory(c) {
t.Errorf("category %q must be routed to the CRA module, not the traditional IACE log", c)
}
}
}
func TestIsCyberSecurityCategory_StaysInIACE(t *testing.T) {
// Physical + functional-safety categories must remain in the traditional CE
// hazard log. communication_failure (bus failure -> loss of control) and
// update_failure (botched update -> lost safety function) are FUNCTIONAL
// faults, not attacks, so they stay too.
keep := []string{
"mechanical_hazard", "electrical_hazard", "thermal_hazard",
"pneumatic_hydraulic", "noise_vibration", "ergonomic_hazard",
"material_environmental", "chemical_risk", "fire_explosion",
"software_control", "software_fault", "safety_function_failure",
"configuration_error", "sensor_fault", "hmi_error",
"communication_failure", "update_failure",
}
for _, c := range keep {
if isCyberSecurityCategory(c) {
t.Errorf("category %q must stay in the traditional IACE log, not be routed to CRA", c)
}
}
}
@@ -1,182 +0,0 @@
package iace
import (
"encoding/json"
"os"
"path/filepath"
"sort"
"testing"
)
// GT #3 — commercial UNDERCOUNTER dishwasher (Winterhalter UC-M). Self-assessed
// ground truth: we can judge what a dishwasher is. The test runs the narrative
// through the SAME chain as production (ParseNarrative -> engine -> relevance
// filter + cyber-skip), so keyword/gating fixes are measured on the hazard set
// the user actually sees — not the raw pattern flood.
// Condensed UC-M limits_form narrative. Deliberately includes "Cool-Ausfuehrung"
// and "Filter" so the known false components (Kuehlaggregat, Absauganlage) are
// reproduced and visible in the baseline.
const warewashingNarrative = `Gewerbliche Untertisch-Geschirrspuelmaschine fuer Gastronomie-Kueche, ` +
`vernetzt ueber LAN und WLAN (Connected Wash Internetportal). Heisswasser-Boiler mit ` +
`Nachspueltemperatur ca. 85 Grad C, Tank mit Hygiene-Tankheizkoerper. Spuelpumpe 150-200 l/min ` +
`mit rotierenden Spuelfeldern und Spuelarmen, Ablaufpumpe. Eingebautes Dosiergeraet fuer Reiniger ` +
`und Klarspueler (aetzende Konzentrate). 4-fach-Laugenfiltration mit Filter. Doppelwandige Tuer ` +
`mit Sicherheitsschalter und Rastposition (Thermostopp). Elektromotor (Drehstrom) 400 V. ` +
`Touch-Steuerung (SPS) mit Bedienfeld und HMI, USB-Schnittstelle fuer Softwareupdates, ` +
`PIN-geschuetzter Servicetechniker-Fernzugriff. Cool-Ausfuehrung mit kalter Nachspuelung. ` +
`Untertischmontage. Eingreifen in die Spuelkammer moeglich. Aerosole und Daempfe der ` +
`Reinigungschemie gelangen in die Atemzone. Manuelles Be- und Entladen der Spuelkoerbe von Hand. ` +
`Reinigung und Wartung durch Servicetechniker. Branche Lebensmittel und Getraenke. ` +
`Siebe und scharfe Blechkanten in der Spuelkammer. Boiler kann bei Wassermangel trockenlaufen. ` +
`Frequenzumrichter und Elektronik mit Restspannung nach dem Abschalten. Wartung nur im ` +
`freigeschalteten Zustand; Gefahr des unerwarteten Wiederanlaufs. Frischwasseranschluss mit ` +
`Rueckflussverhinderer gegen Ruecksaugen in das Trinkwassernetz. Stehwasser im Boiler ` +
`(Hygiene/Legionellen). Standsicherheit bei Untertischmontage.`
// warewashingCyberCategories mirrors handlers.nativeCyberSecurityCategories —
// native cyber/AI hazards are routed to the CRA module, not the CE hazard log.
var warewashingCyberCategories = map[string]bool{
"unauthorized_access": true, "firmware_corruption": true, "cyber_resilience": true,
"logging_audit_failure": true, "cyber_network": true, "sensor_spoofing": true,
"ai_specific": true, "ai_misclassification": true, "false_classification": true,
"model_drift": true, "data_poisoning": true, "unintended_bias": true,
}
// warewashingEngineOutput runs the production chain and returns the filtered
// hazards/mitigations the user would see for the UC-M.
func warewashingEngineOutput() ([]Hazard, []Mitigation, int) {
res := ParseNarrative(warewashingNarrative, "Gewerbliche Untertisch-Geschirrspuelmaschine (vernetzt)")
var compIDs, compNames []string
for _, c := range res.Components {
if c.Negated {
continue
}
compIDs = append(compIDs, c.LibraryID)
compNames = append(compNames, c.NameDE)
}
var energyIDs []string
for _, e := range res.EnergySources {
energyIDs = append(energyIDs, e.SourceID)
}
lifecycles := append([]string{}, res.LifecyclePhases...)
lifecycles = append(lifecycles, "normal_operation", "maintenance", "cleaning", "setup", "fault_clearing")
input := MatchInput{
ComponentLibraryIDs: compIDs,
EnergySourceIDs: energyIDs,
LifecyclePhases: lifecycles,
CustomTags: res.CustomTags,
OperationalStates: append(res.OperationalStates, "normal_operation", "cleaning", "maintenance"),
HumanRoles: res.Roles,
MachineTypes: []string{"food_processing", "Gewerbliche Untertisch-Geschirrspuelmaschine (vernetzt)"},
}
out := NewPatternEngine().Match(input)
var kept []PatternMatch
for _, pm := range out.MatchedPatterns {
if !IsPatternRelevant(pm, warewashingNarrative, compNames) {
continue
}
allCyber := len(pm.HazardCats) > 0
for _, c := range pm.HazardCats {
if !warewashingCyberCategories[c] {
allCyber = false
}
}
if allCyber {
continue
}
kept = append(kept, pm)
}
filtered := *out
filtered.MatchedPatterns = kept
hazards, mitigations := patternsToHazardsAndMitigations(&filtered)
return hazards, mitigations, len(kept)
}
func TestWarewashing_GTCoverage(t *testing.T) {
gtPath := filepath.Join("testdata", "ground_truth_warewashing.json")
raw, err := os.ReadFile(gtPath)
if err != nil {
t.Fatalf("read GT: %v", err)
}
var gt GroundTruth
if err := json.Unmarshal(raw, &gt); err != nil {
t.Fatalf("parse GT: %v", err)
}
{
res := ParseNarrative(warewashingNarrative, "Gewerbliche Untertisch-Geschirrspuelmaschine (vernetzt)")
var cn []string
for _, c := range res.Components {
if !c.Negated {
cn = append(cn, c.NameDE)
}
}
t.Logf("Parsed components: %v", cn)
}
hazards, mitigations, nPatterns := warewashingEngineOutput()
t.Logf("Engine: %d patterns kept (relevance+cyber filter) -> %d hazards", nPatterns, len(hazards))
result := CompareBenchmark(&gt, hazards, mitigations)
precision := 0.0
if result.TotalEngine > 0 {
precision = float64(len(result.MatchedPairs)) / float64(result.TotalEngine)
}
t.Logf("=== Warewashing-GT (GT #3) Baseline ===")
t.Logf("Recall (Coverage): %.1f%% (%d/%d matched, %d missing)",
result.CoverageScore*100, len(result.MatchedPairs), result.TotalGT, len(result.MissingFromEngine))
t.Logf("Precision: %.1f%% (%d engine hazards, %d extra)",
precision*100, result.TotalEngine, len(result.ExtraInEngine))
if len(result.MissingFromEngine) > 0 {
t.Logf("--- MISSING (recall gaps) ---")
for _, m := range result.MissingFromEngine {
t.Logf(" MISS %s: %s", m.Nr, abbrev(m.HazardType, 60))
}
}
// Measure completeness: which generated hazards have NO protective measure?
t.Logf("--- Measure completeness ---")
t.Logf("Measure coverage (GT-matched): %.0f%%", result.MeasureCoverage*100)
withMeas := make(map[string]bool)
for _, m := range mitigations {
withMeas[m.HazardID.String()] = true
}
noMeasure := 0
for _, h := range hazards {
if !withMeas[h.ID.String()] {
noMeasure++
n := h.Name
if n == "" {
n = h.Scenario
}
t.Logf(" NO-MEASURE: [%s] %s", h.Category, abbrev(n, 60))
}
}
t.Logf("Hazards without any measure: %d/%d", noMeasure, len(hazards))
if len(result.ExtraInEngine) > 0 {
t.Logf("--- EXTRA (false positives / precision loss) ---")
names := make([]string, 0, len(result.ExtraInEngine))
for _, e := range result.ExtraInEngine {
n := e.Name
if n == "" {
n = e.Scenario
}
names = append(names, "["+e.Category+"] "+n)
}
sort.Strings(names)
for _, n := range names {
t.Logf(" EXTRA %s", abbrev(n, 85))
}
}
// Loose smoke floor for the baseline — fixes should push recall up, not down.
if result.CoverageScore < 0.4 {
t.Errorf("warewashing recall below 40%% floor: %.1f%%", result.CoverageScore*100)
}
}
@@ -62,13 +62,6 @@ type HazardPattern struct {
// "hazard" = source only, "hazardous_situation" = person exposed, "harm" = injury.
// Empty = default (hazardous_situation).
GeneratedHazardType string `json:"generated_hazard_type,omitempty"`
// GuardableByEnclosure marks a contact/entanglement hazard that an interlocked
// enclosure removes during normal operation. When the project emits the
// "interlocked_enclosure" tag, such a pattern is re-scoped to maintenance/
// cleaning (guard open) and does NOT fire as a normal-operation hazard.
// Generic EN ISO 14120 logic — surfaced by the warewashing GT (the spray
// arm rotates behind the interlocked door).
GuardableByEnclosure bool `json:"guardable_by_enclosure,omitempty"`
// RequiredFailureModes restricts this pattern to fire only when at least one
// of the listed failure modes is relevant (by ComponentType match against project components).
// Empty/nil = fires regardless of failure modes (backwards compatible).
@@ -37,7 +37,6 @@ func GetDGUVExtendedPatterns() []HazardPattern {
},
{
ID: "HP096", NameDE: "Reibung/Abrieb durch rotierende Oberflaechen", NameEN: "Friction/abrasion by rotating surfaces",
GuardableByEnclosure: true,
RequiredComponentTags: []string{"rotating_part"},
RequiredEnergyTags: []string{},
GeneratedHazardCats: []string{"mechanical_hazard"},
@@ -89,7 +88,6 @@ func GetDGUVExtendedPatterns() []HazardPattern {
},
{
ID: "HP101", NameDE: "Aufwickeln von Kleidung/Haaren", NameEN: "Winding up of clothing/hair",
GuardableByEnclosure: true,
RequiredComponentTags: []string{"rotating_part"},
RequiredEnergyTags: []string{"rotational"},
GeneratedHazardCats: []string{"mechanical_hazard"},
@@ -1,178 +0,0 @@
package iace
// GetWarewashingPatterns returns hazard patterns for commercial warewashing
// machines (gewerbliche Geschirrspuelmaschinen / Untertisch-, Hauben-, Korb-
// und Bandspuelmaschinen). These capture the machine-specific hazards a
// Fachmann immediately expects but that the generic library did not cover:
// hot-water/steam scalding on door opening, hot surfaces, hot ware, corrosive
// detergent/rinse-aid contact, door pinch and wet-floor slipping.
//
// Every pattern is gated by the capability tag "dom_warewashing" (emitted only
// by warewashing narrative keywords in keyword_dictionary.go), so none of these
// leak into unrelated machine classes.
//
// HP range: HP2200-HP2206. ISO 12100 Annex B section identifiers only (facts);
// product standard EN 60335-2-58 (commercial dishwashing machines).
func GetWarewashingPatterns() []HazardPattern {
return []HazardPattern{
{
ID: "HP2200", NameDE: "Verbruehung durch Heisswasser/Dampf beim Oeffnen der Tuer", NameEN: "Scalding by hot water/steam when opening the door",
RequiredComponentTags: []string{"dom_warewashing", "steam_emission"},
GeneratedHazardCats: []string{"thermal_hazard"},
SuggestedMeasureIDs: []string{"M2200", "M2201", "M2202", "M2208"},
Priority: 94,
ApplicableLifecycles: []string{"normal_operation", "cleaning"},
ScenarioDE: "Beim Oeffnen der Tuer waehrend oder unmittelbar nach dem Spuelgang tritt ein Schwall aus heissem Wasser und Wrasen (Dampf) aus der Spuelkammer aus und trifft Gesicht, Haende und Arme des Bedieners.",
TriggerDE: "Tuer wird vor Programmende oder bei noch vorhandenem Restdampf geoeffnet; Tuerverriegelung fehlt oder ist ueberbrueckt; Nachspueltemperatur ca. 85 Grad C.",
HarmDE: "Verbruehung 1.-2. Grades an Gesicht, Haenden und Unterarmen; Augenreizung durch heissen Dampf.",
AffectedDE: "Bedienpersonal (Spuelkraft)",
ZoneDE: "Tuer- und Beschickungsoeffnung der Spuelkammer",
ISO12100Section: "6.2.4",
DefaultSeverity: 3, DefaultExposure: 4,
},
{
ID: "HP2201", NameDE: "Verbrennung an heissen Oberflaechen (Boiler/Tank/Spuelkammer)", NameEN: "Burn on hot surfaces (boiler/tank/wash chamber)",
RequiredComponentTags: []string{"dom_warewashing", "high_temperature"},
GeneratedHazardCats: []string{"thermal_hazard"},
SuggestedMeasureIDs: []string{"M2202", "M055", "M2208"},
Priority: 90,
ApplicableLifecycles: []string{"cleaning", "maintenance"},
ScenarioDE: "Beruehrung heisser Oberflaechen von Boiler, Tankheizkoerper oder Spuelkammerwaenden bei Reinigung, Entkalkung oder Wartung fuehrt zu Kontaktverbrennungen.",
TriggerDE: "Reinigung/Entkalkung ohne Abkuehlzeit; Eingriff in die Spuelkammer bei betriebswarmem Geraet.",
HarmDE: "Kontaktverbrennung an Haenden und Unterarmen.",
AffectedDE: "Reinigungspersonal, Wartungspersonal",
ZoneDE: "Boiler, Tankheizkoerper, Spuelkammerwaende",
ISO12100Section: "6.2.4",
DefaultSeverity: 2, DefaultExposure: 3,
},
{
ID: "HP2202", NameDE: "Verbrennung an heissem Spuelgut beim Entladen", NameEN: "Burn on hot ware when unloading",
RequiredComponentTags: []string{"dom_warewashing", "hot_water"},
GeneratedHazardCats: []string{"thermal_hazard"},
SuggestedMeasureIDs: []string{"M2202", "M055", "M2208"},
Priority: 86,
ApplicableLifecycles: []string{"normal_operation"},
ScenarioDE: "Geschirr, Glaeser und Bestecke sind nach dem Spuelgang durch die Heisswasser-Nachspuelung sehr heiss; beim Entladen kommt es zu Verbrennungen.",
TriggerDE: "Sofortiges Entnehmen des Spuelguts nach Programmende ohne Abkuehl-/Trocknungszeit.",
HarmDE: "Verbrennung an Haenden/Fingern beim Greifen heisser Teile.",
AffectedDE: "Bedienpersonal (Spuelkraft)",
ZoneDE: "Spuelkammer, Entnahmebereich/Korb",
ISO12100Section: "6.2.4",
DefaultSeverity: 2, DefaultExposure: 3,
},
{
ID: "HP2203", NameDE: "Chemische Veraetzung (Haut/Augen) durch Reiniger-/Klarspueler-Konzentrat", NameEN: "Chemical burn (skin/eyes) from detergent/rinse-aid concentrate",
RequiredComponentTags: []string{"dom_warewashing", "corrosive_chemical"},
GeneratedHazardCats: []string{"chemical_risk"},
SuggestedMeasureIDs: []string{"M2203", "M2204", "M2208"},
Priority: 92,
ApplicableLifecycles: []string{"normal_operation", "maintenance"},
ScenarioDE: "Direkter Kontakt mit dem aetzenden (alkalischen) Reiniger- bzw. Klarspueler-Konzentrat beim Nachfuellen, Sauglanzenwechsel oder bei Leckage fuehrt zu Veraetzungen von Haut und Augen.",
TriggerDE: "Gebinde-/Sauglanzenwechsel ohne Schutzausruestung; Umfuellen von Konzentrat; undichte Dosierleitung.",
HarmDE: "Veraetzung von Haut und Augen (alkalische Verletzung), bleibende Augenschaeden moeglich.",
AffectedDE: "Bedienpersonal, Reinigungspersonal beim Chemikalien-Handling",
ZoneDE: "Dosiergeraet, Reiniger-/Klarspueler-Gebinde, Sauglanzen",
ISO12100Section: "6.2.4",
DefaultSeverity: 3, DefaultExposure: 3,
ClarificationQuestionsDE: []string{
"Liegt fuer alle eingesetzten Reiniger/Klarspueler/Entkalker ein aktuelles Sicherheitsdatenblatt (SDB) am Geraet vor?",
"Ist ein geschlossenes Dosiersystem mit Sauglanzen vorhanden, sodass kein Umfuellen noetig ist?",
},
},
{
ID: "HP2204", NameDE: "Reizung/Veraetzung der Atemwege durch Reinigungs-Aerosole/Daempfe", NameEN: "Respiratory irritation from cleaning aerosols/vapours",
RequiredComponentTags: []string{"dom_warewashing", "corrosive_chemical"},
GeneratedHazardCats: []string{"chemical_risk"},
SuggestedMeasureIDs: []string{"M2205", "M2203", "M2204"},
Priority: 82,
ApplicableLifecycles: []string{"normal_operation", "maintenance"},
ScenarioDE: "Aerosole und Daempfe der Reinigungschemie (insbesondere beim Oeffnen kurz nach dem Spuelgang oder bei der Entkalkung mit Saeure) gelangen in die Atemzone und reizen Atemwege und Schleimhaeute.",
TriggerDE: "Oeffnen bei laufender/heisser Chemie; Entkalkung mit Saeure; unzureichende Lueftung des Aufstellbereichs.",
HarmDE: "Reizung von Atemwegen, Augen und Schleimhaeuten; bei Saeure-/Laugen-Vermischung gefaehrliche Gase.",
AffectedDE: "Bedienpersonal, Reinigungspersonal",
ZoneDE: "Atemzone vor der Spuelkammer, Aufstellbereich",
ISO12100Section: "6.2.4",
DefaultSeverity: 2, DefaultExposure: 2,
ClarificationQuestionsDE: []string{
"Ist der Aufstellbereich ausreichend be-/entlueftet (Kuechenlueftung)?",
"Wird in der BA vor dem Vermischen von Reiniger und Entkalker/Saeure gewarnt?",
},
},
{
ID: "HP2205", NameDE: "Quetschen der Finger an der Tuer/Haube", NameEN: "Finger crushing at the door/hood",
RequiredComponentTags: []string{"dom_warewashing", "access_door"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M2206", "M003", "M2208"},
Priority: 78,
ApplicableLifecycles: []string{"normal_operation"},
ScenarioDE: "Beim Schliessen der Tuer bzw. Absenken der Haube werden Finger zwischen Tuer/Haube und Gehaeuse gequetscht.",
TriggerDE: "Greifen in den Schliessbereich beim Schliessen; hohe Schliesskraft der Haube; scharfe Kanten.",
HarmDE: "Quetschung und Prellung der Finger.",
AffectedDE: "Bedienpersonal (Spuelkraft)",
ZoneDE: "Tuer-/Haubenkante, Schliessbereich",
ISO12100Section: "6.2.3",
DefaultSeverity: 1, DefaultExposure: 3,
},
{
ID: "HP2206", NameDE: "Ausrutschen auf nassem Boden (Wasseraustritt/Leckage)", NameEN: "Slipping on wet floor (water leakage)",
RequiredComponentTags: []string{"dom_warewashing"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M2207", "M538", "M2208"},
Priority: 76,
ApplicableLifecycles: []string{"normal_operation", "cleaning", "maintenance"},
ScenarioDE: "Aus der Spuelmaschine austretendes Wasser (Beschickung, Tuer oeffnen, Leckage, Tankwasserwechsel) macht den Boden im Aufstellbereich rutschig; der Bediener rutscht aus.",
TriggerDE: "Wasseraustritt beim Oeffnen/Beschicken; undichter Ablauf; fehlender Bodenablauf.",
HarmDE: "Sturz mit Prellungen, Knochenbruechen oder Kopfaufprall.",
AffectedDE: "Bedienpersonal, Reinigungspersonal",
ZoneDE: "Aufstell- und Bedienbereich der Spuelmaschine",
ISO12100Section: "6.3.5.6",
DefaultSeverity: 2, DefaultExposure: 3,
},
{
ID: "HP2207", NameDE: "Rueckfluss / Kontamination des Trinkwassers", NameEN: "Backflow / potable-water contamination",
RequiredComponentTags: []string{"dom_warewashing", "backflow_risk"},
GeneratedHazardCats: []string{"material_environmental"},
SuggestedMeasureIDs: []string{"M2209"},
Priority: 84,
ApplicableLifecycles: []string{"normal_operation"},
ScenarioDE: "Verschmutztes Spuel- oder Chemiewasser wird ueber den Frischwasseranschluss in das Trinkwassernetz zurueckgesaugt und kontaminiert es (Ruecksaugen bei Unterdruck im Netz).",
TriggerDE: "Fehlender oder defekter Rueckflussverhinderer/Systemtrenner; Unterdruck im Trinkwassernetz; kein freier Auslauf.",
HarmDE: "Gesundheitsgefaehrdung Dritter durch kontaminiertes Trinkwasser (Chemie, Keime).",
AffectedDE: "Verbraucher am selben Trinkwassernetz, Betreiber",
ZoneDE: "Frischwasseranschluss, Wasserzulauf",
ISO12100Section: "6.2.4",
DefaultSeverity: 3, DefaultExposure: 2,
},
{
ID: "HP2208", NameDE: "Schnittverletzung an scharfen Kanten/Sieben", NameEN: "Cut injury on sharp edges/screens",
RequiredComponentTags: []string{"dom_warewashing", "sharp_edge"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M003"},
Priority: 74,
ApplicableLifecycles: []string{"cleaning", "maintenance"},
ScenarioDE: "Schneiden an scharfen Blechkanten, Sieben oder dem Ablaufpumpen-Laufrad beim Reinigen oder Eingreifen in die Spuelkammer.",
TriggerDE: "Entnehmen/Reinigen der Siebe; Eingreifen an scharfen Kanten ohne Schutzhandschuhe.",
HarmDE: "Schnittwunden an Haenden und Fingern.",
AffectedDE: "Reinigungspersonal, Bedienpersonal",
ZoneDE: "Zugaengliche Kanten, Siebe, Spuelkammer, Ablaufpumpe",
ISO12100Section: "6.2.2.1",
DefaultSeverity: 1, DefaultExposure: 3,
},
{
ID: "HP2209", NameDE: "Unerwarteter Wiederanlauf bei Wartung/Reinigung", NameEN: "Unexpected restart during maintenance/cleaning",
RequiredComponentTags: []string{"dom_warewashing", "programmable"},
RequiredLifecycles: []string{"maintenance", "cleaning", "fault_clearing"},
GeneratedHazardCats: []string{"safety_function_failure"},
SuggestedMeasureIDs: []string{"M042"},
Priority: 80,
ApplicableLifecycles: []string{"maintenance", "cleaning"},
ScenarioDE: "Waehrend Wartung oder Reinigung laeuft die Maschine durch fehlende Freischaltung (LOTO) oder automatischen Wiederanlauf unerwartet an (Pumpe, Spuelgang).",
TriggerDE: "Kein Freischalten/Sichern gegen Wiedereinschalten; automatischer Wiederanlauf nach Netzunterbrechung.",
HarmDE: "Verbruehung, Quetschen oder elektrischer Schlag durch unerwartet anlaufende Maschine.",
AffectedDE: "Wartungspersonal, Reinigungspersonal",
ZoneDE: "Gesamte Maschine, Pumpe, Antriebe",
ISO12100Section: "6.2.11.4",
DefaultSeverity: 3, DefaultExposure: 2,
},
}
}
@@ -1,112 +0,0 @@
package iace
import "testing"
// firedSet runs the engine for the given custom tags and returns the set of
// fired pattern IDs.
func firedSet(customTags []string) map[string]bool {
engine := NewPatternEngine()
out := engine.Match(MatchInput{CustomTags: customTags})
fired := make(map[string]bool, len(out.MatchedPatterns))
for _, m := range out.MatchedPatterns {
fired[m.PatternID] = true
}
return fired
}
// A warewashing narrative emits these capability + functional tags.
var warewashingTags = []string{
"dom_warewashing", "steam_emission", "hot_water", "high_temperature",
"corrosive_chemical", "access_door", "rotating_part",
}
func TestWarewashing_PatternsFireForDishwasher(t *testing.T) {
fired := firedSet(warewashingTags)
want := []string{"HP2200", "HP2201", "HP2202", "HP2203", "HP2204", "HP2205", "HP2206"}
for _, id := range want {
if !fired[id] {
t.Errorf("expected warewashing pattern %s to fire for a dishwasher, but it did not", id)
}
}
}
func TestWarewashing_PatternsDoNotLeakIntoOtherMachines(t *testing.T) {
// A machine with thermal + electrical + chemical capability but NOT a
// dishwasher must never produce warewashing hazards (dom_warewashing gate).
fired := firedSet([]string{"high_temperature", "electrical_part", "chemical_risk", "rotating_part", "moving_part"})
for _, id := range []string{"HP2200", "HP2201", "HP2202", "HP2203", "HP2204", "HP2205", "HP2206"} {
if fired[id] {
t.Errorf("warewashing pattern %s leaked into a non-dishwasher machine", id)
}
}
}
func TestWarewashing_WeldingAndGlueDoNotLeakIntoDishwasher(t *testing.T) {
// The gate-term additions must stop the welding/flame/glue burn patterns
// from firing for a dishwasher (they previously leaked via high_temperature
// / electrical_part). dom_welding/dom_flame/dom_glue are absent here.
fired := firedSet(warewashingTags)
leak := map[string]string{
"HP530": "Lichtbogen-Verbrennung (Schweissen)",
"HP532": "Schweissrauch",
"HP533": "Brand durch Schweissfunken (Schweissen)",
}
for id, name := range leak {
if fired[id] {
t.Errorf("cross-domain pattern %s (%s) leaked into a dishwasher", id, name)
}
}
}
func TestWarewashing_MeasureIDsExist(t *testing.T) {
lib := GetProtectiveMeasureLibrary()
have := make(map[string]bool, len(lib))
for _, m := range lib {
have[m.ID] = true
}
for _, p := range GetWarewashingPatterns() {
for _, mid := range p.SuggestedMeasureIDs {
if !have[mid] {
t.Errorf("pattern %s references measure %s which is not in the library", p.ID, mid)
}
}
}
}
func TestWarewashing_NarrativeEmitsTags(t *testing.T) {
// Closes the loop: a realistic dishwasher description must emit the tags
// the warewashing patterns gate on (otherwise the patterns are dead).
narrative := "Gewerbliche Untertisch-Geschirrspuelmaschine mit Heisswasser-Boiler " +
"und Nachspuelung ca. 85 Grad C, Spuelpumpe mit rotierenden Spuelfeldern, " +
"Dampf-/Wrasenabgabe beim Oeffnen, Reiniger und Klarspueler ueber Dosiergeraet, " +
"Tuer mit Sicherheitsschalter, Eingreifen in die Spuelkammer."
res := ParseNarrative(narrative, "Gewerbliche Geschirrspuelmaschine")
got := make(map[string]bool, len(res.CustomTags))
for _, tag := range res.CustomTags {
got[tag] = true
}
for _, want := range []string{"dom_warewashing", "steam_emission", "hot_water", "corrosive_chemical", "access_door", "rotating_part"} {
if !got[want] {
t.Errorf("narrative did not emit expected tag %q (got %v)", want, res.CustomTags)
}
}
// And it must NOT emit any welding/flame/glue domain that would re-open leaks.
for _, bad := range []string{"dom_welding", "dom_flame", "dom_glue"} {
if got[bad] {
t.Errorf("dishwasher narrative unexpectedly emitted cross-domain tag %q", bad)
}
}
}
func TestWarewashing_NewMeasuresPresent(t *testing.T) {
lib := GetProtectiveMeasureLibrary()
have := make(map[string]bool, len(lib))
for _, m := range lib {
have[m.ID] = true
}
for _, mid := range []string{"M2200", "M2201", "M2202", "M2203", "M2204", "M2205", "M2206", "M2207", "M2208"} {
if !have[mid] {
t.Errorf("expected warewashing measure %s to be registered in the library", mid)
}
}
}
@@ -88,28 +88,6 @@ func GetKeywordDictionary() []KeywordEntry {
{Keywords: []string{"folienwickler", "wickelmaschine", "konfektioniermaschine", "folienverpackung", "wellpappe"}, ExtraTags: []string{"dom_converting"}},
{Keywords: []string{"bergbau", "untertage", "tunnelbau", "off-grid"}, ExtraTags: []string{"dom_remote"}},
{Keywords: []string{"asbest", "asbestsanierung", "asbestexposition"}, ExtraTags: []string{"dom_asbestos"}},
{Keywords: []string{"gasbrenner", "brennerbetrieb", "offene flamme", "flammhaert", "abflammen", "flammrichten"}, ExtraTags: []string{"dom_flame"}},
{Keywords: []string{"heissleim", "heissleimanlage", "schmelzkleber", "schmelzklebstoff", "klebstoffschmelzer", "leimwerk"}, ExtraTags: []string{"dom_glue"}},
// ── Gewerbliche Spuelmaschine / Warewashing ──────────────────────
// dom_warewashing gates the warewashing-specific patterns
// (hazard_patterns_warewashing.go) so they never leak into other
// machine classes. The functional tags (hot_water, steam_emission,
// corrosive_chemical, access_door) are the within-domain triggers.
{Keywords: []string{"spuelmaschine", "geschirrspuelmaschine", "geschirrspueler", "haubenspuelmaschine", "untertischspuelmaschine", "korbspuelmaschine", "bandspuelmaschine", "glaeserspuelmaschine", "bistrospuelmaschine", "warewashing", "dishwasher"}, ExtraTags: []string{"dom_warewashing"}},
{Keywords: []string{"heisswasser", "nachspuelung", "nachspueltemperatur", "spuelgang", "spuelzyklus", "thermostopp", "thermostop"}, ExtraTags: []string{"hot_water", "high_temperature"}},
{Keywords: []string{"dampf", "wrasen", "schwaden", "brueden"}, ExtraTags: []string{"steam_emission", "high_temperature"}},
{Keywords: []string{"boiler", "spuelboiler", "nachspuelboiler", "tankheiz", "boilerheiz"}, ComponentIDs: []string{"C094"}, ExtraTags: []string{"heating_element", "high_temperature"}},
{Keywords: []string{"reiniger", "klarspueler", "spuelmittel", "reinigungsmittel", "reinigerkonzentrat", "spuelchemie", "dosiergeraet", "dosierpumpe", "sauglanze", "entkalker"}, ExtraTags: []string{"corrosive_chemical"}},
// Spuelarm/Spuelfeld emit only the rotating_part capability tag. They are
// NOT mapped to a library component — C004 is a "Drehtisch" (rotary table)
// and that mislabels the spray arm. Keyword->component must be semantically
// honest (generic hygiene; surfaced by the warewashing GT).
{Keywords: []string{"spuelarm", "spuelfeld", "wascharm", "spruehfeld"}, ExtraTags: []string{"rotating_part"}},
{Keywords: []string{"spuelkammer", "spueltuer", "geraetetuer", "haubentuer", "klapptuer"}, ExtraTags: []string{"access_door"}},
// Frischwasseranschluss an das Trinkwassernetz -> Rueckfluss/Ruecksaug-Risiko (EN 1717).
{Keywords: []string{"rueckfluss", "rueckflussverhinderer", "ruecksaug", "trinkwasser", "frischwasseranschluss", "systemtrenner"}, ExtraTags: []string{"backflow_risk"}},
{Keywords: []string{"scharfe kante", "scharfkant", "blechkante", "scharfe blechkante", "sieb", "siebe"}, ExtraTags: []string{"sharp_edge"}},
// Ghost-Closure (Emit-Seite): macht die 34 toten Required-Tags
// emittierbar, jeweils NUR via domaenenspezifische Keywords -> die 120
// Ghost-Patterns feuern wieder, aber nur fuer ihre echte Maschine (kein
@@ -204,12 +182,6 @@ func GetKeywordDictionary() []KeywordEntry {
{Keywords: []string{"lichtgitter", "lichtvorhang", "light curtain", "light grid"}, ComponentIDs: []string{"C102"}, ExtraTags: []string{"safety_device"}},
{Keywords: []string{"sicherheitsschalter", "safety switch"}, ComponentIDs: []string{"C104"}, ExtraTags: []string{"safety_device", "interlocked"}},
{Keywords: []string{"zuhaltung", "guard locking", "interlock"}, ComponentIDs: []string{"C105"}, ExtraTags: []string{"safety_device", "interlocked"}},
// interlocked_enclosure signals that moving parts are inaccessible behind a
// guard that is monitored/locked — feeds the GuardableByEnclosure re-scoping
// (contact/entanglement becomes a maintenance/guard-open hazard, not a
// normal-operation one). Emitted only by explicit "interlocked door/guard"
// vocabulary so it does not trigger for machines with exposed motion.
{Keywords: []string{"tuer mit sicherheitsschalter", "verriegelte tuer", "verriegelte haube", "verriegelte einhausung", "sicherheitstuer", "tuerverriegelung", "haube mit sicherheitsschalter"}, ExtraTags: []string{"interlocked_enclosure"}},
{Keywords: []string{"zweihand", "two-hand", "zweihandschaltung"}, ComponentIDs: []string{"C106"}, ExtraTags: []string{"safety_device", "two_hand_control_required"}},
{Keywords: []string{"schaltmatte", "safety mat"}, ComponentIDs: []string{"C108"}, ExtraTags: []string{"safety_device"}},
{Keywords: []string{"seilzug", "pull wire"}, ComponentIDs: []string{"C109"}, ExtraTags: []string{"safety_device"}},
@@ -222,9 +194,7 @@ func GetKeywordDictionary() []KeywordEntry {
// ── Absaugung / Umwelt ──────────────────────────────────────────
{Keywords: []string{"absaug", "extraction", "abscheider"}, ComponentIDs: []string{"C124"}, ExtraTags: []string{"noise_source"}},
// "filteranlage" only — bare "filter" falsely mapped any filter (Laugen-,
// Wasser-, Oel-, Netzfilter) to the oil-mist extractor C124.
{Keywords: []string{"filteranlage"}, ComponentIDs: []string{"C124"}, ExtraTags: []string{}},
{Keywords: []string{"filter", "filteranlage"}, ComponentIDs: []string{"C124"}, ExtraTags: []string{}},
// ── IT / Netzwerk ───────────────────────────────────────────────
{Keywords: []string{"switch", "netzwerk"}, ComponentIDs: []string{"C111"}, ExtraTags: []string{"networked"}},
@@ -253,10 +223,7 @@ func GetKeywordDictionary() []KeywordEntry {
{Keywords: []string{"biege", "bend"}, ComponentIDs: []string{"C019"}, ExtraTags: []string{"high_force"}},
{Keywords: []string{"stanz", "stamp", "punch"}, ComponentIDs: []string{"C018"}, ExtraTags: []string{"high_force", "crush_point"}},
{Keywords: []string{"heiz", "heater", "heating"}, ComponentIDs: []string{"C094"}, EnergyIDs: []string{"EN06"}, ExtraTags: []string{"high_temperature"}},
// Cooling UNIT only — not the bare adjectives "kuehl"/"cool", which falsely
// matched product-variant names ("Cool-Ausfuehrung") and outputs ("kuehle
// Glaeser"). Keyword->component must name an actual component.
{Keywords: []string{"kuehlaggregat", "kuehlanlage", "kuehler", "kaeltemaschine", "chiller", "rueckkuehl"}, ComponentIDs: []string{"C095"}, ExtraTags: []string{}},
{Keywords: []string{"kuehl", "cool"}, ComponentIDs: []string{"C095"}, ExtraTags: []string{}},
{Keywords: []string{"luefter", "fan", "geblaese"}, ComponentIDs: []string{"C096"}, ExtraTags: []string{"rotating_part", "noise_source"}},
{Keywords: []string{"spannvorrichtung", "fixture", "clamp"}, ComponentIDs: []string{"C100"}, ExtraTags: []string{"clamping_part"}},
@@ -22,7 +22,6 @@ func GetProtectiveMeasureLibrary() []ProtectiveMeasureEntry {
all = append(all, getGTBremseMeasures()...) // GT-Bremse-Coverage-Gaps (M483-M522)
all = append(all, GetCRAMeasures()...) // CRA / DIN EN 40000-1-2 cyber-resilience (M540-M548)
all = append(all, getLiftEndstopMeasures()...) // Lift/hoist endstop (M600-M604) — bridges OSHA MD library
all = append(all, getWarewashingMeasures()...) // Commercial dishwasher (M2200-M2208) — scald/chemical/door/slip
return all
}
@@ -1,75 +0,0 @@
package iace
// getWarewashingMeasures returns protective measures for commercial warewashing
// machines (gewerbliche Geschirrspuelmaschinen): hot-water/steam scalding,
// hot surfaces, corrosive cleaning chemicals, door pinch and wet-floor slip.
// They complement the generic thermal/mechanical/material measures with the
// machine-specific controls a Fachmann expects for this product class.
//
// M-ID range: M2200-M2208. Norm identifiers only (facts) — no norm text is
// reproduced (DIN/Beuth license). Lead standard: EN 60335-2-58 (safety of
// commercial electric dishwashing machines).
func getWarewashingMeasures() []ProtectiveMeasureEntry {
return []ProtectiveMeasureEntry{
{ID: "M2200", ReductionType: "design", SubType: "interlock",
Name: "Tuer-/Haubenverriegelung beendet Spuelgang vor dem Oeffnen",
Description: "Die Tuer bzw. Haube ist so mit der Steuerung verriegelt, dass beim Oeffnen Spuelpumpe und Nachspuelung sofort abschalten und ein Oeffnen erst nach Programmende (bzw. nach Abbau des Restdampfs) freigegeben wird. Verhindert den Schwall aus Heisswasser/Wrasen und den Kontakt mit noch rotierenden Spuelfeldern.",
HazardCategory: "thermal",
Examples: []string{"Tuerkontaktschalter schaltet Pumpe + Heizung beim Oeffnen ab", "Rastposition mit Restdampf-Verzoegerung vor Freigabe"},
NormReferences: []string{"EN 60335-2-58", "EN ISO 12100 — Inhaerent sichere Konstruktion"}},
{ID: "M2201", ReductionType: "design", SubType: "thermal",
Name: "Wrasen-/Dampfreduzierung (Kondensations- / Waermerueckgewinnungssystem)",
Description: "Der beim Oeffnen austretende Wrasen wird durch ein Kondensations- bzw. Waermerueckgewinnungssystem reduziert, sodass beim Entnehmen kein gefaehrlicher Dampfschwall entsteht. Senkt zugleich die Restwaerme- und Feuchtebelastung am Arbeitsplatz.",
HazardCategory: "thermal",
Examples: []string{"Umluft-Waermerueckgewinnung reduziert austretenden Wrasen", "Kondensationshaube ueber der Spuelkammer"},
NormReferences: []string{"EN 60335-2-58"}},
{ID: "M2202", ReductionType: "protection", SubType: "monitoring",
Name: "Thermostop / Temperaturueberwachung von Boiler und Tank",
Description: "Boiler- und Tanktemperatur werden ueberwacht; ein Thermostop gibt den naechsten Schritt erst frei, wenn die Solltemperatur erreicht ist, und begrenzt die maximale Nachspueltemperatur. Schuetzt vor Verbruehung durch unkontrolliert heisses Nachspuelwasser.",
HazardCategory: "thermal",
Examples: []string{"Temperatursensor in Boiler und Tank mit Abschaltgrenze", "Thermostop-Funktion im Spuelprogramm"},
NormReferences: []string{"EN 60335-2-58", "EN ISO 13732-1"}},
{ID: "M2203", ReductionType: "design", SubType: "containment",
Name: "Geschlossenes Dosiersystem mit Sauglanzen und Niveauueberwachung",
Description: "Reiniger und Klarspueler werden ausschliesslich ueber ein geschlossenes Dosiersystem mit Sauglanzen aus dem Originalgebinde gefoerdert (Niveau-Ueberwachung statt Umfuellen). Direkter Haut-/Augenkontakt mit dem aetzenden Konzentrat beim Nachfuellen wird konstruktiv vermieden.",
HazardCategory: "material_environmental",
Examples: []string{"Sauglanze mit Leermeldung im Reiniger-Kanister", "Kein Umfuellen — Gebindewechsel ohne offenen Chemiekontakt"},
NormReferences: []string{"EN 60335-2-58", "Verordnung (EG) Nr. 1272/2008 (CLP/GHS)"}},
{ID: "M2204", ReductionType: "information", SubType: "ppe",
Name: "PSA (Augen-/Hautschutz) + GHS-Kennzeichnung und Sicherheitsdatenblatt",
Description: "Fuer Handhabung, Gebindewechsel und Entkalkung werden Augen- und Handschutz vorgeschrieben; Reiniger/Klarspueler/Entkalker sind GHS-gekennzeichnet und das Sicherheitsdatenblatt liegt am Geraet vor. Stellt die sichere Handhabung der aetzenden Konzentrate sicher.",
HazardCategory: "material_environmental",
Examples: []string{"Schutzbrille + chemikalienbestaendige Handschuhe bei Gebindewechsel", "GHS-Etikett und SDB im Chemikalienschrank am Geraet"},
NormReferences: []string{"Verordnung (EG) Nr. 1272/2008 (CLP/GHS)", "TRGS 500"}},
{ID: "M2205", ReductionType: "protection", SubType: "ventilation",
Name: "Be-/Entlueftung bzw. geschlossene Haube gegen Chemie-Aerosole und Wrasen",
Description: "Der Aufstellbereich ist ausreichend be- und entlueftet bzw. die Spuelkammer bleibt waehrend des Programms geschlossen, sodass Reinigungs-Aerosole und heisser Wrasen nicht in die Atemzone des Bedieners gelangen.",
HazardCategory: "material_environmental",
Examples: []string{"Kuechenlueftung ueber dem Spuelbereich", "Programmstart nur bei geschlossener Haube"},
NormReferences: []string{"EN 60335-2-58", "TRGS 500"}},
{ID: "M2206", ReductionType: "design", SubType: "geometry",
Name: "Tuerkanten mit geringer Schliesskraft / Einklemmschutz",
Description: "Die Tuer-/Haubenmechanik ist so gestaltet (gefuehrte Bewegung, begrenzte Schliesskraft, abgerundete Kanten), dass beim Schliessen keine Finger gequetscht werden.",
HazardCategory: "mechanical",
Examples: []string{"Gefuehrte Haube mit gedaempfter Schliessbewegung", "Abgerundete Tuerkanten ohne Quetschspalt"},
NormReferences: []string{"EN 60335-2-58", "EN ISO 12100 — Geometrie und Anordnung"}},
{ID: "M2207", ReductionType: "design", SubType: "environment",
Name: "Rutschhemmender Bodenbelag + Ablauf/Leckagewanne im Aufstellbereich",
Description: "Im Aufstell- und Bedienbereich der Spuelmaschine sorgen rutschhemmender Bodenbelag und ein definierter Ablauf bzw. eine Leckagewanne dafuer, dass austretendes Wasser nicht zur Sturzgefahr wird.",
HazardCategory: "mechanical",
Examples: []string{"Rutschhemmender Industrieboden (Bewertungsgruppe R11/R12)", "Bodenablauf bzw. Leckagewanne unter dem Geraet"},
NormReferences: []string{"ASR A1.5/1,2", "DGUV Regel 108-003"}},
{ID: "M2208", ReductionType: "information", SubType: "signage",
Name: "Warnhinweis heisser Dampf/Heisswasser — Tuer erst nach Programmende oeffnen",
Description: "Am Geraet und in der Betriebsanleitung wird vor heissem Dampf und Heisswasser gewarnt und das Oeffnen der Tuer erst nach Programmende mit vorsichtigem Anheben vorgeschrieben. Sprachneutrale Piktogramme ergaenzen den Hinweis.",
HazardCategory: "general",
Examples: []string{"Warnpiktogramm 'Heisser Dampf' an der Tuer", "BA-Hinweis 'Tuer nach Programmende langsam oeffnen'"},
NormReferences: []string{"ISO 7010", "EN 60335-2-58"}},
{ID: "M2209", ReductionType: "design", SubType: "containment",
Name: "Rueckflussverhinderer / Systemtrenner nach EN 1717",
Description: "Der Frischwasseranschluss ist durch einen Rueckflussverhinderer bzw. Systemtrenner der passenden Schutzklasse oder durch einen freien Auslauf gegen Ruecksaugen verschmutzten Wassers in das Trinkwassernetz gesichert.",
HazardCategory: "material_environmental",
Examples: []string{"Systemtrenner Typ BA nach EN 1717", "Freier Auslauf Typ AB ueber dem hoechsten Wasserstand"},
NormReferences: []string{"EN 1717", "EN 60335-2-58"}},
}
}
@@ -46,20 +46,6 @@ var domainGateTerms = map[string]string{
"widerstandsschweiss": "dom_welding", "lichtbogenschweiss": "dom_welding",
"schutzgasschweiss": "dom_welding", "punktschweiss": "dom_welding",
"schweisselektrod": "dom_welding", "elektrodenspalt": "dom_welding",
// Schweissen — Oberflaechenformen die bisher ungegatet leakten (z.B. in
// thermische Hazards einer Spuelmaschine ueber high_temperature/electrical_part)
"schweissarbeitsplatz": "dom_welding", "schweissfunke": "dom_welding",
"schweisshelm": "dom_welding", "schweisserschutz": "dom_welding",
"lichtbogenzone": "dom_welding", "lichtbogen-verbrennung": "dom_welding",
"schweissrauch": "dom_welding", "schweissgeraet": "dom_welding",
"schweisszone": "dom_welding", "schweissbrenner": "dom_welding",
"schweissspritzer": "dom_welding", "schweissstrom": "dom_welding",
// Offene Flamme / Brenner (Gasbrenner, Flammhaerten, Abflammen)
"offene flamme": "dom_flame", "brennerbereich": "dom_flame",
"flammenzone": "dom_flame", "gasbrenner": "dom_flame",
// Heissleim / Schmelzkleber
"heissleimanlage": "dom_glue", "klebstoffschmelzer": "dom_glue",
"heisskleber": "dom_glue", "schmelzkleber": "dom_glue",
// Solar / PV
"pv-modul": "dom_solar", "photovoltaik": "dom_solar", "pv-anlage": "dom_solar",
"dc-steckverbindung": "dom_solar", "solarmodul": "dom_solar",
@@ -67,7 +53,6 @@ var domainGateTerms = map[string]string{
"gondel": "dom_wind", "rotorblatt": "dom_wind", "windenergieanlage": "dom_wind",
// CNC / Zerspanung
"drehmaschine": "dom_cnc", "fraesmaschine": "dom_cnc",
"spanende": "dom_cnc", "spanenden bearbeitung": "dom_cnc",
// Landwirtschaft
"maehdrescher": "dom_agri", "ballenpresse": "dom_agri", "feldhaecksler": "dom_agri",
// Roll-/Fahrtreppe
@@ -1,44 +0,0 @@
package iace
// Interlocked-enclosure model (EN ISO 14120 / EN ISO 12100).
//
// A contact or entanglement hazard from a moving part is removed during NORMAL
// operation when that part is inaccessible behind an interlocked guard. The
// hazard then remains only when the guard is open — maintenance, cleaning or
// fault clearing. Patterns flagged GuardableByEnclosure express this; a project
// emits the "interlocked_enclosure" tag (interlocked door/hood, see
// keyword_dictionary.go) to declare the guard.
//
// This is GENERIC: it applies to every enclosed machine (dishwasher spray arm,
// enclosed mixer, centrifuge ...) and is regression-safe — machines that do not
// emit interlocked_enclosure are unaffected.
const (
phaseMaintenance = "maintenance"
phaseCleaning = "cleaning"
phaseFaultClearing = "fault_clearing"
)
// suppressedByEnclosure reports whether a guardable hazard must be dropped: the
// part is enclosed AND none of the project's lifecycle phases opens the guard.
func suppressedByEnclosure(p HazardPattern, tagSet map[string]bool, lifecycles []string) bool {
if !p.GuardableByEnclosure || !tagSet["interlocked_enclosure"] || len(lifecycles) == 0 {
return false
}
for _, lc := range lifecycles {
if lc == phaseMaintenance || lc == phaseCleaning || lc == phaseFaultClearing {
return false // guard is open in some phase → hazard remains there
}
}
return true
}
// guardedLifecycles re-scopes a guardable hazard to the guard-open phases when
// the project declares an interlocked enclosure, so it is documented as a
// maintenance/cleaning hazard rather than a normal-operation one.
func guardedLifecycles(p HazardPattern, tagSet map[string]bool) []string {
if p.GuardableByEnclosure && tagSet["interlocked_enclosure"] {
return []string{phaseMaintenance, phaseCleaning}
}
return p.ApplicableLifecycles
}
@@ -223,7 +223,7 @@ func (e *PatternEngine) Match(input MatchInput) *MatchOutput {
HumanRoles: p.HumanRoles,
GeneratedHazardType: p.GeneratedHazardType,
MatchedFailureModes: matchedFMs,
ApplicableLifecycles: guardedLifecycles(p, tagSet),
ApplicableLifecycles: p.ApplicableLifecycles,
SuggestedMeasureIDs: p.SuggestedMeasureIDs,
ClarificationQuestionsDE: p.ClarificationQuestionsDE,
ISO12100Section: p.ISO12100Section,
@@ -411,11 +411,6 @@ func patternMatches(p HazardPattern, tagSet map[string]bool, input MatchInput) b
}
}
// Interlocked-enclosure gate (guardable contact/entanglement). See pattern_enclosure.go.
if suppressedByEnclosure(p, tagSet, input.LifecyclePhases) {
return false
}
return true
}
@@ -44,7 +44,6 @@ func collectAllPatterns() []HazardPattern {
patterns = append(patterns, GetCRAPatterns()...) // HP1910-HP1918 CRA / DIN EN 40000-1-2 cyber-resilience spur
patterns = append(patterns, GetSecondaryHarmDemoPatterns()...) // HP2000-HP2001 secondary harm chain demos (Cola splitter, Pharma)
patterns = append(patterns, GetLiftEndstopPatterns()...) // HP2100-HP2102 lift body-part crush at endstops
patterns = append(patterns, GetWarewashingPatterns()...) // HP2200-HP2206 commercial dishwasher (scald/chemical/door/slip)
patterns = applyMachineTypeOverrides(patterns) // Fill MachineTypes on legacy patterns to prevent drift
patterns = applyDomainGates(patterns) // Capability-domain gate: stop domain-specific patterns leaking cross-machine
return patterns
@@ -1,383 +0,0 @@
{
"machine_name": "Gewerbliche Untertisch-Geschirrspuelmaschine (Winterhalter UC-M)",
"machine_description": "Untertisch-Gewerbespuelmaschine, vernetzt (Connected Wash), Heisswasser-Boiler, Spuelpumpe mit rotierenden Spuelfeldern, Tuer mit Sicherheitsschalter, Reiniger-/Klarspueler-Dosierung.",
"source": "Selbstbewertung GT #3 (Fachmann-Erwartung, EN 60335-2-58 + EN ISO 12100)",
"version": "1.0",
"entries": [
{
"nr": "1.1",
"hazard_group": "Thermische Gefährdungen",
"hazard_group_applicable": true,
"hazard_type": "Verbrühung durch Heißwasser und Dampf",
"hazard_cause": "Beim Öffnen der Tür während oder kurz nach dem Spülgang tritt heißes Wasser und Wrasen (Dampf) aus der Spülkammer aus und trifft Gesicht, Hände und Arme",
"lifecycle_phases": ["Betrieb", "Reinigung"],
"component_zone": "Tür und Beschickungsöffnung der Spülkammer",
"risk_in": {"f": 4, "w": 3, "p": 2, "s": 3, "r": 27},
"measures": ["Türverriegelung beendet Spülgang vor dem Öffnen", "Wrasen-/Dampfreduzierung", "Warnhinweis heißer Dampf"],
"measure_type": "KM",
"risk_out": {"f": 2, "w": 1, "p": 1, "s": 2, "r": 8},
"norm_references": ["EN 60335-2-58"],
"sufficient": true
},
{
"nr": "1.2",
"hazard_group": "Thermische Gefährdungen",
"hazard_group_applicable": true,
"hazard_type": "Verbrennung an heißen Oberflächen",
"hazard_cause": "Berührung heißer Oberflächen von Boiler, Tankheizkörper oder Spülkammerwänden bei Reinigung, Entkalkung oder Wartung",
"lifecycle_phases": ["Reinigung", "Instandhaltung"],
"component_zone": "Boiler, Tankheizkörper, Spülkammerwände",
"risk_in": {"f": 3, "w": 2, "p": 2, "s": 2, "r": 14},
"measures": ["Temperaturbegrenzung zugänglicher Oberflächen", "Warnhinweis heiße Oberfläche"],
"measure_type": "KM",
"risk_out": {"f": 1, "w": 1, "p": 1, "s": 2, "r": 6},
"norm_references": ["EN ISO 13732-1"],
"sufficient": true
},
{
"nr": "1.3",
"hazard_group": "Thermische Gefährdungen",
"hazard_group_applicable": true,
"hazard_type": "Verbrennung an heißem Spülgut",
"hazard_cause": "Geschirr und Gläser sind nach der Heißwasser-Nachspülung sehr heiß, beim Entladen kommt es zu Verbrennungen an den Händen",
"lifecycle_phases": ["Betrieb"],
"component_zone": "Spülkammer, Entnahmebereich, Korb",
"risk_in": {"f": 3, "w": 3, "p": 2, "s": 2, "r": 16},
"measures": ["Abkühl-/Trocknungszeit", "Warnhinweis heißes Spülgut"],
"measure_type": "BI",
"risk_out": {"f": 1, "w": 1, "p": 1, "s": 2, "r": 6},
"norm_references": ["EN 60335-2-58"],
"sufficient": true
},
{
"nr": "2.1",
"hazard_group": "Gefährdungen durch Materialien und Substanzen",
"hazard_group_applicable": true,
"hazard_type": "Verätzung von Haut und Augen durch Reiniger-/Klarspüler-Konzentrat",
"hazard_cause": "Direkter Kontakt mit dem ätzenden Reiniger- bzw. Klarspüler-Konzentrat beim Nachfüllen, Sauglanzenwechsel oder bei Leckage des Dosiergeräts",
"lifecycle_phases": ["Betrieb", "Instandhaltung"],
"component_zone": "Dosiergerät, Reiniger- und Klarspüler-Gebinde, Sauglanzen",
"risk_in": {"f": 3, "w": 3, "p": 2, "s": 3, "r": 24},
"measures": ["Geschlossenes Dosiersystem mit Sauglanzen", "PSA Augen-/Hautschutz", "GHS-Kennzeichnung und Sicherheitsdatenblatt"],
"measure_type": "KM",
"risk_out": {"f": 1, "w": 1, "p": 1, "s": 3, "r": 9},
"norm_references": ["Verordnung (EG) Nr. 1272/2008", "TRGS 500"],
"sufficient": true
},
{
"nr": "2.2",
"hazard_group": "Gefährdungen durch Materialien und Substanzen",
"hazard_group_applicable": true,
"hazard_type": "Reizung der Atemwege durch Reinigungs-Aerosole und Dämpfe",
"hazard_cause": "Einatmen von Aerosolen und Dämpfen der Reinigungschemie beim Öffnen kurz nach dem Spülgang oder bei der Entkalkung mit Säure",
"lifecycle_phases": ["Betrieb", "Instandhaltung"],
"component_zone": "Atemzone vor der Spülkammer, Aufstellbereich",
"risk_in": {"f": 2, "w": 2, "p": 2, "s": 2, "r": 12},
"measures": ["Be-/Entlüftung", "geschlossene Haube", "Warnung vor Vermischen von Reiniger und Säure"],
"measure_type": "BI",
"risk_out": {"f": 1, "w": 1, "p": 1, "s": 2, "r": 6},
"norm_references": ["TRGS 500"],
"sufficient": true
},
{
"nr": "3.1",
"hazard_group": "Elektrische Gefährdungen",
"hazard_group_applicable": true,
"hazard_type": "Elektrischer Schlag in Nassumgebung",
"hazard_cause": "Berührung spannungsführender Teile bei unzureichendem IP-Schutz, defekten Kabeldurchführungen oder Feuchtigkeit im Steuerungsgehäuse",
"lifecycle_phases": ["Betrieb", "Reinigung", "Instandhaltung"],
"component_zone": "Steuerungsgehäuse, Kabelübergänge, Antriebsgehäuse",
"risk_in": {"f": 3, "w": 2, "p": 3, "s": 4, "r": 32},
"measures": ["IP-Schutz gegen eindringendes Wasser", "Fehlerstrom-Schutzeinrichtung (RCD)"],
"measure_type": "KM",
"risk_out": {"f": 1, "w": 1, "p": 1, "s": 4, "r": 12},
"norm_references": ["IEC 60335-1"],
"sufficient": true
},
{
"nr": "3.2",
"hazard_group": "Elektrische Gefährdungen",
"hazard_group_applicable": true,
"hazard_type": "Kurzschluss und Brand bei Reinigung am Schaltschrank",
"hazard_cause": "Reinigung ohne vorherige Freischaltung oder mit Hochdruckreiniger am elektrisch aktiven Schaltschrank führt zu Kurzschluss und Brand",
"lifecycle_phases": ["Reinigung", "Instandhaltung"],
"component_zone": "Schaltschrank, elektrisch aktive Komponenten",
"risk_in": {"f": 2, "w": 2, "p": 2, "s": 3, "r": 18},
"measures": ["Netztrenneinrichtung", "Warnhinweis Reinigung nur spannungsfrei, kein Hochdruckreiniger"],
"measure_type": "KM",
"risk_out": {"f": 1, "w": 1, "p": 1, "s": 3, "r": 9},
"norm_references": ["IEC 60204-1"],
"sufficient": true
},
{
"nr": "3.3",
"hazard_group": "Elektrische Gefährdungen",
"hazard_group_applicable": true,
"hazard_type": "Motorüberlast mit Überhitzung",
"hazard_cause": "Blockierter oder überlasteter Pumpenmotor überhitzt, Wicklungsbrand und Rauchentwicklung",
"lifecycle_phases": ["Betrieb"],
"component_zone": "Motorgehäuse, Umgebung",
"risk_in": {"f": 2, "w": 2, "p": 2, "s": 2, "r": 12},
"measures": ["Überstromschutz", "Motorschutzschalter"],
"measure_type": "KM",
"risk_out": {"f": 1, "w": 1, "p": 1, "s": 2, "r": 6},
"norm_references": ["IEC 60204-1"],
"sufficient": true
},
{
"nr": "4.1",
"hazard_group": "Mechanische Gefährdungen",
"hazard_group_applicable": true,
"hazard_type": "Ausrutschen auf nassem Boden",
"hazard_cause": "Aus der Spülmaschine austretendes Wasser durch Leckage oder beim Öffnen macht den Boden im Aufstellbereich rutschig, Person rutscht aus und stürzt",
"lifecycle_phases": ["Betrieb", "Reinigung", "Instandhaltung"],
"component_zone": "Aufstell- und Bedienbereich der Spülmaschine",
"risk_in": {"f": 3, "w": 3, "p": 2, "s": 2, "r": 16},
"measures": ["Rutschhemmender Bodenbelag", "Bodenablauf bzw. Leckagewanne"],
"measure_type": "KM",
"risk_out": {"f": 1, "w": 1, "p": 1, "s": 2, "r": 6},
"norm_references": ["ASR A1.5/1,2"],
"sufficient": true
},
{
"nr": "4.2",
"hazard_group": "Mechanische Gefährdungen",
"hazard_group_applicable": true,
"hazard_type": "Quetschen der Finger an der Tür/Haube",
"hazard_cause": "Beim Schließen der Tür bzw. Absenken der Haube werden Finger zwischen Tür/Haube und Gehäuse gequetscht",
"lifecycle_phases": ["Betrieb"],
"component_zone": "Tür- und Haubenkante, Schließbereich",
"risk_in": {"f": 3, "w": 2, "p": 2, "s": 1, "r": 7},
"measures": ["Geringe Schließkraft, Einklemmschutz", "Abgerundete Türkanten"],
"measure_type": "KM",
"risk_out": {"f": 1, "w": 1, "p": 1, "s": 1, "r": 3},
"norm_references": ["EN ISO 12100"],
"sufficient": true
},
{
"nr": "4.3",
"hazard_group": "Mechanische Gefährdungen",
"hazard_group_applicable": true,
"hazard_type": "Kontakt mit rotierendem Spülarm bei geöffneter Tür",
"hazard_cause": "Eingreifen in die Spülkammer bei noch nachlaufendem rotierendem Spülarm/Spülfeld nach dem Öffnen der Tür",
"lifecycle_phases": ["Betrieb", "Reinigung"],
"component_zone": "Spülkammer, Spülarm und Spülfeld",
"risk_in": {"f": 2, "w": 2, "p": 2, "s": 1, "r": 6},
"measures": ["Türverriegelung stoppt Spülarm beim Öffnen"],
"measure_type": "KM",
"risk_out": {"f": 1, "w": 1, "p": 1, "s": 1, "r": 3},
"norm_references": ["EN ISO 12100"],
"sufficient": true
},
{
"nr": "5.1",
"hazard_group": "Ergonomische Gefährdungen",
"hazard_group_applicable": true,
"hazard_type": "Belastung des Bewegungsapparats durch wiederholte Be- und Entladung",
"hazard_cause": "Wiederholtes Heben und Bücken beim manuellen Be- und Entladen der Spülkörbe am Untertischgerät",
"lifecycle_phases": ["Betrieb"],
"component_zone": "Be- und Entladestelle, Spülkorb",
"risk_in": {"f": 4, "w": 3, "p": 2, "s": 1, "r": 9},
"measures": ["Ergonomische Arbeitshöhe", "Be-/Entladung auf günstiger Greifhöhe"],
"measure_type": "KM",
"risk_out": {"f": 2, "w": 1, "p": 1, "s": 1, "r": 4},
"norm_references": ["EN 1005-2"],
"sufficient": true
},
{
"nr": "5.2",
"hazard_group": "Ergonomische Gefährdungen",
"hazard_group_applicable": true,
"hazard_type": "Zwangshaltung durch ungünstige Bedienelement-Position",
"hazard_cause": "Bedienelemente am HMI außerhalb der ergonomisch günstigen Reichweite führen bei dauerhafter Bedienung zu Zwangshaltung",
"lifecycle_phases": ["Betrieb"],
"component_zone": "Bedienstand HMI, Steuerpult",
"risk_in": {"f": 3, "w": 2, "p": 1, "s": 1, "r": 6},
"measures": ["Bedienelemente in ergonomisch günstiger Höhe"],
"measure_type": "KM",
"risk_out": {"f": 1, "w": 1, "p": 1, "s": 1, "r": 3},
"norm_references": ["EN 894-3"],
"sufficient": true
},
{
"nr": "6.1",
"hazard_group": "zusätzliche Gefährdungen",
"hazard_group_applicable": true,
"hazard_type": "Verlust einer Sicherheitsfunktion durch Steuerungs- oder Softwarefehler",
"hazard_cause": "Steuerungs- oder Softwarefehler der eigenen Maschinensteuerung führt zu unkontrolliertem Verhalten oder Verlust einer Sicherheitsfunktion",
"lifecycle_phases": ["Betrieb", "Instandhaltung"],
"component_zone": "Gesamte Maschine, Steuerung",
"risk_in": {"f": 2, "w": 2, "p": 2, "s": 3, "r": 18},
"measures": ["Sichere Fehlerbehandlung", "Sichere Software-Fallbacks", "Watchdog"],
"measure_type": "KM",
"risk_out": {"f": 1, "w": 1, "p": 1, "s": 3, "r": 9},
"norm_references": ["EN ISO 13849-1"],
"sufficient": true
},
{
"nr": "6.2",
"hazard_group": "zusätzliche Gefährdungen",
"hazard_group_applicable": true,
"hazard_type": "Verlust der Sicherheitsfunktion nach fehlerhaftem Software-Update",
"hazard_cause": "Korrupte oder inkompatible Firmware nach fehlerhaftem Update über die USB-Schnittstelle lässt die Steuerung undefiniert verhalten oder Sicherheitsfunktion verlieren",
"lifecycle_phases": ["Instandhaltung"],
"component_zone": "Gesamte Maschine, Steuerung, Update-Schnittstelle",
"risk_in": {"f": 2, "w": 2, "p": 2, "s": 3, "r": 18},
"measures": ["Atomares Update mit Rückfall auf lauffähige Version", "Kompatibilitätsprüfung vor Update"],
"measure_type": "KM",
"risk_out": {"f": 1, "w": 1, "p": 1, "s": 3, "r": 9},
"norm_references": ["EN ISO 13849-1"],
"sufficient": true
},
{
"nr": "4.4",
"hazard_group": "Mechanische Gefährdungen",
"hazard_group_applicable": true,
"hazard_type": "Erfassen/Aufwickeln an rotierenden Teilen bei geöffneter Schutztür",
"hazard_cause": "Bei geöffneter Tür im Wartungs- oder Reinigungsfall können lose Kleidung oder Haare an noch zugänglichen rotierenden Wellen erfasst und aufgewickelt werden",
"lifecycle_phases": ["Instandhaltung", "Reinigung"],
"component_zone": "Rotierende Wellen, Spülarm bei geöffneter Schutztür",
"risk_in": {"f": 1, "w": 1, "p": 2, "s": 3, "r": 12},
"measures": ["Rotation stoppt bei geöffneter Tür durch Verriegelung", "Warnhinweis"],
"measure_type": "KM",
"risk_out": {"f": 1, "w": 1, "p": 1, "s": 3, "r": 6},
"norm_references": ["EN ISO 14120"],
"sufficient": true
},
{
"nr": "4.5",
"hazard_group": "Mechanische Gefährdungen",
"hazard_group_applicable": true,
"hazard_type": "Reibung/Hautabschürfung an rotierenden Teilen bei geöffneter Schutztür",
"hazard_cause": "Berührung rotierender Wellen oder Oberflächen bei geöffneter Tür im Wartungsfall führt zu Hautabschürfungen durch Reibung",
"lifecycle_phases": ["Instandhaltung"],
"component_zone": "Rotierende Welle bei geöffneter Schutztür",
"risk_in": {"f": 1, "w": 1, "p": 2, "s": 2, "r": 8},
"measures": ["Rotation stoppt bei geöffneter Tür durch Verriegelung"],
"measure_type": "KM",
"risk_out": {"f": 1, "w": 1, "p": 1, "s": 2, "r": 4},
"norm_references": ["EN ISO 14120"],
"sufficient": true
},
{
"nr": "1.4",
"hazard_group": "Thermische Gefährdungen",
"hazard_group_applicable": true,
"hazard_type": "Trockenlauf-Überhitzung von Boiler/Heizung",
"hazard_cause": "Das Heizelement bzw. der Boiler läuft bei Wassermangel trocken, überhitzt und kann einen Brand oder eine Verbrühung durch überhitztes Wasser auslösen",
"lifecycle_phases": ["Betrieb"],
"component_zone": "Boiler, Tankheizkörper, Heizelement",
"risk_in": {"f": 2, "w": 2, "p": 2, "s": 3, "r": 18},
"measures": ["Trockengehschutz / Niveauüberwachung der Heizung", "Temperaturbegrenzer (STB)"],
"measure_type": "KM",
"risk_out": {"f": 1, "w": 1, "p": 1, "s": 3, "r": 9},
"norm_references": ["EN 60335-2-58", "EN 60335-1"],
"sufficient": true
},
{
"nr": "3.4",
"hazard_group": "Elektrische Gefährdungen",
"hazard_group_applicable": true,
"hazard_type": "Restspannung / gespeicherte elektrische Energie nach Abschalten",
"hazard_cause": "Nach dem Abschalten der Spannungsversorgung stehen durch Kondensatoren im Frequenzumrichter oder Netzfilter noch gefährliche Berührungsspannungen an",
"lifecycle_phases": ["Instandhaltung", "Fehlersuche und -beseitigung"],
"component_zone": "Frequenzumrichter, Netzfilter, Schaltschrank",
"risk_in": {"f": 1, "w": 2, "p": 3, "s": 4, "r": 24},
"measures": ["Sichere Energieentladung nach Abschalten", "Warnhinweis Restspannung, Entladezeit abwarten"],
"measure_type": "KM",
"risk_out": {"f": 1, "w": 1, "p": 1, "s": 4, "r": 12},
"norm_references": ["IEC 60204-1"],
"sufficient": true
},
{
"nr": "4.6",
"hazard_group": "Mechanische Gefährdungen",
"hazard_group_applicable": true,
"hazard_type": "Schnittverletzung an scharfen Kanten",
"hazard_cause": "Schneiden an scharfen Blechkanten, Sieben oder dem Ablaufpumpen-Laufrad beim Reinigen oder Eingreifen in die Spülkammer",
"lifecycle_phases": ["Reinigung", "Instandhaltung"],
"component_zone": "Zugängliche Kanten, Siebe, Spülkammer, Ablaufpumpe",
"risk_in": {"f": 3, "w": 2, "p": 2, "s": 1, "r": 7},
"measures": ["Brechen oder Runden aller zugänglichen Kanten"],
"measure_type": "KM",
"risk_out": {"f": 1, "w": 1, "p": 1, "s": 1, "r": 3},
"norm_references": ["EN ISO 12100"],
"sufficient": true
},
{
"nr": "4.7",
"hazard_group": "Mechanische Gefährdungen",
"hazard_group_applicable": true,
"hazard_type": "Kippen / mangelnde Standsicherheit",
"hazard_cause": "Unzureichende Standsicherheit bei Untertischmontage, Transport oder Installation führt zum Kippen oder Umstürzen der Maschine",
"lifecycle_phases": ["Transport", "Montage und Installation"],
"component_zone": "Gesamte Maschine, Aufstellbereich",
"risk_in": {"f": 1, "w": 1, "p": 2, "s": 2, "r": 8},
"measures": ["Standsichere Aufstellung / Befestigung", "Kippsichere Konstruktion"],
"measure_type": "KM",
"risk_out": {"f": 1, "w": 1, "p": 1, "s": 2, "r": 4},
"norm_references": ["EN ISO 12100"],
"sufficient": true
},
{
"nr": "2.3",
"hazard_group": "Gefährdungen durch Materialien und Substanzen",
"hazard_group_applicable": true,
"hazard_type": "Rückfluss / Kontamination des Trinkwassers",
"hazard_cause": "Verschmutztes Spül- oder Chemiewasser wird ohne Rückflussverhinderer in das Trinkwassernetz zurückgesaugt und kontaminiert es",
"lifecycle_phases": ["Betrieb"],
"component_zone": "Frischwasseranschluss, Wasserzulauf",
"risk_in": {"f": 2, "w": 2, "p": 2, "s": 3, "r": 18},
"measures": ["Rückflussverhinderer / Systemtrenner nach EN 1717", "Freier Auslauf"],
"measure_type": "KM",
"risk_out": {"f": 1, "w": 1, "p": 1, "s": 3, "r": 9},
"norm_references": ["EN 1717", "EN 60335-2-58"],
"sufficient": true
},
{
"nr": "2.4",
"hazard_group": "Gefährdungen durch Materialien und Substanzen",
"hazard_group_applicable": true,
"hazard_type": "Mikrobielle Belastung / Legionellen im Stehwasser",
"hazard_cause": "Stehwasser im Boiler oder Tank bei niedrigen Temperaturen begünstigt mikrobielles Wachstum und Legionellen, die über Aerosole eingeatmet werden",
"lifecycle_phases": ["Betrieb", "Instandhaltung"],
"component_zone": "Boiler, Tank, Stehwasser",
"risk_in": {"f": 1, "w": 1, "p": 2, "s": 3, "r": 12},
"measures": ["Thermische Desinfektion / ausreichende Wassertemperatur", "Regelmäßiger Wasserwechsel"],
"measure_type": "KM",
"risk_out": {"f": 1, "w": 1, "p": 1, "s": 3, "r": 9},
"norm_references": ["EN 60335-2-58"],
"sufficient": true
},
{
"nr": "6.3",
"hazard_group": "zusätzliche Gefährdungen",
"hazard_group_applicable": true,
"hazard_type": "Versagen der Tür-/Schutzeinrichtungs-Verriegelung",
"hazard_cause": "Die Verriegelung des Tür-Sicherheitsschalters versagt oder wird überbrückt, sodass der Zugriff in die Spülkammer bei laufendem Spülgang (Heißwasser, rotierender Spülarm) möglich wird",
"lifecycle_phases": ["Betrieb", "Instandhaltung"],
"component_zone": "Tür-Sicherheitsschalter, Verriegelung, Spülkammer",
"risk_in": {"f": 3, "w": 2, "p": 2, "s": 3, "r": 21},
"measures": ["Sichere Verriegelung mit Fehlerüberwachung (PL nach ISO 13849)", "Zwangsöffnende Kontakte"],
"measure_type": "KM",
"risk_out": {"f": 1, "w": 1, "p": 1, "s": 3, "r": 9},
"norm_references": ["EN ISO 14119", "EN ISO 13849-1"],
"sufficient": true
},
{
"nr": "6.4",
"hazard_group": "zusätzliche Gefährdungen",
"hazard_group_applicable": true,
"hazard_type": "Unerwarteter Wiederanlauf bei Wartung",
"hazard_cause": "Während Wartung oder Reinigung läuft die Maschine durch fehlende Freischaltung (LOTO) oder automatischen Wiederanlauf unerwartet an",
"lifecycle_phases": ["Instandhaltung", "Reinigung"],
"component_zone": "Gesamte Maschine, Antriebe, Pumpe",
"risk_in": {"f": 2, "w": 2, "p": 2, "s": 3, "r": 18},
"measures": ["Freischalten und gegen Wiedereinschalten sichern (LOTO)", "Kein automatischer Wiederanlauf"],
"measure_type": "KM",
"risk_out": {"f": 1, "w": 1, "p": 1, "s": 3, "r": 9},
"norm_references": ["IEC 60204-1", "EN ISO 12100"],
"sufficient": true
}
]
}
+1 -8
View File
@@ -40,14 +40,6 @@ func classifyAuthority(r LegalSearchResult) authorityInfo {
if jur == "" {
jur = inferJurisdiction(r)
}
hay := r.ArticleLabel + " " + r.RegulationShort + " " + r.RegulationName + " " + r.RegulationCode
// A recognised standard NAME (NIST/OWASP/ISO 27001/CIS/CSA CCM/Grundschutz) is authoritative
// even when the corpus mis-tagged the chunk as supervisory_guidance (weight 70) — many
// standards were ingested with a generic guidance source_class. The name wins, so they
// classify (and rank) as technical_standard / control_standard. binding_law is preserved.
if r.SourceClass != "binding_law" && containsAny(hay, standardMarkers) {
return authorityInfo{weight: 80, sourceClass: "technical_standard", jurisdiction: jur}
}
if r.SourceClass != "" {
w := r.AuthorityWeight
if w == 0 && r.SourceClass == "binding_law" {
@@ -58,6 +50,7 @@ func classifyAuthority(r LegalSearchResult) authorityInfo {
if r.AuthorityWeight > 0 {
return authorityInfo{weight: r.AuthorityWeight, sourceClass: sourceClassFromWeight(r.AuthorityWeight), jurisdiction: jur}
}
hay := r.ArticleLabel + " " + r.RegulationShort + " " + r.RegulationName + " " + r.RegulationCode
switch {
case containsAny(hay, foreignMarkers):
return authorityInfo{weight: 0, sourceClass: "foreign_law", jurisdiction: "CH"}
@@ -15,7 +15,6 @@ func TestClassifyAuthority(t *testing.T) {
{"tagged foreign CH", LegalSearchResult{AuthorityWeight: 0, SourceClass: "foreign_law", Jurisdiction: "CH"}, 0, "foreign_law", "CH"},
{"untagged ENISA guidance", LegalSearchResult{RegulationShort: "ENISA", ArticleLabel: "ENISA CRA Standards Mapping"}, 70, "supervisory_guidance", "EU"},
{"untagged NIST standard", LegalSearchResult{RegulationShort: "NIST SP 800-82r3", ArticleLabel: "AU-8"}, 80, "technical_standard", "EU"},
{"mis-tagged NIST guidance -> standard by name", LegalSearchResult{SourceClass: "supervisory_guidance", AuthorityWeight: 70, RegulationShort: "NIST SP 800-82r3", ArticleLabel: "NIST SP 800-82r3"}, 80, "technical_standard", "EU"},
{"BSI Grundschutz standard beats BSI guidance", LegalSearchResult{RegulationShort: "BSI Grundschutz", ArticleLabel: "BSI Grundschutz Baustein"}, 80, "technical_standard", "DE"},
{"weight-only 85 TRGS standard", LegalSearchResult{AuthorityWeight: 85, RegulationShort: "TRGS 529"}, 85, "technical_standard", "EU"},
{"tagged technical_standard", LegalSearchResult{AuthorityWeight: 80, SourceClass: "technical_standard", Jurisdiction: "EU"}, 80, "technical_standard", "EU"},
@@ -121,54 +121,3 @@ func controlRoleOf(payload map[string]interface{}) string {
IsRecital: getBool(payload, "is_recital"),
})
}
// ensureControlDiversity guarantees that the returned top-K of a control question surfaces at
// least one operational_requirement and one control_standard WHEN the pool contains them —
// without forcing them to Top-1. implementation_guidance (e.g. ENISA good practices) keeps its
// earned semantic lead; the rule only promotes the best hit of a missing control role into the
// top-K by overwriting the lowest-ranked redundant guidance slot. So an implementation question
// shows the relevant source ROLES (binding requirement + standard + guidance) side by side
// instead of one role flooding the list. The promoted hit's original (now duplicate) position
// stays in the tail and is dropped by the caller's truncation to topK.
func ensureControlDiversity(results []LegalSearchResult, topK int) []LegalSearchResult {
if topK <= 0 || topK >= len(results) {
return results // everything is already returned — nothing to promote
}
roleAt := make([]string, len(results))
for i := range results {
roleAt[i] = classifyRole(results[i])
}
present := make(map[string]bool, topK)
for i := 0; i < topK; i++ {
present[roleAt[i]] = true
}
for _, want := range []string{roleOperationalReq, roleControlStandard} {
if present[want] {
continue
}
src := -1
for i := topK; i < len(results); i++ {
if roleAt[i] == want {
src = i
break
}
}
if src < 0 {
continue // role absent from the whole pool — nothing to promote
}
dst := -1
for j := topK - 1; j >= 0; j-- {
if roleAt[j] == roleImplGuidance {
dst = j
break
}
}
if dst < 0 {
continue // no redundant guidance to sacrifice — leave the head untouched
}
results[dst] = results[src]
roleAt[dst] = want
present[want] = true
}
return results
}
@@ -77,58 +77,3 @@ func TestControlRoleOf_Payload(t *testing.T) {
t.Errorf("DORA abstract article role = %q must be excluded from the control-pool", got)
}
}
func headHasRole(head []LegalSearchResult, role string) bool {
for _, r := range head {
if classifyRole(r) == role {
return true
}
}
return false
}
func TestEnsureControlDiversity(t *testing.T) {
ig := func(n string) LegalSearchResult {
return LegalSearchResult{RegulationShort: "ENISA " + n + " Good Practices"}
}
opReq := LegalSearchResult{RegulationShort: "CRA", ArticleLabel: "CRA Anhang I", Category: "regulation"}
std := LegalSearchResult{RegulationShort: "NIST SP 800-53"}
t.Run("injects missing op_req + control_standard, guidance keeps Top-1", func(t *testing.T) {
out := ensureControlDiversity([]LegalSearchResult{ig("A"), ig("B"), ig("C"), std, opReq}, 3)
head := out[:3]
if classifyRole(head[0]) != roleImplGuidance {
t.Errorf("Top-1 should stay implementation_guidance, got %q", classifyRole(head[0]))
}
if !headHasRole(head, roleOperationalReq) {
t.Error("top-K must contain an operational_requirement after diversity")
}
if !headHasRole(head, roleControlStandard) {
t.Error("top-K must contain a control_standard after diversity")
}
})
t.Run("no-op when both roles already present", func(t *testing.T) {
out := ensureControlDiversity([]LegalSearchResult{opReq, std, ig("A"), ig("B")}, 3)
if classifyRole(out[0]) != roleOperationalReq || classifyRole(out[1]) != roleControlStandard {
t.Error("already-diverse top-K must be left untouched")
}
})
t.Run("absent role is not forced (no panic)", func(t *testing.T) {
out := ensureControlDiversity([]LegalSearchResult{ig("A"), ig("B"), ig("C"), std}, 3)
if !headHasRole(out[:3], roleControlStandard) {
t.Error("present control_standard should be injected")
}
if headHasRole(out[:3], roleOperationalReq) {
t.Error("operational_requirement absent from the pool must NOT appear")
}
})
t.Run("topK covering the whole pool is unchanged", func(t *testing.T) {
out := ensureControlDiversity([]LegalSearchResult{ig("A"), opReq}, 5)
if len(out) != 2 || classifyRole(out[0]) != roleImplGuidance {
t.Error("topK >= len must return results unchanged")
}
})
}
@@ -166,15 +166,6 @@ func (c *LegalRAGClient) searchInternal(ctx context.Context, collection string,
// Response-Schema unveraendert. Score traegt den Authority-Score, damit nachgelagerte
// Multi-Collection-Merges (Advisor) die Ordnung bewahren.
results = rerankByAuthority(query, results)
// Control-Diversity: auf einer Umsetzungsfrage darf impl_guidance (ENISA) Top-1 bleiben,
// aber die Top-K soll mindestens eine binding operational_requirement (CRA Anhang I) und
// einen control_standard (NIST/ISO) zeigen, falls im Pool — Quellenarten sichtbar machen
// statt sie kuenstlich auf Top-1 zu heben. Nur Reihenfolge, vor der Truncation.
if queryWantsControls(query) {
results = ensureControlDiversity(results, topK)
}
if topK > 0 && len(results) > topK {
results = results[:topK]
}