From 5318a70f9e5612b140a33f458e811e7a96adfa19 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Wed, 24 Jun 2026 22:13:34 +0200 Subject: [PATCH] =?UTF-8?q?feat(ai-sdk):=20interlocked-enclosure=20model?= =?UTF-8?q?=20=E2=80=94=20guard-open=20re-scoping=20of=20contact=20hazards?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Class C (phase-aware, generic EN ISO 14120). A contact/entanglement hazard from a moving part is removed during NORMAL operation when the part is behind an interlocked guard; it remains only when the guard is open (maintenance/cleaning). - New HazardPattern.GuardableByEnclosure flag; set on HP096 (friction at rotating surfaces) and HP101 (entanglement of hair/clothing). - Narrative emits interlocked_enclosure for an interlocked door/hood. - pattern_enclosure.go: suppressedByEnclosure (drop in normal-op-only contexts) + guardedLifecycles (re-scope to maintenance/cleaning). - GT #3 gains the maintenance-phase entanglement/friction rows. Generic + regression-safe: machines that do not emit interlocked_enclosure are unaffected. GT #3 recall 80% -> 82.4%, one false positive removed (Aufwickeln). Kistenhub 97.1% and all 26 Bremse pinned mappings unchanged. Co-Authored-By: Claude Opus 4.7 --- .../internal/iace/hazard_pattern_types.go | 7 +++ .../iace/hazard_patterns_extended_dguv.go | 2 + .../internal/iace/keyword_dictionary.go | 6 +++ .../internal/iace/pattern_enclosure.go | 44 +++++++++++++++++++ .../internal/iace/pattern_engine.go | 7 ++- .../testdata/ground_truth_warewashing.json | 30 +++++++++++++ 6 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 ai-compliance-sdk/internal/iace/pattern_enclosure.go diff --git a/ai-compliance-sdk/internal/iace/hazard_pattern_types.go b/ai-compliance-sdk/internal/iace/hazard_pattern_types.go index 624862ad..9b63d207 100644 --- a/ai-compliance-sdk/internal/iace/hazard_pattern_types.go +++ b/ai-compliance-sdk/internal/iace/hazard_pattern_types.go @@ -62,6 +62,13 @@ 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). diff --git a/ai-compliance-sdk/internal/iace/hazard_patterns_extended_dguv.go b/ai-compliance-sdk/internal/iace/hazard_patterns_extended_dguv.go index 5a888cb5..2e810efb 100644 --- a/ai-compliance-sdk/internal/iace/hazard_patterns_extended_dguv.go +++ b/ai-compliance-sdk/internal/iace/hazard_patterns_extended_dguv.go @@ -37,6 +37,7 @@ 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"}, @@ -88,6 +89,7 @@ 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"}, diff --git a/ai-compliance-sdk/internal/iace/keyword_dictionary.go b/ai-compliance-sdk/internal/iace/keyword_dictionary.go index 5cf36550..abea20ea 100644 --- a/ai-compliance-sdk/internal/iace/keyword_dictionary.go +++ b/ai-compliance-sdk/internal/iace/keyword_dictionary.go @@ -201,6 +201,12 @@ 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"}}, diff --git a/ai-compliance-sdk/internal/iace/pattern_enclosure.go b/ai-compliance-sdk/internal/iace/pattern_enclosure.go new file mode 100644 index 00000000..a10cdf0e --- /dev/null +++ b/ai-compliance-sdk/internal/iace/pattern_enclosure.go @@ -0,0 +1,44 @@ +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 +} diff --git a/ai-compliance-sdk/internal/iace/pattern_engine.go b/ai-compliance-sdk/internal/iace/pattern_engine.go index 7197cbf4..cc37a560 100644 --- a/ai-compliance-sdk/internal/iace/pattern_engine.go +++ b/ai-compliance-sdk/internal/iace/pattern_engine.go @@ -223,7 +223,7 @@ func (e *PatternEngine) Match(input MatchInput) *MatchOutput { HumanRoles: p.HumanRoles, GeneratedHazardType: p.GeneratedHazardType, MatchedFailureModes: matchedFMs, - ApplicableLifecycles: p.ApplicableLifecycles, + ApplicableLifecycles: guardedLifecycles(p, tagSet), SuggestedMeasureIDs: p.SuggestedMeasureIDs, ClarificationQuestionsDE: p.ClarificationQuestionsDE, ISO12100Section: p.ISO12100Section, @@ -411,6 +411,11 @@ 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 } diff --git a/ai-compliance-sdk/internal/iace/testdata/ground_truth_warewashing.json b/ai-compliance-sdk/internal/iace/testdata/ground_truth_warewashing.json index 725972a0..3c050083 100644 --- a/ai-compliance-sdk/internal/iace/testdata/ground_truth_warewashing.json +++ b/ai-compliance-sdk/internal/iace/testdata/ground_truth_warewashing.json @@ -228,6 +228,36 @@ "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 } ] }