diff --git a/ai-compliance-sdk/internal/api/handlers/iace_handler_init.go b/ai-compliance-sdk/internal/api/handlers/iace_handler_init.go index a6cda39..6d695b7 100644 --- a/ai-compliance-sdk/internal/api/handlers/iace_handler_init.go +++ b/ai-compliance-sdk/internal/api/handlers/iace_handler_init.go @@ -140,13 +140,21 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) { name = cat } scenario := mp.ScenarioDE + hazardType := mp.GeneratedHazardType + if hazardType == "" { + hazardType = iace.DefaultHazardType + } hz, cerr := h.store.CreateHazard(ctx, iace.CreateHazardRequest{ - ProjectID: projectID, - ComponentID: defaultCompID, - Name: name, - Description: scenario, - Category: cat, - Scenario: scenario, + ProjectID: projectID, + ComponentID: defaultCompID, + Name: name, + Description: scenario, + Category: cat, + Scenario: scenario, + TriggerEvent: mp.TriggerDE, + PossibleHarm: mp.HarmDE, + AffectedPerson: mp.AffectedDE, + HazardousZone: mp.ZoneDE, }) if cerr == nil { created++ diff --git a/ai-compliance-sdk/internal/iace/hazard_pattern_types.go b/ai-compliance-sdk/internal/iace/hazard_pattern_types.go index 58b825e..5c4ad74 100644 --- a/ai-compliance-sdk/internal/iace/hazard_pattern_types.go +++ b/ai-compliance-sdk/internal/iace/hazard_pattern_types.go @@ -46,6 +46,10 @@ type HazardPattern struct { // interact with the machine. Empty/nil = fires for all roles (backwards compatible). // Standard roles: operator, maintenance_tech, programmer, cleaning_staff, bystander, supervisor. HumanRoles []string `json:"human_roles,omitempty"` + // GeneratedHazardType sets the ISO 12100 type for hazards created by this pattern. + // "hazard" = source only, "hazardous_situation" = person exposed, "harm" = injury. + // Empty = default (hazardous_situation). + GeneratedHazardType string `json:"generated_hazard_type,omitempty"` } // Standard human roles for machinery interaction (ISO 12100 + BetrSichV). diff --git a/ai-compliance-sdk/internal/iace/models.go b/ai-compliance-sdk/internal/iace/models.go index f596e12..fc58b94 100644 --- a/ai-compliance-sdk/internal/iace/models.go +++ b/ai-compliance-sdk/internal/iace/models.go @@ -57,6 +57,37 @@ const ( HazardStatusClosed HazardStatus = "closed" ) +// HazardType distinguishes ISO 12100 concepts in the hazard chain: +// Hazard → Hazardous Situation → Harm +const ( + HazardTypeHazard = "hazard" // Source of potential harm (e.g. rotating shaft) + HazardTypeHazardousSituation = "hazardous_situation" // Person exposed to hazard (e.g. operator near shaft) + HazardTypeHarm = "harm" // Injury outcome (e.g. entanglement) + DefaultHazardType = HazardTypeHazardousSituation +) + +// DeriveHazardType determines the ISO 12100 hazard type from the hazard's fields. +// If an explicit type is set, it is returned as-is. Otherwise: +// - PossibleHarm filled + Scenario filled → "hazardous_situation" (most specific) +// - Only PossibleHarm filled → "harm" +// - Only TriggerEvent/Category → "hazard" (source only) +// - Default fallback → "hazardous_situation" +func DeriveHazardType(h *Hazard) string { + if h.HazardType != "" { + return h.HazardType + } + if h.Scenario != "" && h.PossibleHarm != "" { + return HazardTypeHazardousSituation + } + if h.PossibleHarm != "" && h.Scenario == "" { + return HazardTypeHarm + } + if h.Scenario == "" && h.PossibleHarm == "" && h.Category != "" { + return HazardTypeHazard + } + return DefaultHazardType +} + // AssessmentType represents the type of risk assessment type AssessmentType string diff --git a/ai-compliance-sdk/internal/iace/models_entities.go b/ai-compliance-sdk/internal/iace/models_entities.go index 0c304a7..e0b30da 100644 --- a/ai-compliance-sdk/internal/iace/models_entities.go +++ b/ai-compliance-sdk/internal/iace/models_entities.go @@ -111,6 +111,12 @@ type Hazard struct { TriggerEvent string `json:"trigger_event,omitempty"` AffectedPerson string `json:"affected_person,omitempty"` PossibleHarm string `json:"possible_harm,omitempty"` + // HazardType distinguishes ISO 12100 concepts: + // "hazard" — the source of potential harm (e.g. mechanical movement) + // "hazardous_situation" — person exposed to hazard (e.g. person in motion range) + // "harm" — the injury outcome (e.g. crushing) + // Derived field — not stored in DB. Computed by DeriveHazardType(). + HazardType string `json:"hazard_type,omitempty"` ReviewStatus ReviewStatus `json:"review_status,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` diff --git a/ai-compliance-sdk/internal/iace/pattern_engine.go b/ai-compliance-sdk/internal/iace/pattern_engine.go index 95007f0..1c8aef0 100644 --- a/ai-compliance-sdk/internal/iace/pattern_engine.go +++ b/ai-compliance-sdk/internal/iace/pattern_engine.go @@ -56,7 +56,8 @@ type PatternMatch struct { RequiresExpert bool `json:"requires_expert,omitempty"` OperationalStates []string `json:"operational_states,omitempty"` StateTransitions []string `json:"state_transitions,omitempty"` - HumanRoles []string `json:"human_roles,omitempty"` + HumanRoles []string `json:"human_roles,omitempty"` + GeneratedHazardType string `json:"generated_hazard_type,omitempty"` } // HazardSuggestion is a suggested hazard from pattern matching. @@ -225,9 +226,10 @@ func (e *PatternEngine) Match(input MatchInput) *MatchOutput { HazardCats: p.GeneratedHazardCats, ExpertHintDE: p.ExpertHintDE, RequiresExpert: p.RequiresExpertCalculation, - OperationalStates: p.OperationalStates, - StateTransitions: p.StateTransitions, - HumanRoles: p.HumanRoles, + OperationalStates: p.OperationalStates, + StateTransitions: p.StateTransitions, + HumanRoles: p.HumanRoles, + GeneratedHazardType: p.GeneratedHazardType, }) for _, cat := range p.GeneratedHazardCats { diff --git a/ai-compliance-sdk/internal/iace/phase3_4_test.go b/ai-compliance-sdk/internal/iace/phase3_4_test.go index 1fcd031..ba09af1 100644 --- a/ai-compliance-sdk/internal/iace/phase3_4_test.go +++ b/ai-compliance-sdk/internal/iace/phase3_4_test.go @@ -338,6 +338,62 @@ func TestEvidenceTypes_UniqueIDs(t *testing.T) { } } +// ══════════════════════════════════════════════════════════════════ +// Sprint 4B: HazardType / ISO 12100 Trennung +// ══════════════════════════════════════════════════════════════════ + +func TestDeriveHazardType_WithScenarioAndHarm(t *testing.T) { + h := &Hazard{Scenario: "Person im Bewegungsraum", PossibleHarm: "Quetschung"} + if got := DeriveHazardType(h); got != HazardTypeHazardousSituation { + t.Errorf("expected hazardous_situation, got %s", got) + } +} + +func TestDeriveHazardType_HarmOnly(t *testing.T) { + h := &Hazard{PossibleHarm: "Quetschung", Category: "mechanical"} + if got := DeriveHazardType(h); got != HazardTypeHarm { + t.Errorf("expected harm, got %s", got) + } +} + +func TestDeriveHazardType_CategoryOnly(t *testing.T) { + h := &Hazard{Category: "mechanical_hazard"} + if got := DeriveHazardType(h); got != HazardTypeHazard { + t.Errorf("expected hazard, got %s", got) + } +} + +func TestDeriveHazardType_ExplicitOverride(t *testing.T) { + h := &Hazard{HazardType: "harm", Scenario: "ignored", PossibleHarm: "ignored"} + if got := DeriveHazardType(h); got != "harm" { + t.Errorf("expected explicit harm, got %s", got) + } +} + +func TestDeriveHazardType_EmptyFallback(t *testing.T) { + h := &Hazard{} + if got := DeriveHazardType(h); got != DefaultHazardType { + t.Errorf("expected default %s, got %s", DefaultHazardType, got) + } +} + +func TestPatternMatch_GeneratedHazardType(t *testing.T) { + // Verify PatternMatch carries GeneratedHazardType + engine := NewPatternEngine() + result := engine.Match(MatchInput{ + ComponentLibraryIDs: []string{"C001"}, + EnergySourceIDs: []string{"EN01"}, + }) + // Most patterns don't set GeneratedHazardType (empty = default) + // Just verify the field exists and doesn't crash + for _, p := range result.MatchedPatterns { + _ = p.GeneratedHazardType // Should compile and not panic + } + if len(result.MatchedPatterns) == 0 { + t.Fatal("expected matched patterns") + } +} + func TestEvidenceTypes_SortOrder(t *testing.T) { types := GetEvidenceTypeLibrary() for i := 1; i < len(types); i++ { diff --git a/ai-compliance-sdk/internal/iace/store_hazards.go b/ai-compliance-sdk/internal/iace/store_hazards.go index 67af66e..c6b6336 100644 --- a/ai-compliance-sdk/internal/iace/store_hazards.go +++ b/ai-compliance-sdk/internal/iace/store_hazards.go @@ -95,6 +95,7 @@ func (s *Store) GetHazard(ctx context.Context, id uuid.UUID) (*Hazard, error) { h.Status = HazardStatus(status) h.ReviewStatus = ReviewStatus(reviewStatus) + h.HazardType = DeriveHazardType(&h) return &h, nil } @@ -133,6 +134,7 @@ func (s *Store) ListHazards(ctx context.Context, projectID uuid.UUID) ([]Hazard, h.Status = HazardStatus(status) h.ReviewStatus = ReviewStatus(reviewStatus) + h.HazardType = DeriveHazardType(&h) hazards = append(hazards, h) }