feat(iace): risk as confidence range + label in benchmark tab

Report the tool's risk number as a plausible range with a confidence
label instead of a false-precision point value (confidence-aware
tonality — the assessment is confirmed by the DSB / safety expert).

- risk_estimation.go: EstimateConfidence (hoch/mittel/niedrig from how the
  contact mode resolved), EstimateRiskRange (S±1 and aggregate L=F+W+P ±1,
  the empirically validated per-parameter accuracy), RiskLevelRange; share
  the riskBandLabel thresholds with EstimateRiskLevel.
- risk_benchmark.go: RiskComparisonPair gains eng_risk_point/low/high +
  level + level_range + confidence; RiskAgreement gains high_confidence_pct.
- RiskComparison.tsx: per-hazard range "low–high (level range)" + point,
  confidence chip, and an aggregate confidence line; types in useBenchmark.ts.
- Unit tests for the range/confidence helpers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-06-10 23:04:56 +02:00
parent 97575cc9c0
commit a7dc12f30f
5 changed files with 225 additions and 37 deletions
@@ -0,0 +1,58 @@
package iace
import "testing"
func TestEstimateRiskRange(t *testing.T) {
tests := []struct {
name string
s, f, w, p int
wantLow, wantP, wantH int
}{
// S=4, L=F+W+P=8 → point 32; low 3*clampL(7)=21; high 5*clampL(9)=45.
{"typical electrical", 4, 3, 2, 3, 21, 32, 45},
// Min likelihood: L=3; low clamps L to 3 (clampL(2)=3) and S to 1.
{"low end clamps", 2, 1, 1, 1, 3, 6, 12},
// Max: S=5, L=15 → point 75; high clamps S to 5 and L to 15.
{"high end clamps", 5, 5, 5, 5, 56, 75, 75},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
low, point, high := EstimateRiskRange(tc.s, tc.f, tc.w, tc.p)
if low != tc.wantLow || point != tc.wantP || high != tc.wantH {
t.Errorf("EstimateRiskRange(%d,%d,%d,%d) = (%d,%d,%d), want (%d,%d,%d)",
tc.s, tc.f, tc.w, tc.p, low, point, high, tc.wantLow, tc.wantP, tc.wantH)
}
if low > point || point > high {
t.Errorf("range not ordered: low=%d point=%d high=%d", low, point, high)
}
})
}
}
func TestEstimateConfidence(t *testing.T) {
cases := []struct {
cats []string
scenario string
want string
}{
{[]string{"mechanical_hazard"}, "Quetschen der Hand im Werkzeugraum", "hoch"}, // keyword "quetsch"
{[]string{"electrical_hazard"}, "Elektrischer Schlag am Gehaeuse", "hoch"}, // keyword "elektrisch"
{[]string{"mechanical_hazard"}, "Allgemeine Restgefahr an der Anlage", "mittel"}, // category fallback
{[]string{"made_up_category"}, "Unspezifische Situation", "niedrig"}, // nothing
}
for _, tc := range cases {
if got := EstimateConfidence(tc.cats, tc.scenario); got != tc.want {
t.Errorf("EstimateConfidence(%v, %q) = %q, want %q", tc.cats, tc.scenario, got, tc.want)
}
}
}
func TestRiskLevelRange(t *testing.T) {
// Same band low+high → single label; spanning bands → "lowhigh".
if lvl, rng := RiskLevelRange(9, 12, 16); lvl != "gering" || rng != "gering" {
t.Errorf("single-band: got (%q,%q), want (gering,gering)", lvl, rng)
}
if lvl, rng := RiskLevelRange(21, 32, 45); lvl != "hoch" || rng != "mittelkritisch" {
t.Errorf("multi-band: got (%q,%q), want (hoch, mittelkritisch)", lvl, rng)
}
}