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()
|
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
|
// ExportClarificationsHTML handles GET /projects/:id/clarifications.html
|
||||||
// and returns a print-friendly standalone HTML document that the browser
|
// and returns a print-friendly standalone HTML document that the browser
|
||||||
// can render to PDF (no server-side PDF dependency needed). The Bediener
|
// 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>
|
<div class="noprint">Tipp: Mit <kbd>Strg+P</kbd> / <kbd>Cmd+P</kbd> als PDF speichern.</div>
|
||||||
<h1>Klaerungsliste — %s</h1>
|
<h1>Klaerungsliste — %s</h1>
|
||||||
<div class="sub">Projekt-ID %s · Stand %s</div>
|
<div class="sub">Projekt-ID %s · Stand %s</div>
|
||||||
|
` + methodologyBlock + `
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
<span class="bar open">%d offen</span>
|
<span class="bar open">%d offen</span>
|
||||||
<span class="bar done">%d beantwortet</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
|
// scenario itself. Only the aggregated norm-references
|
||||||
// block is appended below for an at-a-glance audit trail.
|
// block is appended below for an at-a-glance audit trail.
|
||||||
desc := mp.ScenarioDE
|
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{
|
hz, cerr := h.store.CreateHazard(ctx, iace.CreateHazardRequest{
|
||||||
ProjectID: projectID,
|
ProjectID: projectID,
|
||||||
|
|||||||
@@ -76,6 +76,52 @@ type HazardPattern struct {
|
|||||||
// keep the library urheberrechtlich neutral (DIN/Beuth license).
|
// keep the library urheberrechtlich neutral (DIN/Beuth license).
|
||||||
// The frontend renders it as "EN ISO 12100 Abschnitt 6.3.5.5".
|
// The frontend renders it as "EN ISO 12100 Abschnitt 6.3.5.5".
|
||||||
ISO12100Section string `json:"iso_12100_section,omitempty"`
|
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).
|
// Standard human roles for machinery interaction (ISO 12100 + BetrSichV).
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ type PatternMatch struct {
|
|||||||
SuggestedMeasureIDs []string `json:"suggested_measure_ids,omitempty"`
|
SuggestedMeasureIDs []string `json:"suggested_measure_ids,omitempty"`
|
||||||
ClarificationQuestionsDE []string `json:"clarification_questions_de,omitempty"`
|
ClarificationQuestionsDE []string `json:"clarification_questions_de,omitempty"`
|
||||||
ISO12100Section string `json:"iso_12100_section,omitempty"`
|
ISO12100Section string `json:"iso_12100_section,omitempty"`
|
||||||
|
DefaultAvoidability int `json:"default_avoidability,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// HazardSuggestion is a suggested hazard from pattern matching.
|
// HazardSuggestion is a suggested hazard from pattern matching.
|
||||||
@@ -226,6 +227,7 @@ func (e *PatternEngine) Match(input MatchInput) *MatchOutput {
|
|||||||
SuggestedMeasureIDs: p.SuggestedMeasureIDs,
|
SuggestedMeasureIDs: p.SuggestedMeasureIDs,
|
||||||
ClarificationQuestionsDE: p.ClarificationQuestionsDE,
|
ClarificationQuestionsDE: p.ClarificationQuestionsDE,
|
||||||
ISO12100Section: p.ISO12100Section,
|
ISO12100Section: p.ISO12100Section,
|
||||||
|
DefaultAvoidability: p.DefaultAvoidability,
|
||||||
})
|
})
|
||||||
|
|
||||||
for _, cat := range p.GeneratedHazardCats {
|
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