Files
breakpilot-compliance/ai-compliance-sdk/internal/iace/risk_suggestion.go
T
Benjamin Admin ee64b7e95c feat(iace): cite ESAW source + license on risk-frequency anchors
Surfaces the public-statistics provenance for the contact-mode probability
tiers so generated risk numbers are auditable and attributed (not RAG —
~a dozen stable aggregate facts are better as a license-tagged code table).

- risk_data_sources.go: RiskEvidence register (Eurostat ESAW figures + CC BY
  4.0 attribution) for the documented contact modes; RiskDataSourcesNote.
- risk_suggestion.go: the W justification now cites the actual ESAW share +
  license where documented; RiskSuggestion gains a data_source field.
- GET /iace/risk-data-sources returns the evidence register + attribution.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 09:14:36 +02:00

122 lines
4.4 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 (
"fmt"
"strings"
)
// Dual-model risk suggestion for the "Risikobewertung" tab. BreakPilot proposes
// justified starting values for BOTH a EN-62061-style model (F/W/P/S) and the
// Fine-Kinney model (P/E/C); the professional adjusts them (e.g. from his own
// licensed DIN/Beuth data). We expose the FORMULAS and computed values only —
// no norm table is stored or reproduced.
// SuggestedValue is a proposed parameter value plus the plain-language reason.
type SuggestedValue struct {
Value float64 `json:"value"`
Justification string `json:"justification"`
}
// EN62061Suggestion is the EN-62061-style risk (the Excel format the
// professional knows): R = S * (F + W + P).
type EN62061Suggestion struct {
Severity SuggestedValue `json:"severity"`
Frequency SuggestedValue `json:"frequency"`
Probability SuggestedValue `json:"probability"`
Avoidance SuggestedValue `json:"avoidance"`
Score int `json:"score"`
Level string `json:"level"`
Formula string `json:"formula"`
}
// FineKinneySuggestion is the Fine-Kinney risk (US-recognized): R = P * E * C.
type FineKinneySuggestion struct {
Probability SuggestedValue `json:"probability"`
Exposure SuggestedValue `json:"exposure"`
Consequence SuggestedValue `json:"consequence"`
Score float64 `json:"score"`
Band string `json:"band"`
Action string `json:"action"`
Formula string `json:"formula"`
}
// RiskSuggestion carries both models for one hazard.
type RiskSuggestion struct {
HazardID string `json:"hazard_id"`
ContactMode string `json:"contact_mode"`
EN62061 EN62061Suggestion `json:"en62061"`
FineKinney FineKinneySuggestion `json:"fine_kinney"`
DataSource *RiskEvidence `json:"data_source,omitempty"`
Note string `json:"note"`
}
// BuildRiskSuggestion derives both models' justified starting values from the
// hazard's category/scenario/lifecycle, using only public-data anchors.
func BuildRiskSuggestion(hz *Hazard) RiskSuggestion {
cats := []string{hz.Category}
scenario := hz.Scenario
if scenario == "" {
scenario = hz.Name
}
lifecycle := splitLifecyclePhases(hz.LifecyclePhase)
mode := DetectContactMode(cats, scenario)
modeLabel := mode
if modeLabel == "" {
modeLabel = "unbestimmt"
}
// Cite the actual public statistic + license for the W anchor where documented.
wJustification := fmt.Sprintf("Wahrscheinlichkeit W aus ESAW-Haeufigkeit der Kontaktart '%s'", modeLabel)
var dataSource *RiskEvidence
if ev, ok := RiskEvidenceFor(mode); ok {
wJustification += fmt.Sprintf(" (%s: %s; %s)", ev.Label, ev.Stat, ev.Attribution)
dataSource = &ev
}
// EN-62061-style (F/W/P/S)
s := EstimateSeverity(cats, scenario, 0)
f := EstimateFrequency(lifecycle)
w := EstimateProbabilityW(cats, scenario)
p := EstimateAvoidabilityP(cats, scenario)
idx, level := EstimateRiskLevel(s, f, w, p)
// Fine-Kinney (P/E/C)
fk := SuggestFineKinney(cats, scenario, lifecycle, 0)
return RiskSuggestion{
HazardID: hz.ID.String(),
ContactMode: modeLabel,
EN62061: EN62061Suggestion{
Severity: SuggestedValue{float64(s), fmt.Sprintf("Schwere S%d aus Verletzungsbild der Kontaktart '%s' (NIOSH/OSHA/MIL-STD-882)", s, modeLabel)},
Frequency: SuggestedValue{float64(f), "Haeufigkeit F aus Lebensphasen-Exposition des Projekts"},
Probability: SuggestedValue{float64(w), wJustification},
Avoidance: SuggestedValue{float64(p), fmt.Sprintf("Vermeidbarkeit P aus Kinematik der Kontaktart '%s'", modeLabel)},
Score: idx,
Level: level,
Formula: "R = S × (F + W + P)",
},
FineKinney: FineKinneySuggestion{
Probability: SuggestedValue{fk.Probability.Value, fk.Probability.Justification},
Exposure: SuggestedValue{fk.Exposure.Value, fk.Exposure.Justification},
Consequence: SuggestedValue{fk.Consequence.Value, fk.Consequence.Justification},
Score: fk.Score,
Band: fk.Band,
Action: fk.Action,
Formula: "R = P × E × C",
},
DataSource: dataSource,
Note: "Begruendete Vorschlagswerte (BreakPilot, oeffentliche Datenquellen). Vom Sachverstaendigen anpassbar.",
}
}
func splitLifecyclePhases(s string) []string {
var out []string
for _, p := range strings.Split(s, ",") {
if p = strings.TrimSpace(p); p != "" {
out = append(out, p)
}
}
return out
}