Files
breakpilot-compliance/ai-compliance-sdk/internal/iace/risk_matrix.go
T
Benjamin Admin 577ceae4e6 feat(iace): project-wide risk matrix (Severity × Probability)
Adds GET /projects/:id/risk-matrix — a confidence-aware risk view computed
on read from each hazard's category/scenario/lifecycle using the SAME model
as the GT benchmark (no persistence, so it never goes stale against the
model; the hand-defaulted iace_hazards risk columns stay untouched).

- risk_matrix.go: EstimateHazardRisk (single source of truth for S/F/W/P +
  range + level + confidence) and BuildRiskMatrix (per-hazard list + a 5×5
  Severity×Probability aggregation grid with dominant level per cell).
- Frontend: RiskMatrix grid in the Risikobewertung tab (muted colours per
  the confidence-aware tonality), level counts + tool-confidence summary,
  fed by useRiskMatrix. Shows risk for EVERY project, not only GT ones.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 08:54:47 +02:00

130 lines
4.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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. "mittelhoch"
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
}