package handlers import ( "encoding/json" "fmt" "net/http" "strings" "github.com/breakpilot/ai-compliance-sdk/internal/iace" "github.com/breakpilot/ai-compliance-sdk/internal/llm" "github.com/breakpilot/ai-compliance-sdk/internal/rbac" "github.com/breakpilot/ai-compliance-sdk/internal/ucca" "github.com/gin-gonic/gin" "github.com/google/uuid" ) // ============================================================================ // Handler Struct & Constructor // ============================================================================ // IACEHandler handles HTTP requests for the IACE module (Inherent-risk Adjusted // Control Effectiveness). It provides endpoints for project management, component // onboarding, regulatory classification, hazard/risk analysis, evidence management, // CE technical file generation, and post-market monitoring. type IACEHandler struct { store *iace.Store engine *iace.RiskEngine classifier *iace.Classifier checker *iace.CompletenessChecker ragClient *ucca.LegalRAGClient techFileGen *iace.TechFileGenerator exporter *iace.DocumentExporter } // NewIACEHandler creates a new IACEHandler with all required dependencies. func NewIACEHandler(store *iace.Store, providerRegistry *llm.ProviderRegistry) *IACEHandler { ragClient := ucca.NewLegalRAGClient() return &IACEHandler{ store: store, engine: iace.NewRiskEngine(), classifier: iace.NewClassifier(), checker: iace.NewCompletenessChecker(), ragClient: ragClient, techFileGen: iace.NewTechFileGenerator(providerRegistry, ragClient, store), exporter: iace.NewDocumentExporter(), } } // ============================================================================ // Helper: Tenant ID extraction // ============================================================================ // getTenantID extracts the tenant UUID from the X-Tenant-Id header. // It first checks the rbac middleware context; if not present, falls back to the // raw header value. func getTenantID(c *gin.Context) (uuid.UUID, error) { // Prefer value set by RBAC middleware tid := rbac.GetTenantID(c) if tid != uuid.Nil { return tid, nil } tenantStr := c.GetHeader("X-Tenant-Id") if tenantStr == "" { return uuid.Nil, fmt.Errorf("X-Tenant-Id header required") } return uuid.Parse(tenantStr) } // ============================================================================ // Project Management // ============================================================================ // CreateProject handles POST /projects // Creates a new IACE compliance project for a machine or system. func (h *IACEHandler) CreateProject(c *gin.Context) { tenantID, err := getTenantID(c) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } var req iace.CreateProjectRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } project, err := h.store.CreateProject(c.Request.Context(), tenantID, req) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, gin.H{"project": project}) } // ListProjects handles GET /projects // Lists all IACE projects for the authenticated tenant. func (h *IACEHandler) ListProjects(c *gin.Context) { tenantID, err := getTenantID(c) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } projects, err := h.store.ListProjects(c.Request.Context(), tenantID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if projects == nil { projects = []iace.Project{} } c.JSON(http.StatusOK, iace.ProjectListResponse{ Projects: projects, Total: len(projects), }) } // GetProject handles GET /projects/:id // Returns a project with its components, classifications, and completeness gates. func (h *IACEHandler) GetProject(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } project, err := h.store.GetProject(c.Request.Context(), projectID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if project == nil { c.JSON(http.StatusNotFound, gin.H{"error": "project not found"}) return } components, _ := h.store.ListComponents(c.Request.Context(), projectID) classifications, _ := h.store.GetClassifications(c.Request.Context(), projectID) if components == nil { components = []iace.Component{} } if classifications == nil { classifications = []iace.RegulatoryClassification{} } // Build completeness context to compute gates ctx := h.buildCompletenessContext(c, project, components, classifications) result := h.checker.Check(ctx) c.JSON(http.StatusOK, iace.ProjectDetailResponse{ Project: *project, Components: components, Classifications: classifications, CompletenessGates: result.Gates, }) } // UpdateProject handles PUT /projects/:id // Partially updates a project's mutable fields. func (h *IACEHandler) UpdateProject(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } var req iace.UpdateProjectRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } project, err := h.store.UpdateProject(c.Request.Context(), projectID, req) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if project == nil { c.JSON(http.StatusNotFound, gin.H{"error": "project not found"}) return } c.JSON(http.StatusOK, gin.H{"project": project}) } // ArchiveProject handles DELETE /projects/:id // Archives a project by setting its status to archived. func (h *IACEHandler) ArchiveProject(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } if err := h.store.ArchiveProject(c.Request.Context(), projectID); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "project archived"}) } // ============================================================================ // Onboarding // ============================================================================ // InitFromProfile handles POST /projects/:id/init-from-profile // Initializes a project from a company profile and compliance scope JSON payload. func (h *IACEHandler) InitFromProfile(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } project, err := h.store.GetProject(c.Request.Context(), projectID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if project == nil { c.JSON(http.StatusNotFound, gin.H{"error": "project not found"}) return } var req iace.InitFromProfileRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Parse compliance_scope to extract machine data var scope struct { MachineName string `json:"machine_name"` MachineType string `json:"machine_type"` IntendedUse string `json:"intended_use"` HasSoftware bool `json:"has_software"` HasFirmware bool `json:"has_firmware"` HasAI bool `json:"has_ai"` IsNetworked bool `json:"is_networked"` ApplicableRegulations []string `json:"applicable_regulations"` } _ = json.Unmarshal(req.ComplianceScope, &scope) // Parse company_profile to extract manufacturer var profile struct { CompanyName string `json:"company_name"` ContactName string `json:"contact_name"` ContactEmail string `json:"contact_email"` Address string `json:"address"` } _ = json.Unmarshal(req.CompanyProfile, &profile) // Store the profile and scope in project metadata profileData := map[string]json.RawMessage{ "company_profile": req.CompanyProfile, "compliance_scope": req.ComplianceScope, } metadataBytes, _ := json.Marshal(profileData) metadataRaw := json.RawMessage(metadataBytes) // Build update request — fill project fields from scope/profile updateReq := iace.UpdateProjectRequest{ Metadata: &metadataRaw, } if scope.MachineName != "" { updateReq.MachineName = &scope.MachineName } if scope.MachineType != "" { updateReq.MachineType = &scope.MachineType } if scope.IntendedUse != "" { updateReq.Description = &scope.IntendedUse } if profile.CompanyName != "" { updateReq.Manufacturer = &profile.CompanyName } project, err = h.store.UpdateProject(c.Request.Context(), projectID, updateReq) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } ctx := c.Request.Context() // Create initial components from scope var createdComponents []iace.Component if scope.HasSoftware { comp, err := h.store.CreateComponent(ctx, iace.CreateComponentRequest{ ProjectID: projectID, Name: "Software", ComponentType: iace.ComponentTypeSoftware, IsSafetyRelevant: true, IsNetworked: scope.IsNetworked, }) if err == nil { createdComponents = append(createdComponents, *comp) } } if scope.HasFirmware { comp, err := h.store.CreateComponent(ctx, iace.CreateComponentRequest{ ProjectID: projectID, Name: "Firmware", ComponentType: iace.ComponentTypeFirmware, IsSafetyRelevant: true, }) if err == nil { createdComponents = append(createdComponents, *comp) } } if scope.HasAI { comp, err := h.store.CreateComponent(ctx, iace.CreateComponentRequest{ ProjectID: projectID, Name: "KI-Modell", ComponentType: iace.ComponentTypeAIModel, IsSafetyRelevant: true, IsNetworked: scope.IsNetworked, }) if err == nil { createdComponents = append(createdComponents, *comp) } } if scope.IsNetworked { comp, err := h.store.CreateComponent(ctx, iace.CreateComponentRequest{ ProjectID: projectID, Name: "Netzwerk-Schnittstelle", ComponentType: iace.ComponentTypeNetwork, IsSafetyRelevant: false, IsNetworked: true, }) if err == nil { createdComponents = append(createdComponents, *comp) } } // Trigger initial classifications for applicable regulations regulationMap := map[string]iace.RegulationType{ "machinery_regulation": iace.RegulationMachineryRegulation, "ai_act": iace.RegulationAIAct, "cra": iace.RegulationCRA, "nis2": iace.RegulationNIS2, } var triggeredRegulations []string for _, regStr := range scope.ApplicableRegulations { if regType, ok := regulationMap[regStr]; ok { triggeredRegulations = append(triggeredRegulations, regStr) // Create initial classification entry h.store.UpsertClassification(ctx, projectID, regType, "pending", "medium", 0.5, "Initialisiert aus Compliance-Scope", nil, nil) } } // Advance project status to onboarding if err := h.store.UpdateProjectStatus(ctx, projectID, iace.ProjectStatusOnboarding); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Add audit trail entry userID := rbac.GetUserID(c) h.store.AddAuditEntry( ctx, projectID, "project", projectID, iace.AuditActionUpdate, userID.String(), nil, metadataBytes, ) c.JSON(http.StatusOK, gin.H{ "message": "project initialized from profile", "project": project, "components_created": len(createdComponents), "regulations_triggered": triggeredRegulations, }) } // CreateComponent handles POST /projects/:id/components // Adds a new component to a project. func (h *IACEHandler) CreateComponent(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } var req iace.CreateComponentRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Override project ID from URL path req.ProjectID = projectID component, err := h.store.CreateComponent(c.Request.Context(), req) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Audit trail userID := rbac.GetUserID(c) newVals, _ := json.Marshal(component) h.store.AddAuditEntry( c.Request.Context(), projectID, "component", component.ID, iace.AuditActionCreate, userID.String(), nil, newVals, ) c.JSON(http.StatusCreated, gin.H{"component": component}) } // ListComponents handles GET /projects/:id/components // Lists all components for a project. func (h *IACEHandler) ListComponents(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } components, err := h.store.ListComponents(c.Request.Context(), projectID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if components == nil { components = []iace.Component{} } c.JSON(http.StatusOK, gin.H{ "components": components, "total": len(components), }) } // UpdateComponent handles PUT /projects/:id/components/:cid // Updates a component with the provided fields. func (h *IACEHandler) UpdateComponent(c *gin.Context) { _, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } componentID, err := uuid.Parse(c.Param("cid")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid component ID"}) return } var updates map[string]interface{} if err := c.ShouldBindJSON(&updates); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } component, err := h.store.UpdateComponent(c.Request.Context(), componentID, updates) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if component == nil { c.JSON(http.StatusNotFound, gin.H{"error": "component not found"}) return } c.JSON(http.StatusOK, gin.H{"component": component}) } // DeleteComponent handles DELETE /projects/:id/components/:cid // Deletes a component from a project. func (h *IACEHandler) DeleteComponent(c *gin.Context) { _, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } componentID, err := uuid.Parse(c.Param("cid")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid component ID"}) return } if err := h.store.DeleteComponent(c.Request.Context(), componentID); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "component deleted"}) } // CheckCompleteness handles POST /projects/:id/completeness-check // Loads all project data, evaluates all 25 CE completeness gates, updates the // project's completeness score, and returns the result. func (h *IACEHandler) CheckCompleteness(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } project, err := h.store.GetProject(c.Request.Context(), projectID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if project == nil { c.JSON(http.StatusNotFound, gin.H{"error": "project not found"}) return } // Load all related entities components, _ := h.store.ListComponents(c.Request.Context(), projectID) classifications, _ := h.store.GetClassifications(c.Request.Context(), projectID) hazards, _ := h.store.ListHazards(c.Request.Context(), projectID) // Collect all assessments and mitigations across all hazards var allAssessments []iace.RiskAssessment var allMitigations []iace.Mitigation for _, hazard := range hazards { assessments, _ := h.store.ListAssessments(c.Request.Context(), hazard.ID) allAssessments = append(allAssessments, assessments...) mitigations, _ := h.store.ListMitigations(c.Request.Context(), hazard.ID) allMitigations = append(allMitigations, mitigations...) } evidence, _ := h.store.ListEvidence(c.Request.Context(), projectID) techFileSections, _ := h.store.ListTechFileSections(c.Request.Context(), projectID) // Determine if the project has AI components hasAI := false for _, comp := range components { if comp.ComponentType == iace.ComponentTypeAIModel { hasAI = true break } } // Check audit trail for pattern matching patternMatchingPerformed, _ := h.store.HasAuditEntryForType(c.Request.Context(), projectID, "pattern_matching") // Build completeness context completenessCtx := &iace.CompletenessContext{ Project: project, Components: components, Classifications: classifications, Hazards: hazards, Assessments: allAssessments, Mitigations: allMitigations, Evidence: evidence, TechFileSections: techFileSections, HasAI: hasAI, PatternMatchingPerformed: patternMatchingPerformed, } // Run the checker result := h.checker.Check(completenessCtx) // Build risk summary for the project update riskSummary := map[string]int{ "total_hazards": len(hazards), } for _, a := range allAssessments { riskSummary[string(a.RiskLevel)]++ } // Update project completeness score and risk summary if err := h.store.UpdateProjectCompleteness( c.Request.Context(), projectID, result.Score, riskSummary, ); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "completeness": result, }) } // ============================================================================ // Classification // ============================================================================ // Classify handles POST /projects/:id/classify // Runs all regulatory classifiers (AI Act, Machinery Regulation, CRA, NIS2), // upserts each result into the store, and returns classifications. func (h *IACEHandler) Classify(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } project, err := h.store.GetProject(c.Request.Context(), projectID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if project == nil { c.JSON(http.StatusNotFound, gin.H{"error": "project not found"}) return } components, err := h.store.ListComponents(c.Request.Context(), projectID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Run all classifiers results := h.classifier.ClassifyAll(project, components) // Upsert each classification result into the store var classifications []iace.RegulatoryClassification for _, r := range results { reqsJSON, _ := json.Marshal(r.Requirements) classification, err := h.store.UpsertClassification( c.Request.Context(), projectID, r.Regulation, r.ClassificationResult, r.RiskLevel, r.Confidence, r.Reasoning, nil, // ragSources - not available from rule-based classifier reqsJSON, ) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if classification != nil { classifications = append(classifications, *classification) } } // Advance project status to classification h.store.UpdateProjectStatus(c.Request.Context(), projectID, iace.ProjectStatusClassification) // Audit trail userID := rbac.GetUserID(c) newVals, _ := json.Marshal(classifications) h.store.AddAuditEntry( c.Request.Context(), projectID, "classification", projectID, iace.AuditActionCreate, userID.String(), nil, newVals, ) c.JSON(http.StatusOK, gin.H{ "classifications": classifications, "total": len(classifications), }) } // GetClassifications handles GET /projects/:id/classifications // Returns all regulatory classifications for a project. func (h *IACEHandler) GetClassifications(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } classifications, err := h.store.GetClassifications(c.Request.Context(), projectID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if classifications == nil { classifications = []iace.RegulatoryClassification{} } c.JSON(http.StatusOK, gin.H{ "classifications": classifications, "total": len(classifications), }) } // ClassifySingle handles POST /projects/:id/classify/:regulation // Runs a single regulatory classifier for the specified regulation type. func (h *IACEHandler) ClassifySingle(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } regulation := iace.RegulationType(c.Param("regulation")) project, err := h.store.GetProject(c.Request.Context(), projectID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if project == nil { c.JSON(http.StatusNotFound, gin.H{"error": "project not found"}) return } components, err := h.store.ListComponents(c.Request.Context(), projectID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Run the appropriate classifier var result iace.ClassificationResult switch regulation { case iace.RegulationAIAct: result = h.classifier.ClassifyAIAct(project, components) case iace.RegulationMachineryRegulation: result = h.classifier.ClassifyMachineryRegulation(project, components) case iace.RegulationCRA: result = h.classifier.ClassifyCRA(project, components) case iace.RegulationNIS2: result = h.classifier.ClassifyNIS2(project, components) default: c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("unknown regulation type: %s", regulation)}) return } // Upsert the classification result reqsJSON, _ := json.Marshal(result.Requirements) classification, err := h.store.UpsertClassification( c.Request.Context(), projectID, result.Regulation, result.ClassificationResult, result.RiskLevel, result.Confidence, result.Reasoning, nil, reqsJSON, ) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"classification": classification}) } // ============================================================================ // Hazard & Risk // ============================================================================ // ListHazardLibrary handles GET /hazard-library // Returns built-in hazard library entries merged with any custom DB entries, // optionally filtered by ?category and ?componentType. func (h *IACEHandler) ListHazardLibrary(c *gin.Context) { category := c.Query("category") componentType := c.Query("componentType") // Start with built-in templates from Go code builtinEntries := iace.GetBuiltinHazardLibrary() // Apply filters to built-in entries var entries []iace.HazardLibraryEntry for _, entry := range builtinEntries { if category != "" && entry.Category != category { continue } if componentType != "" && !containsString(entry.ApplicableComponentTypes, componentType) { continue } entries = append(entries, entry) } // Merge with custom DB entries (tenant-specific) dbEntries, err := h.store.ListHazardLibrary(c.Request.Context(), category, componentType) if err == nil && len(dbEntries) > 0 { // Add DB entries that are not built-in (avoid duplicates) builtinIDs := make(map[string]bool) for _, e := range entries { builtinIDs[e.ID.String()] = true } for _, dbEntry := range dbEntries { if !builtinIDs[dbEntry.ID.String()] { entries = append(entries, dbEntry) } } } if entries == nil { entries = []iace.HazardLibraryEntry{} } c.JSON(http.StatusOK, gin.H{ "hazard_library": entries, "total": len(entries), }) } // ListControlsLibrary handles GET /controls-library // Returns the built-in controls library, optionally filtered by ?domain and ?category. func (h *IACEHandler) ListControlsLibrary(c *gin.Context) { domain := c.Query("domain") category := c.Query("category") all := iace.GetControlsLibrary() var filtered []iace.ControlLibraryEntry for _, entry := range all { if domain != "" && entry.Domain != domain { continue } if category != "" && !containsString(entry.MapsToHazardCategories, category) { continue } filtered = append(filtered, entry) } if filtered == nil { filtered = []iace.ControlLibraryEntry{} } c.JSON(http.StatusOK, gin.H{ "controls": filtered, "total": len(filtered), }) } // containsString checks if a string slice contains the given value. func containsString(slice []string, val string) bool { for _, s := range slice { if s == val { return true } } return false } // CreateHazard handles POST /projects/:id/hazards // Creates a new hazard within a project. func (h *IACEHandler) CreateHazard(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } var req iace.CreateHazardRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Override project ID from URL path req.ProjectID = projectID hazard, err := h.store.CreateHazard(c.Request.Context(), req) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Audit trail userID := rbac.GetUserID(c) newVals, _ := json.Marshal(hazard) h.store.AddAuditEntry( c.Request.Context(), projectID, "hazard", hazard.ID, iace.AuditActionCreate, userID.String(), nil, newVals, ) c.JSON(http.StatusCreated, gin.H{"hazard": hazard}) } // ListHazards handles GET /projects/:id/hazards // Lists all hazards for a project. func (h *IACEHandler) ListHazards(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } hazards, err := h.store.ListHazards(c.Request.Context(), projectID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if hazards == nil { hazards = []iace.Hazard{} } c.JSON(http.StatusOK, gin.H{ "hazards": hazards, "total": len(hazards), }) } // UpdateHazard handles PUT /projects/:id/hazards/:hid // Updates a hazard with the provided fields. func (h *IACEHandler) UpdateHazard(c *gin.Context) { _, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } hazardID, err := uuid.Parse(c.Param("hid")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid hazard ID"}) return } var updates map[string]interface{} if err := c.ShouldBindJSON(&updates); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } hazard, err := h.store.UpdateHazard(c.Request.Context(), hazardID, updates) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if hazard == nil { c.JSON(http.StatusNotFound, gin.H{"error": "hazard not found"}) return } c.JSON(http.StatusOK, gin.H{"hazard": hazard}) } // SuggestHazards handles POST /projects/:id/hazards/suggest // Returns hazard library matches based on the project's components. // TODO: Enhance with LLM-based suggestions for more intelligent matching. func (h *IACEHandler) SuggestHazards(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } components, err := h.store.ListComponents(c.Request.Context(), projectID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Collect unique component types from the project componentTypes := make(map[string]bool) for _, comp := range components { componentTypes[string(comp.ComponentType)] = true } // Match built-in hazard templates against project component types var suggestions []iace.HazardLibraryEntry seen := make(map[uuid.UUID]bool) builtinEntries := iace.GetBuiltinHazardLibrary() for _, entry := range builtinEntries { for _, applicableType := range entry.ApplicableComponentTypes { if componentTypes[applicableType] && !seen[entry.ID] { seen[entry.ID] = true suggestions = append(suggestions, entry) break } } } // Also check DB for custom tenant-specific hazard templates for compType := range componentTypes { dbEntries, err := h.store.ListHazardLibrary(c.Request.Context(), "", compType) if err != nil { continue } for _, entry := range dbEntries { if !seen[entry.ID] { seen[entry.ID] = true suggestions = append(suggestions, entry) } } } if suggestions == nil { suggestions = []iace.HazardLibraryEntry{} } c.JSON(http.StatusOK, gin.H{ "suggestions": suggestions, "total": len(suggestions), "component_types": componentTypeKeys(componentTypes), "_note": "TODO: LLM-based suggestion ranking not yet implemented", }) } // AssessRisk handles POST /projects/:id/hazards/:hid/assess // Performs a quantitative risk assessment for a hazard using the IACE risk engine. func (h *IACEHandler) AssessRisk(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } hazardID, err := uuid.Parse(c.Param("hid")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid hazard ID"}) return } // Verify hazard exists hazard, err := h.store.GetHazard(c.Request.Context(), hazardID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if hazard == nil { c.JSON(http.StatusNotFound, gin.H{"error": "hazard not found"}) return } var req iace.AssessRiskRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Override hazard ID from URL path req.HazardID = hazardID userID := rbac.GetUserID(c) // Calculate risk using the engine inherentRisk := h.engine.CalculateInherentRisk(req.Severity, req.Exposure, req.Probability, req.Avoidance) controlEff := h.engine.CalculateControlEffectiveness(req.ControlMaturity, req.ControlCoverage, req.TestEvidenceStrength) residualRisk := h.engine.CalculateResidualRisk(req.Severity, req.Exposure, req.Probability, controlEff) // ISO 12100 mode: use ISO thresholds when avoidance is set var riskLevel iace.RiskLevel if req.Avoidance >= 1 { riskLevel = h.engine.DetermineRiskLevelISO(inherentRisk) } else { riskLevel = h.engine.DetermineRiskLevel(residualRisk) } acceptable, acceptanceReason := h.engine.IsAcceptable(residualRisk, false, req.AcceptanceJustification != "") // Determine version by checking existing assessments existingAssessments, _ := h.store.ListAssessments(c.Request.Context(), hazardID) version := len(existingAssessments) + 1 assessment := &iace.RiskAssessment{ HazardID: hazardID, Version: version, AssessmentType: iace.AssessmentTypeInitial, Severity: req.Severity, Exposure: req.Exposure, Probability: req.Probability, Avoidance: req.Avoidance, InherentRisk: inherentRisk, ControlMaturity: req.ControlMaturity, ControlCoverage: req.ControlCoverage, TestEvidenceStrength: req.TestEvidenceStrength, CEff: controlEff, ResidualRisk: residualRisk, RiskLevel: riskLevel, IsAcceptable: acceptable, AcceptanceJustification: req.AcceptanceJustification, AssessedBy: userID, } if err := h.store.CreateRiskAssessment(c.Request.Context(), assessment); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Update hazard status h.store.UpdateHazard(c.Request.Context(), hazardID, map[string]interface{}{ "status": string(iace.HazardStatusAssessed), }) // Audit trail newVals, _ := json.Marshal(assessment) h.store.AddAuditEntry( c.Request.Context(), projectID, "risk_assessment", assessment.ID, iace.AuditActionCreate, userID.String(), nil, newVals, ) c.JSON(http.StatusCreated, gin.H{ "assessment": assessment, "acceptable": acceptable, "acceptance_reason": acceptanceReason, }) } // GetRiskSummary handles GET /projects/:id/risk-summary // Returns an aggregated risk overview for a project. func (h *IACEHandler) GetRiskSummary(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } summary, err := h.store.GetRiskSummary(c.Request.Context(), projectID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"risk_summary": summary}) } // CreateMitigation handles POST /projects/:id/hazards/:hid/mitigations // Creates a new mitigation measure for a hazard. func (h *IACEHandler) CreateMitigation(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } hazardID, err := uuid.Parse(c.Param("hid")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid hazard ID"}) return } var req iace.CreateMitigationRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Override hazard ID from URL path req.HazardID = hazardID mitigation, err := h.store.CreateMitigation(c.Request.Context(), req) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Update hazard status to mitigated h.store.UpdateHazard(c.Request.Context(), hazardID, map[string]interface{}{ "status": string(iace.HazardStatusMitigated), }) // Audit trail userID := rbac.GetUserID(c) newVals, _ := json.Marshal(mitigation) h.store.AddAuditEntry( c.Request.Context(), projectID, "mitigation", mitigation.ID, iace.AuditActionCreate, userID.String(), nil, newVals, ) c.JSON(http.StatusCreated, gin.H{"mitigation": mitigation}) } // UpdateMitigation handles PUT /mitigations/:mid // Updates a mitigation measure with the provided fields. func (h *IACEHandler) UpdateMitigation(c *gin.Context) { mitigationID, err := uuid.Parse(c.Param("mid")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid mitigation ID"}) return } var updates map[string]interface{} if err := c.ShouldBindJSON(&updates); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } mitigation, err := h.store.UpdateMitigation(c.Request.Context(), mitigationID, updates) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if mitigation == nil { c.JSON(http.StatusNotFound, gin.H{"error": "mitigation not found"}) return } c.JSON(http.StatusOK, gin.H{"mitigation": mitigation}) } // VerifyMitigation handles POST /mitigations/:mid/verify // Marks a mitigation as verified with a verification result. func (h *IACEHandler) VerifyMitigation(c *gin.Context) { mitigationID, err := uuid.Parse(c.Param("mid")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid mitigation ID"}) return } var req struct { VerificationResult string `json:"verification_result" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } userID := rbac.GetUserID(c) if err := h.store.VerifyMitigation( c.Request.Context(), mitigationID, req.VerificationResult, userID.String(), ); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "mitigation verified"}) } // ReassessRisk handles POST /projects/:id/hazards/:hid/reassess // Creates a post-mitigation risk reassessment for a hazard. func (h *IACEHandler) ReassessRisk(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } hazardID, err := uuid.Parse(c.Param("hid")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid hazard ID"}) return } // Verify hazard exists hazard, err := h.store.GetHazard(c.Request.Context(), hazardID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if hazard == nil { c.JSON(http.StatusNotFound, gin.H{"error": "hazard not found"}) return } var req iace.AssessRiskRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } userID := rbac.GetUserID(c) // Calculate risk using the engine inherentRisk := h.engine.CalculateInherentRisk(req.Severity, req.Exposure, req.Probability, req.Avoidance) controlEff := h.engine.CalculateControlEffectiveness(req.ControlMaturity, req.ControlCoverage, req.TestEvidenceStrength) residualRisk := h.engine.CalculateResidualRisk(req.Severity, req.Exposure, req.Probability, controlEff) riskLevel := h.engine.DetermineRiskLevel(residualRisk) // For reassessment, check if all reduction steps have been applied mitigations, _ := h.store.ListMitigations(c.Request.Context(), hazardID) allReductionStepsApplied := len(mitigations) > 0 for _, m := range mitigations { if m.Status != iace.MitigationStatusVerified { allReductionStepsApplied = false break } } acceptable, acceptanceReason := h.engine.IsAcceptable(residualRisk, allReductionStepsApplied, req.AcceptanceJustification != "") // Determine version existingAssessments, _ := h.store.ListAssessments(c.Request.Context(), hazardID) version := len(existingAssessments) + 1 assessment := &iace.RiskAssessment{ HazardID: hazardID, Version: version, AssessmentType: iace.AssessmentTypePostMitigation, Severity: req.Severity, Exposure: req.Exposure, Probability: req.Probability, Avoidance: req.Avoidance, InherentRisk: inherentRisk, ControlMaturity: req.ControlMaturity, ControlCoverage: req.ControlCoverage, TestEvidenceStrength: req.TestEvidenceStrength, CEff: controlEff, ResidualRisk: residualRisk, RiskLevel: riskLevel, IsAcceptable: acceptable, AcceptanceJustification: req.AcceptanceJustification, AssessedBy: userID, } if err := h.store.CreateRiskAssessment(c.Request.Context(), assessment); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Audit trail newVals, _ := json.Marshal(assessment) h.store.AddAuditEntry( c.Request.Context(), projectID, "risk_assessment", assessment.ID, iace.AuditActionCreate, userID.String(), nil, newVals, ) c.JSON(http.StatusCreated, gin.H{ "assessment": assessment, "acceptable": acceptable, "acceptance_reason": acceptanceReason, "all_reduction_steps_applied": allReductionStepsApplied, }) } // ============================================================================ // Evidence & Verification // ============================================================================ // UploadEvidence handles POST /projects/:id/evidence // Creates a new evidence record for a project. func (h *IACEHandler) UploadEvidence(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } var req struct { MitigationID *uuid.UUID `json:"mitigation_id,omitempty"` VerificationPlanID *uuid.UUID `json:"verification_plan_id,omitempty"` FileName string `json:"file_name" binding:"required"` FilePath string `json:"file_path" binding:"required"` FileHash string `json:"file_hash" binding:"required"` FileSize int64 `json:"file_size" binding:"required"` MimeType string `json:"mime_type" binding:"required"` Description string `json:"description,omitempty"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } userID := rbac.GetUserID(c) evidence := &iace.Evidence{ ProjectID: projectID, MitigationID: req.MitigationID, VerificationPlanID: req.VerificationPlanID, FileName: req.FileName, FilePath: req.FilePath, FileHash: req.FileHash, FileSize: req.FileSize, MimeType: req.MimeType, Description: req.Description, UploadedBy: userID, } if err := h.store.CreateEvidence(c.Request.Context(), evidence); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Audit trail newVals, _ := json.Marshal(evidence) h.store.AddAuditEntry( c.Request.Context(), projectID, "evidence", evidence.ID, iace.AuditActionCreate, userID.String(), nil, newVals, ) c.JSON(http.StatusCreated, gin.H{"evidence": evidence}) } // ListEvidence handles GET /projects/:id/evidence // Lists all evidence records for a project. func (h *IACEHandler) ListEvidence(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } evidence, err := h.store.ListEvidence(c.Request.Context(), projectID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if evidence == nil { evidence = []iace.Evidence{} } c.JSON(http.StatusOK, gin.H{ "evidence": evidence, "total": len(evidence), }) } // CreateVerificationPlan handles POST /projects/:id/verification-plan // Creates a new verification plan for a project. func (h *IACEHandler) CreateVerificationPlan(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } var req iace.CreateVerificationPlanRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Override project ID from URL path req.ProjectID = projectID plan, err := h.store.CreateVerificationPlan(c.Request.Context(), req) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Audit trail userID := rbac.GetUserID(c) newVals, _ := json.Marshal(plan) h.store.AddAuditEntry( c.Request.Context(), projectID, "verification_plan", plan.ID, iace.AuditActionCreate, userID.String(), nil, newVals, ) c.JSON(http.StatusCreated, gin.H{"verification_plan": plan}) } // UpdateVerificationPlan handles PUT /verification-plan/:vid // Updates a verification plan with the provided fields. func (h *IACEHandler) UpdateVerificationPlan(c *gin.Context) { planID, err := uuid.Parse(c.Param("vid")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid verification plan ID"}) return } var updates map[string]interface{} if err := c.ShouldBindJSON(&updates); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } plan, err := h.store.UpdateVerificationPlan(c.Request.Context(), planID, updates) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if plan == nil { c.JSON(http.StatusNotFound, gin.H{"error": "verification plan not found"}) return } c.JSON(http.StatusOK, gin.H{"verification_plan": plan}) } // CompleteVerification handles POST /verification-plan/:vid/complete // Marks a verification plan as completed with a result. func (h *IACEHandler) CompleteVerification(c *gin.Context) { planID, err := uuid.Parse(c.Param("vid")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid verification plan ID"}) return } var req struct { Result string `json:"result" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } userID := rbac.GetUserID(c) if err := h.store.CompleteVerification( c.Request.Context(), planID, req.Result, userID.String(), ); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "verification completed"}) } // ============================================================================ // CE Technical File // ============================================================================ // GenerateTechFile handles POST /projects/:id/tech-file/generate // Generates technical file sections for a project. // TODO: Integrate LLM for intelligent content generation based on project data. func (h *IACEHandler) GenerateTechFile(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } project, err := h.store.GetProject(c.Request.Context(), projectID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if project == nil { c.JSON(http.StatusNotFound, gin.H{"error": "project not found"}) return } // Define the standard CE technical file sections to generate sectionDefinitions := []struct { SectionType string Title string }{ {"general_description", "General Description of the Machinery"}, {"risk_assessment_report", "Risk Assessment Report"}, {"hazard_log_combined", "Combined Hazard Log"}, {"essential_requirements", "Essential Health and Safety Requirements"}, {"design_specifications", "Design Specifications and Drawings"}, {"test_reports", "Test Reports and Verification Results"}, {"standards_applied", "Applied Harmonised Standards"}, {"declaration_of_conformity", "EU Declaration of Conformity"}, } // Check if project has AI components for additional sections components, _ := h.store.ListComponents(c.Request.Context(), projectID) hasAI := false for _, comp := range components { if comp.ComponentType == iace.ComponentTypeAIModel { hasAI = true break } } if hasAI { sectionDefinitions = append(sectionDefinitions, struct { SectionType string Title string }{"ai_intended_purpose", "AI System Intended Purpose"}, struct { SectionType string Title string }{"ai_model_description", "AI Model Description and Training Data"}, struct { SectionType string Title string }{"ai_risk_management", "AI Risk Management System"}, struct { SectionType string Title string }{"ai_human_oversight", "AI Human Oversight Measures"}, ) } // Generate each section with LLM-based content var sections []iace.TechFileSection existingSections, _ := h.store.ListTechFileSections(c.Request.Context(), projectID) existingMap := make(map[string]bool) for _, s := range existingSections { existingMap[s.SectionType] = true } for _, def := range sectionDefinitions { // Skip sections that already exist if existingMap[def.SectionType] { continue } // Generate content via LLM (falls back to structured placeholder if LLM unavailable) content, _ := h.techFileGen.GenerateSection(c.Request.Context(), projectID, def.SectionType) if content == "" { content = fmt.Sprintf("[Sektion: %s — Inhalt wird generiert]", def.Title) } section, err := h.store.CreateTechFileSection( c.Request.Context(), projectID, def.SectionType, def.Title, content, ) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } sections = append(sections, *section) } // Update project status h.store.UpdateProjectStatus(c.Request.Context(), projectID, iace.ProjectStatusTechFile) // Audit trail userID := rbac.GetUserID(c) h.store.AddAuditEntry( c.Request.Context(), projectID, "tech_file", projectID, iace.AuditActionCreate, userID.String(), nil, nil, ) c.JSON(http.StatusCreated, gin.H{ "sections_created": len(sections), "sections": sections, }) } // GenerateSingleSection handles POST /projects/:id/tech-file/:section/generate // Generates or regenerates a single tech file section using LLM. func (h *IACEHandler) GenerateSingleSection(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } sectionType := c.Param("section") if sectionType == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "section type required"}) return } // Generate content via LLM content, err := h.techFileGen.GenerateSection(c.Request.Context(), projectID, sectionType) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("generation failed: %v", err)}) return } // Find existing section and update, or create new sections, _ := h.store.ListTechFileSections(c.Request.Context(), projectID) var sectionID uuid.UUID found := false for _, s := range sections { if s.SectionType == sectionType { sectionID = s.ID found = true break } } if found { if err := h.store.UpdateTechFileSection(c.Request.Context(), sectionID, content); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } } else { title := sectionType // fallback sectionTitles := map[string]string{ "general_description": "General Description of the Machinery", "risk_assessment_report": "Risk Assessment Report", "hazard_log_combined": "Combined Hazard Log", "essential_requirements": "Essential Health and Safety Requirements", "design_specifications": "Design Specifications and Drawings", "test_reports": "Test Reports and Verification Results", "standards_applied": "Applied Harmonised Standards", "declaration_of_conformity": "EU Declaration of Conformity", "component_list": "Component List", "classification_report": "Regulatory Classification Report", "mitigation_report": "Mitigation Measures Report", "verification_report": "Verification Report", "evidence_index": "Evidence Index", "instructions_for_use": "Instructions for Use", "monitoring_plan": "Post-Market Monitoring Plan", "ai_intended_purpose": "AI System Intended Purpose", "ai_model_description": "AI Model Description and Training Data", "ai_risk_management": "AI Risk Management System", "ai_human_oversight": "AI Human Oversight Measures", } if t, ok := sectionTitles[sectionType]; ok { title = t } _, err := h.store.CreateTechFileSection(c.Request.Context(), projectID, sectionType, title, content) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } } // Audit trail userID := rbac.GetUserID(c) h.store.AddAuditEntry( c.Request.Context(), projectID, "tech_file_section", projectID, iace.AuditActionCreate, userID.String(), nil, nil, ) c.JSON(http.StatusOK, gin.H{ "message": "section generated", "section_type": sectionType, "content": content, }) } // ListTechFileSections handles GET /projects/:id/tech-file // Lists all technical file sections for a project. func (h *IACEHandler) ListTechFileSections(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } sections, err := h.store.ListTechFileSections(c.Request.Context(), projectID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if sections == nil { sections = []iace.TechFileSection{} } c.JSON(http.StatusOK, gin.H{ "sections": sections, "total": len(sections), }) } // UpdateTechFileSection handles PUT /projects/:id/tech-file/:section // Updates the content of a technical file section (identified by section_type). func (h *IACEHandler) UpdateTechFileSection(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } sectionType := c.Param("section") if sectionType == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "section type required"}) return } var req struct { Content string `json:"content" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Find the section by project ID and section type sections, err := h.store.ListTechFileSections(c.Request.Context(), projectID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } var sectionID uuid.UUID found := false for _, s := range sections { if s.SectionType == sectionType { sectionID = s.ID found = true break } } if !found { c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("tech file section '%s' not found", sectionType)}) return } if err := h.store.UpdateTechFileSection(c.Request.Context(), sectionID, req.Content); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Audit trail userID := rbac.GetUserID(c) h.store.AddAuditEntry( c.Request.Context(), projectID, "tech_file_section", sectionID, iace.AuditActionUpdate, userID.String(), nil, nil, ) c.JSON(http.StatusOK, gin.H{"message": "tech file section updated"}) } // ApproveTechFileSection handles POST /projects/:id/tech-file/:section/approve // Marks a technical file section as approved. func (h *IACEHandler) ApproveTechFileSection(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } sectionType := c.Param("section") if sectionType == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "section type required"}) return } // Find the section by project ID and section type sections, err := h.store.ListTechFileSections(c.Request.Context(), projectID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } var sectionID uuid.UUID found := false for _, s := range sections { if s.SectionType == sectionType { sectionID = s.ID found = true break } } if !found { c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("tech file section '%s' not found", sectionType)}) return } userID := rbac.GetUserID(c) if err := h.store.ApproveTechFileSection(c.Request.Context(), sectionID, userID.String()); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Audit trail h.store.AddAuditEntry( c.Request.Context(), projectID, "tech_file_section", sectionID, iace.AuditActionApprove, userID.String(), nil, nil, ) c.JSON(http.StatusOK, gin.H{"message": "tech file section approved"}) } // ExportTechFile handles GET /projects/:id/tech-file/export?format=pdf|xlsx|docx|md|json // Exports all tech file sections in the requested format. func (h *IACEHandler) ExportTechFile(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } project, err := h.store.GetProject(c.Request.Context(), projectID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if project == nil { c.JSON(http.StatusNotFound, gin.H{"error": "project not found"}) return } sections, err := h.store.ListTechFileSections(c.Request.Context(), projectID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Load hazards, assessments, mitigations, classifications for export hazards, _ := h.store.ListHazards(c.Request.Context(), projectID) var allAssessments []iace.RiskAssessment var allMitigations []iace.Mitigation for _, hazard := range hazards { assessments, _ := h.store.ListAssessments(c.Request.Context(), hazard.ID) allAssessments = append(allAssessments, assessments...) mitigations, _ := h.store.ListMitigations(c.Request.Context(), hazard.ID) allMitigations = append(allMitigations, mitigations...) } classifications, _ := h.store.GetClassifications(c.Request.Context(), projectID) format := c.DefaultQuery("format", "json") safeName := strings.ReplaceAll(project.MachineName, " ", "_") switch format { case "pdf": data, err := h.exporter.ExportPDF(project, sections, hazards, allAssessments, allMitigations, classifications) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("PDF export failed: %v", err)}) return } c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="CE-Akte-%s.pdf"`, safeName)) c.Data(http.StatusOK, "application/pdf", data) case "xlsx": data, err := h.exporter.ExportExcel(project, sections, hazards, allAssessments, allMitigations) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Excel export failed: %v", err)}) return } c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="CE-Akte-%s.xlsx"`, safeName)) c.Data(http.StatusOK, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", data) case "docx": data, err := h.exporter.ExportDOCX(project, sections) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("DOCX export failed: %v", err)}) return } c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="CE-Akte-%s.docx"`, safeName)) c.Data(http.StatusOK, "application/vnd.openxmlformats-officedocument.wordprocessingml.document", data) case "md": data, err := h.exporter.ExportMarkdown(project, sections) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Markdown export failed: %v", err)}) return } c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="CE-Akte-%s.md"`, safeName)) c.Data(http.StatusOK, "text/markdown", data) default: // JSON export (original behavior) allApproved := true for _, s := range sections { if s.Status != iace.TechFileSectionStatusApproved { allApproved = false break } } riskSummary, _ := h.store.GetRiskSummary(c.Request.Context(), projectID) c.JSON(http.StatusOK, gin.H{ "project": project, "sections": sections, "classifications": classifications, "risk_summary": riskSummary, "all_approved": allApproved, "export_format": "json", }) } } // ============================================================================ // Monitoring // ============================================================================ // CreateMonitoringEvent handles POST /projects/:id/monitoring // Creates a new post-market monitoring event. func (h *IACEHandler) CreateMonitoringEvent(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } var req iace.CreateMonitoringEventRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Override project ID from URL path req.ProjectID = projectID event, err := h.store.CreateMonitoringEvent(c.Request.Context(), req) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Audit trail userID := rbac.GetUserID(c) newVals, _ := json.Marshal(event) h.store.AddAuditEntry( c.Request.Context(), projectID, "monitoring_event", event.ID, iace.AuditActionCreate, userID.String(), nil, newVals, ) c.JSON(http.StatusCreated, gin.H{"monitoring_event": event}) } // ListMonitoringEvents handles GET /projects/:id/monitoring // Lists all monitoring events for a project. func (h *IACEHandler) ListMonitoringEvents(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } events, err := h.store.ListMonitoringEvents(c.Request.Context(), projectID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if events == nil { events = []iace.MonitoringEvent{} } c.JSON(http.StatusOK, gin.H{ "monitoring_events": events, "total": len(events), }) } // UpdateMonitoringEvent handles PUT /projects/:id/monitoring/:eid // Updates a monitoring event with the provided fields. func (h *IACEHandler) UpdateMonitoringEvent(c *gin.Context) { _, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } eventID, err := uuid.Parse(c.Param("eid")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid monitoring event ID"}) return } var updates map[string]interface{} if err := c.ShouldBindJSON(&updates); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } event, err := h.store.UpdateMonitoringEvent(c.Request.Context(), eventID, updates) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if event == nil { c.JSON(http.StatusNotFound, gin.H{"error": "monitoring event not found"}) return } c.JSON(http.StatusOK, gin.H{"monitoring_event": event}) } // GetAuditTrail handles GET /projects/:id/audit-trail // Returns all audit trail entries for a project, newest first. func (h *IACEHandler) GetAuditTrail(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } entries, err := h.store.ListAuditTrail(c.Request.Context(), projectID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if entries == nil { entries = []iace.AuditTrailEntry{} } c.JSON(http.StatusOK, gin.H{ "audit_trail": entries, "total": len(entries), }) } // ============================================================================ // Internal Helpers // ============================================================================ // buildCompletenessContext constructs the CompletenessContext needed by the checker // by loading all related entities for a project. func (h *IACEHandler) buildCompletenessContext( c *gin.Context, project *iace.Project, components []iace.Component, classifications []iace.RegulatoryClassification, ) *iace.CompletenessContext { projectID := project.ID hazards, _ := h.store.ListHazards(c.Request.Context(), projectID) var allAssessments []iace.RiskAssessment var allMitigations []iace.Mitigation for _, hazard := range hazards { assessments, _ := h.store.ListAssessments(c.Request.Context(), hazard.ID) allAssessments = append(allAssessments, assessments...) mitigations, _ := h.store.ListMitigations(c.Request.Context(), hazard.ID) allMitigations = append(allMitigations, mitigations...) } evidence, _ := h.store.ListEvidence(c.Request.Context(), projectID) techFileSections, _ := h.store.ListTechFileSections(c.Request.Context(), projectID) hasAI := false for _, comp := range components { if comp.ComponentType == iace.ComponentTypeAIModel { hasAI = true break } } return &iace.CompletenessContext{ Project: project, Components: components, Classifications: classifications, Hazards: hazards, Assessments: allAssessments, Mitigations: allMitigations, Evidence: evidence, TechFileSections: techFileSections, HasAI: hasAI, } } // componentTypeKeys extracts keys from a map[string]bool and returns them as a sorted slice. func componentTypeKeys(m map[string]bool) []string { keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } // Sort for deterministic output sortStrings(keys) return keys } // sortStrings sorts a slice of strings in place using a simple insertion sort. func sortStrings(s []string) { for i := 1; i < len(s); i++ { for j := i; j > 0 && strings.Compare(s[j-1], s[j]) > 0; j-- { s[j-1], s[j] = s[j], s[j-1] } } } // ============================================================================ // ISO 12100 Endpoints // ============================================================================ // ListLifecyclePhases handles GET /lifecycle-phases // Returns the 12 machine lifecycle phases with DE/EN labels. func (h *IACEHandler) ListLifecyclePhases(c *gin.Context) { phases, err := h.store.ListLifecyclePhases(c.Request.Context()) if err != nil { // Fallback: return hardcoded 25 phases if DB table not yet migrated phases = []iace.LifecyclePhaseInfo{ {ID: "transport", LabelDE: "Transport", LabelEN: "Transport", Sort: 1}, {ID: "storage", LabelDE: "Lagerung", LabelEN: "Storage", Sort: 2}, {ID: "assembly", LabelDE: "Montage", LabelEN: "Assembly", Sort: 3}, {ID: "installation", LabelDE: "Installation", LabelEN: "Installation", Sort: 4}, {ID: "commissioning", LabelDE: "Inbetriebnahme", LabelEN: "Commissioning", Sort: 5}, {ID: "parameterization", LabelDE: "Parametrierung", LabelEN: "Parameterization", Sort: 6}, {ID: "setup", LabelDE: "Einrichten / Setup", LabelEN: "Setup", Sort: 7}, {ID: "normal_operation", LabelDE: "Normalbetrieb", LabelEN: "Normal Operation", Sort: 8}, {ID: "automatic_operation", LabelDE: "Automatikbetrieb", LabelEN: "Automatic Operation", Sort: 9}, {ID: "manual_operation", LabelDE: "Handbetrieb", LabelEN: "Manual Operation", Sort: 10}, {ID: "teach_mode", LabelDE: "Teach-Modus", LabelEN: "Teach Mode", Sort: 11}, {ID: "production_start", LabelDE: "Produktionsstart", LabelEN: "Production Start", Sort: 12}, {ID: "production_stop", LabelDE: "Produktionsstopp", LabelEN: "Production Stop", Sort: 13}, {ID: "process_monitoring", LabelDE: "Prozessueberwachung", LabelEN: "Process Monitoring", Sort: 14}, {ID: "cleaning", LabelDE: "Reinigung", LabelEN: "Cleaning", Sort: 15}, {ID: "maintenance", LabelDE: "Wartung", LabelEN: "Maintenance", Sort: 16}, {ID: "inspection", LabelDE: "Inspektion", LabelEN: "Inspection", Sort: 17}, {ID: "calibration", LabelDE: "Kalibrierung", LabelEN: "Calibration", Sort: 18}, {ID: "fault_clearing", LabelDE: "Stoerungsbeseitigung", LabelEN: "Fault Clearing", Sort: 19}, {ID: "repair", LabelDE: "Reparatur", LabelEN: "Repair", Sort: 20}, {ID: "changeover", LabelDE: "Umruestung", LabelEN: "Changeover", Sort: 21}, {ID: "software_update", LabelDE: "Software-Update", LabelEN: "Software Update", Sort: 22}, {ID: "remote_maintenance", LabelDE: "Fernwartung", LabelEN: "Remote Maintenance", Sort: 23}, {ID: "decommissioning", LabelDE: "Ausserbetriebnahme", LabelEN: "Decommissioning", Sort: 24}, {ID: "disposal", LabelDE: "Demontage / Entsorgung", LabelEN: "Dismantling / Disposal", Sort: 25}, } } if phases == nil { phases = []iace.LifecyclePhaseInfo{} } c.JSON(http.StatusOK, gin.H{ "lifecycle_phases": phases, "total": len(phases), }) } // ListProtectiveMeasures handles GET /protective-measures-library // Returns the protective measures library, optionally filtered by ?reduction_type and ?hazard_category. func (h *IACEHandler) ListProtectiveMeasures(c *gin.Context) { reductionType := c.Query("reduction_type") hazardCategory := c.Query("hazard_category") all := iace.GetProtectiveMeasureLibrary() var filtered []iace.ProtectiveMeasureEntry for _, entry := range all { if reductionType != "" && entry.ReductionType != reductionType { continue } if hazardCategory != "" && entry.HazardCategory != hazardCategory && entry.HazardCategory != "general" && entry.HazardCategory != "" { continue } filtered = append(filtered, entry) } if filtered == nil { filtered = []iace.ProtectiveMeasureEntry{} } c.JSON(http.StatusOK, gin.H{ "protective_measures": filtered, "total": len(filtered), }) } // ValidateMitigationHierarchy handles POST /projects/:id/validate-mitigation-hierarchy // Validates if the proposed mitigation type follows the 3-step hierarchy principle. func (h *IACEHandler) ValidateMitigationHierarchy(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } var req iace.ValidateMitigationHierarchyRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Get existing mitigations for the hazard mitigations, err := h.store.ListMitigations(c.Request.Context(), req.HazardID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } _ = projectID // projectID used for authorization context warnings := h.engine.ValidateProtectiveMeasureHierarchy(req.ReductionType, mitigations) c.JSON(http.StatusOK, iace.ValidateMitigationHierarchyResponse{ Valid: len(warnings) == 0, Warnings: warnings, }) } // ListRoles handles GET /roles // Returns the 20 affected person roles reference data. func (h *IACEHandler) ListRoles(c *gin.Context) { roles, err := h.store.ListRoles(c.Request.Context()) if err != nil { // Fallback: return hardcoded roles if DB table not yet migrated roles = []iace.RoleInfo{ {ID: "operator", LabelDE: "Maschinenbediener", LabelEN: "Machine Operator", Sort: 1}, {ID: "setter", LabelDE: "Einrichter", LabelEN: "Setter", Sort: 2}, {ID: "maintenance_tech", LabelDE: "Wartungstechniker", LabelEN: "Maintenance Technician", Sort: 3}, {ID: "service_tech", LabelDE: "Servicetechniker", LabelEN: "Service Technician", Sort: 4}, {ID: "cleaning_staff", LabelDE: "Reinigungspersonal", LabelEN: "Cleaning Staff", Sort: 5}, {ID: "production_manager", LabelDE: "Produktionsleiter", LabelEN: "Production Manager", Sort: 6}, {ID: "safety_officer", LabelDE: "Sicherheitsbeauftragter", LabelEN: "Safety Officer", Sort: 7}, {ID: "electrician", LabelDE: "Elektriker", LabelEN: "Electrician", Sort: 8}, {ID: "software_engineer", LabelDE: "Softwareingenieur", LabelEN: "Software Engineer", Sort: 9}, {ID: "maintenance_manager", LabelDE: "Instandhaltungsleiter", LabelEN: "Maintenance Manager", Sort: 10}, {ID: "plant_operator", LabelDE: "Anlagenfahrer", LabelEN: "Plant Operator", Sort: 11}, {ID: "qa_inspector", LabelDE: "Qualitaetssicherung", LabelEN: "Quality Assurance", Sort: 12}, {ID: "logistics_staff", LabelDE: "Logistikpersonal", LabelEN: "Logistics Staff", Sort: 13}, {ID: "subcontractor", LabelDE: "Fremdfirma / Subunternehmer", LabelEN: "Subcontractor", Sort: 14}, {ID: "visitor", LabelDE: "Besucher", LabelEN: "Visitor", Sort: 15}, {ID: "auditor", LabelDE: "Auditor", LabelEN: "Auditor", Sort: 16}, {ID: "it_admin", LabelDE: "IT-Administrator", LabelEN: "IT Administrator", Sort: 17}, {ID: "remote_service", LabelDE: "Fernwartungsdienst", LabelEN: "Remote Service", Sort: 18}, {ID: "plant_owner", LabelDE: "Betreiber", LabelEN: "Plant Owner / Operator", Sort: 19}, {ID: "emergency_responder", LabelDE: "Notfallpersonal", LabelEN: "Emergency Responder", Sort: 20}, } } if roles == nil { roles = []iace.RoleInfo{} } c.JSON(http.StatusOK, gin.H{ "roles": roles, "total": len(roles), }) } // ListEvidenceTypes handles GET /evidence-types // Returns the 50 evidence/verification types reference data. func (h *IACEHandler) ListEvidenceTypes(c *gin.Context) { types, err := h.store.ListEvidenceTypes(c.Request.Context()) if err != nil { // Fallback: return empty if not migrated types = []iace.EvidenceTypeInfo{} } if types == nil { types = []iace.EvidenceTypeInfo{} } category := c.Query("category") if category != "" { var filtered []iace.EvidenceTypeInfo for _, t := range types { if t.Category == category { filtered = append(filtered, t) } } if filtered == nil { filtered = []iace.EvidenceTypeInfo{} } types = filtered } c.JSON(http.StatusOK, gin.H{ "evidence_types": types, "total": len(types), }) } // ============================================================================ // Component Library & Energy Sources (Phase 1) // ============================================================================ // ListComponentLibrary handles GET /component-library // Returns the built-in component library with optional category filter. func (h *IACEHandler) ListComponentLibrary(c *gin.Context) { category := c.Query("category") all := iace.GetComponentLibrary() var filtered []iace.ComponentLibraryEntry for _, entry := range all { if category != "" && entry.Category != category { continue } filtered = append(filtered, entry) } if filtered == nil { filtered = []iace.ComponentLibraryEntry{} } c.JSON(http.StatusOK, gin.H{ "components": filtered, "total": len(filtered), }) } // ListEnergySources handles GET /energy-sources // Returns the built-in energy source library. func (h *IACEHandler) ListEnergySources(c *gin.Context) { sources := iace.GetEnergySources() c.JSON(http.StatusOK, gin.H{ "energy_sources": sources, "total": len(sources), }) } // ============================================================================ // Tag Taxonomy (Phase 2) // ============================================================================ // ListTags handles GET /tags // Returns the tag taxonomy with optional domain filter. func (h *IACEHandler) ListTags(c *gin.Context) { domain := c.Query("domain") all := iace.GetTagTaxonomy() var filtered []iace.TagEntry for _, entry := range all { if domain != "" && entry.Domain != domain { continue } filtered = append(filtered, entry) } if filtered == nil { filtered = []iace.TagEntry{} } c.JSON(http.StatusOK, gin.H{ "tags": filtered, "total": len(filtered), }) } // ============================================================================ // Hazard Patterns & Pattern Engine (Phase 3+4) // ============================================================================ // ListHazardPatterns handles GET /hazard-patterns // Returns all built-in hazard patterns. func (h *IACEHandler) ListHazardPatterns(c *gin.Context) { patterns := iace.GetBuiltinHazardPatterns() patterns = append(patterns, iace.GetExtendedHazardPatterns()...) c.JSON(http.StatusOK, gin.H{ "patterns": patterns, "total": len(patterns), }) } // MatchPatterns handles POST /projects/:id/match-patterns // Runs the pattern engine against the project's components and energy sources. func (h *IACEHandler) MatchPatterns(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } // Verify project exists project, err := h.store.GetProject(c.Request.Context(), projectID) if err != nil || project == nil { c.JSON(http.StatusNotFound, gin.H{"error": "project not found"}) return } var input iace.MatchInput if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } engine := iace.NewPatternEngine() result := engine.Match(input) c.JSON(http.StatusOK, result) } // ApplyPatternResults handles POST /projects/:id/apply-patterns // Accepts matched patterns and creates concrete hazards, mitigations, and // verification plans in the project. func (h *IACEHandler) ApplyPatternResults(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 } project, err := h.store.GetProject(c.Request.Context(), projectID) if err != nil || project == nil { c.JSON(http.StatusNotFound, gin.H{"error": "project not found"}) return } var req struct { AcceptedHazards []iace.CreateHazardRequest `json:"accepted_hazards"` AcceptedMeasures []iace.CreateMitigationRequest `json:"accepted_measures"` AcceptedEvidence []iace.CreateVerificationPlanRequest `json:"accepted_evidence"` SourcePatternIDs []string `json:"source_pattern_ids"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } ctx := c.Request.Context() var createdHazards int var createdMeasures int var createdEvidence int // Create hazards for _, hazardReq := range req.AcceptedHazards { hazardReq.ProjectID = projectID _, err := h.store.CreateHazard(ctx, hazardReq) if err != nil { continue } createdHazards++ } // Create mitigations for _, mitigReq := range req.AcceptedMeasures { _, err := h.store.CreateMitigation(ctx, mitigReq) if err != nil { continue } createdMeasures++ } // Create verification plans for _, evidReq := range req.AcceptedEvidence { evidReq.ProjectID = projectID _, err := h.store.CreateVerificationPlan(ctx, evidReq) if err != nil { continue } createdEvidence++ } // Audit trail h.store.AddAuditEntry(ctx, projectID, "pattern_matching", projectID, iace.AuditActionCreate, tenantID.String(), nil, mustMarshalJSON(map[string]interface{}{ "source_patterns": req.SourcePatternIDs, "created_hazards": createdHazards, "created_measures": createdMeasures, "created_evidence": createdEvidence, }), ) c.JSON(http.StatusOK, gin.H{ "created_hazards": createdHazards, "created_measures": createdMeasures, "created_evidence": createdEvidence, "message": "Pattern results applied successfully", }) } // SuggestMeasuresForHazard handles POST /projects/:id/hazards/:hid/suggest-measures // Suggests measures for a specific hazard based on its tags and category. func (h *IACEHandler) SuggestMeasuresForHazard(c *gin.Context) { hazardID, err := uuid.Parse(c.Param("hid")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid hazard ID"}) return } hazard, err := h.store.GetHazard(c.Request.Context(), hazardID) if err != nil || hazard == nil { c.JSON(http.StatusNotFound, gin.H{"error": "hazard not found"}) return } // Find measures matching the hazard category all := iace.GetProtectiveMeasureLibrary() var suggested []iace.ProtectiveMeasureEntry for _, m := range all { if m.HazardCategory == hazard.Category || m.HazardCategory == "general" { suggested = append(suggested, m) } } if suggested == nil { suggested = []iace.ProtectiveMeasureEntry{} } c.JSON(http.StatusOK, gin.H{ "hazard_id": hazardID.String(), "hazard_category": hazard.Category, "suggested_measures": suggested, "total": len(suggested), }) } // SuggestEvidenceForMitigation handles POST /projects/:id/mitigations/:mid/suggest-evidence // Suggests evidence types for a specific mitigation. func (h *IACEHandler) SuggestEvidenceForMitigation(c *gin.Context) { mitigationID, err := uuid.Parse(c.Param("mid")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid mitigation ID"}) return } mitigation, err := h.store.GetMitigation(c.Request.Context(), mitigationID) if err != nil || mitigation == nil { c.JSON(http.StatusNotFound, gin.H{"error": "mitigation not found"}) return } // Map reduction type to relevant evidence tags var relevantTags []string switch mitigation.ReductionType { case iace.ReductionTypeDesign: relevantTags = []string{"design_evidence", "analysis_evidence"} case iace.ReductionTypeProtective: relevantTags = []string{"test_evidence", "inspection_evidence"} case iace.ReductionTypeInformation: relevantTags = []string{"training_evidence", "operational_evidence"} } resolver := iace.NewTagResolver() suggested := resolver.FindEvidenceByTags(relevantTags) if suggested == nil { suggested = []iace.EvidenceTypeInfo{} } c.JSON(http.StatusOK, gin.H{ "mitigation_id": mitigationID.String(), "reduction_type": string(mitigation.ReductionType), "suggested_evidence": suggested, "total": len(suggested), }) } // ============================================================================ // RAG Library Search (Phase 6) // ============================================================================ // IACELibrarySearchRequest represents a semantic search against the IACE library corpus. type IACELibrarySearchRequest struct { Query string `json:"query" binding:"required"` Category string `json:"category,omitempty"` TopK int `json:"top_k,omitempty"` Filters []string `json:"filters,omitempty"` } // SearchLibrary handles POST /iace/library-search // Performs semantic search across the IACE hazard/component/measure library in Qdrant. func (h *IACEHandler) SearchLibrary(c *gin.Context) { var req IACELibrarySearchRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } topK := req.TopK if topK <= 0 || topK > 50 { topK = 10 } // Use regulation filter for category-based search within the IACE collection var filters []string if req.Category != "" { filters = append(filters, req.Category) } filters = append(filters, req.Filters...) results, err := h.ragClient.SearchCollection( c.Request.Context(), "bp_iace_libraries", req.Query, filters, topK, ) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": "RAG search failed", "details": err.Error(), }) return } if results == nil { results = []ucca.LegalSearchResult{} } c.JSON(http.StatusOK, gin.H{ "query": req.Query, "results": results, "total": len(results), }) } // EnrichTechFileSection handles POST /projects/:id/tech-file/:section/enrich // Uses RAG to find relevant library content for a specific tech file section. func (h *IACEHandler) EnrichTechFileSection(c *gin.Context) { projectID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) return } sectionType := c.Param("section") if sectionType == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "section type required"}) return } project, err := h.store.GetProject(c.Request.Context(), projectID) if err != nil || project == nil { c.JSON(http.StatusNotFound, gin.H{"error": "project not found"}) return } // Build a contextual query based on section type and project data queryParts := []string{project.MachineName, project.MachineType} switch sectionType { case "risk_assessment_report", "hazard_log_combined": queryParts = append(queryParts, "Gefaehrdungen", "Risikobewertung", "ISO 12100") case "essential_requirements": queryParts = append(queryParts, "Sicherheitsanforderungen", "Maschinenrichtlinie") case "design_specifications": queryParts = append(queryParts, "Konstruktionsspezifikation", "Sicherheitskonzept") case "test_reports": queryParts = append(queryParts, "Pruefbericht", "Verifikation", "Nachweis") case "standards_applied": queryParts = append(queryParts, "harmonisierte Normen", "EN ISO") case "ai_risk_management": queryParts = append(queryParts, "KI-Risikomanagement", "AI Act", "Algorithmen") case "ai_human_oversight": queryParts = append(queryParts, "menschliche Aufsicht", "Human Oversight", "KI-Transparenz") default: queryParts = append(queryParts, sectionType) } query := strings.Join(queryParts, " ") results, err := h.ragClient.SearchCollection( c.Request.Context(), "bp_iace_libraries", query, nil, 5, ) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": "RAG enrichment failed", "details": err.Error(), }) return } if results == nil { results = []ucca.LegalSearchResult{} } c.JSON(http.StatusOK, gin.H{ "project_id": projectID.String(), "section_type": sectionType, "query": query, "context": results, "total": len(results), }) } // mustMarshalJSON marshals the given value to json.RawMessage. func mustMarshalJSON(v interface{}) json.RawMessage { data, err := json.Marshal(v) if err != nil { return nil } return data }