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:
@@ -554,7 +554,16 @@ func (s *Store) CreateHazard(ctx context.Context, req CreateHazardRequest) (*Haz
|
||||
Description: req.Description,
|
||||
Scenario: req.Scenario,
|
||||
Category: req.Category,
|
||||
SubCategory: req.SubCategory,
|
||||
Status: HazardStatusIdentified,
|
||||
MachineModule: req.MachineModule,
|
||||
Function: req.Function,
|
||||
LifecyclePhase: req.LifecyclePhase,
|
||||
HazardousZone: req.HazardousZone,
|
||||
TriggerEvent: req.TriggerEvent,
|
||||
AffectedPerson: req.AffectedPerson,
|
||||
PossibleHarm: req.PossibleHarm,
|
||||
ReviewStatus: ReviewStatusDraft,
|
||||
CreatedAt: time.Now().UTC(),
|
||||
UpdatedAt: time.Now().UTC(),
|
||||
}
|
||||
@@ -562,16 +571,22 @@ func (s *Store) CreateHazard(ctx context.Context, req CreateHazardRequest) (*Haz
|
||||
_, err := s.pool.Exec(ctx, `
|
||||
INSERT INTO iace_hazards (
|
||||
id, project_id, component_id, library_hazard_id,
|
||||
name, description, scenario, category, status,
|
||||
name, description, scenario, category, sub_category, status,
|
||||
machine_module, function, lifecycle_phase, hazardous_zone,
|
||||
trigger_event, affected_person, possible_harm, review_status,
|
||||
created_at, updated_at
|
||||
) VALUES (
|
||||
$1, $2, $3, $4,
|
||||
$5, $6, $7, $8, $9,
|
||||
$10, $11
|
||||
$5, $6, $7, $8, $9, $10,
|
||||
$11, $12, $13, $14,
|
||||
$15, $16, $17, $18,
|
||||
$19, $20
|
||||
)
|
||||
`,
|
||||
h.ID, h.ProjectID, h.ComponentID, h.LibraryHazardID,
|
||||
h.Name, h.Description, h.Scenario, h.Category, string(h.Status),
|
||||
h.Name, h.Description, h.Scenario, h.Category, h.SubCategory, string(h.Status),
|
||||
h.MachineModule, h.Function, h.LifecyclePhase, h.HazardousZone,
|
||||
h.TriggerEvent, h.AffectedPerson, h.PossibleHarm, string(h.ReviewStatus),
|
||||
h.CreatedAt, h.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -584,17 +599,21 @@ func (s *Store) CreateHazard(ctx context.Context, req CreateHazardRequest) (*Haz
|
||||
// GetHazard retrieves a hazard by ID
|
||||
func (s *Store) GetHazard(ctx context.Context, id uuid.UUID) (*Hazard, error) {
|
||||
var h Hazard
|
||||
var status string
|
||||
var status, reviewStatus string
|
||||
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
SELECT
|
||||
id, project_id, component_id, library_hazard_id,
|
||||
name, description, scenario, category, status,
|
||||
name, description, scenario, category, sub_category, status,
|
||||
machine_module, function, lifecycle_phase, hazardous_zone,
|
||||
trigger_event, affected_person, possible_harm, review_status,
|
||||
created_at, updated_at
|
||||
FROM iace_hazards WHERE id = $1
|
||||
`, id).Scan(
|
||||
&h.ID, &h.ProjectID, &h.ComponentID, &h.LibraryHazardID,
|
||||
&h.Name, &h.Description, &h.Scenario, &h.Category, &status,
|
||||
&h.Name, &h.Description, &h.Scenario, &h.Category, &h.SubCategory, &status,
|
||||
&h.MachineModule, &h.Function, &h.LifecyclePhase, &h.HazardousZone,
|
||||
&h.TriggerEvent, &h.AffectedPerson, &h.PossibleHarm, &reviewStatus,
|
||||
&h.CreatedAt, &h.UpdatedAt,
|
||||
)
|
||||
if err == pgx.ErrNoRows {
|
||||
@@ -605,6 +624,7 @@ func (s *Store) GetHazard(ctx context.Context, id uuid.UUID) (*Hazard, error) {
|
||||
}
|
||||
|
||||
h.Status = HazardStatus(status)
|
||||
h.ReviewStatus = ReviewStatus(reviewStatus)
|
||||
return &h, nil
|
||||
}
|
||||
|
||||
@@ -613,7 +633,9 @@ func (s *Store) ListHazards(ctx context.Context, projectID uuid.UUID) ([]Hazard,
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT
|
||||
id, project_id, component_id, library_hazard_id,
|
||||
name, description, scenario, category, status,
|
||||
name, description, scenario, category, sub_category, status,
|
||||
machine_module, function, lifecycle_phase, hazardous_zone,
|
||||
trigger_event, affected_person, possible_harm, review_status,
|
||||
created_at, updated_at
|
||||
FROM iace_hazards WHERE project_id = $1
|
||||
ORDER BY created_at ASC
|
||||
@@ -626,11 +648,13 @@ func (s *Store) ListHazards(ctx context.Context, projectID uuid.UUID) ([]Hazard,
|
||||
var hazards []Hazard
|
||||
for rows.Next() {
|
||||
var h Hazard
|
||||
var status string
|
||||
var status, reviewStatus string
|
||||
|
||||
err := rows.Scan(
|
||||
&h.ID, &h.ProjectID, &h.ComponentID, &h.LibraryHazardID,
|
||||
&h.Name, &h.Description, &h.Scenario, &h.Category, &status,
|
||||
&h.Name, &h.Description, &h.Scenario, &h.Category, &h.SubCategory, &status,
|
||||
&h.MachineModule, &h.Function, &h.LifecyclePhase, &h.HazardousZone,
|
||||
&h.TriggerEvent, &h.AffectedPerson, &h.PossibleHarm, &reviewStatus,
|
||||
&h.CreatedAt, &h.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -638,6 +662,7 @@ func (s *Store) ListHazards(ctx context.Context, projectID uuid.UUID) ([]Hazard,
|
||||
}
|
||||
|
||||
h.Status = HazardStatus(status)
|
||||
h.ReviewStatus = ReviewStatus(reviewStatus)
|
||||
hazards = append(hazards, h)
|
||||
}
|
||||
|
||||
@@ -654,20 +679,19 @@ func (s *Store) UpdateHazard(ctx context.Context, id uuid.UUID, updates map[stri
|
||||
args := []interface{}{id}
|
||||
argIdx := 2
|
||||
|
||||
allowedFields := map[string]bool{
|
||||
"name": true, "description": true, "scenario": true, "category": true,
|
||||
"sub_category": true, "status": true, "component_id": true,
|
||||
"machine_module": true, "function": true, "lifecycle_phase": true,
|
||||
"hazardous_zone": true, "trigger_event": true, "affected_person": true,
|
||||
"possible_harm": true, "review_status": true,
|
||||
}
|
||||
|
||||
for key, val := range updates {
|
||||
switch key {
|
||||
case "name", "description", "scenario", "category":
|
||||
if allowedFields[key] {
|
||||
query += fmt.Sprintf(", %s = $%d", key, argIdx)
|
||||
args = append(args, val)
|
||||
argIdx++
|
||||
case "status":
|
||||
query += fmt.Sprintf(", status = $%d", argIdx)
|
||||
args = append(args, val)
|
||||
argIdx++
|
||||
case "component_id":
|
||||
query += fmt.Sprintf(", component_id = $%d", argIdx)
|
||||
args = append(args, val)
|
||||
argIdx++
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1591,10 +1615,20 @@ func (s *Store) ListAuditTrail(ctx context.Context, projectID uuid.UUID) ([]Audi
|
||||
func (s *Store) ListHazardLibrary(ctx context.Context, category string, componentType string) ([]HazardLibraryEntry, error) {
|
||||
query := `
|
||||
SELECT
|
||||
id, category, name, description,
|
||||
id, category, COALESCE(sub_category, ''), name, description,
|
||||
default_severity, default_probability,
|
||||
COALESCE(default_exposure, 3), COALESCE(default_avoidance, 3),
|
||||
applicable_component_types, regulation_references,
|
||||
suggested_mitigations, is_builtin, tenant_id,
|
||||
suggested_mitigations,
|
||||
COALESCE(typical_causes, '[]'::jsonb),
|
||||
COALESCE(typical_harm, ''),
|
||||
COALESCE(relevant_lifecycle_phases, '[]'::jsonb),
|
||||
COALESCE(recommended_measures_design, '[]'::jsonb),
|
||||
COALESCE(recommended_measures_technical, '[]'::jsonb),
|
||||
COALESCE(recommended_measures_information, '[]'::jsonb),
|
||||
COALESCE(suggested_evidence, '[]'::jsonb),
|
||||
COALESCE(related_keywords, '[]'::jsonb),
|
||||
is_builtin, tenant_id,
|
||||
created_at
|
||||
FROM iace_hazard_library WHERE 1=1`
|
||||
|
||||
@@ -1625,12 +1659,18 @@ func (s *Store) ListHazardLibrary(ctx context.Context, category string, componen
|
||||
for rows.Next() {
|
||||
var e HazardLibraryEntry
|
||||
var applicableComponentTypes, regulationReferences, suggestedMitigations []byte
|
||||
var typicalCauses, relevantPhases, measuresDesign, measuresTechnical, measuresInfo, evidence, keywords []byte
|
||||
|
||||
err := rows.Scan(
|
||||
&e.ID, &e.Category, &e.Name, &e.Description,
|
||||
&e.ID, &e.Category, &e.SubCategory, &e.Name, &e.Description,
|
||||
&e.DefaultSeverity, &e.DefaultProbability,
|
||||
&e.DefaultExposure, &e.DefaultAvoidance,
|
||||
&applicableComponentTypes, ®ulationReferences,
|
||||
&suggestedMitigations, &e.IsBuiltin, &e.TenantID,
|
||||
&suggestedMitigations,
|
||||
&typicalCauses, &e.TypicalHarm, &relevantPhases,
|
||||
&measuresDesign, &measuresTechnical, &measuresInfo,
|
||||
&evidence, &keywords,
|
||||
&e.IsBuiltin, &e.TenantID,
|
||||
&e.CreatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -1640,6 +1680,13 @@ func (s *Store) ListHazardLibrary(ctx context.Context, category string, componen
|
||||
json.Unmarshal(applicableComponentTypes, &e.ApplicableComponentTypes)
|
||||
json.Unmarshal(regulationReferences, &e.RegulationReferences)
|
||||
json.Unmarshal(suggestedMitigations, &e.SuggestedMitigations)
|
||||
json.Unmarshal(typicalCauses, &e.TypicalCauses)
|
||||
json.Unmarshal(relevantPhases, &e.RelevantLifecyclePhases)
|
||||
json.Unmarshal(measuresDesign, &e.RecommendedMeasuresDesign)
|
||||
json.Unmarshal(measuresTechnical, &e.RecommendedMeasuresTechnical)
|
||||
json.Unmarshal(measuresInfo, &e.RecommendedMeasuresInformation)
|
||||
json.Unmarshal(evidence, &e.SuggestedEvidence)
|
||||
json.Unmarshal(keywords, &e.RelatedKeywords)
|
||||
|
||||
if e.ApplicableComponentTypes == nil {
|
||||
e.ApplicableComponentTypes = []string{}
|
||||
@@ -1658,6 +1705,9 @@ func (s *Store) ListHazardLibrary(ctx context.Context, category string, componen
|
||||
func (s *Store) GetHazardLibraryEntry(ctx context.Context, id uuid.UUID) (*HazardLibraryEntry, error) {
|
||||
var e HazardLibraryEntry
|
||||
var applicableComponentTypes, regulationReferences, suggestedMitigations []byte
|
||||
var typicalCauses, relevantLifecyclePhases []byte
|
||||
var recommendedMeasuresDesign, recommendedMeasuresTechnical, recommendedMeasuresInformation []byte
|
||||
var suggestedEvidence, relatedKeywords []byte
|
||||
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
SELECT
|
||||
@@ -1665,7 +1715,18 @@ func (s *Store) GetHazardLibraryEntry(ctx context.Context, id uuid.UUID) (*Hazar
|
||||
default_severity, default_probability,
|
||||
applicable_component_types, regulation_references,
|
||||
suggested_mitigations, is_builtin, tenant_id,
|
||||
created_at
|
||||
created_at,
|
||||
COALESCE(sub_category, ''),
|
||||
COALESCE(default_exposure, 3),
|
||||
COALESCE(default_avoidance, 3),
|
||||
COALESCE(typical_causes, '[]'),
|
||||
COALESCE(typical_harm, ''),
|
||||
COALESCE(relevant_lifecycle_phases, '[]'),
|
||||
COALESCE(recommended_measures_design, '[]'),
|
||||
COALESCE(recommended_measures_technical, '[]'),
|
||||
COALESCE(recommended_measures_information, '[]'),
|
||||
COALESCE(suggested_evidence, '[]'),
|
||||
COALESCE(related_keywords, '[]')
|
||||
FROM iace_hazard_library WHERE id = $1
|
||||
`, id).Scan(
|
||||
&e.ID, &e.Category, &e.Name, &e.Description,
|
||||
@@ -1673,6 +1734,12 @@ func (s *Store) GetHazardLibraryEntry(ctx context.Context, id uuid.UUID) (*Hazar
|
||||
&applicableComponentTypes, ®ulationReferences,
|
||||
&suggestedMitigations, &e.IsBuiltin, &e.TenantID,
|
||||
&e.CreatedAt,
|
||||
&e.SubCategory,
|
||||
&e.DefaultExposure, &e.DefaultAvoidance,
|
||||
&typicalCauses, &e.TypicalHarm,
|
||||
&relevantLifecyclePhases,
|
||||
&recommendedMeasuresDesign, &recommendedMeasuresTechnical, &recommendedMeasuresInformation,
|
||||
&suggestedEvidence, &relatedKeywords,
|
||||
)
|
||||
if err == pgx.ErrNoRows {
|
||||
return nil, nil
|
||||
@@ -1684,6 +1751,13 @@ func (s *Store) GetHazardLibraryEntry(ctx context.Context, id uuid.UUID) (*Hazar
|
||||
json.Unmarshal(applicableComponentTypes, &e.ApplicableComponentTypes)
|
||||
json.Unmarshal(regulationReferences, &e.RegulationReferences)
|
||||
json.Unmarshal(suggestedMitigations, &e.SuggestedMitigations)
|
||||
json.Unmarshal(typicalCauses, &e.TypicalCauses)
|
||||
json.Unmarshal(relevantLifecyclePhases, &e.RelevantLifecyclePhases)
|
||||
json.Unmarshal(recommendedMeasuresDesign, &e.RecommendedMeasuresDesign)
|
||||
json.Unmarshal(recommendedMeasuresTechnical, &e.RecommendedMeasuresTechnical)
|
||||
json.Unmarshal(recommendedMeasuresInformation, &e.RecommendedMeasuresInformation)
|
||||
json.Unmarshal(suggestedEvidence, &e.SuggestedEvidence)
|
||||
json.Unmarshal(relatedKeywords, &e.RelatedKeywords)
|
||||
|
||||
if e.ApplicableComponentTypes == nil {
|
||||
e.ApplicableComponentTypes = []string{}
|
||||
@@ -1731,6 +1805,10 @@ func (s *Store) GetRiskSummary(ctx context.Context, projectID uuid.UUID) (*RiskS
|
||||
}
|
||||
|
||||
switch latest.RiskLevel {
|
||||
case RiskLevelNotAcceptable:
|
||||
summary.NotAcceptable++
|
||||
case RiskLevelVeryHigh:
|
||||
summary.VeryHigh++
|
||||
case RiskLevelCritical:
|
||||
summary.Critical++
|
||||
case RiskLevelHigh:
|
||||
@@ -1761,6 +1839,10 @@ func (s *Store) GetRiskSummary(ctx context.Context, projectID uuid.UUID) (*RiskS
|
||||
// riskLevelSeverity returns a numeric severity for risk level comparison
|
||||
func riskLevelSeverity(rl RiskLevel) int {
|
||||
switch rl {
|
||||
case RiskLevelNotAcceptable:
|
||||
return 7
|
||||
case RiskLevelVeryHigh:
|
||||
return 6
|
||||
case RiskLevelCritical:
|
||||
return 5
|
||||
case RiskLevelHigh:
|
||||
@@ -1775,3 +1857,72 @@ func riskLevelSeverity(rl RiskLevel) int {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// ListLifecyclePhases returns all 12 lifecycle phases with DE/EN labels
|
||||
func (s *Store) ListLifecyclePhases(ctx context.Context) ([]LifecyclePhaseInfo, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT id, label_de, label_en, sort_order
|
||||
FROM iace_lifecycle_phases
|
||||
ORDER BY sort_order ASC
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list lifecycle phases: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var phases []LifecyclePhaseInfo
|
||||
for rows.Next() {
|
||||
var p LifecyclePhaseInfo
|
||||
if err := rows.Scan(&p.ID, &p.LabelDE, &p.LabelEN, &p.Sort); err != nil {
|
||||
return nil, fmt.Errorf("list lifecycle phases scan: %w", err)
|
||||
}
|
||||
phases = append(phases, p)
|
||||
}
|
||||
return phases, nil
|
||||
}
|
||||
|
||||
// ListRoles returns all affected person roles from the reference table
|
||||
func (s *Store) ListRoles(ctx context.Context) ([]RoleInfo, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT id, label_de, label_en, sort_order
|
||||
FROM iace_roles
|
||||
ORDER BY sort_order ASC
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list roles: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var roles []RoleInfo
|
||||
for rows.Next() {
|
||||
var r RoleInfo
|
||||
if err := rows.Scan(&r.ID, &r.LabelDE, &r.LabelEN, &r.Sort); err != nil {
|
||||
return nil, fmt.Errorf("list roles scan: %w", err)
|
||||
}
|
||||
roles = append(roles, r)
|
||||
}
|
||||
return roles, nil
|
||||
}
|
||||
|
||||
// ListEvidenceTypes returns all evidence types from the reference table
|
||||
func (s *Store) ListEvidenceTypes(ctx context.Context) ([]EvidenceTypeInfo, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT id, category, label_de, label_en, sort_order
|
||||
FROM iace_evidence_types
|
||||
ORDER BY sort_order ASC
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list evidence types: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var types []EvidenceTypeInfo
|
||||
for rows.Next() {
|
||||
var e EvidenceTypeInfo
|
||||
if err := rows.Scan(&e.ID, &e.Category, &e.LabelDE, &e.LabelEN, &e.Sort); err != nil {
|
||||
return nil, fmt.Errorf("list evidence types scan: %w", err)
|
||||
}
|
||||
types = append(types, e)
|
||||
}
|
||||
return types, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user