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"` } // ============================================================================ // 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 }