feat(iace): add Fine-Kinney risk model (citable, free, US-recognized)
CI / detect-changes (push) Successful in 6s
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 / sbom-scan (push) Has been skipped
CI / build-sha-integrity (push) Failing after 5s
CI / validate-canonical-controls (push) Successful in 11s
CI / loc-budget (push) Successful in 15s
CI / go-lint (push) Has been skipped
CI / test-go (push) Failing after 38s
CI / iace-gt-coverage (push) Successful in 23s
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
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Has been skipped

Fine-Kinney (Fine 1971 / Kinney-Wiruth 1976): Risk = Probability x Exposure x
Consequence — a PUBLISHED, freely-usable method (not a DIN/Beuth/ISO standard),
widely used incl. CE-marking. Gives the professional a second, US-recognized
model alongside the EN-62061-style one; German exporters get both for free and
adjust with their own licensed norm data.

risk_fine_kinney.go: SuggestFineKinney derives justified P/E/C from public
anchors (ESAW frequency -> P, lifecycle -> E, de-biased severity -> C on the
Fine-Kinney consequence scale) + ComputeFineKinney(p,e,c) so the professional
can override with his own values. No norm table stored.

GT benchmark (rank concordance vs the professional): Fine-Kinney 75.4% — beats
the EN-62061-style model (69.3%) and the raw engine (57%).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-06-09 15:22:44 +02:00
parent a910793d12
commit 0bf9c54d27
2 changed files with 149 additions and 2 deletions
@@ -103,6 +103,7 @@ type riskAgg struct {
noAvoidDefault int
engineRisk []float64
newEngineRisk []float64
fkRisk []float64
gtRisk []float64
matched int
noParam int
@@ -243,11 +244,15 @@ func TestGT_RiskBenchmark(t *testing.T) {
// NEW = de-biased severity scaled by summed likelihood incl. W + P.
oldProxy := float64(maxInt(rp.s, 1) * maxInt(rp.f, 1) * maxInt(rp.a, 1))
newProxy := float64(maxInt(estS, 1) * (maxInt(rp.f, 1) + estW + estP))
// Fine-Kinney score (our citable backbone) for rank comparison.
fk := SuggestFineKinney(rp.cats, rp.scenario, pr.LifecyclePhases, rp.s)
local.engineRisk = append(local.engineRisk, oldProxy)
local.newEngineRisk = append(local.newEngineRisk, newProxy)
local.fkRisk = append(local.fkRisk, fk.Score)
local.gtRisk = append(local.gtRisk, float64(gtR.R))
overall.engineRisk = append(overall.engineRisk, oldProxy)
overall.newEngineRisk = append(overall.newEngineRisk, newProxy)
overall.fkRisk = append(overall.fkRisk, fk.Score)
overall.gtRisk = append(overall.gtRisk, float64(gtR.R))
}
@@ -260,7 +265,8 @@ func TestGT_RiskBenchmark(t *testing.T) {
t.Logf(" Frequency F: MAE %.2f | within±1 %.0f%% | exact %.0f%% (n=%d)", local.freq.mae(), local.freq.pct(local.freq.within1), local.freq.pct(local.freq.exact), local.freq.n)
t.Logf(" Probability W (NEW estimate): MAE %.2f | within±1 %.0f%% | exact %.0f%% (n=%d)", local.wEst.mae(), local.wEst.pct(local.wEst.within1), local.wEst.pct(local.wEst.exact), local.wEst.n)
t.Logf(" Avoidance P (NEW estimate): MAE %.2f | within±1 %.0f%% | exact %.0f%% (n=%d)", local.pEst.mae(), local.pEst.pct(local.pEst.within1), local.pEst.pct(local.pEst.exact), local.pEst.n)
t.Logf(" Risk RANK concordance: OLD %.1f%% -> NEW %.1f%% (over %d comparable pairs)", oldConc*100, newConc*100, pairs)
fkConc, _ := kendallConcordance(local.fkRisk, local.gtRisk)
t.Logf(" Risk RANK concordance: OLD %.1f%% -> NEW %.1f%% | Fine-Kinney %.1f%% (over %d pairs)", oldConc*100, newConc*100, fkConc*100, pairs)
}
oldConc, _ := kendallConcordance(overall.engineRisk, overall.gtRisk)
@@ -271,5 +277,6 @@ func TestGT_RiskBenchmark(t *testing.T) {
t.Logf(" Frequency F: MAE %.2f | within±1 %.0f%% | exact %.0f%% (n=%d)", overall.freq.mae(), overall.freq.pct(overall.freq.within1), overall.freq.pct(overall.freq.exact), overall.freq.n)
t.Logf(" Probability W (NEW): MAE %.2f | within±1 %.0f%% | exact %.0f%% (n=%d)", overall.wEst.mae(), overall.wEst.pct(overall.wEst.within1), overall.wEst.pct(overall.wEst.exact), overall.wEst.n)
t.Logf(" Avoidance P (NEW): MAE %.2f | within±1 %.0f%% | exact %.0f%% (n=%d)", overall.pEst.mae(), overall.pEst.pct(overall.pEst.within1), overall.pEst.pct(overall.pEst.exact), overall.pEst.n)
t.Logf(" Risk RANK concordance: OLD %.1f%% -> NEW %.1f%% (%d pairs)", oldConc*100, newConc*100, pairs)
fkConc, _ := kendallConcordance(overall.fkRisk, overall.gtRisk)
t.Logf(" Risk RANK concordance: OLD %.1f%% -> NEW %.1f%% | Fine-Kinney %.1f%% (%d pairs)", oldConc*100, newConc*100, fkConc*100, pairs)
}