feat(sdk,iace): add Personalized Drafting Pipeline v2 and IACE engine
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 44s
CI / test-python-backend-compliance (push) Successful in 37s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 20s
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 44s
CI / test-python-backend-compliance (push) Successful in 37s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 20s
Drafting Engine: 7-module pipeline with narrative tags, allowed facts governance, PII sanitizer, prose validator with repair loop, hash-based cache, and terminology guide. v1 fallback via ?v=1 query param. IACE: Initial AI-Act Conformity Engine with risk classifier, completeness checker, hazard library, and PostgreSQL store for AI system assessments. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
485
ai-compliance-sdk/internal/iace/completeness.go
Normal file
485
ai-compliance-sdk/internal/iace/completeness.go
Normal file
@@ -0,0 +1,485 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user