package handlers import ( "encoding/json" "fmt" "net/http" "github.com/breakpilot/ai-compliance-sdk/internal/iace" "github.com/gin-gonic/gin" "github.com/google/uuid" ) // InitStep tracks progress of each initialization step. type InitStep struct { Name string `json:"name"` Status string `json:"status"` // "done", "skipped", "error" Count int `json:"count,omitempty"` Details string `json:"details,omitempty"` } // InitializeProject handles POST /projects/:id/initialize // Chains: parse narrative → create components → fire patterns → // create hazards + measures + verification → suggest norms. // Idempotent: skips steps that are already populated. func (h *IACEHandler) InitializeProject(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } tenantID, err := getTenantID(c) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } ctx := c.Request.Context() project, err := h.store.GetProject(ctx, projectID) if err != nil || project == nil { c.JSON(http.StatusNotFound, gin.H{"error": "project not found"}) return } steps := make([]InitStep, 0, 6) // ── Step 1: Extract narrative from limits_form ── narrativeText := extractNarrativeFromMetadata(project.Metadata) if narrativeText == "" { c.JSON(http.StatusBadRequest, gin.H{ "error": "Grenzen-Formular ist leer. Bitte zuerst die Maschinenbeschreibung ausfuellen.", }) return } // ── Step 2: Parse narrative deterministically ── parseResult := iace.ParseNarrative(narrativeText, project.MachineType) steps = append(steps, InitStep{ Name: "Narrative analysiert", Status: "done", Count: len(parseResult.Components), Details: fmt.Sprintf("%d Komponenten, %d Energiequellen, %d Tags", len(parseResult.Components), len(parseResult.EnergySources), len(parseResult.CustomTags)), }) // ── Step 3: Create components (skip if already exist) ── existingComps, _ := h.store.ListComponents(ctx, projectID) compStep := InitStep{Name: "Komponenten erstellt", Status: "skipped"} if len(existingComps) == 0 && len(parseResult.Components) > 0 { created := 0 for _, comp := range parseResult.Components { // Derive component type from tags compType := deriveComponentType(comp.Tags) _, cerr := h.store.CreateComponent(ctx, iace.CreateComponentRequest{ ProjectID: projectID, Name: comp.NameDE, ComponentType: compType, Description: "Auto-erkannt aus Maschinenbeschreibung (" + comp.MatchedOn + ")", }) if cerr == nil { created++ } } compStep = InitStep{Name: "Komponenten erstellt", Status: "done", Count: created} } else if len(existingComps) > 0 { compStep.Details = "Bereits vorhanden" compStep.Count = len(existingComps) } steps = append(steps, compStep) // ── Step 4: Fire pattern engine ── var componentIDs, energyIDs []string for _, comp := range parseResult.Components { componentIDs = append(componentIDs, comp.LibraryID) } for _, e := range parseResult.EnergySources { energyIDs = append(energyIDs, e.SourceID) } engine := iace.NewPatternEngine() matchOutput := engine.Match(iace.MatchInput{ ComponentLibraryIDs: componentIDs, EnergySourceIDs: energyIDs, LifecyclePhases: parseResult.LifecyclePhases, CustomTags: parseResult.CustomTags, }) steps = append(steps, InitStep{ Name: "Patterns abgeglichen", Status: "done", Count: len(matchOutput.MatchedPatterns), }) // ── Step 5: Create hazards from matched patterns (skip if exist) ── existingHazards, _ := h.store.ListHazards(ctx, projectID) hazardStep := InitStep{Name: "Gefaehrdungen erstellt", Status: "skipped"} hazardIDsByCategory := make(map[string]uuid.UUID) if len(existingHazards) == 0 && len(matchOutput.MatchedPatterns) > 0 { // Get first component for hazard assignment comps, _ := h.store.ListComponents(ctx, projectID) var defaultCompID uuid.UUID if len(comps) > 0 { defaultCompID = comps[0].ID } // Deduplicate by category — one hazard per category created := 0 seenCat := make(map[string]bool) for _, mp := range matchOutput.MatchedPatterns { for _, cat := range mp.HazardCats { if seenCat[cat] { continue } seenCat[cat] = true name := mp.PatternName if name == "" { name = cat } scenario := mp.ScenarioDE hz, cerr := h.store.CreateHazard(ctx, iace.CreateHazardRequest{ ProjectID: projectID, ComponentID: defaultCompID, Name: name, Description: scenario, Category: cat, Scenario: scenario, }) if cerr == nil { created++ hazardIDsByCategory[cat] = hz.ID } } } hazardStep = InitStep{Name: "Gefaehrdungen erstellt", Status: "done", Count: created} } else if len(existingHazards) > 0 { hazardStep.Details = "Bereits vorhanden" hazardStep.Count = len(existingHazards) for _, eh := range existingHazards { hazardIDsByCategory[eh.Category] = eh.ID } } steps = append(steps, hazardStep) // ── Step 6: Create mitigations (pattern-suggested + category fallback) ── existingMits, _ := h.store.ListMitigationsByProject(ctx, projectID) mitStep := InitStep{Name: "Massnahmen erstellt", Status: "skipped"} if len(existingMits) == 0 && len(hazardIDsByCategory) > 0 { measureLib := iace.GetProtectiveMeasureLibrary() measureByID := make(map[string]iace.ProtectiveMeasureEntry, len(measureLib)) measuresByCat := make(map[string][]iace.ProtectiveMeasureEntry) for _, m := range measureLib { measureByID[m.ID] = m measuresByCat[m.HazardCategory] = append(measuresByCat[m.HazardCategory], m) } created := 0 usedMeasureIDs := make(map[string]bool) // A) Pattern-suggested measures (direct reference) for _, sm := range matchOutput.SuggestedMeasures { entry, ok := measureByID[sm.MeasureID] if !ok || usedMeasureIDs[sm.MeasureID] { continue } hazardID := findHazardForMeasureByCategory(entry.HazardCategory, hazardIDsByCategory) if hazardID == uuid.Nil { continue } rt := iace.ReductionType(entry.ReductionType) if rt == "" { rt = iace.ReductionTypeInformation } _, cerr := h.store.CreateMitigation(ctx, iace.CreateMitigationRequest{ HazardID: hazardID, ReductionType: rt, Name: entry.Name, Description: entry.Description, }) if cerr == nil { created++ usedMeasureIDs[sm.MeasureID] = true } } // B) Category fallback — for each hazard category, add measures // from the library that match (but weren't pattern-suggested) for hazCat, hazID := range hazardIDsByCategory { measCat := patternCatToMeasureCat(hazCat) candidates := measuresByCat[measCat] added := 0 for _, m := range candidates { if usedMeasureIDs[m.ID] || added >= 8 { break } rt := iace.ReductionType(m.ReductionType) if rt == "" { rt = iace.ReductionTypeInformation } _, cerr := h.store.CreateMitigation(ctx, iace.CreateMitigationRequest{ HazardID: hazID, ReductionType: rt, Name: m.Name, Description: m.Description, }) if cerr == nil { created++ usedMeasureIDs[m.ID] = true added++ } } } mitStep = InitStep{Name: "Massnahmen erstellt", Status: "done", Count: created} } else if len(existingMits) > 0 { mitStep.Details = "Bereits vorhanden" mitStep.Count = len(existingMits) } steps = append(steps, mitStep) // ── Step 7: Suggest norms ── var hazardCats []string for cat := range hazardIDsByCategory { hazardCats = append(hazardCats, cat) } normResult := iace.SuggestNorms(project.MachineType, hazardCats, parseResult.CustomTags) normCount := 0 if normResult != nil { normCount = len(normResult.ANorms) + len(normResult.B1Norms) + len(normResult.B2Norms) + len(normResult.CNorms) } steps = append(steps, InitStep{ Name: "Normen vorgeschlagen", Status: "done", Count: normCount, }) // ── Audit trail ── h.store.AddAuditEntry(ctx, projectID, "project_initialization", projectID, iace.AuditActionCreate, tenantID.String(), nil, mustMarshalJSON(map[string]interface{}{"steps": steps}), ) c.JSON(http.StatusOK, gin.H{ "project_id": projectID.String(), "steps": steps, "summary": gin.H{ "components": steps[1].Count, "patterns": steps[2].Count, "hazards": steps[3].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 } // 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 }