ee64b7e95c
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>
122 lines
4.4 KiB
Go
122 lines
4.4 KiB
Go
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
|
||
}
|