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)
+ }
+ }
+}