feat(ai-sdk): interlocked-enclosure model — guard-open re-scoping of contact hazards

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 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-06-24 22:13:34 +02:00
parent cf86dc241b
commit 5318a70f9e
6 changed files with 95 additions and 1 deletions
@@ -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).
@@ -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"},
@@ -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"}},
@@ -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
}
@@ -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
}
@@ -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
}
]
}