Files
breakpilot-compliance/ai-compliance-sdk/internal/api/handlers/iace_handler_init.go
T
Benjamin Admin f5664612ad
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
feat(iace): Einsatzbereich / Branche — filtert branchenspezifische Patterns
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>
2026-05-12 09:09:28 +02:00

288 lines
9.1 KiB
Go

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,
},
})
}