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 } // 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 != "" }, }, // ===================================================================== // 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 with status "implemented" must also be verified for _, m := range ctx.Mitigations { if m.Status == MitigationStatusImplemented { // Implemented but not yet verified -> gate fails return false } } // All mitigations are either planned, verified, or rejected 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 }