5236864521
462 einzelne Queries (Assessments + Mitigations pro Hazard) durch 2 Batch-Queries ersetzt. GetProject von ~22s auf <1s. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
155 lines
4.8 KiB
Go
155 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)
|
|
|
|
// Batch queries instead of N+1 per hazard (was 462 queries for 231 hazards)
|
|
assessmentMap, _ := h.store.GetLatestAssessmentsByProject(c.Request.Context(), projectID)
|
|
var allAssessments []iace.RiskAssessment
|
|
for _, a := range assessmentMap {
|
|
allAssessments = append(allAssessments, a)
|
|
}
|
|
allMitigations, _ := h.store.ListMitigationsByProject(c.Request.Context(), projectID)
|
|
|
|
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
|
|
}
|