Files
breakpilot-compliance/ai-compliance-sdk/internal/iace/risk_graph_test.go
T
Benjamin Admin 2afa5a179b feat(iace): Risikograph EN ISO 13849-1 PLr + Methoden-Kopf im Bericht
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) <noreply@anthropic.com>
2026-05-17 02:03:10 +02:00

43 lines
1.2 KiB
Go

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