feat(iace): integrate ISO 12100 machine risk model with 4-factor assessment
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 36s
CI/CD / test-python-backend-compliance (push) Successful in 36s
CI/CD / test-python-document-crawler (push) Successful in 22s
CI/CD / test-python-dsms-gateway (push) Successful in 18s
CI/CD / validate-canonical-controls (push) Successful in 12s
CI/CD / Deploy (push) Successful in 2s
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 36s
CI/CD / test-python-backend-compliance (push) Successful in 36s
CI/CD / test-python-document-crawler (push) Successful in 22s
CI/CD / test-python-dsms-gateway (push) Successful in 18s
CI/CD / validate-canonical-controls (push) Successful in 12s
CI/CD / Deploy (push) Successful in 2s
Add dual-mode risk engine: legacy S×E×P (avoidance=0) and ISO mode S×F×P×A (avoidance>=1) with new thresholds (low/medium/high/very_high/not_acceptable). - 150+ hazard library entries across 28 categories incl. physical hazards (mechanical, electrical, thermal, pneumatic/hydraulic, noise/vibration, ergonomic, material/environmental) - 160-entry protective measures library with 3-step hierarchy validation (design → protective → information) - 25 lifecycle phases, 20 affected person roles, 50 evidence types - 10 verification methods (expanded from 7) - New API endpoints: lifecycle-phases, roles, evidence-types, protective-measures-library, validate-mitigation-hierarchy - DB migrations 018+019 for extended schema - Frontend: 4-slider risk assessment, hierarchy warnings, measures library modal - MkDocs wiki updated with ISO mode docs and legal notice (no norm text) All content uses original wording — norms referenced as methodology only. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -917,7 +917,14 @@ func (h *IACEHandler) AssessRisk(c *gin.Context) {
|
||||
inherentRisk := h.engine.CalculateInherentRisk(req.Severity, req.Exposure, req.Probability, req.Avoidance)
|
||||
controlEff := h.engine.CalculateControlEffectiveness(req.ControlMaturity, req.ControlCoverage, req.TestEvidenceStrength)
|
||||
residualRisk := h.engine.CalculateResidualRisk(req.Severity, req.Exposure, req.Probability, controlEff)
|
||||
riskLevel := h.engine.DetermineRiskLevel(residualRisk)
|
||||
|
||||
// ISO 12100 mode: use ISO thresholds when avoidance is set
|
||||
var riskLevel iace.RiskLevel
|
||||
if req.Avoidance >= 1 {
|
||||
riskLevel = h.engine.DetermineRiskLevelISO(inherentRisk)
|
||||
} else {
|
||||
riskLevel = h.engine.DetermineRiskLevel(residualRisk)
|
||||
}
|
||||
acceptable, acceptanceReason := h.engine.IsAcceptable(residualRisk, false, req.AcceptanceJustification != "")
|
||||
|
||||
// Determine version by checking existing assessments
|
||||
@@ -1862,3 +1869,186 @@ func sortStrings(s []string) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ISO 12100 Endpoints
|
||||
// ============================================================================
|
||||
|
||||
// ListLifecyclePhases handles GET /lifecycle-phases
|
||||
// Returns the 12 machine lifecycle phases with DE/EN labels.
|
||||
func (h *IACEHandler) ListLifecyclePhases(c *gin.Context) {
|
||||
phases, err := h.store.ListLifecyclePhases(c.Request.Context())
|
||||
if err != nil {
|
||||
// Fallback: return hardcoded 25 phases if DB table not yet migrated
|
||||
phases = []iace.LifecyclePhaseInfo{
|
||||
{ID: "transport", LabelDE: "Transport", LabelEN: "Transport", Sort: 1},
|
||||
{ID: "storage", LabelDE: "Lagerung", LabelEN: "Storage", Sort: 2},
|
||||
{ID: "assembly", LabelDE: "Montage", LabelEN: "Assembly", Sort: 3},
|
||||
{ID: "installation", LabelDE: "Installation", LabelEN: "Installation", Sort: 4},
|
||||
{ID: "commissioning", LabelDE: "Inbetriebnahme", LabelEN: "Commissioning", Sort: 5},
|
||||
{ID: "parameterization", LabelDE: "Parametrierung", LabelEN: "Parameterization", Sort: 6},
|
||||
{ID: "setup", LabelDE: "Einrichten / Setup", LabelEN: "Setup", Sort: 7},
|
||||
{ID: "normal_operation", LabelDE: "Normalbetrieb", LabelEN: "Normal Operation", Sort: 8},
|
||||
{ID: "automatic_operation", LabelDE: "Automatikbetrieb", LabelEN: "Automatic Operation", Sort: 9},
|
||||
{ID: "manual_operation", LabelDE: "Handbetrieb", LabelEN: "Manual Operation", Sort: 10},
|
||||
{ID: "teach_mode", LabelDE: "Teach-Modus", LabelEN: "Teach Mode", Sort: 11},
|
||||
{ID: "production_start", LabelDE: "Produktionsstart", LabelEN: "Production Start", Sort: 12},
|
||||
{ID: "production_stop", LabelDE: "Produktionsstopp", LabelEN: "Production Stop", Sort: 13},
|
||||
{ID: "process_monitoring", LabelDE: "Prozessueberwachung", LabelEN: "Process Monitoring", Sort: 14},
|
||||
{ID: "cleaning", LabelDE: "Reinigung", LabelEN: "Cleaning", Sort: 15},
|
||||
{ID: "maintenance", LabelDE: "Wartung", LabelEN: "Maintenance", Sort: 16},
|
||||
{ID: "inspection", LabelDE: "Inspektion", LabelEN: "Inspection", Sort: 17},
|
||||
{ID: "calibration", LabelDE: "Kalibrierung", LabelEN: "Calibration", Sort: 18},
|
||||
{ID: "fault_clearing", LabelDE: "Stoerungsbeseitigung", LabelEN: "Fault Clearing", Sort: 19},
|
||||
{ID: "repair", LabelDE: "Reparatur", LabelEN: "Repair", Sort: 20},
|
||||
{ID: "changeover", LabelDE: "Umruestung", LabelEN: "Changeover", Sort: 21},
|
||||
{ID: "software_update", LabelDE: "Software-Update", LabelEN: "Software Update", Sort: 22},
|
||||
{ID: "remote_maintenance", LabelDE: "Fernwartung", LabelEN: "Remote Maintenance", Sort: 23},
|
||||
{ID: "decommissioning", LabelDE: "Ausserbetriebnahme", LabelEN: "Decommissioning", Sort: 24},
|
||||
{ID: "disposal", LabelDE: "Demontage / Entsorgung", LabelEN: "Dismantling / Disposal", Sort: 25},
|
||||
}
|
||||
}
|
||||
|
||||
if phases == nil {
|
||||
phases = []iace.LifecyclePhaseInfo{}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"lifecycle_phases": phases,
|
||||
"total": len(phases),
|
||||
})
|
||||
}
|
||||
|
||||
// ListProtectiveMeasures handles GET /protective-measures-library
|
||||
// Returns the protective measures library, optionally filtered by ?reduction_type and ?hazard_category.
|
||||
func (h *IACEHandler) ListProtectiveMeasures(c *gin.Context) {
|
||||
reductionType := c.Query("reduction_type")
|
||||
hazardCategory := c.Query("hazard_category")
|
||||
|
||||
all := iace.GetProtectiveMeasureLibrary()
|
||||
|
||||
var filtered []iace.ProtectiveMeasureEntry
|
||||
for _, entry := range all {
|
||||
if reductionType != "" && entry.ReductionType != reductionType {
|
||||
continue
|
||||
}
|
||||
if hazardCategory != "" && entry.HazardCategory != hazardCategory && entry.HazardCategory != "general" && entry.HazardCategory != "" {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, entry)
|
||||
}
|
||||
|
||||
if filtered == nil {
|
||||
filtered = []iace.ProtectiveMeasureEntry{}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"protective_measures": filtered,
|
||||
"total": len(filtered),
|
||||
})
|
||||
}
|
||||
|
||||
// ValidateMitigationHierarchy handles POST /projects/:id/validate-mitigation-hierarchy
|
||||
// Validates if the proposed mitigation type follows the 3-step hierarchy principle.
|
||||
func (h *IACEHandler) ValidateMitigationHierarchy(c *gin.Context) {
|
||||
projectID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var req iace.ValidateMitigationHierarchyRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Get existing mitigations for the hazard
|
||||
mitigations, err := h.store.ListMitigations(c.Request.Context(), req.HazardID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
_ = projectID // projectID used for authorization context
|
||||
|
||||
warnings := h.engine.ValidateProtectiveMeasureHierarchy(req.ReductionType, mitigations)
|
||||
|
||||
c.JSON(http.StatusOK, iace.ValidateMitigationHierarchyResponse{
|
||||
Valid: len(warnings) == 0,
|
||||
Warnings: warnings,
|
||||
})
|
||||
}
|
||||
|
||||
// ListRoles handles GET /roles
|
||||
// Returns the 20 affected person roles reference data.
|
||||
func (h *IACEHandler) ListRoles(c *gin.Context) {
|
||||
roles, err := h.store.ListRoles(c.Request.Context())
|
||||
if err != nil {
|
||||
// Fallback: return hardcoded roles if DB table not yet migrated
|
||||
roles = []iace.RoleInfo{
|
||||
{ID: "operator", LabelDE: "Maschinenbediener", LabelEN: "Machine Operator", Sort: 1},
|
||||
{ID: "setter", LabelDE: "Einrichter", LabelEN: "Setter", Sort: 2},
|
||||
{ID: "maintenance_tech", LabelDE: "Wartungstechniker", LabelEN: "Maintenance Technician", Sort: 3},
|
||||
{ID: "service_tech", LabelDE: "Servicetechniker", LabelEN: "Service Technician", Sort: 4},
|
||||
{ID: "cleaning_staff", LabelDE: "Reinigungspersonal", LabelEN: "Cleaning Staff", Sort: 5},
|
||||
{ID: "production_manager", LabelDE: "Produktionsleiter", LabelEN: "Production Manager", Sort: 6},
|
||||
{ID: "safety_officer", LabelDE: "Sicherheitsbeauftragter", LabelEN: "Safety Officer", Sort: 7},
|
||||
{ID: "electrician", LabelDE: "Elektriker", LabelEN: "Electrician", Sort: 8},
|
||||
{ID: "software_engineer", LabelDE: "Softwareingenieur", LabelEN: "Software Engineer", Sort: 9},
|
||||
{ID: "maintenance_manager", LabelDE: "Instandhaltungsleiter", LabelEN: "Maintenance Manager", Sort: 10},
|
||||
{ID: "plant_operator", LabelDE: "Anlagenfahrer", LabelEN: "Plant Operator", Sort: 11},
|
||||
{ID: "qa_inspector", LabelDE: "Qualitaetssicherung", LabelEN: "Quality Assurance", Sort: 12},
|
||||
{ID: "logistics_staff", LabelDE: "Logistikpersonal", LabelEN: "Logistics Staff", Sort: 13},
|
||||
{ID: "subcontractor", LabelDE: "Fremdfirma / Subunternehmer", LabelEN: "Subcontractor", Sort: 14},
|
||||
{ID: "visitor", LabelDE: "Besucher", LabelEN: "Visitor", Sort: 15},
|
||||
{ID: "auditor", LabelDE: "Auditor", LabelEN: "Auditor", Sort: 16},
|
||||
{ID: "it_admin", LabelDE: "IT-Administrator", LabelEN: "IT Administrator", Sort: 17},
|
||||
{ID: "remote_service", LabelDE: "Fernwartungsdienst", LabelEN: "Remote Service", Sort: 18},
|
||||
{ID: "plant_owner", LabelDE: "Betreiber", LabelEN: "Plant Owner / Operator", Sort: 19},
|
||||
{ID: "emergency_responder", LabelDE: "Notfallpersonal", LabelEN: "Emergency Responder", Sort: 20},
|
||||
}
|
||||
}
|
||||
|
||||
if roles == nil {
|
||||
roles = []iace.RoleInfo{}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"roles": roles,
|
||||
"total": len(roles),
|
||||
})
|
||||
}
|
||||
|
||||
// ListEvidenceTypes handles GET /evidence-types
|
||||
// Returns the 50 evidence/verification types reference data.
|
||||
func (h *IACEHandler) ListEvidenceTypes(c *gin.Context) {
|
||||
types, err := h.store.ListEvidenceTypes(c.Request.Context())
|
||||
if err != nil {
|
||||
// Fallback: return empty if not migrated
|
||||
types = []iace.EvidenceTypeInfo{}
|
||||
}
|
||||
|
||||
if types == nil {
|
||||
types = []iace.EvidenceTypeInfo{}
|
||||
}
|
||||
|
||||
category := c.Query("category")
|
||||
if category != "" {
|
||||
var filtered []iace.EvidenceTypeInfo
|
||||
for _, t := range types {
|
||||
if t.Category == category {
|
||||
filtered = append(filtered, t)
|
||||
}
|
||||
}
|
||||
if filtered == nil {
|
||||
filtered = []iace.EvidenceTypeInfo{}
|
||||
}
|
||||
types = filtered
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"evidence_types": types,
|
||||
"total": len(types),
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user