From 2afa5a179ba88dc76d7e8680dff1b2d48d85c38c Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Sun, 17 May 2026 02:03:10 +0200 Subject: [PATCH] feat(iace): Risikograph EN ISO 13849-1 PLr + Methoden-Kopf im Bericht MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 17 of the risk-assessment polish. Two pieces: A) PLr per EN ISO 13849-1 Anhang A (Risikograph) - HazardPattern.DefaultAvoidability (1 = P1, 2 = P2). Optional; defaults to P1 if unset (conservative — operator can raise after review). - ComputePLr(s,f,p) implements the canonical 8-leaf binary tree (S1F1P1 -> a, ..., S2F2P2 -> e). Pinned by 8 table-driven tests. - SeverityToS / ExposureToF map the existing 1-5 fields to the binary S/F at the documented threshold (3). - At project initialise, every hazard's Description is appended with "Risikograph EN ISO 13849-1 (Anhang A): S2 · F1 · P1 -> PLr c" so the audit value is visible without leaving the hazard view. - PatternMatch carries DefaultAvoidability so the init handler can pick it up without a second pattern lookup. B) Methoden-Kopf am Bericht - GET /clarifications.html now opens with a standardised methodology block: ISO 12100 Anhang B (hazard ID) + ISO 13849-1 Anhang A (PLr graph) + ISO 12100 6.2/6.3/6.4 (reduction hierarchy). Same wording on every export, ready for the Anlagenbauer-Uebergabe. - Only norm identifiers — no norm text reproduced. C) ISO12100Section in Hazard Description - When a pattern is labeled with ISO12100Section, the hazard description gets a "Klassifikation: EN ISO 12100 Anhang B, Abschnitt 6.3.5.4" suffix. Provenance for the auditor. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../handlers/iace_handler_clarifications.go | 15 ++++++ .../api/handlers/iace_handler_init.go | 21 +++++++++ .../internal/iace/hazard_pattern_types.go | 46 +++++++++++++++++++ .../internal/iace/pattern_engine.go | 2 + .../internal/iace/risk_graph_test.go | 42 +++++++++++++++++ 5 files changed, 126 insertions(+) create mode 100644 ai-compliance-sdk/internal/iace/risk_graph_test.go diff --git a/ai-compliance-sdk/internal/api/handlers/iace_handler_clarifications.go b/ai-compliance-sdk/internal/api/handlers/iace_handler_clarifications.go index 1adef823..814d0ca6 100644 --- a/ai-compliance-sdk/internal/api/handlers/iace_handler_clarifications.go +++ b/ai-compliance-sdk/internal/api/handlers/iace_handler_clarifications.go @@ -415,6 +415,20 @@ func (h *IACEHandler) ExportClarificationsCSV(c *gin.Context) { w.Flush() } +// methodologyBlock returns the standardised methodology paragraph that +// must be printed at the start of every IACE risk-assessment report. +// Pure references to norm identifiers (no norm text) — kept here so +// the same wording appears in every export. +const methodologyBlock = `
+

Methodik der Risikobeurteilung

+
    +
  • Gefaehrdungsidentifikation nach EN ISO 12100, Anhang B (Tabelle B.1) — Mechanik, Elektrik, Thermik, Laerm, Vibration, Strahlung, Materialien/Substanzen, Ergonomie.
  • +
  • Bestimmung des erforderlichen Performance Levels (PLr) nach EN ISO 13849-1, Anhang A (Risikograph) aus S (Schwere), F (Haeufigkeit/Dauer) und P (Vermeidungsmoeglichkeit).
  • +
  • Massnahmen-Hierarchie nach ISO 12100, Abschnitt 6: 6.2 Inhaerent sichere Konstruktion (Design) → 6.3 Technische Schutzmassnahmen (Protection) → 6.4 Benutzerinformation (Information).
  • +
  • Klaerungspunkte mit dem Anlagenbauer werden separat in der Klaerungs-Liste verwaltet (Audit-Trail mit Bearbeiter und Zeitstempel).
  • +
+
` + // ExportClarificationsHTML handles GET /projects/:id/clarifications.html // and returns a print-friendly standalone HTML document that the browser // can render to PDF (no server-side PDF dependency needed). The Bediener @@ -492,6 +506,7 @@ section .src { font-size: 8pt; color: #6b7280; margin-bottom: 6px; }
Tipp: Mit Strg+P / Cmd+P als PDF speichern.

Klaerungsliste — %s

Projekt-ID %s · Stand %s
+` + methodologyBlock + `
%d offen %d beantwortet diff --git a/ai-compliance-sdk/internal/api/handlers/iace_handler_init.go b/ai-compliance-sdk/internal/api/handlers/iace_handler_init.go index 5355d2c0..c84442b5 100644 --- a/ai-compliance-sdk/internal/api/handlers/iace_handler_init.go +++ b/ai-compliance-sdk/internal/api/handlers/iace_handler_init.go @@ -219,6 +219,27 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) { // scenario itself. Only the aggregated norm-references // block is appended below for an at-a-glance audit trail. desc := mp.ScenarioDE + // Phase 17: PLr per EN ISO 13849-1 Anhang A. The graph + // inputs come from the pattern's DefaultSeverity/Exposure + // (mapped to S1/S2 and F1/F2 at threshold 3) plus + // DefaultAvoidability (P1/P2). If avoidability is unset + // we default to P1 — the conservative direction is + // downward (lower PLr), the operator can raise it + // manually after expert review. + avoid := 1 + if mp.DefaultAvoidability == 2 { + avoid = 2 + } + if mp.DefaultSeverity > 0 && mp.DefaultExposure > 0 { + sBin := iace.SeverityToS(mp.DefaultSeverity) + fBin := iace.ExposureToF(mp.DefaultExposure) + plr := iace.ComputePLr(sBin, fBin, avoid) + desc += fmt.Sprintf("\n\nRisikograph EN ISO 13849-1 (Anhang A): S%d · F%d · P%d → PLr %s", + sBin, fBin, avoid, plr) + } + if mp.ISO12100Section != "" { + desc += "\n\nKlassifikation: EN ISO 12100 Anhang B, Abschnitt " + mp.ISO12100Section + } hz, cerr := h.store.CreateHazard(ctx, iace.CreateHazardRequest{ ProjectID: projectID, diff --git a/ai-compliance-sdk/internal/iace/hazard_pattern_types.go b/ai-compliance-sdk/internal/iace/hazard_pattern_types.go index 4984c036..0ea3f0fe 100644 --- a/ai-compliance-sdk/internal/iace/hazard_pattern_types.go +++ b/ai-compliance-sdk/internal/iace/hazard_pattern_types.go @@ -76,6 +76,52 @@ type HazardPattern struct { // keep the library urheberrechtlich neutral (DIN/Beuth license). // The frontend renders it as "EN ISO 12100 Abschnitt 6.3.5.5". ISO12100Section string `json:"iso_12100_section,omitempty"` + // DefaultAvoidability is the P parameter of the EN ISO 13849-1 + // risk graph (Annex A): 1 = avoidable under certain conditions, 2 = + // hardly avoidable. Combined with DefaultSeverity (S1/S2 derived + // at threshold 3) and DefaultExposure (F1/F2 at threshold 3) it + // feeds into the PLr (required Performance Level) computation, + // see ComputePLr. + DefaultAvoidability int `json:"default_avoidability,omitempty"` // 1 or 2 +} + +// ComputePLr returns the required Performance Level (PLr) per EN ISO +// 13849-1 Anhang A (Risikograph). Inputs are the three parameters of +// the graph in their 1/2 form: +// - s (Schwere): 1 = leicht/reversibel, 2 = schwer/irreversibel inkl. Tod +// - f (Haeufigkeit/Dauer): 1 = selten/kurz, 2 = haeufig/dauernd +// - p (Moeglichkeit Vermeidung): 1 = unter Bedingungen moeglich, 2 = kaum +// Return value is one of "a", "b", "c", "d", "e" (PLa..PLe). +// +// The mapping follows the canonical 8-leaf binary tree of the standard: +// S1 F1 P1 -> a +// S1 F1 P2 -> b +// S1 F2 P1 -> b +// S1 F2 P2 -> c +// S2 F1 P1 -> c +// S2 F1 P2 -> d +// S2 F2 P1 -> d +// S2 F2 P2 -> e +func ComputePLr(s, f, p int) string { + idx := 0 + if s == 2 { idx += 4 } + if f == 2 { idx += 2 } + if p == 2 { idx += 1 } + return []string{"a", "b", "b", "c", "c", "d", "d", "e"}[idx] +} + +// SeverityToS maps a 1-5 DefaultSeverity to the binary S parameter of +// EN ISO 13849-1: 1-2 -> S1 (leicht/reversibel), 3-5 -> S2 (schwer/Tod). +func SeverityToS(severity int) int { + if severity >= 3 { return 2 } + return 1 +} + +// ExposureToF maps a 1-5 DefaultExposure to the binary F parameter of +// EN ISO 13849-1: 1-2 -> F1 (selten/kurz), 3-5 -> F2 (haeufig/dauernd). +func ExposureToF(exposure int) int { + if exposure >= 3 { return 2 } + return 1 } // Standard human roles for machinery interaction (ISO 12100 + BetrSichV). diff --git a/ai-compliance-sdk/internal/iace/pattern_engine.go b/ai-compliance-sdk/internal/iace/pattern_engine.go index 523d3cdd..7197cbf4 100644 --- a/ai-compliance-sdk/internal/iace/pattern_engine.go +++ b/ai-compliance-sdk/internal/iace/pattern_engine.go @@ -70,6 +70,7 @@ type PatternMatch struct { SuggestedMeasureIDs []string `json:"suggested_measure_ids,omitempty"` ClarificationQuestionsDE []string `json:"clarification_questions_de,omitempty"` ISO12100Section string `json:"iso_12100_section,omitempty"` + DefaultAvoidability int `json:"default_avoidability,omitempty"` } // HazardSuggestion is a suggested hazard from pattern matching. @@ -226,6 +227,7 @@ func (e *PatternEngine) Match(input MatchInput) *MatchOutput { SuggestedMeasureIDs: p.SuggestedMeasureIDs, ClarificationQuestionsDE: p.ClarificationQuestionsDE, ISO12100Section: p.ISO12100Section, + DefaultAvoidability: p.DefaultAvoidability, }) for _, cat := range p.GeneratedHazardCats { diff --git a/ai-compliance-sdk/internal/iace/risk_graph_test.go b/ai-compliance-sdk/internal/iace/risk_graph_test.go new file mode 100644 index 00000000..77418b1a --- /dev/null +++ b/ai-compliance-sdk/internal/iace/risk_graph_test.go @@ -0,0 +1,42 @@ +package iace + +import "testing" + +// TestComputePLr_Canonical8 pins the 8 leaves of the EN ISO 13849-1 +// Annex A risk graph: S1/S2 x F1/F2 x P1/P2 -> a..e. +func TestComputePLr_Canonical8(t *testing.T) { + cases := []struct { + s, f, p int + want string + }{ + {1, 1, 1, "a"}, + {1, 1, 2, "b"}, + {1, 2, 1, "b"}, + {1, 2, 2, "c"}, + {2, 1, 1, "c"}, + {2, 1, 2, "d"}, + {2, 2, 1, "d"}, + {2, 2, 2, "e"}, // worst case: severe + frequent + hardly avoidable + } + for _, c := range cases { + got := ComputePLr(c.s, c.f, c.p) + if got != c.want { + t.Errorf("ComputePLr(S%d F%d P%d) = %q, want %q", c.s, c.f, c.p, got, c.want) + } + } +} + +// TestSeverityExposureMapping ensures the 1-5 internal fields map to the +// correct binary S/F parameter at the documented threshold (3). +func TestSeverityExposureMapping(t *testing.T) { + for sev, wantS := range map[int]int{1: 1, 2: 1, 3: 2, 4: 2, 5: 2} { + if got := SeverityToS(sev); got != wantS { + t.Errorf("SeverityToS(%d) = %d, want %d", sev, got, wantS) + } + } + for exp, wantF := range map[int]int{1: 1, 2: 1, 3: 2, 4: 2, 5: 2} { + if got := ExposureToF(exp); got != wantF { + t.Errorf("ExposureToF(%d) = %d, want %d", exp, got, wantF) + } + } +}