package handlers import ( "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 } // Support ?force=true to clear existing hazards + mitigations before re-init forceReinit := c.Query("force") == "true" steps := make([]InitStep, 0, 6) // ── Step 0 (optional): Clear existing data for force re-init ── if forceReinit { cleared := 0 if mits, _ := h.store.ListMitigationsByProject(ctx, projectID); len(mits) > 0 { for _, m := range mits { _ = h.store.DeleteMitigation(ctx, m.ID) cleared++ } } if hazards, _ := h.store.ListHazards(ctx, projectID); len(hazards) > 0 { for _, hz := range hazards { _ = h.store.DeleteHazard(ctx, hz.ID) cleared++ } } steps = append(steps, InitStep{Name: "Alte Daten geloescht", Status: "done", Count: cleared}) } // ── 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 { 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) } operationalStates := mergeStringSlices(parseResult.OperationalStates, extractOperationalStatesFromMetadata(project.Metadata)) machineTypes := extractIndustrySectorsFromMetadata(project.Metadata) engine := iace.NewPatternEngine() matchOutput := engine.Match(iace.MatchInput{ ComponentLibraryIDs: componentIDs, EnergySourceIDs: energyIDs, LifecyclePhases: parseResult.LifecyclePhases, CustomTags: parseResult.CustomTags, OperationalStates: operationalStates, StateTransitions: parseResult.StateTransitions, HumanRoles: parseResult.Roles, MachineTypes: machineTypes, }) 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 { comps, _ := h.store.ListComponents(ctx, projectID) var defaultCompID uuid.UUID if len(comps) > 0 { defaultCompID = comps[0].ID } 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 } hz, cerr := h.store.CreateHazard(ctx, iace.CreateHazardRequest{ ProjectID: projectID, ComponentID: defaultCompID, Name: name, Description: mp.ScenarioDE, Category: cat, Scenario: mp.ScenarioDE, Function: iace.EncodeOpStates(mp.OperationalStates), TriggerEvent: mp.TriggerDE, PossibleHarm: mp.HarmDE, AffectedPerson: mp.AffectedDE, HazardousZone: mp.ZoneDE, }) 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 ── 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) 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 } } for hazCat, hazID := range hazardIDsByCategory { measCat := patternCatToMeasureCat(hazCat) added := 0 for _, m := range measuresByCat[measCat] { 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, }, }) }