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:
@@ -912,6 +912,242 @@ func TestClamp(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 10. ISO 12100 Mode — S × F × P × A (direct multiplication)
|
||||
// ============================================================================
|
||||
|
||||
func TestCalculateInherentRisk_ISO12100Mode(t *testing.T) {
|
||||
e := NewRiskEngine()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
s, f, p, a int
|
||||
expected float64
|
||||
}{
|
||||
// Minimum: 1×1×1×1 = 1
|
||||
{"min 1×1×1×1", 1, 1, 1, 1, 1},
|
||||
// Maximum: 5×5×5×5 = 625
|
||||
{"max 5×5×5×5", 5, 5, 5, 5, 625},
|
||||
// Typical mid-range: 3×3×3×3 = 81
|
||||
{"mid 3×3×3×3", 3, 3, 3, 3, 81},
|
||||
// High severity, low avoidance: 5×3×4×1 = 60
|
||||
{"high S low A", 5, 3, 4, 1, 60},
|
||||
// All factors high: 4×4×4×4 = 256
|
||||
{"high 4×4×4×4", 4, 4, 4, 4, 256},
|
||||
// Low risk: 2×1×2×1 = 4
|
||||
{"low risk", 2, 1, 2, 1, 4},
|
||||
// At not_acceptable boundary: 5×5×3×5 = 375
|
||||
{"above 300", 5, 5, 3, 5, 375},
|
||||
// At very_high boundary: 4×3×4×4 = 192
|
||||
{"very high range", 4, 3, 4, 4, 192},
|
||||
// At high boundary: 3×3×3×3 = 81
|
||||
{"high range", 3, 3, 3, 3, 81},
|
||||
// At medium range: 2×3×3×2 = 36
|
||||
{"medium range", 2, 3, 3, 2, 36},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := e.CalculateInherentRisk(tt.s, tt.f, tt.p, tt.a)
|
||||
if !almostEqual(result, tt.expected) {
|
||||
t.Errorf("CalculateInherentRisk(%d, %d, %d, %d) = %v, want %v",
|
||||
tt.s, tt.f, tt.p, tt.a, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetermineRiskLevelISO(t *testing.T) {
|
||||
e := NewRiskEngine()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
risk float64
|
||||
expected RiskLevel
|
||||
}{
|
||||
// not_acceptable: > 300
|
||||
{"not_acceptable at 301", 301, RiskLevelNotAcceptable},
|
||||
{"not_acceptable at 625", 625, RiskLevelNotAcceptable},
|
||||
{"not_acceptable at 400", 400, RiskLevelNotAcceptable},
|
||||
// very_high: 151-300
|
||||
{"very_high at 300", 300, RiskLevelVeryHigh},
|
||||
{"very_high at 151", 151, RiskLevelVeryHigh},
|
||||
{"very_high at 200", 200, RiskLevelVeryHigh},
|
||||
// high: 61-150
|
||||
{"high at 150", 150, RiskLevelHigh},
|
||||
{"high at 61", 61, RiskLevelHigh},
|
||||
{"high at 100", 100, RiskLevelHigh},
|
||||
// medium: 21-60
|
||||
{"medium at 60", 60, RiskLevelMedium},
|
||||
{"medium at 21", 21, RiskLevelMedium},
|
||||
{"medium at 40", 40, RiskLevelMedium},
|
||||
// low: 1-20
|
||||
{"low at 20", 20, RiskLevelLow},
|
||||
{"low at 1", 1, RiskLevelLow},
|
||||
{"low at 10", 10, RiskLevelLow},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := e.DetermineRiskLevelISO(tt.risk)
|
||||
if result != tt.expected {
|
||||
t.Errorf("DetermineRiskLevelISO(%v) = %v, want %v", tt.risk, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAvoidanceBackwardCompat(t *testing.T) {
|
||||
e := NewRiskEngine()
|
||||
|
||||
// With avoidance=0, formula must remain S×E×P (legacy mode)
|
||||
tests := []struct {
|
||||
s, ex, p int
|
||||
expected float64
|
||||
}{
|
||||
{5, 5, 5, 125},
|
||||
{3, 3, 3, 27},
|
||||
{1, 1, 1, 1},
|
||||
{4, 2, 5, 40},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := e.CalculateInherentRisk(tt.s, tt.ex, tt.p, 0)
|
||||
if !almostEqual(result, tt.expected) {
|
||||
t.Errorf("Legacy mode: CalculateInherentRisk(%d,%d,%d,0) = %v, want %v",
|
||||
tt.s, tt.ex, tt.p, result, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateProtectiveMeasureHierarchy(t *testing.T) {
|
||||
e := NewRiskEngine()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
reductionType ReductionType
|
||||
existing []Mitigation
|
||||
wantWarnings int
|
||||
}{
|
||||
{
|
||||
name: "design measure — no warning",
|
||||
reductionType: ReductionTypeDesign,
|
||||
existing: nil,
|
||||
wantWarnings: 0,
|
||||
},
|
||||
{
|
||||
name: "information without any — full warning",
|
||||
reductionType: ReductionTypeInformation,
|
||||
existing: nil,
|
||||
wantWarnings: 1,
|
||||
},
|
||||
{
|
||||
name: "information with design — no warning",
|
||||
reductionType: ReductionTypeInformation,
|
||||
existing: []Mitigation{
|
||||
{ReductionType: ReductionTypeDesign, Status: MitigationStatusImplemented},
|
||||
{ReductionType: ReductionTypeProtective, Status: MitigationStatusPlanned},
|
||||
},
|
||||
wantWarnings: 0,
|
||||
},
|
||||
{
|
||||
name: "information with only protective — missing design warning",
|
||||
reductionType: ReductionTypeInformation,
|
||||
existing: []Mitigation{
|
||||
{ReductionType: ReductionTypeProtective, Status: MitigationStatusImplemented},
|
||||
},
|
||||
wantWarnings: 1,
|
||||
},
|
||||
{
|
||||
name: "information with rejected measures — full warning",
|
||||
reductionType: ReductionTypeInformation,
|
||||
existing: []Mitigation{
|
||||
{ReductionType: ReductionTypeDesign, Status: MitigationStatusRejected},
|
||||
{ReductionType: ReductionTypeProtective, Status: MitigationStatusRejected},
|
||||
},
|
||||
wantWarnings: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
warnings := e.ValidateProtectiveMeasureHierarchy(tt.reductionType, tt.existing)
|
||||
if len(warnings) != tt.wantWarnings {
|
||||
t.Errorf("got %d warnings, want %d: %v", len(warnings), tt.wantWarnings, warnings)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestComputeRisk_ISOMode(t *testing.T) {
|
||||
e := NewRiskEngine()
|
||||
|
||||
// ISO mode: avoidance >= 1 → uses DetermineRiskLevelISO for inherent risk classification
|
||||
tests := []struct {
|
||||
name string
|
||||
input RiskComputeInput
|
||||
wantLevel RiskLevel
|
||||
}{
|
||||
{
|
||||
name: "ISO low risk",
|
||||
input: RiskComputeInput{
|
||||
Severity: 2, Exposure: 1, Probability: 2, Avoidance: 1,
|
||||
ControlMaturity: 2, ControlCoverage: 0.5, TestEvidence: 0.5,
|
||||
},
|
||||
wantLevel: RiskLevelLow, // 2×1×2×1 = 4 → low
|
||||
},
|
||||
{
|
||||
name: "ISO medium risk",
|
||||
input: RiskComputeInput{
|
||||
Severity: 3, Exposure: 2, Probability: 3, Avoidance: 2,
|
||||
ControlMaturity: 2, ControlCoverage: 0.5, TestEvidence: 0.5,
|
||||
},
|
||||
wantLevel: RiskLevelMedium, // 3×2×3×2 = 36 → medium
|
||||
},
|
||||
{
|
||||
name: "ISO high risk",
|
||||
input: RiskComputeInput{
|
||||
Severity: 4, Exposure: 3, Probability: 3, Avoidance: 3,
|
||||
ControlMaturity: 2, ControlCoverage: 0.5, TestEvidence: 0.5,
|
||||
},
|
||||
wantLevel: RiskLevelHigh, // 4×3×3×3 = 108 → high
|
||||
},
|
||||
{
|
||||
name: "ISO very high risk",
|
||||
input: RiskComputeInput{
|
||||
Severity: 4, Exposure: 4, Probability: 4, Avoidance: 3,
|
||||
ControlMaturity: 2, ControlCoverage: 0.5, TestEvidence: 0.5,
|
||||
},
|
||||
wantLevel: RiskLevelVeryHigh, // 4×4×4×3 = 192 → very_high
|
||||
},
|
||||
{
|
||||
name: "ISO not acceptable",
|
||||
input: RiskComputeInput{
|
||||
Severity: 5, Exposure: 5, Probability: 4, Avoidance: 4,
|
||||
ControlMaturity: 4, ControlCoverage: 1.0, TestEvidence: 1.0,
|
||||
},
|
||||
wantLevel: RiskLevelNotAcceptable, // 5×5×4×4 = 400 → not_acceptable
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := e.ComputeRisk(tt.input)
|
||||
if err != nil {
|
||||
t.Fatalf("ComputeRisk returned error: %v", err)
|
||||
}
|
||||
if result.RiskLevel != tt.wantLevel {
|
||||
t.Errorf("RiskLevel = %v, want %v (inherent=%v)",
|
||||
result.RiskLevel, tt.wantLevel, result.InherentRisk)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 11. Edge Cases (continued)
|
||||
// ============================================================================
|
||||
|
||||
func TestClampFloat(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
Reference in New Issue
Block a user