feat(iace): de-bias severity estimate; risk ranking 57%->69% vs Fachmann
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / detect-changes (push) Successful in 8s
CI / sbom-scan (push) Has been skipped
CI / build-sha-integrity (push) Failing after 4s
CI / validate-canonical-controls (push) Successful in 11s
CI / loc-budget (push) Successful in 15s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Has been skipped
CI / test-go (push) Failing after 44s
CI / iace-gt-coverage (push) Successful in 22s
CI / test-python-backend (push) Has been skipped
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped

The engine's hand-set DefaultSeverity systematically over-estimates severity
(GT shows crushing 3.3 vs 2.2, struck_by 3.1 vs 2.5; electrical was already
close). EstimateSeverity blends the pattern default 50/50 with the contact
mode's GT-calibrated typical severity (baseS) — keeps pattern-specific signal,
removes the bias. Our own model, no norm table.

Effect across both GTs: severity within +-1 78%->88%; risk RANK concordance
57%->69% (Kistenhub 45%->70%). Wired into iace_handler_init.go so the
BreakPilot risk line uses the de-biased severity.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-06-09 13:52:19 +02:00
parent bc78ddd3e5
commit a910793d12
3 changed files with 78 additions and 35 deletions
@@ -35,6 +35,10 @@ type contactMode struct {
// Anchored to injury kinematics (sudden, no-warning events are hard to
// avoid; gradual exposure is easy). OUR reasoning, no norm table.
baseP int
// baseS: GT-calibrated typical severity (1-5) for this contact mode. Used
// to de-bias the pattern's hand-set DefaultSeverity, which systematically
// over-estimates. OUR calibrated scale, no norm table.
baseS int
}
// contactModeTable — our tiers. Initially anchored to the public ESAW
@@ -46,19 +50,20 @@ type contactMode struct {
// hard-code per-machine values into patterns. See DATA_SOURCES.md for the
// public-data provenance and license.
var contactModeTable = map[string]contactMode{
"impact_stationary": {"impact_stationary", 3, 1}, // seen coming -> easy to avoid
"struck_by": {"struck_by", 2, 3}, // GT-calibrated (n=14)
"crushing": {"crushing", 2, 3}, // GT-calibrated (n=40)
"cutting": {"cutting", 2, 3},
"entanglement": {"entanglement", 3, 3},
"shearing": {"shearing", 2, 3},
"fall": {"fall", 3, 4}, // higher avoidance difficulty in GT
"electrical": {"electrical", 2, 3}, // GT-calibrated (n=20)
"thermal": {"thermal", 2, 2},
"ergonomic": {"ergonomic", 2, 3},
"chemical": {"chemical", 2, 3},
"pressure_burst": {"pressure_burst", 2, 3},
"radiation": {"radiation", 2, 3},
// name W P S (S = GT-calibrated typical severity)
"impact_stationary": {"impact_stationary", 3, 1, 2},
"struck_by": {"struck_by", 2, 3, 3}, // GT n=14 (S̄ 2.5)
"crushing": {"crushing", 2, 3, 2}, // GT n=40 (S̄ 2.2)
"cutting": {"cutting", 2, 3, 3},
"entanglement": {"entanglement", 3, 3, 3},
"shearing": {"shearing", 2, 3, 3}, // GT n=4 (S̄ 3.2)
"fall": {"fall", 3, 4, 3},
"electrical": {"electrical", 2, 3, 4}, // GT n=20 (S̄ 3.6)
"thermal": {"thermal", 2, 2, 2},
"ergonomic": {"ergonomic", 2, 3, 2},
"chemical": {"chemical", 2, 3, 2},
"pressure_burst": {"pressure_burst", 2, 3, 2},
"radiation": {"radiation", 2, 3, 3},
}
// contactModeKeywords maps umlaut-normalised scenario keywords to a contact
@@ -134,6 +139,33 @@ func EstimateAvoidabilityP(cats []string, scenario string) int {
return 3
}
// EstimateSeverity de-biases the pattern's hand-set DefaultSeverity by blending
// it 50/50 with the contact mode's GT-calibrated typical severity (baseS). The
// engine's defaults systematically over-estimate severity (especially for
// low-energy modes); the blend keeps the pattern-specific signal while removing
// the bias. OUR model, no norm table. Falls back to the default when the mode
// is unknown.
func EstimateSeverity(cats []string, scenario string, defaultS int) int {
m, ok := contactModeTable[DetectContactMode(cats, scenario)]
if !ok || m.baseS == 0 {
if defaultS < 1 {
return 3
}
return defaultS
}
if defaultS < 1 {
return m.baseS
}
s := (defaultS + m.baseS + 1) / 2 // 50/50 blend, round half up
if s > 5 {
s = 5
}
if s < 1 {
s = 1
}
return s
}
// EstimateRiskLevel combines the four parameters into BreakPilot's OWN risk
// index and band. The index is a generic severity-weighted sum of the
// likelihood factors — index = S * (F + W + P) — i.e. basic arithmetic on the