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>
This commit is contained in:
@@ -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 = `<section style="background:#f9fafb;border:1px solid #d1d5db;border-radius:6px;padding:12px;margin-bottom:18px;">
|
||||
<h3 style="font-size:11pt;margin:0 0 6px 0;">Methodik der Risikobeurteilung</h3>
|
||||
<ul style="margin:0;padding-left:18px;font-size:9.5pt;line-height:1.45;">
|
||||
<li>Gefaehrdungsidentifikation nach <strong>EN ISO 12100</strong>, Anhang B (Tabelle B.1) — Mechanik, Elektrik, Thermik, Laerm, Vibration, Strahlung, Materialien/Substanzen, Ergonomie.</li>
|
||||
<li>Bestimmung des erforderlichen Performance Levels (PLr) nach <strong>EN ISO 13849-1</strong>, Anhang A (Risikograph) aus S (Schwere), F (Haeufigkeit/Dauer) und P (Vermeidungsmoeglichkeit).</li>
|
||||
<li>Massnahmen-Hierarchie nach ISO 12100, Abschnitt 6: <strong>6.2 Inhaerent sichere Konstruktion</strong> (Design) → <strong>6.3 Technische Schutzmassnahmen</strong> (Protection) → <strong>6.4 Benutzerinformation</strong> (Information).</li>
|
||||
<li>Klaerungspunkte mit dem Anlagenbauer werden separat in der Klaerungs-Liste verwaltet (Audit-Trail mit Bearbeiter und Zeitstempel).</li>
|
||||
</ul>
|
||||
</section>`
|
||||
|
||||
// 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; }
|
||||
<div class="noprint">Tipp: Mit <kbd>Strg+P</kbd> / <kbd>Cmd+P</kbd> als PDF speichern.</div>
|
||||
<h1>Klaerungsliste — %s</h1>
|
||||
<div class="sub">Projekt-ID %s · Stand %s</div>
|
||||
` + methodologyBlock + `
|
||||
<div class="meta">
|
||||
<span class="bar open">%d offen</span>
|
||||
<span class="bar done">%d beantwortet</span>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user