feat(iace): Sprint 4B — ISO 12100 Hazard/Situation/Harm Trennung
ISO 12100 trennt: Hazard (Quelle) → Hazardous Situation (Person exponiert) → Harm (Verletzung).
Bisher war alles in einem Hazard-Record vermischt.
Implementierung als abgeleitetes Feld (keine DB-Migration noetig):
- HazardType Feld auf Hazard Entity ("hazard"|"hazardous_situation"|"harm")
- DeriveHazardType() berechnet Typ aus Scenario/PossibleHarm/Category
- Explizites Override moeglich (HazardType direkt setzen)
- GeneratedHazardType auf HazardPattern fuer Pattern-gesteuerte Zuweisung
- Store: GetHazard/ListHazards setzen HazardType automatisch
- Init-Handler: Fuellt jetzt TriggerEvent, PossibleHarm, AffectedPerson, HazardousZone
aus Pattern-Match-Daten (vorher leer gelassen)
6 neue Tests: ScenarioAndHarm, HarmOnly, CategoryOnly, ExplicitOverride,
EmptyFallback, PatternMatchField
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -140,13 +140,21 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
|
|||||||
name = cat
|
name = cat
|
||||||
}
|
}
|
||||||
scenario := mp.ScenarioDE
|
scenario := mp.ScenarioDE
|
||||||
|
hazardType := mp.GeneratedHazardType
|
||||||
|
if hazardType == "" {
|
||||||
|
hazardType = iace.DefaultHazardType
|
||||||
|
}
|
||||||
hz, cerr := h.store.CreateHazard(ctx, iace.CreateHazardRequest{
|
hz, cerr := h.store.CreateHazard(ctx, iace.CreateHazardRequest{
|
||||||
ProjectID: projectID,
|
ProjectID: projectID,
|
||||||
ComponentID: defaultCompID,
|
ComponentID: defaultCompID,
|
||||||
Name: name,
|
Name: name,
|
||||||
Description: scenario,
|
Description: scenario,
|
||||||
Category: cat,
|
Category: cat,
|
||||||
Scenario: scenario,
|
Scenario: scenario,
|
||||||
|
TriggerEvent: mp.TriggerDE,
|
||||||
|
PossibleHarm: mp.HarmDE,
|
||||||
|
AffectedPerson: mp.AffectedDE,
|
||||||
|
HazardousZone: mp.ZoneDE,
|
||||||
})
|
})
|
||||||
if cerr == nil {
|
if cerr == nil {
|
||||||
created++
|
created++
|
||||||
|
|||||||
@@ -46,6 +46,10 @@ type HazardPattern struct {
|
|||||||
// interact with the machine. Empty/nil = fires for all roles (backwards compatible).
|
// interact with the machine. Empty/nil = fires for all roles (backwards compatible).
|
||||||
// Standard roles: operator, maintenance_tech, programmer, cleaning_staff, bystander, supervisor.
|
// Standard roles: operator, maintenance_tech, programmer, cleaning_staff, bystander, supervisor.
|
||||||
HumanRoles []string `json:"human_roles,omitempty"`
|
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).
|
// Standard human roles for machinery interaction (ISO 12100 + BetrSichV).
|
||||||
|
|||||||
@@ -57,6 +57,37 @@ const (
|
|||||||
HazardStatusClosed HazardStatus = "closed"
|
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
|
// AssessmentType represents the type of risk assessment
|
||||||
type AssessmentType string
|
type AssessmentType string
|
||||||
|
|
||||||
|
|||||||
@@ -111,6 +111,12 @@ type Hazard struct {
|
|||||||
TriggerEvent string `json:"trigger_event,omitempty"`
|
TriggerEvent string `json:"trigger_event,omitempty"`
|
||||||
AffectedPerson string `json:"affected_person,omitempty"`
|
AffectedPerson string `json:"affected_person,omitempty"`
|
||||||
PossibleHarm string `json:"possible_harm,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"`
|
ReviewStatus ReviewStatus `json:"review_status,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|||||||
@@ -56,7 +56,8 @@ type PatternMatch struct {
|
|||||||
RequiresExpert bool `json:"requires_expert,omitempty"`
|
RequiresExpert bool `json:"requires_expert,omitempty"`
|
||||||
OperationalStates []string `json:"operational_states,omitempty"`
|
OperationalStates []string `json:"operational_states,omitempty"`
|
||||||
StateTransitions []string `json:"state_transitions,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.
|
// HazardSuggestion is a suggested hazard from pattern matching.
|
||||||
@@ -225,9 +226,10 @@ func (e *PatternEngine) Match(input MatchInput) *MatchOutput {
|
|||||||
HazardCats: p.GeneratedHazardCats,
|
HazardCats: p.GeneratedHazardCats,
|
||||||
ExpertHintDE: p.ExpertHintDE,
|
ExpertHintDE: p.ExpertHintDE,
|
||||||
RequiresExpert: p.RequiresExpertCalculation,
|
RequiresExpert: p.RequiresExpertCalculation,
|
||||||
OperationalStates: p.OperationalStates,
|
OperationalStates: p.OperationalStates,
|
||||||
StateTransitions: p.StateTransitions,
|
StateTransitions: p.StateTransitions,
|
||||||
HumanRoles: p.HumanRoles,
|
HumanRoles: p.HumanRoles,
|
||||||
|
GeneratedHazardType: p.GeneratedHazardType,
|
||||||
})
|
})
|
||||||
|
|
||||||
for _, cat := range p.GeneratedHazardCats {
|
for _, cat := range p.GeneratedHazardCats {
|
||||||
|
|||||||
@@ -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) {
|
func TestEvidenceTypes_SortOrder(t *testing.T) {
|
||||||
types := GetEvidenceTypeLibrary()
|
types := GetEvidenceTypeLibrary()
|
||||||
for i := 1; i < len(types); i++ {
|
for i := 1; i < len(types); i++ {
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ func (s *Store) GetHazard(ctx context.Context, id uuid.UUID) (*Hazard, error) {
|
|||||||
|
|
||||||
h.Status = HazardStatus(status)
|
h.Status = HazardStatus(status)
|
||||||
h.ReviewStatus = ReviewStatus(reviewStatus)
|
h.ReviewStatus = ReviewStatus(reviewStatus)
|
||||||
|
h.HazardType = DeriveHazardType(&h)
|
||||||
return &h, nil
|
return &h, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,6 +134,7 @@ func (s *Store) ListHazards(ctx context.Context, projectID uuid.UUID) ([]Hazard,
|
|||||||
|
|
||||||
h.Status = HazardStatus(status)
|
h.Status = HazardStatus(status)
|
||||||
h.ReviewStatus = ReviewStatus(reviewStatus)
|
h.ReviewStatus = ReviewStatus(reviewStatus)
|
||||||
|
h.HazardType = DeriveHazardType(&h)
|
||||||
hazards = append(hazards, h)
|
hazards = append(hazards, h)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user