feat(iace): Einsatzbereich / Branche — filtert branchenspezifische Patterns
Build + Deploy / build-admin-compliance (push) Successful in 2m7s
Build + Deploy / build-backend-compliance (push) Successful in 13s
Build + Deploy / build-ai-sdk (push) Successful in 55s
Build + Deploy / build-developer-portal (push) Successful in 12s
Build + Deploy / build-tts (push) Successful in 34s
Build + Deploy / build-document-crawler (push) Successful in 12s
Build + Deploy / build-dsms-gateway (push) Successful in 13s
Build + Deploy / build-dsms-node (push) Successful in 14s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 18s
CI / secret-scan (push) Has been skipped
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 3m5s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Successful in 46s
CI / test-python-backend (push) Successful in 37s
CI / test-python-document-crawler (push) Successful in 26s
CI / test-python-dsms-gateway (push) Successful in 22s
CI / validate-canonical-controls (push) Successful in 15s
Build + Deploy / trigger-orca (push) Successful in 2m19s
Build + Deploy / build-admin-compliance (push) Successful in 2m7s
Build + Deploy / build-backend-compliance (push) Successful in 13s
Build + Deploy / build-ai-sdk (push) Successful in 55s
Build + Deploy / build-developer-portal (push) Successful in 12s
Build + Deploy / build-tts (push) Successful in 34s
Build + Deploy / build-document-crawler (push) Successful in 12s
Build + Deploy / build-dsms-gateway (push) Successful in 13s
Build + Deploy / build-dsms-node (push) Successful in 14s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 18s
CI / secret-scan (push) Has been skipped
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 3m5s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Successful in 46s
CI / test-python-backend (push) Successful in 37s
CI / test-python-document-crawler (push) Successful in 26s
CI / test-python-dsms-gateway (push) Successful in 22s
CI / validate-canonical-controls (push) Successful in 15s
Build + Deploy / trigger-orca (push) Successful in 2m19s
Neues Feld "Einsatzbereich" auf Interview-Seite (Sektion 7) mit 15 Branchen. Pattern Engine bekommt MachineTypes aus MatchInput → branchenfremde Patterns (Medizin, Aufzug, Bau etc.) feuern nur wenn die Branche ausgewählt ist. Refactoring: iace_handler_init.go aufgeteilt in init + init_helpers (LOC-Limit). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+17
@@ -7,6 +7,7 @@ import {
|
|||||||
AREA_OF_USE_OPTIONS,
|
AREA_OF_USE_OPTIONS,
|
||||||
OPERATING_MODE_OPTIONS,
|
OPERATING_MODE_OPTIONS,
|
||||||
PERSON_GROUP_OPTIONS,
|
PERSON_GROUP_OPTIONS,
|
||||||
|
INDUSTRY_SECTOR_OPTIONS,
|
||||||
type LimitsFormData,
|
type LimitsFormData,
|
||||||
} from '../_types'
|
} from '../_types'
|
||||||
|
|
||||||
@@ -204,6 +205,22 @@ export function LimitsFormSections({ data, onChange, prefilled }: LimitsFormSect
|
|||||||
rows={4}
|
rows={4}
|
||||||
/>
|
/>
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
|
|
||||||
|
{/* Section 7: Einsatzbereich / Branche */}
|
||||||
|
<SectionCard section={FORM_SECTIONS[6]}>
|
||||||
|
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-3 mb-2">
|
||||||
|
<p className="text-xs text-blue-700 dark:text-blue-300">
|
||||||
|
Die Branchenauswahl steuert welche branchenspezifischen Gefaehrdungsmuster (z.B. Medizintechnik, Lebensmittel, Aufzuege) bei der Risikoanalyse beruecksichtigt werden. Branchenfremde Muster werden automatisch ausgeblendet.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<CheckboxGroup
|
||||||
|
label="Einsatzbereiche"
|
||||||
|
values={data.industry_sectors}
|
||||||
|
onChange={(v) => onChange('industry_sectors', v)}
|
||||||
|
options={INDUSTRY_SECTOR_OPTIONS}
|
||||||
|
helpText="Waehlen Sie alle zutreffenden Branchen. Bei Mehrfachauswahl werden alle relevanten Gefaehrdungen beruecksichtigt."
|
||||||
|
/>
|
||||||
|
</SectionCard>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ export interface LimitsFormData {
|
|||||||
// Section 6: Betroffene Personen
|
// Section 6: Betroffene Personen
|
||||||
person_groups: string[]
|
person_groups: string[]
|
||||||
qualification_requirements: string
|
qualification_requirements: string
|
||||||
|
|
||||||
|
// Section 7: Einsatzbereich / Branche (fuer Pattern-Filterung)
|
||||||
|
industry_sectors: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EMPTY_LIMITS_FORM: LimitsFormData = {
|
export const EMPTY_LIMITS_FORM: LimitsFormData = {
|
||||||
@@ -59,6 +62,7 @@ export const EMPTY_LIMITS_FORM: LimitsFormData = {
|
|||||||
pneumatic_hydraulic_interfaces: '',
|
pneumatic_hydraulic_interfaces: '',
|
||||||
person_groups: [],
|
person_groups: [],
|
||||||
qualification_requirements: '',
|
qualification_requirements: '',
|
||||||
|
industry_sectors: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AREA_OF_USE_OPTIONS = [
|
export const AREA_OF_USE_OPTIONS = [
|
||||||
@@ -77,6 +81,43 @@ export const OPERATING_MODE_OPTIONS = [
|
|||||||
'Wartung',
|
'Wartung',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export const INDUSTRY_SECTOR_OPTIONS = [
|
||||||
|
'Allgemeiner Maschinenbau',
|
||||||
|
'Automobil / Zulieferer',
|
||||||
|
'Robotik / Cobot',
|
||||||
|
'Medizintechnik',
|
||||||
|
'Lebensmittel / Getraenke',
|
||||||
|
'Verpackung',
|
||||||
|
'Pharma / Chemie',
|
||||||
|
'Bau / Baumaschinen',
|
||||||
|
'Forst / Holzbearbeitung',
|
||||||
|
'Aufzuege / Foerdertechnik',
|
||||||
|
'Textil',
|
||||||
|
'Landmaschinen',
|
||||||
|
'Druck / Papier',
|
||||||
|
'Metall / CNC',
|
||||||
|
'Schweissen / Oberflaechentechnik',
|
||||||
|
]
|
||||||
|
|
||||||
|
/** Maps display labels to MachineTypes for pattern engine filtering */
|
||||||
|
export const INDUSTRY_TO_MACHINE_TYPES: Record<string, string[]> = {
|
||||||
|
'Allgemeiner Maschinenbau': ['general_industry'],
|
||||||
|
'Automobil / Zulieferer': ['automotive'],
|
||||||
|
'Robotik / Cobot': ['robotics_cobot', 'cobot'],
|
||||||
|
'Medizintechnik': ['medical_device', 'infusion_pump', 'ventilator', 'patient_monitor'],
|
||||||
|
'Lebensmittel / Getraenke': ['food_processing'],
|
||||||
|
'Verpackung': ['packaging'],
|
||||||
|
'Pharma / Chemie': ['chemical', 'pharmaceutical'],
|
||||||
|
'Bau / Baumaschinen': ['construction', 'crane', 'excavator'],
|
||||||
|
'Forst / Holzbearbeitung': ['forestry', 'woodworking', 'circular_saw'],
|
||||||
|
'Aufzuege / Foerdertechnik': ['elevator', 'lift', 'escalator', 'conveyor'],
|
||||||
|
'Textil': ['textile', 'spinning', 'weaving', 'finishing'],
|
||||||
|
'Landmaschinen': ['agricultural', 'tractor', 'harvester'],
|
||||||
|
'Druck / Papier': ['printing'],
|
||||||
|
'Metall / CNC': ['cnc', 'metalworking', 'lathe', 'milling'],
|
||||||
|
'Schweissen / Oberflaechentechnik': ['welding', 'surface_treatment'],
|
||||||
|
}
|
||||||
|
|
||||||
export const PERSON_GROUP_OPTIONS = [
|
export const PERSON_GROUP_OPTIONS = [
|
||||||
'Bedienpersonal',
|
'Bedienpersonal',
|
||||||
'Einrichter',
|
'Einrichter',
|
||||||
@@ -93,7 +134,7 @@ export interface FormSection {
|
|||||||
number: number
|
number: number
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
icon: 'clipboard' | 'target' | 'alert' | 'box' | 'link' | 'users'
|
icon: 'clipboard' | 'target' | 'alert' | 'box' | 'link' | 'users' | 'briefcase'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FORM_SECTIONS: FormSection[] = [
|
export const FORM_SECTIONS: FormSection[] = [
|
||||||
@@ -139,4 +180,11 @@ export const FORM_SECTIONS: FormSection[] = [
|
|||||||
description: 'Personengruppen und Qualifikationsanforderungen',
|
description: 'Personengruppen und Qualifikationsanforderungen',
|
||||||
icon: 'users',
|
icon: 'users',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'industry_sector',
|
||||||
|
number: 7,
|
||||||
|
title: 'Einsatzbereich / Branche',
|
||||||
|
description: 'Branche bestimmt welche branchenspezifischen Gefaehrdungen beruecksichtigt werden',
|
||||||
|
icon: 'briefcase',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
@@ -89,7 +88,6 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
|
|||||||
if len(existingComps) == 0 && len(parseResult.Components) > 0 {
|
if len(existingComps) == 0 && len(parseResult.Components) > 0 {
|
||||||
created := 0
|
created := 0
|
||||||
for _, comp := range parseResult.Components {
|
for _, comp := range parseResult.Components {
|
||||||
// Derive component type from tags
|
|
||||||
compType := deriveComponentType(comp.Tags)
|
compType := deriveComponentType(comp.Tags)
|
||||||
_, cerr := h.store.CreateComponent(ctx, iace.CreateComponentRequest{
|
_, cerr := h.store.CreateComponent(ctx, iace.CreateComponentRequest{
|
||||||
ProjectID: projectID,
|
ProjectID: projectID,
|
||||||
@@ -117,9 +115,8 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
|
|||||||
energyIDs = append(energyIDs, e.SourceID)
|
energyIDs = append(energyIDs, e.SourceID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge explicit operational_states from UI with parsed states from narrative
|
|
||||||
operationalStates := mergeStringSlices(parseResult.OperationalStates, extractOperationalStatesFromMetadata(project.Metadata))
|
operationalStates := mergeStringSlices(parseResult.OperationalStates, extractOperationalStatesFromMetadata(project.Metadata))
|
||||||
stateTransitions := parseResult.StateTransitions
|
machineTypes := extractIndustrySectorsFromMetadata(project.Metadata)
|
||||||
|
|
||||||
engine := iace.NewPatternEngine()
|
engine := iace.NewPatternEngine()
|
||||||
matchOutput := engine.Match(iace.MatchInput{
|
matchOutput := engine.Match(iace.MatchInput{
|
||||||
@@ -128,8 +125,9 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
|
|||||||
LifecyclePhases: parseResult.LifecyclePhases,
|
LifecyclePhases: parseResult.LifecyclePhases,
|
||||||
CustomTags: parseResult.CustomTags,
|
CustomTags: parseResult.CustomTags,
|
||||||
OperationalStates: operationalStates,
|
OperationalStates: operationalStates,
|
||||||
StateTransitions: stateTransitions,
|
StateTransitions: parseResult.StateTransitions,
|
||||||
HumanRoles: parseResult.Roles,
|
HumanRoles: parseResult.Roles,
|
||||||
|
MachineTypes: machineTypes,
|
||||||
})
|
})
|
||||||
steps = append(steps, InitStep{
|
steps = append(steps, InitStep{
|
||||||
Name: "Patterns abgeglichen",
|
Name: "Patterns abgeglichen",
|
||||||
@@ -143,14 +141,12 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
|
|||||||
hazardIDsByCategory := make(map[string]uuid.UUID)
|
hazardIDsByCategory := make(map[string]uuid.UUID)
|
||||||
|
|
||||||
if len(existingHazards) == 0 && len(matchOutput.MatchedPatterns) > 0 {
|
if len(existingHazards) == 0 && len(matchOutput.MatchedPatterns) > 0 {
|
||||||
// Get first component for hazard assignment
|
|
||||||
comps, _ := h.store.ListComponents(ctx, projectID)
|
comps, _ := h.store.ListComponents(ctx, projectID)
|
||||||
var defaultCompID uuid.UUID
|
var defaultCompID uuid.UUID
|
||||||
if len(comps) > 0 {
|
if len(comps) > 0 {
|
||||||
defaultCompID = comps[0].ID
|
defaultCompID = comps[0].ID
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deduplicate by category — one hazard per category
|
|
||||||
created := 0
|
created := 0
|
||||||
seenCat := make(map[string]bool)
|
seenCat := make(map[string]bool)
|
||||||
for _, mp := range matchOutput.MatchedPatterns {
|
for _, mp := range matchOutput.MatchedPatterns {
|
||||||
@@ -164,18 +160,13 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
|
|||||||
if name == "" {
|
if name == "" {
|
||||||
name = cat
|
name = cat
|
||||||
}
|
}
|
||||||
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: mp.ScenarioDE,
|
||||||
Category: cat,
|
Category: cat,
|
||||||
Scenario: scenario,
|
Scenario: mp.ScenarioDE,
|
||||||
Function: iace.EncodeOpStates(mp.OperationalStates),
|
Function: iace.EncodeOpStates(mp.OperationalStates),
|
||||||
TriggerEvent: mp.TriggerDE,
|
TriggerEvent: mp.TriggerDE,
|
||||||
PossibleHarm: mp.HarmDE,
|
PossibleHarm: mp.HarmDE,
|
||||||
@@ -198,7 +189,7 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
steps = append(steps, hazardStep)
|
steps = append(steps, hazardStep)
|
||||||
|
|
||||||
// ── Step 6: Create mitigations (pattern-suggested + category fallback) ──
|
// ── Step 6: Create mitigations ──
|
||||||
existingMits, _ := h.store.ListMitigationsByProject(ctx, projectID)
|
existingMits, _ := h.store.ListMitigationsByProject(ctx, projectID)
|
||||||
mitStep := InitStep{Name: "Massnahmen erstellt", Status: "skipped"}
|
mitStep := InitStep{Name: "Massnahmen erstellt", Status: "skipped"}
|
||||||
|
|
||||||
@@ -214,7 +205,6 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
|
|||||||
created := 0
|
created := 0
|
||||||
usedMeasureIDs := make(map[string]bool)
|
usedMeasureIDs := make(map[string]bool)
|
||||||
|
|
||||||
// A) Pattern-suggested measures (direct reference)
|
|
||||||
for _, sm := range matchOutput.SuggestedMeasures {
|
for _, sm := range matchOutput.SuggestedMeasures {
|
||||||
entry, ok := measureByID[sm.MeasureID]
|
entry, ok := measureByID[sm.MeasureID]
|
||||||
if !ok || usedMeasureIDs[sm.MeasureID] {
|
if !ok || usedMeasureIDs[sm.MeasureID] {
|
||||||
@@ -229,10 +219,8 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
|
|||||||
rt = iace.ReductionTypeInformation
|
rt = iace.ReductionTypeInformation
|
||||||
}
|
}
|
||||||
_, cerr := h.store.CreateMitigation(ctx, iace.CreateMitigationRequest{
|
_, cerr := h.store.CreateMitigation(ctx, iace.CreateMitigationRequest{
|
||||||
HazardID: hazardID,
|
HazardID: hazardID, ReductionType: rt,
|
||||||
ReductionType: rt,
|
Name: entry.Name, Description: entry.Description,
|
||||||
Name: entry.Name,
|
|
||||||
Description: entry.Description,
|
|
||||||
})
|
})
|
||||||
if cerr == nil {
|
if cerr == nil {
|
||||||
created++
|
created++
|
||||||
@@ -240,13 +228,10 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// B) Category fallback — for each hazard category, add measures
|
|
||||||
// from the library that match (but weren't pattern-suggested)
|
|
||||||
for hazCat, hazID := range hazardIDsByCategory {
|
for hazCat, hazID := range hazardIDsByCategory {
|
||||||
measCat := patternCatToMeasureCat(hazCat)
|
measCat := patternCatToMeasureCat(hazCat)
|
||||||
candidates := measuresByCat[measCat]
|
|
||||||
added := 0
|
added := 0
|
||||||
for _, m := range candidates {
|
for _, m := range measuresByCat[measCat] {
|
||||||
if usedMeasureIDs[m.ID] || added >= 8 {
|
if usedMeasureIDs[m.ID] || added >= 8 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -255,10 +240,8 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
|
|||||||
rt = iace.ReductionTypeInformation
|
rt = iace.ReductionTypeInformation
|
||||||
}
|
}
|
||||||
_, cerr := h.store.CreateMitigation(ctx, iace.CreateMitigationRequest{
|
_, cerr := h.store.CreateMitigation(ctx, iace.CreateMitigationRequest{
|
||||||
HazardID: hazID,
|
HazardID: hazID, ReductionType: rt,
|
||||||
ReductionType: rt,
|
Name: m.Name, Description: m.Description,
|
||||||
Name: m.Name,
|
|
||||||
Description: m.Description,
|
|
||||||
})
|
})
|
||||||
if cerr == nil {
|
if cerr == nil {
|
||||||
created++
|
created++
|
||||||
@@ -267,7 +250,6 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mitStep = InitStep{Name: "Massnahmen erstellt", Status: "done", Count: created}
|
mitStep = InitStep{Name: "Massnahmen erstellt", Status: "done", Count: created}
|
||||||
} else if len(existingMits) > 0 {
|
} else if len(existingMits) > 0 {
|
||||||
mitStep.Details = "Bereits vorhanden"
|
mitStep.Details = "Bereits vorhanden"
|
||||||
@@ -285,11 +267,7 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
|
|||||||
if normResult != nil {
|
if normResult != nil {
|
||||||
normCount = len(normResult.ANorms) + len(normResult.B1Norms) + len(normResult.B2Norms) + len(normResult.CNorms)
|
normCount = len(normResult.ANorms) + len(normResult.B1Norms) + len(normResult.B2Norms) + len(normResult.CNorms)
|
||||||
}
|
}
|
||||||
steps = append(steps, InitStep{
|
steps = append(steps, InitStep{Name: "Normen vorgeschlagen", Status: "done", Count: normCount})
|
||||||
Name: "Normen vorgeschlagen",
|
|
||||||
Status: "done",
|
|
||||||
Count: normCount,
|
|
||||||
})
|
|
||||||
|
|
||||||
// ── Audit trail ──
|
// ── Audit trail ──
|
||||||
h.store.AddAuditEntry(ctx, projectID, "project_initialization", projectID,
|
h.store.AddAuditEntry(ctx, projectID, "project_initialization", projectID,
|
||||||
@@ -301,172 +279,9 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
|
|||||||
"project_id": projectID.String(),
|
"project_id": projectID.String(),
|
||||||
"steps": steps,
|
"steps": steps,
|
||||||
"summary": gin.H{
|
"summary": gin.H{
|
||||||
"components": steps[1].Count,
|
"components": steps[1].Count, "patterns": steps[2].Count,
|
||||||
"patterns": steps[2].Count,
|
"hazards": steps[3].Count, "mitigations": steps[4].Count,
|
||||||
"hazards": steps[3].Count,
|
"norms": steps[5].Count,
|
||||||
"mitigations": steps[4].Count,
|
|
||||||
"norms": steps[5].Count,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// extractNarrativeFromMetadata builds a combined text from the limits_form.
|
|
||||||
func extractNarrativeFromMetadata(metadata json.RawMessage) string {
|
|
||||||
if metadata == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
var meta map[string]json.RawMessage
|
|
||||||
if err := json.Unmarshal(metadata, &meta); err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
limitsRaw, ok := meta["limits_form"]
|
|
||||||
if !ok {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
var limits map[string]interface{}
|
|
||||||
if err := json.Unmarshal(limitsRaw, &limits); err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
textFields := []string{
|
|
||||||
"general_description", "intended_purpose", "foreseeable_misuse",
|
|
||||||
"space_limits", "time_limits", "environmental_conditions",
|
|
||||||
"energy_sources", "materials_processed", "operating_modes",
|
|
||||||
"maintenance_requirements", "personnel_requirements",
|
|
||||||
"interfaces_description", "control_system_description",
|
|
||||||
"safety_functions_description",
|
|
||||||
}
|
|
||||||
var result string
|
|
||||||
for _, field := range textFields {
|
|
||||||
if v, ok := limits[field]; ok {
|
|
||||||
if s, ok := v.(string); ok && s != "" {
|
|
||||||
result += s + "\n\n"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// patternCatToMeasureCat maps pattern hazard categories to measure categories.
|
|
||||||
// Patterns use "mechanical_hazard", measures use "mechanical".
|
|
||||||
func patternCatToMeasureCat(patternCat string) string {
|
|
||||||
m := map[string]string{
|
|
||||||
"mechanical_hazard": "mechanical",
|
|
||||||
"electrical_hazard": "electrical",
|
|
||||||
"thermal_hazard": "thermal",
|
|
||||||
"noise_vibration": "noise_vibration",
|
|
||||||
"pneumatic_hydraulic": "pneumatic_hydraulic",
|
|
||||||
"material_environmental": "material_environmental",
|
|
||||||
"ergonomic": "ergonomic",
|
|
||||||
"ergonomic_hazard": "ergonomic",
|
|
||||||
"software_fault": "software_control",
|
|
||||||
"safety_function_failure": "safety_function",
|
|
||||||
"fire_explosion": "thermal",
|
|
||||||
"radiation_hazard": "material_environmental",
|
|
||||||
"unauthorized_access": "cyber_network",
|
|
||||||
"communication_failure": "cyber_network",
|
|
||||||
"firmware_corruption": "cyber_network",
|
|
||||||
"logging_audit_failure": "cyber_network",
|
|
||||||
"ai_misclassification": "ai_specific",
|
|
||||||
"false_classification": "ai_specific",
|
|
||||||
"model_drift": "ai_specific",
|
|
||||||
"data_poisoning": "ai_specific",
|
|
||||||
"sensor_spoofing": "ai_specific",
|
|
||||||
"unintended_bias": "ai_specific",
|
|
||||||
"sensor_fault": "software_control",
|
|
||||||
"configuration_error": "software_control",
|
|
||||||
"update_failure": "software_control",
|
|
||||||
"hmi_error": "software_control",
|
|
||||||
"emc_hazard": "electrical",
|
|
||||||
"maintenance_hazard": "mechanical",
|
|
||||||
"mode_confusion": "software_control",
|
|
||||||
}
|
|
||||||
if cat, ok := m[patternCat]; ok {
|
|
||||||
return cat
|
|
||||||
}
|
|
||||||
return "general"
|
|
||||||
}
|
|
||||||
|
|
||||||
// deriveComponentType guesses the component type from its tags.
|
|
||||||
func deriveComponentType(tags []string) iace.ComponentType {
|
|
||||||
for _, t := range tags {
|
|
||||||
switch {
|
|
||||||
case t == "software" || t == "has_software":
|
|
||||||
return iace.ComponentTypeSoftware
|
|
||||||
case t == "firmware" || t == "has_firmware":
|
|
||||||
return iace.ComponentTypeFirmware
|
|
||||||
case t == "has_ai" || t == "ai_model":
|
|
||||||
return iace.ComponentTypeAIModel
|
|
||||||
case t == "hmi" || t == "display" || t == "touchscreen":
|
|
||||||
return iace.ComponentTypeHMI
|
|
||||||
case t == "sensor" || t == "camera":
|
|
||||||
return iace.ComponentTypeSensor
|
|
||||||
case t == "electric_motor" || t == "electric_drive":
|
|
||||||
return iace.ComponentTypeElectrical
|
|
||||||
case t == "networked" || t == "ethernet" || t == "wifi":
|
|
||||||
return iace.ComponentTypeNetwork
|
|
||||||
case t == "hydraulic" || t == "pneumatic":
|
|
||||||
return iace.ComponentTypeActuator
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return iace.ComponentTypeMechanical
|
|
||||||
}
|
|
||||||
|
|
||||||
// extractOperationalStatesFromMetadata reads the explicit operational_states
|
|
||||||
// selection that the user set via the Betriebszustand-UI.
|
|
||||||
func extractOperationalStatesFromMetadata(metadata json.RawMessage) []string {
|
|
||||||
if metadata == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var meta map[string]json.RawMessage
|
|
||||||
if err := json.Unmarshal(metadata, &meta); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
raw, ok := meta["operational_states"]
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var states []string
|
|
||||||
if err := json.Unmarshal(raw, &states); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return states
|
|
||||||
}
|
|
||||||
|
|
||||||
// mergeStringSlices merges two string slices, deduplicating entries.
|
|
||||||
func mergeStringSlices(a, b []string) []string {
|
|
||||||
seen := make(map[string]bool, len(a)+len(b))
|
|
||||||
var result []string
|
|
||||||
for _, s := range a {
|
|
||||||
if !seen[s] {
|
|
||||||
seen[s] = true
|
|
||||||
result = append(result, s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, s := range b {
|
|
||||||
if !seen[s] {
|
|
||||||
seen[s] = true
|
|
||||||
result = append(result, s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// findHazardForMeasureByCategory finds a matching hazard for a measure.
|
|
||||||
func findHazardForMeasureByCategory(measureCat string, hazardsByCategory map[string]uuid.UUID) uuid.UUID {
|
|
||||||
// Direct match
|
|
||||||
if id, ok := hazardsByCategory[measureCat]; ok {
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
// Fuzzy match — "mechanical" matches "mechanical_hazard"
|
|
||||||
for cat, id := range hazardsByCategory {
|
|
||||||
if len(measureCat) > 3 && len(cat) > 3 && cat[:4] == measureCat[:4] {
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Fallback: first hazard
|
|
||||||
for _, id := range hazardsByCategory {
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
return uuid.Nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,207 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/breakpilot/ai-compliance-sdk/internal/iace"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// extractNarrativeFromMetadata builds a combined text from the limits_form.
|
||||||
|
func extractNarrativeFromMetadata(metadata json.RawMessage) string {
|
||||||
|
if metadata == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var meta map[string]json.RawMessage
|
||||||
|
if err := json.Unmarshal(metadata, &meta); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
limitsRaw, ok := meta["limits_form"]
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var limits map[string]interface{}
|
||||||
|
if err := json.Unmarshal(limitsRaw, &limits); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
textFields := []string{
|
||||||
|
"general_description", "intended_purpose", "foreseeable_misuse",
|
||||||
|
"space_limits", "time_limits", "environmental_conditions",
|
||||||
|
"energy_sources", "materials_processed", "operating_modes",
|
||||||
|
"maintenance_requirements", "personnel_requirements",
|
||||||
|
"interfaces_description", "control_system_description",
|
||||||
|
"safety_functions_description",
|
||||||
|
}
|
||||||
|
var result string
|
||||||
|
for _, field := range textFields {
|
||||||
|
if v, ok := limits[field]; ok {
|
||||||
|
if s, ok := v.(string); ok && s != "" {
|
||||||
|
result += s + "\n\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// patternCatToMeasureCat maps pattern hazard categories to measure categories.
|
||||||
|
func patternCatToMeasureCat(patternCat string) string {
|
||||||
|
m := map[string]string{
|
||||||
|
"mechanical_hazard": "mechanical", "electrical_hazard": "electrical",
|
||||||
|
"thermal_hazard": "thermal", "noise_vibration": "noise_vibration",
|
||||||
|
"pneumatic_hydraulic": "pneumatic_hydraulic", "material_environmental": "material_environmental",
|
||||||
|
"ergonomic": "ergonomic", "ergonomic_hazard": "ergonomic",
|
||||||
|
"software_fault": "software_control", "safety_function_failure": "safety_function",
|
||||||
|
"fire_explosion": "thermal", "radiation_hazard": "material_environmental",
|
||||||
|
"unauthorized_access": "cyber_network", "communication_failure": "cyber_network",
|
||||||
|
"firmware_corruption": "cyber_network", "logging_audit_failure": "cyber_network",
|
||||||
|
"ai_misclassification": "ai_specific", "false_classification": "ai_specific",
|
||||||
|
"model_drift": "ai_specific", "data_poisoning": "ai_specific",
|
||||||
|
"sensor_spoofing": "ai_specific", "unintended_bias": "ai_specific",
|
||||||
|
"sensor_fault": "software_control", "configuration_error": "software_control",
|
||||||
|
"update_failure": "software_control", "hmi_error": "software_control",
|
||||||
|
"emc_hazard": "electrical", "maintenance_hazard": "mechanical",
|
||||||
|
"mode_confusion": "software_control", "chemical_risk": "material_environmental",
|
||||||
|
}
|
||||||
|
if cat, ok := m[patternCat]; ok {
|
||||||
|
return cat
|
||||||
|
}
|
||||||
|
return "general"
|
||||||
|
}
|
||||||
|
|
||||||
|
// deriveComponentType guesses the component type from its tags.
|
||||||
|
func deriveComponentType(tags []string) iace.ComponentType {
|
||||||
|
for _, t := range tags {
|
||||||
|
switch {
|
||||||
|
case t == "software" || t == "has_software":
|
||||||
|
return iace.ComponentTypeSoftware
|
||||||
|
case t == "firmware" || t == "has_firmware":
|
||||||
|
return iace.ComponentTypeFirmware
|
||||||
|
case t == "has_ai" || t == "ai_model":
|
||||||
|
return iace.ComponentTypeAIModel
|
||||||
|
case t == "hmi" || t == "display" || t == "touchscreen":
|
||||||
|
return iace.ComponentTypeHMI
|
||||||
|
case t == "sensor" || t == "camera":
|
||||||
|
return iace.ComponentTypeSensor
|
||||||
|
case t == "electric_motor" || t == "electric_drive":
|
||||||
|
return iace.ComponentTypeElectrical
|
||||||
|
case t == "networked" || t == "ethernet" || t == "wifi":
|
||||||
|
return iace.ComponentTypeNetwork
|
||||||
|
case t == "hydraulic" || t == "pneumatic":
|
||||||
|
return iace.ComponentTypeActuator
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return iace.ComponentTypeMechanical
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractOperationalStatesFromMetadata reads the explicit operational_states
|
||||||
|
// selection that the user set via the Betriebszustand-UI.
|
||||||
|
func extractOperationalStatesFromMetadata(metadata json.RawMessage) []string {
|
||||||
|
if metadata == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var meta map[string]json.RawMessage
|
||||||
|
if err := json.Unmarshal(metadata, &meta); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
raw, ok := meta["operational_states"]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var states []string
|
||||||
|
if err := json.Unmarshal(raw, &states); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return states
|
||||||
|
}
|
||||||
|
|
||||||
|
// mergeStringSlices merges two string slices, deduplicating entries.
|
||||||
|
func mergeStringSlices(a, b []string) []string {
|
||||||
|
seen := make(map[string]bool, len(a)+len(b))
|
||||||
|
var result []string
|
||||||
|
for _, s := range a {
|
||||||
|
if !seen[s] {
|
||||||
|
seen[s] = true
|
||||||
|
result = append(result, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, s := range b {
|
||||||
|
if !seen[s] {
|
||||||
|
seen[s] = true
|
||||||
|
result = append(result, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractIndustrySectorsFromMetadata reads the industry_sectors selection
|
||||||
|
// from project metadata and maps them to MachineTypes for pattern filtering.
|
||||||
|
func extractIndustrySectorsFromMetadata(metadata json.RawMessage) []string {
|
||||||
|
if metadata == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var meta map[string]json.RawMessage
|
||||||
|
if err := json.Unmarshal(metadata, &meta); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
limitsRaw, ok := meta["limits_form"]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var limits map[string]json.RawMessage
|
||||||
|
if err := json.Unmarshal(limitsRaw, &limits); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
sectorsRaw, ok := limits["industry_sectors"]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var sectors []string
|
||||||
|
if err := json.Unmarshal(sectorsRaw, §ors); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
labelMap := map[string][]string{
|
||||||
|
"Allgemeiner Maschinenbau": {"general_industry"},
|
||||||
|
"Automobil / Zulieferer": {"automotive"},
|
||||||
|
"Robotik / Cobot": {"robotics_cobot", "cobot"},
|
||||||
|
"Medizintechnik": {"medical_device", "infusion_pump", "ventilator", "patient_monitor"},
|
||||||
|
"Lebensmittel / Getraenke": {"food_processing"},
|
||||||
|
"Verpackung": {"packaging"},
|
||||||
|
"Pharma / Chemie": {"chemical", "pharmaceutical"},
|
||||||
|
"Bau / Baumaschinen": {"construction", "crane", "excavator"},
|
||||||
|
"Forst / Holzbearbeitung": {"forestry", "woodworking", "circular_saw"},
|
||||||
|
"Aufzuege / Foerdertechnik": {"elevator", "lift", "escalator", "conveyor"},
|
||||||
|
"Textil": {"textile", "spinning", "weaving", "finishing"},
|
||||||
|
"Landmaschinen": {"agricultural", "tractor", "harvester"},
|
||||||
|
"Druck / Papier": {"printing"},
|
||||||
|
"Metall / CNC": {"cnc", "metalworking", "lathe", "milling"},
|
||||||
|
"Schweissen / Oberflaechentechnik": {"welding", "surface_treatment"},
|
||||||
|
}
|
||||||
|
var result []string
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
for _, sector := range sectors {
|
||||||
|
for _, mt := range labelMap[sector] {
|
||||||
|
if !seen[mt] {
|
||||||
|
seen[mt] = true
|
||||||
|
result = append(result, mt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// findHazardForMeasureByCategory finds a matching hazard for a measure.
|
||||||
|
func findHazardForMeasureByCategory(measureCat string, hazardsByCategory map[string]uuid.UUID) uuid.UUID {
|
||||||
|
if id, ok := hazardsByCategory[measureCat]; ok {
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
for cat, id := range hazardsByCategory {
|
||||||
|
if len(measureCat) > 3 && len(cat) > 3 && cat[:4] == measureCat[:4] {
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, id := range hazardsByCategory {
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
return uuid.Nil
|
||||||
|
}
|
||||||
@@ -20,6 +20,10 @@ type MatchInput struct {
|
|||||||
// FailureModes are the active failure mode IDs relevant for this project.
|
// FailureModes are the active failure mode IDs relevant for this project.
|
||||||
// Used to filter patterns that require specific failure modes.
|
// Used to filter patterns that require specific failure modes.
|
||||||
FailureModes []string `json:"failure_modes,omitempty"`
|
FailureModes []string `json:"failure_modes,omitempty"`
|
||||||
|
// MachineTypes are the industry sectors / machine types for this project.
|
||||||
|
// Patterns with MachineTypes filter only fire if at least one matches.
|
||||||
|
// Empty = all patterns fire (backwards compatible).
|
||||||
|
MachineTypes []string `json:"machine_types,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// MatchOutput contains the results of pattern matching.
|
// MatchOutput contains the results of pattern matching.
|
||||||
@@ -317,6 +321,22 @@ func (e *PatternEngine) Match(input MatchInput) *MatchOutput {
|
|||||||
// patternMatches checks if a pattern fires given the resolved tag set, lifecycle phases,
|
// patternMatches checks if a pattern fires given the resolved tag set, lifecycle phases,
|
||||||
// operational states, and state transitions.
|
// operational states, and state transitions.
|
||||||
func patternMatches(p HazardPattern, tagSet map[string]bool, input MatchInput) bool {
|
func patternMatches(p HazardPattern, tagSet map[string]bool, input MatchInput) bool {
|
||||||
|
// If pattern requires specific machine types, project must match at least one.
|
||||||
|
// Patterns without MachineTypes fire for ALL projects (backwards compatible).
|
||||||
|
if len(p.MachineTypes) > 0 && len(input.MachineTypes) > 0 {
|
||||||
|
found := false
|
||||||
|
mtSet := toSet(input.MachineTypes)
|
||||||
|
for _, mt := range p.MachineTypes {
|
||||||
|
if mtSet[mt] {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// All required component tags must be present (AND)
|
// All required component tags must be present (AND)
|
||||||
for _, t := range p.RequiredComponentTags {
|
for _, t := range p.RequiredComponentTags {
|
||||||
if !tagSet[t] {
|
if !tagSet[t] {
|
||||||
|
|||||||
Reference in New Issue
Block a user