package audit import ( "sort" "strings" ) // runHierarchyImpl checks the ISO 12100 / EN 12100 risk-reduction // hierarchy on the generated mitigation set: every safety-relevant // hazard should have at least one "inherently safe design" measure // (design) and additionally either a guarding/protective device // (protection) or an information-for-use measure (information). // // Cyber-, ergonomic-, and software-only hazards have looser // expectations — design alone or information alone may legitimately // suffice. The audit reports which level is missing, not whether the // remaining measures are individually correct. That is a different // check (E2 — semantic quality), out of scope here. func init() { runHierarchyImpl = runHierarchy } // hazardExpectsProtection lists hazard categories where a pure // design+information combination is usually not enough — the engine // should produce at least one explicit protective measure (guard, // interlock, sensor, presence detector, …). var hazardExpectsProtection = map[string]bool{ "mechanical_hazard": true, "electrical_hazard": true, "thermal_hazard": true, "pneumatic_hydraulic": true, "radiation_hazard": true, "laser_hazard": true, "fire_explosion_hazard": true, "chemical_hazard": true, } func runHierarchy(hazards, mitigations []map[string]any) HierarchyReport { report := HierarchyReport{TotalHazards: len(hazards)} // Index mitigations by hazard_id byHazard := map[string][]map[string]any{} for _, m := range mitigations { hid, _ := m["hazard_id"].(string) if hid == "" { continue } byHazard[hid] = append(byHazard[hid], m) } for _, h := range hazards { hid, _ := h["id"].(string) category, _ := h["category"].(string) name, _ := h["name"].(string) levels := levelsForHazard(byHazard[hid]) missing := expectedMissing(category, levels) if len(missing) == 0 { report.Complete++ continue } for _, m := range missing { switch m { case "design": report.MissingDesign++ case "protection": report.MissingProtection++ case "information": report.MissingInfo++ } } report.IncompleteHazards = append(report.IncompleteHazards, HazardHierarchyResult{ HazardID: hid, Name: name, Category: category, Levels: levels, MissingLevels: missing, }) } // Sort: protection-missing first (most consequential), then by category sort.Slice(report.IncompleteHazards, func(i, j int) bool { a := report.IncompleteHazards[i] b := report.IncompleteHazards[j] ap := contains(a.MissingLevels, "protection") bp := contains(b.MissingLevels, "protection") if ap != bp { return ap } return a.Category < b.Category }) return report } // levelsForHazard returns the distinct reduction-type levels present // for a hazard's mitigation set. Possible values: design, protection, // information. func levelsForHazard(mits []map[string]any) []string { seen := map[string]bool{} for _, m := range mits { rt, _ := m["reduction_type"].(string) switch strings.ToLower(rt) { case "design": seen["design"] = true case "protection", "protective": seen["protection"] = true case "information": seen["information"] = true } } var out []string for k := range seen { out = append(out, k) } sort.Strings(out) return out } // expectedMissing returns the levels that the hierarchy demands but // the mitigation set does not provide. // // Rule: // - Every hazard with mitigations should have a design measure. // - Categories in hazardExpectsProtection additionally need a // protection measure. // - All hazards should have an information measure unless they // already have both design + protection (the information layer // can then be considered subsumed for the audit's purpose; the // real engine usually still adds it). func expectedMissing(category string, present []string) []string { have := toBoolSet(present) var missing []string if !have["design"] { missing = append(missing, "design") } if hazardExpectsProtection[category] && !have["protection"] { missing = append(missing, "protection") } // Information is only flagged if both design and protection are // also absent — otherwise too noisy. We still surface the case // where information is the SOLE present level: that means the // hazard is mitigated only by warning labels, which is rarely // adequate. if !have["information"] && !have["design"] && !have["protection"] { missing = append(missing, "information") } return missing } func contains(list []string, target string) bool { for _, x := range list { if x == target { return true } } return false }