Files
breakpilot-compliance/ai-compliance-sdk/internal/api/handlers/iace_handler.go
Benjamin Admin 6d2de9b897
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 36s
CI/CD / test-python-backend-compliance (push) Successful in 33s
CI/CD / test-python-document-crawler (push) Successful in 24s
CI/CD / test-python-dsms-gateway (push) Successful in 21s
CI/CD / validate-canonical-controls (push) Successful in 13s
CI/CD / Deploy (push) Successful in 2s
feat(iace): complete CE risk assessment — LLM tech-file generation, multi-format export, TipTap editor
Phase 1: Fix completeness gates G23 (require verified/rejected mitigations) and G09 (audit trail check)
Phase 2: LLM-based tech-file section generation with 19 German prompts and RAG enrichment
Phase 3: Multi-format document export (PDF/Excel/DOCX/Markdown/JSON)
Phase 4: Company profile → IACE data flow with auto component/classification creation
Phase 5: TipTap WYSIWYG editor replacing textarea for tech-file sections
Phase 6: User journey tests, developer portal API reference, updated documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 12:50:53 +01:00

2707 lines
83 KiB
Go

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
}