Split 4 oversized files (503-679 LOC each) into focused units all under 500 LOC: - tech_file_generator.go → +_prompts, +_prompt_builder, +_fallback - hazard_patterns_extended.go → +_extended2.go (HP074-HP102 extracted) - models.go → +_entities.go, +_api.go (enums / DB entities / API types) - completeness.go → +_gates.go (gate definitions extracted) All files remain in package iace. Zero behavior changes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
200 lines
5.7 KiB
Go
200 lines
5.7 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"`
|
|
}
|
|
|
|
// ============================================================================
|
|
// 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
|
|
}
|