package main import ( "encoding/json" "fmt" "os" "path/filepath" "strings" ) var requiredPacketKeys = []string{ "task_id", "task_revision", "architecture_revision", "base_repo_revision", "risk_tier", "intent", "requirement_ids", "allowed_change_scope", "forbidden_change_scope", "allowed_architecture_delta", "contracts", "semantic_owner", "canonical_state", "state_transitions", "side_effects_and_privileges", "persistence_and_migrations", "failure_retry_restart", "assumptions", "acceptance_scenarios", "required_tests", "terminal_gap_states", } var requiredDeltaKeys = []string{ "new_cross_module_dependencies", "new_canonical_owners", "new_privileges", "new_third_party_dependencies", "new_database_owner_roles", } func validateTaskPacket(root string, task TaskPacket, taskPath string) error { if task.TaskID == "" || task.TaskRevision < 1 || task.ArchitectureRevision == "" || task.BaseRepoRevision == "" || task.RiskTier == "" || task.Intent == "" { return fmt.Errorf("task %s is incomplete: identity, revision, architecture_revision, base_repo_revision, risk_tier and intent are required", task.TaskID) } if taskPath == "" || !matchesAny(taskPath, mustCompile([]string{"dev/tasks/*.json"})) { return fmt.Errorf("task %s must be selected from dev/tasks", task.TaskID) } if task.RiskTier < "A" || task.RiskTier > "E" { return fmt.Errorf("task %s has invalid risk_tier %q", task.TaskID, task.RiskTier) } if len(task.RequirementIDs) == 0 || len(task.AcceptanceScenarios) == 0 || len(task.RequiredTests) == 0 || len(task.TerminalGapStates) == 0 { return fmt.Errorf("task %s must declare requirements, acceptance_scenarios, required_tests and terminal_gap_states", task.TaskID) } if len(task.AllowedChangeScope.Paths) == 0 { return fmt.Errorf("task %s must declare allowed_change_scope.paths", task.TaskID) } if err := validateScopeOverlap(task.AllowedChangeScope.Paths, task.ForbiddenChangeScope.Paths); err != nil { return fmt.Errorf("task %s scope: %w", task.TaskID, err) } if err := validateDelta(task.AllowedArchitectureDelta); err != nil { return fmt.Errorf("task %s architecture delta: %w", task.TaskID, err) } if err := validateRequiredTests(task.RequiredTests); err != nil { return fmt.Errorf("task %s required_tests: %w", task.TaskID, err) } if err := validateArchitectureRevision(root, task.ArchitectureRevision); err != nil { return fmt.Errorf("task %s architecture_revision: %w", task.TaskID, err) } if err := validateGenerationTask(task); err != nil { return err } if _, err := resolveBaseCommit(root, task.BaseRepoRevision); err != nil { return fmt.Errorf("task %s base_repo_revision: %w", task.TaskID, err) } if err := validateObservedDelta(root, task); err != nil { return fmt.Errorf("task %s observed architecture delta: %w", task.TaskID, err) } return nil } func validateGenerationTask(task TaskPacket) error { switch { case task.TaskID == "PRE-GEN2-000": if task.BaseRepoRevision != "origin/main" || task.ArchitectureRevision != "ADR-0019" { return fmt.Errorf("task %s must use origin/main and ADR-0019", task.TaskID) } case task.TaskID == "ARCH-GEN2-000": if task.BaseRepoRevision != "gen2-integration" || task.ArchitectureRevision != "ADR-0019" { return fmt.Errorf("task %s must use gen2-integration and ADR-0019", task.TaskID) } case strings.HasPrefix(task.TaskID, "GEN2-"): if task.BaseRepoRevision != "gen2-integration" || task.ArchitectureRevision != "ARCH-GEN2-000" { return fmt.Errorf("task %s must use gen2-integration and ARCH-GEN2-000", task.TaskID) } } return nil } func validateRequiredTests(tests []string) error { joined := strings.Join(tests, "\n") for _, required := range []string{"go test", "go vet", "preflight", "check-diff", "git diff --check", "independent"} { if !strings.Contains(joined, required) { return fmt.Errorf("missing required governance evidence %q", required) } } return nil } func validateObservedDelta(root string, task TaskPacket) error { changed, err := changedPaths(root, task.BaseRepoRevision) if err != nil { return err } if err := validateRegistryDelta(root, task); err != nil { return err } for _, changedPath := range changed { if (strings.HasPrefix(changedPath, "security/") || strings.HasPrefix(changedPath, "deploy/")) && task.AllowedArchitectureDelta["new_privileges"] == 0 { return fmt.Errorf("privileged surface changed with new_privileges=0: %s", changedPath) } if strings.HasPrefix(changedPath, "database/") && task.AllowedArchitectureDelta["new_database_owner_roles"] == 0 { return fmt.Errorf("database surface changed with new_database_owner_roles=0: %s", changedPath) } if (changedPath == "go.mod" || changedPath == "go.sum") && task.AllowedArchitectureDelta["new_third_party_dependencies"] == 0 { return fmt.Errorf("dependency manifest changed with new_third_party_dependencies=0: %s", changedPath) } if strings.HasPrefix(changedPath, "contracts/") && len(task.Contracts.MayChange) == 0 { return fmt.Errorf("contract surface changed but contracts.may_change is empty: %s", changedPath) } } return nil } func validateRegistryDelta(root string, task TaskPacket) error { baseBytes, err := runGit(root, "show", task.BaseRepoRevision+":architecture/architecture-registry.json") if err != nil { return fmt.Errorf("read base architecture registry: %w", err) } currentBytes, err := os.ReadFile(filepath.Join(root, "architecture", "architecture-registry.json")) if err != nil { return err } var base, current Registry if err := json.Unmarshal(baseBytes, &base); err != nil { return fmt.Errorf("parse base architecture registry: %w", err) } if err := json.Unmarshal(currentBytes, ¤t); err != nil { return fmt.Errorf("parse current architecture registry: %w", err) } if task.AllowedArchitectureDelta["new_canonical_owners"] == 0 { for name := range current.Modules { if _, ok := base.Modules[name]; !ok { return fmt.Errorf("new canonical module owner %q with new_canonical_owners=0", name) } } } if task.AllowedArchitectureDelta["new_cross_module_dependencies"] == 0 { for name, module := range current.Modules { previous, ok := base.Modules[name] if !ok { continue } old := map[string]bool{} for _, dep := range previous.MayDependOn { old[dep] = true } for _, dep := range module.MayDependOn { if !old[dep] { return fmt.Errorf("new dependency %q in module %q with new_cross_module_dependencies=0", dep, name) } } } } if task.AllowedArchitectureDelta["new_privileges"] == 0 { for name, module := range current.Modules { if previous, ok := base.Modules[name]; ok && module.Privileged && !previous.Privileged { return fmt.Errorf("new privilege in module %q with new_privileges=0", name) } } } return nil } func validateDelta(delta map[string]int) error { if len(delta) != len(requiredDeltaKeys) { return fmt.Errorf("must contain exactly %s", strings.Join(requiredDeltaKeys, ", ")) } for _, key := range requiredDeltaKeys { value, ok := delta[key] if !ok || value < 0 { return fmt.Errorf("%s must be a non-negative integer", key) } } return nil } func validateArchitectureRevision(root, revision string) error { if strings.HasPrefix(revision, "ADR-") { matches, err := filepath.Glob(filepath.Join(root, "architecture", "adrs", revision+"-*.md")) if err != nil || len(matches) != 1 { return fmt.Errorf("accepted ADR %q is missing or ambiguous", revision) } return nil } if strings.HasPrefix(revision, "ARCH-") { matches, err := filepath.Glob(filepath.Join(root, "dev", "tasks", revision+"*.json")) if err != nil || len(matches) == 0 { return fmt.Errorf("architecture task %q is missing", revision) } return nil } return fmt.Errorf("must reference an accepted ADR-NNNN or architecture task") } func validateScopeOverlap(allowed, forbidden []string) error { allowedPatterns, err := compileScopePatterns(allowed) if err != nil { return err } forbiddenPatterns, err := compileScopePatterns(forbidden) if err != nil { return fmt.Errorf("forbidden_change_scope: %w", err) } for _, a := range allowedPatterns { for _, f := range forbiddenPatterns { if scopePatternsOverlap(a, f) { return fmt.Errorf("allowed path %q overlaps forbidden path %q", a.raw, f.raw) } } } return nil } func scopePatternsOverlap(a, b scopePattern) bool { if a.subtree != "" && b.subtree != "" { return a.subtree == b.subtree || strings.HasPrefix(a.subtree, b.subtree+"/") || strings.HasPrefix(b.subtree, a.subtree+"/") } if a.subtree != "" { return b.raw == a.subtree || strings.HasPrefix(b.raw, a.subtree+"/") } if b.subtree != "" { return a.raw == b.subtree || strings.HasPrefix(a.raw, b.subtree+"/") } return a.raw == b.raw } func mustCompile(values []string) []scopePattern { patterns, err := compileScopePatterns(values) if err != nil { panic(err) } return patterns }