package iace import "sort" // Project-wide risk view, computed on read from each hazard's category / scenario // / lifecycle using the SAME confidence-aware model as the benchmark // (EstimateSeverity/Frequency/ProbabilityW/AvoidabilityP/RiskRange). Nothing is // persisted — the risk is always derived from the hazard, so it can never go // stale against the model (unlike the hand-defaulted iace_hazards risk columns). // The matrix axis is Severity × Probability(W) — the classic risk matrix — since // those are the two parameters an assessor reads off the grid. // HazardRisk is the full confidence-aware risk estimate for one hazard. type HazardRisk struct { Severity int `json:"severity"` Frequency int `json:"frequency"` Probability int `json:"probability"` // W (occurrence) Avoidance int `json:"avoidance"` // P RiskPoint int `json:"risk_point"` RiskLow int `json:"risk_low"` RiskHigh int `json:"risk_high"` Level string `json:"level"` // band of the point LevelRange string `json:"level_range"` // e.g. "mittel–hoch" Confidence string `json:"confidence"` // hoch / mittel / niedrig } // EstimateHazardRisk derives the confidence-aware risk estimate for a hazard from // its hazard category, scenario text and lifecycle phases. Single source of truth // reused by the risk matrix endpoint. func EstimateHazardRisk(cats []string, scenario string, lifecyclePhases []string) HazardRisk { s := EstimateSeverity(cats, scenario, 0) f := EstimateFrequency(lifecyclePhases) w := EstimateProbabilityW(cats, scenario) p := EstimateAvoidabilityP(cats, scenario) low, point, high := EstimateRiskRange(s, f, w, p) level, levelRange := RiskLevelRange(low, point, high) return HazardRisk{ Severity: s, Frequency: f, Probability: w, Avoidance: p, RiskPoint: point, RiskLow: low, RiskHigh: high, Level: level, LevelRange: levelRange, Confidence: EstimateConfidence(cats, scenario), } } // HazardRiskDetail is one hazard plus its risk estimate (for the per-hazard list). type HazardRiskDetail struct { HazardID string `json:"hazard_id"` Name string `json:"name"` Category string `json:"category"` Zone string `json:"zone"` HazardRisk } // RiskMatrixCell is one Severity×Probability bucket of the 5×5 grid. type RiskMatrixCell struct { Severity int `json:"severity"` Probability int `json:"probability"` Count int `json:"count"` DominantLevel string `json:"dominant_level"` HazardIDs []string `json:"hazard_ids"` } // RiskMatrix is the project risk view: per-hazard detail + the aggregated grid. type RiskMatrix struct { Hazards []HazardRiskDetail `json:"hazards"` Matrix []RiskMatrixCell `json:"matrix"` // only non-empty cells LevelCounts map[string]int `json:"level_counts"` Total int `json:"total"` HighConfidencePct float64 `json:"high_confidence_pct"` } // bandRank orders risk levels so a cell's "dominant" (worst) level can be picked. var bandRank = map[string]int{ "vernachlaessigbar": 0, "gering": 1, "mittel": 2, "hoch": 3, "kritisch": 4, } // BuildRiskMatrix computes the confidence-aware risk for every hazard and // aggregates them into a Severity×Probability grid. func BuildRiskMatrix(hazards []Hazard) RiskMatrix { out := RiskMatrix{ LevelCounts: map[string]int{}, Total: len(hazards), } type key struct{ s, w int } cells := map[key]*RiskMatrixCell{} hiConf := 0 for _, h := range hazards { scenario := h.Scenario if scenario == "" { scenario = h.Name } risk := EstimateHazardRisk([]string{h.Category}, scenario, splitLifecyclePhases(h.LifecyclePhase)) out.Hazards = append(out.Hazards, HazardRiskDetail{ HazardID: h.ID.String(), Name: h.Name, Category: h.Category, Zone: h.HazardousZone, HazardRisk: risk, }) out.LevelCounts[risk.Level]++ if risk.Confidence == "hoch" { hiConf++ } k := key{risk.Severity, risk.Probability} c := cells[k] if c == nil { c = &RiskMatrixCell{Severity: risk.Severity, Probability: risk.Probability, DominantLevel: risk.Level} cells[k] = c } c.Count++ c.HazardIDs = append(c.HazardIDs, h.ID.String()) if bandRank[risk.Level] > bandRank[c.DominantLevel] { c.DominantLevel = risk.Level } } for _, c := range cells { out.Matrix = append(out.Matrix, *c) } // Deterministic order: by severity desc, then probability desc. sort.Slice(out.Matrix, func(i, j int) bool { if out.Matrix[i].Severity != out.Matrix[j].Severity { return out.Matrix[i].Severity > out.Matrix[j].Severity } return out.Matrix[i].Probability > out.Matrix[j].Probability }) if len(hazards) > 0 { out.HighConfidencePct = pct(hiConf, len(hazards)) } return out }