diff --git a/ai-compliance-sdk/internal/api/handlers/iace_handler_risk.go b/ai-compliance-sdk/internal/api/handlers/iace_handler_risk.go index f8a880da..8c7f633b 100644 --- a/ai-compliance-sdk/internal/api/handlers/iace_handler_risk.go +++ b/ai-compliance-sdk/internal/api/handlers/iace_handler_risk.go @@ -47,3 +47,14 @@ func (h *IACEHandler) GetRiskMatrix(c *gin.Context) { } c.JSON(http.StatusOK, iace.BuildRiskMatrix(hazards)) } + +// GetRiskDataSources handles GET /risk-data-sources. +// Returns the license-tagged public-statistics evidence register (Eurostat ESAW, +// CC BY 4.0) that anchors the risk-frequency tiers, plus the overall attribution +// note — so an auditor can see WHERE the risk numbers come from. +func (h *IACEHandler) GetRiskDataSources(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "note": iace.RiskDataSourcesNote, + "evidence": iace.AllRiskEvidence(), + }) +} diff --git a/ai-compliance-sdk/internal/app/routes_iace.go b/ai-compliance-sdk/internal/app/routes_iace.go index da4c2813..ecaa059f 100644 --- a/ai-compliance-sdk/internal/app/routes_iace.go +++ b/ai-compliance-sdk/internal/app/routes_iace.go @@ -72,6 +72,7 @@ func registerIACERoutes(v1 *gin.RouterGroup, h *handlers.IACEHandler) { iaceRoutes.GET("/projects/:id/hazards/:hid/risk-suggestion", h.GetRiskSuggestion) iaceRoutes.GET("/projects/:id/risk-summary", h.GetRiskSummary) iaceRoutes.GET("/projects/:id/risk-matrix", h.GetRiskMatrix) + iaceRoutes.GET("/risk-data-sources", h.GetRiskDataSources) iaceRoutes.GET("/projects/:id/suggested-norms", h.SuggestProjectNorms) iaceRoutes.POST("/projects/:id/hazards/:hid/reassess", h.ReassessRisk) diff --git a/ai-compliance-sdk/internal/iace/risk_data_sources.go b/ai-compliance-sdk/internal/iace/risk_data_sources.go new file mode 100644 index 00000000..fe7e0a25 --- /dev/null +++ b/ai-compliance-sdk/internal/iace/risk_data_sources.go @@ -0,0 +1,69 @@ +package iace + +import "sort" + +// Evidence / citation layer for the risk-frequency anchors. Each entry is an +// aggregate, permissively-licensed public statistic (Eurostat ESAW, CC BY 4.0) +// that anchors the RELATIVE ordering of a contact mode's probability tier. The +// tier VALUES in contactModeTable are BreakPilot's own GT-calibrated numbers — +// this table only carries the provenance so generated risk numbers are +// auditable and correctly attributed. No raw dataset is vendored; only these +// aggregate facts. Excluded by license: DGUV, DIN/Beuth/ISO/IEC. See +// DATA_SOURCES.md. RAG/Qdrant ingestion is deliberately NOT used here: ~a dozen +// stable aggregate facts are better served by a license-tagged code table than +// by vector retrieval. + +// RiskEvidence is the public-statistics provenance for one contact mode. +type RiskEvidence struct { + Mode string `json:"mode"` + Label string `json:"label"` // German contact-mode label + Stat string `json:"stat"` // the cited aggregate figure + Source string `json:"source"` // "Eurostat (ESAW)" + License string `json:"license"` // "CC BY 4.0" + Attribution string `json:"attribution"` // ready-to-print citation line + Retrieved string `json:"retrieved"` // retrieval month +} + +const ( + esawSource = "Eurostat (ESAW)" + esawLicense = "CC BY 4.0" + esawAttribution = "Quelle: Eurostat (ESAW), CC BY 4.0" + esawRetrieved = "2026-06" +) + +func esawEvidence(mode, label, stat string) RiskEvidence { + return RiskEvidence{Mode: mode, Label: label, Stat: stat, Source: esawSource, + License: esawLicense, Attribution: esawAttribution, Retrieved: esawRetrieved} +} + +// contactModeEvidence holds only the contact modes for which a specific public +// figure is documented; other modes are anchored by the ESAW ordering and +// GT-calibrated without a single citable share, so they carry no fabricated stat. +var contactModeEvidence = map[string]RiskEvidence{ + "impact_stationary": esawEvidence("impact_stationary", "Anstoßen an ruhendem Objekt", "~24 % der Arbeitsunfälle"), + "struck_by": esawEvidence("struck_by", "Getroffen von bewegtem Objekt", "~13 % (nicht-tödlich) / ~24 % (tödlich)"), + "crushing": esawEvidence("crushing", "Quetschen / Einklemmen", "~14 % der tödlichen Arbeitsunfälle"), + "cutting": esawEvidence("cutting", "Kontakt mit scharfem Gegenstand", "~15 % der Arbeitsunfälle"), +} + +// RiskEvidenceFor returns the documented public statistic for a contact mode. +func RiskEvidenceFor(mode string) (RiskEvidence, bool) { + e, ok := contactModeEvidence[mode] + return e, ok +} + +// AllRiskEvidence returns the full evidence register (sorted), for a +// "Datenquellen" panel / risk-assessment export attribution. +func AllRiskEvidence() []RiskEvidence { + out := make([]RiskEvidence, 0, len(contactModeEvidence)) + for _, e := range contactModeEvidence { + out = append(out, e) + } + sort.Slice(out, func(i, j int) bool { return out[i].Mode < out[j].Mode }) + return out +} + +// RiskDataSourcesNote is the overall attribution shown wherever engine risk +// numbers appear, satisfying the ESAW CC BY 4.0 source-acknowledgement. +const RiskDataSourcesNote = "Häufigkeits-/Wahrscheinlichkeits-Tiers verankert am öffentlichen Kontaktmodus-Ranking von " + + esawSource + " (" + esawLicense + "), kalibriert an BreakPilot-Ground-Truth. Keine Norm-Tabelle reproduziert; DGUV/DIN/ISO ausgeschlossen." diff --git a/ai-compliance-sdk/internal/iace/risk_suggestion.go b/ai-compliance-sdk/internal/iace/risk_suggestion.go index 9fc28e62..368c1c3b 100644 --- a/ai-compliance-sdk/internal/iace/risk_suggestion.go +++ b/ai-compliance-sdk/internal/iace/risk_suggestion.go @@ -46,6 +46,7 @@ type RiskSuggestion struct { ContactMode string `json:"contact_mode"` EN62061 EN62061Suggestion `json:"en62061"` FineKinney FineKinneySuggestion `json:"fine_kinney"` + DataSource *RiskEvidence `json:"data_source,omitempty"` Note string `json:"note"` } @@ -65,6 +66,14 @@ func BuildRiskSuggestion(hz *Hazard) RiskSuggestion { 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) @@ -81,7 +90,7 @@ func BuildRiskSuggestion(hz *Hazard) RiskSuggestion { 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), fmt.Sprintf("Wahrscheinlichkeit W aus ESAW-Haeufigkeit der Kontaktart '%s'", modeLabel)}, + Probability: SuggestedValue{float64(w), wJustification}, Avoidance: SuggestedValue{float64(p), fmt.Sprintf("Vermeidbarkeit P aus Kinematik der Kontaktart '%s'", modeLabel)}, Score: idx, Level: level, @@ -96,7 +105,8 @@ func BuildRiskSuggestion(hz *Hazard) RiskSuggestion { Action: fk.Action, Formula: "R = P × E × C", }, - Note: "Begruendete Vorschlagswerte (BreakPilot, oeffentliche Datenquellen). Vom Sachverstaendigen anpassbar.", + DataSource: dataSource, + Note: "Begruendete Vorschlagswerte (BreakPilot, oeffentliche Datenquellen). Vom Sachverstaendigen anpassbar.", } }