Files
breakpilot-compliance/ai-compliance-sdk/internal/iace/completeness.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

504 lines
14 KiB
Go

package iace
import (
"encoding/json"
"fmt"
)
// ============================================================================
// Completeness Types
// ============================================================================
// GateDefinition describes a single completeness gate with its check function.
type GateDefinition struct {
ID string
Category string // onboarding, classification, hazard_risk, evidence, tech_file
Label string
Required bool
Recommended bool
CheckFunc func(ctx *CompletenessContext) bool
}
// CompletenessContext provides all project data needed to evaluate completeness gates.
type CompletenessContext struct {
Project *Project
Components []Component
Classifications []RegulatoryClassification
Hazards []Hazard
Assessments []RiskAssessment
Mitigations []Mitigation
Evidence []Evidence
TechFileSections []TechFileSection
HasAI bool
PatternMatchingPerformed bool // set from audit trail (entity_type="pattern_matching")
}
// CompletenessResult contains the aggregated result of all gate checks.
type CompletenessResult struct {
Score float64 `json:"score"`
Gates []CompletenessGate `json:"gates"`
PassedRequired int `json:"passed_required"`
TotalRequired int `json:"total_required"`
PassedRecommended int `json:"passed_recommended"`
TotalRecommended int `json:"total_recommended"`
CanExport bool `json:"can_export"`
}
// ============================================================================
// Gate Definitions (25 CE Completeness Gates)
// ============================================================================
// buildGateDefinitions returns the full set of 25 CE completeness gate definitions.
func buildGateDefinitions() []GateDefinition {
return []GateDefinition{
// =====================================================================
// Onboarding Gates (G01-G08) - Required
// =====================================================================
{
ID: "G01",
Category: "onboarding",
Label: "Machine identity set",
Required: true,
CheckFunc: func(ctx *CompletenessContext) bool {
return ctx.Project != nil && ctx.Project.MachineName != ""
},
},
{
ID: "G02",
Category: "onboarding",
Label: "Intended use described",
Required: true,
CheckFunc: func(ctx *CompletenessContext) bool {
return ctx.Project != nil && ctx.Project.Description != ""
},
},
{
ID: "G03",
Category: "onboarding",
Label: "Operating limits defined",
Required: true,
CheckFunc: func(ctx *CompletenessContext) bool {
return ctx.Project != nil && hasMetadataKey(ctx.Project.Metadata, "operating_limits")
},
},
{
ID: "G04",
Category: "onboarding",
Label: "Foreseeable misuse documented",
Required: true,
CheckFunc: func(ctx *CompletenessContext) bool {
return ctx.Project != nil && hasMetadataKey(ctx.Project.Metadata, "foreseeable_misuse")
},
},
{
ID: "G05",
Category: "onboarding",
Label: "Component tree exists",
Required: true,
CheckFunc: func(ctx *CompletenessContext) bool {
return len(ctx.Components) > 0
},
},
{
ID: "G06",
Category: "onboarding",
Label: "AI classification done (if applicable)",
Required: true,
CheckFunc: func(ctx *CompletenessContext) bool {
// If no AI present, this gate passes automatically
if !ctx.HasAI {
return true
}
return hasClassificationFor(ctx.Classifications, RegulationAIAct)
},
},
{
ID: "G07",
Category: "onboarding",
Label: "Safety relevance marked",
Required: true,
CheckFunc: func(ctx *CompletenessContext) bool {
for _, comp := range ctx.Components {
if comp.IsSafetyRelevant {
return true
}
}
return false
},
},
{
ID: "G08",
Category: "onboarding",
Label: "Manufacturer info present",
Required: true,
CheckFunc: func(ctx *CompletenessContext) bool {
return ctx.Project != nil && ctx.Project.Manufacturer != ""
},
},
// =====================================================================
// Pattern Matching Gate (G09) - Recommended
// =====================================================================
{
ID: "G09",
Category: "onboarding",
Label: "Pattern matching performed",
Required: false,
Recommended: true,
CheckFunc: func(ctx *CompletenessContext) bool {
return ctx.PatternMatchingPerformed
},
},
// =====================================================================
// Classification Gates (G10-G13) - Required
// =====================================================================
{
ID: "G10",
Category: "classification",
Label: "AI Act classification complete",
Required: true,
CheckFunc: func(ctx *CompletenessContext) bool {
return hasClassificationFor(ctx.Classifications, RegulationAIAct)
},
},
{
ID: "G11",
Category: "classification",
Label: "Machinery Regulation check done",
Required: true,
CheckFunc: func(ctx *CompletenessContext) bool {
return hasClassificationFor(ctx.Classifications, RegulationMachineryRegulation)
},
},
{
ID: "G12",
Category: "classification",
Label: "NIS2 check done",
Required: true,
CheckFunc: func(ctx *CompletenessContext) bool {
return hasClassificationFor(ctx.Classifications, RegulationNIS2)
},
},
{
ID: "G13",
Category: "classification",
Label: "CRA check done",
Required: true,
CheckFunc: func(ctx *CompletenessContext) bool {
return hasClassificationFor(ctx.Classifications, RegulationCRA)
},
},
// =====================================================================
// Hazard & Risk Gates (G20-G24) - Required
// =====================================================================
{
ID: "G20",
Category: "hazard_risk",
Label: "Hazards identified",
Required: true,
CheckFunc: func(ctx *CompletenessContext) bool {
return len(ctx.Hazards) > 0
},
},
{
ID: "G21",
Category: "hazard_risk",
Label: "All hazards assessed",
Required: true,
CheckFunc: func(ctx *CompletenessContext) bool {
if len(ctx.Hazards) == 0 {
return false
}
// Build a set of hazard IDs that have at least one assessment
assessedHazards := make(map[string]bool)
for _, a := range ctx.Assessments {
assessedHazards[a.HazardID.String()] = true
}
for _, h := range ctx.Hazards {
if !assessedHazards[h.ID.String()] {
return false
}
}
return true
},
},
{
ID: "G22",
Category: "hazard_risk",
Label: "Critical/High risks mitigated",
Required: true,
CheckFunc: func(ctx *CompletenessContext) bool {
// Find all hazards that have a critical or high assessment
criticalHighHazards := make(map[string]bool)
for _, a := range ctx.Assessments {
if a.RiskLevel == RiskLevelCritical || a.RiskLevel == RiskLevelHigh {
criticalHighHazards[a.HazardID.String()] = true
}
}
// If no critical/high hazards, gate passes
if len(criticalHighHazards) == 0 {
return true
}
// Check that every critical/high hazard has at least one mitigation
mitigatedHazards := make(map[string]bool)
for _, m := range ctx.Mitigations {
mitigatedHazards[m.HazardID.String()] = true
}
for hazardID := range criticalHighHazards {
if !mitigatedHazards[hazardID] {
return false
}
}
return true
},
},
{
ID: "G23",
Category: "hazard_risk",
Label: "Mitigations verified",
Required: true,
CheckFunc: func(ctx *CompletenessContext) bool {
// All mitigations must be in a terminal state (verified or rejected).
// Planned and implemented mitigations block export — they haven't been
// verified yet, so the project cannot be considered complete.
if len(ctx.Mitigations) == 0 {
return true
}
for _, m := range ctx.Mitigations {
if m.Status != MitigationStatusVerified && m.Status != MitigationStatusRejected {
return false
}
}
return true
},
},
{
ID: "G24",
Category: "hazard_risk",
Label: "Residual risk accepted",
Required: true,
CheckFunc: func(ctx *CompletenessContext) bool {
if len(ctx.Assessments) == 0 {
return false
}
for _, a := range ctx.Assessments {
if !a.IsAcceptable && a.RiskLevel != RiskLevelLow && a.RiskLevel != RiskLevelNegligible {
return false
}
}
return true
},
},
// =====================================================================
// Evidence Gate (G30) - Recommended
// =====================================================================
{
ID: "G30",
Category: "evidence",
Label: "Test evidence linked",
Required: false,
Recommended: true,
CheckFunc: func(ctx *CompletenessContext) bool {
return len(ctx.Evidence) > 0
},
},
// =====================================================================
// Tech File Gates (G40-G42) - Required for completion
// =====================================================================
{
ID: "G40",
Category: "tech_file",
Label: "Risk assessment report generated",
Required: true,
CheckFunc: func(ctx *CompletenessContext) bool {
return hasTechFileSection(ctx.TechFileSections, "risk_assessment_report")
},
},
{
ID: "G41",
Category: "tech_file",
Label: "Hazard log generated",
Required: true,
CheckFunc: func(ctx *CompletenessContext) bool {
return hasTechFileSection(ctx.TechFileSections, "hazard_log_combined")
},
},
{
ID: "G42",
Category: "tech_file",
Label: "AI documents present (if applicable)",
Required: true,
CheckFunc: func(ctx *CompletenessContext) bool {
// If no AI present, this gate passes automatically
if !ctx.HasAI {
return true
}
hasIntendedPurpose := hasTechFileSection(ctx.TechFileSections, "ai_intended_purpose")
hasModelDescription := hasTechFileSection(ctx.TechFileSections, "ai_model_description")
return hasIntendedPurpose && hasModelDescription
},
},
}
}
// ============================================================================
// CompletenessChecker
// ============================================================================
// CompletenessChecker evaluates the 25 CE completeness gates for an IACE project.
type CompletenessChecker struct{}
// NewCompletenessChecker creates a new CompletenessChecker instance.
func NewCompletenessChecker() *CompletenessChecker { return &CompletenessChecker{} }
// Check evaluates all 25 completeness gates against the provided context and
// returns an aggregated result with a weighted score.
//
// Scoring formula:
//
// score = (passed_required / total_required) * 80
// + (passed_recommended / total_recommended) * 15
// + (passed_optional / total_optional) * 5
//
// Optional gates are those that are neither required nor recommended.
// CanExport is true only when all required gates have passed.
func (c *CompletenessChecker) Check(ctx *CompletenessContext) CompletenessResult {
gates := buildGateDefinitions()
var result CompletenessResult
var passedOptional, totalOptional int
for _, gate := range gates {
passed := gate.CheckFunc(ctx)
details := ""
if !passed {
details = fmt.Sprintf("Gate %s not satisfied: %s", gate.ID, gate.Label)
}
result.Gates = append(result.Gates, CompletenessGate{
ID: gate.ID,
Category: gate.Category,
Label: gate.Label,
Required: gate.Required,
Passed: passed,
Details: details,
})
switch {
case gate.Required:
result.TotalRequired++
if passed {
result.PassedRequired++
}
case gate.Recommended:
result.TotalRecommended++
if passed {
result.PassedRecommended++
}
default:
// Optional gate (neither required nor recommended)
totalOptional++
if passed {
passedOptional++
}
}
}
// Calculate weighted score
result.Score = calculateWeightedScore(
result.PassedRequired, result.TotalRequired,
result.PassedRecommended, result.TotalRecommended,
passedOptional, totalOptional,
)
// CanExport is true only when ALL required gates pass
result.CanExport = result.PassedRequired == result.TotalRequired
return result
}
// ============================================================================
// Helper Functions
// ============================================================================
// hasMetadataKey checks whether a JSON metadata blob contains a non-empty value
// for the given key.
func hasMetadataKey(metadata json.RawMessage, key string) bool {
if metadata == nil {
return false
}
var m map[string]interface{}
if err := json.Unmarshal(metadata, &m); err != nil {
return false
}
val, exists := m[key]
if !exists {
return false
}
// Check that the value is not empty/nil
switch v := val.(type) {
case string:
return v != ""
case nil:
return false
default:
return true
}
}
// hasClassificationFor checks whether a classification exists for the given regulation type.
func hasClassificationFor(classifications []RegulatoryClassification, regulation RegulationType) bool {
for _, c := range classifications {
if c.Regulation == regulation {
return true
}
}
return false
}
// hasTechFileSection checks whether a tech file section of the given type exists.
func hasTechFileSection(sections []TechFileSection, sectionType string) bool {
for _, s := range sections {
if s.SectionType == sectionType {
return true
}
}
return false
}
// calculateWeightedScore computes the weighted completeness score (0-100).
//
// Formula:
//
// score = (passedRequired/totalRequired) * 80
// + (passedRecommended/totalRecommended) * 15
// + (passedOptional/totalOptional) * 5
//
// If any denominator is 0, that component contributes 0 to the score.
func calculateWeightedScore(passedRequired, totalRequired, passedRecommended, totalRecommended, passedOptional, totalOptional int) float64 {
var score float64
if totalRequired > 0 {
score += (float64(passedRequired) / float64(totalRequired)) * 80
}
if totalRecommended > 0 {
score += (float64(passedRecommended) / float64(totalRecommended)) * 15
}
if totalOptional > 0 {
score += (float64(passedOptional) / float64(totalOptional)) * 5
}
return score
}