Files
breakpilot-compliance/ai-compliance-sdk/internal/api/handlers/iace_handler.go
Sharang Parnerkar 3f306fb6f0 refactor(go/handlers): split iace_handler and training_handlers into focused files
iace_handler.go (2706 LOC) split into 9 files:
- iace_handler.go: struct, constructor, shared helpers (~156 LOC)
- iace_handler_projects.go: project CRUD + InitFromProfile (~310 LOC)
- iace_handler_components.go: components + classification (~387 LOC)
- iace_handler_hazards.go: hazard library, CRUD, risk assessment (~469 LOC)
- iace_handler_mitigations.go: mitigations, evidence, verification plans (~293 LOC)
- iace_handler_techfile.go: CE tech file generation/export (~452 LOC)
- iace_handler_monitoring.go: monitoring events + audit trail (~134 LOC)
- iace_handler_refdata.go: ISO 12100 ref data, patterns, suggestions (~465 LOC)
- iace_handler_rag.go: RAG library search + section enrichment (~142 LOC)

training_handlers.go (1864 LOC) split into 9 files:
- training_handlers.go: struct + constructor (~23 LOC)
- training_handlers_modules.go: module CRUD (~226 LOC)
- training_handlers_matrix.go: CTM matrix endpoints (~95 LOC)
- training_handlers_assignments.go: assignment lifecycle (~243 LOC)
- training_handlers_quiz.go: quiz submit/grade/attempts (~185 LOC)
- training_handlers_content.go: LLM content/audio/video generation (~274 LOC)
- training_handlers_media.go: media, streaming, interactive video (~325 LOC)
- training_handlers_blocks.go: block configs + canonical controls (~280 LOC)
- training_handlers_stats.go: deadlines, escalation, audit, certificates (~290 LOC)

All files remain in package handlers. Zero behavior changes. All exported
function names preserved. All files under 500 LOC hard cap.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 09:17:20 +02:00

157 lines
4.8 KiB
Go

package handlers
import (
"encoding/json"
"fmt"
"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)
}
// ============================================================================
// 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,
}
}
// 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
}
// 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]
}
}
}
// 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
}