package main import ( "encoding/json" "fmt" "go/parser" "go/token" "io/fs" "os" "os/exec" "path/filepath" "strings" ) type Registry struct { RegistryVersion int `json:"registry_version"` SemanticGeneration int `json:"semantic_generation"` ArchitectureGeneration int `json:"architecture_generation"` Modules map[string]struct { ID string `json:"id"` Owns []string `json:"owns"` DatabaseSchema string `json:"database_schema"` BoundaryClass string `json:"boundary_class"` MayDependOn []string `json:"may_depend_on"` Forbidden []string `json:"forbidden"` Privileged bool `json:"privileged"` } `json:"modules"` ProtectedPaths []string `json:"protected_paths"` } type TaskPacket struct { TaskID string `json:"task_id"` TaskRevision int `json:"task_revision"` ArchitectureRevision string `json:"architecture_revision"` BaseRepoRevision string `json:"base_repo_revision"` RiskTier string `json:"risk_tier"` Intent string `json:"intent"` RequirementIDs []string `json:"requirement_ids"` AllowedChangeScope struct { Modules []string `json:"modules"` Paths []string `json:"paths"` } `json:"allowed_change_scope"` ForbiddenChangeScope struct { Paths []string `json:"paths"` } `json:"forbidden_change_scope"` AllowedArchitectureDelta map[string]int `json:"allowed_architecture_delta"` Contracts struct { Consume []string `json:"consume"` MayChange []string `json:"may_change"` } `json:"contracts"` SemanticOwner string `json:"semantic_owner"` CanonicalState []string `json:"canonical_state"` StateTransitions []string `json:"state_transitions"` SideEffectsAndPrivileges []string `json:"side_effects_and_privileges"` PersistenceAndMigrations []string `json:"persistence_and_migrations"` FailureRetryRestart []string `json:"failure_retry_restart"` Assumptions []string `json:"assumptions"` AcceptanceScenarios []string `json:"acceptance_scenarios"` RequiredTests []string `json:"required_tests"` TerminalGapStates []string `json:"terminal_gap_states"` } func main() { if len(os.Args) < 2 { usage() os.Exit(2) } var err error switch os.Args[1] { case "doctor": err = doctor() case "check-architecture": err = checkArchitecture() case "show-registry": err = showRegistry() case "preflight": err = preflight(os.Args[2:]) case "check-diff": err = checkDiff(os.Args[2:]) default: usage() os.Exit(2) } if err != nil { fmt.Fprintln(os.Stderr, "devctl:", err) os.Exit(1) } } func usage() { fmt.Println("devctl doctor | check-architecture | show-registry | preflight (-active | -task ) | check-diff [-active | -task ]") } func loadRegistry() (Registry, error) { b, err := os.ReadFile("architecture/architecture-registry.json") if err != nil { return Registry{}, err } var r Registry if err = json.Unmarshal(b, &r); err != nil { return Registry{}, err } return r, nil } func doctor() error { r, err := loadRegistry() if err != nil { return err } fmt.Printf("architecture generation=%d semantic generation=%d modules=%d\n", r.ArchitectureGeneration, r.SemanticGeneration, len(r.Modules)) for _, tool := range []string{"git", "go", "docker"} { p, e := exec.LookPath(tool) if e != nil { fmt.Printf("WARN: %s not found\n", tool) } else { fmt.Printf("OK: %s -> %s\n", tool, p) } } cmd := exec.Command("git", "remote", "get-url", "origin") out, e := cmd.CombinedOutput() if e == nil { fmt.Printf("git origin: %s", out) } else { fmt.Println("WARN: git origin not configured; run scripts/bootstrap.ps1") } return nil } func showRegistry() error { r, e := loadRegistry() if e != nil { return e } b, _ := json.MarshalIndent(r, "", " ") fmt.Println(string(b)) return nil } func checkArchitecture() error { return checkArchitectureAt(".") } func checkArchitectureAt(root string) error { b, err := os.ReadFile(filepath.Join(root, "architecture", "architecture-registry.json")) if err != nil { return err } var r Registry if err := json.Unmarshal(b, &r); err != nil { return err } var violations []string err = filepath.WalkDir(filepath.Join(root, "modules"), func(path string, d fs.DirEntry, e error) error { if e != nil { return e } if d.IsDir() || !strings.HasSuffix(path, ".go") { return nil } relative, err := filepath.Rel(root, path) if err != nil { return err } parts := strings.Split(filepath.ToSlash(relative), "/") if len(parts) < 3 { return nil } owner := parts[1] module, ok := r.Modules[owner] if !ok { violations = append(violations, relative+": unknown module owner "+owner) return nil } parsed, parseErr := parser.ParseFile(token.NewFileSet(), path, nil, parser.ImportsOnly) if parseErr != nil { return parseErr } allowed := map[string]bool{} for _, dependency := range module.MayDependOn { allowed[dependency] = true } for _, imported := range parsed.Imports { importPath := strings.Trim(imported.Path.Value, "\"") if strings.Contains(filepath.ToSlash(relative), "/domain/") { for _, bad := range []string{"/adapters/", "/runtime/", "/cmd/"} { if strings.Contains(importPath, "git.cin.su/m/Assistant"+bad) { violations = append(violations, relative+": domain imports forbidden "+bad) } } } modulePrefix := "git.cin.su/m/Assistant/modules/" if strings.HasPrefix(importPath, modulePrefix) { other := strings.Split(strings.TrimPrefix(importPath, modulePrefix), "/")[0] if other != owner { violations = append(violations, relative+": cross-module implementation import "+other) } } contractPrefix := "git.cin.su/m/Assistant/contracts/" if strings.HasPrefix(importPath, contractPrefix) { contractOwner := strings.Split(strings.TrimPrefix(importPath, contractPrefix), "/")[0] if contractOwner != owner && !allowed["contracts."+contractOwner] { violations = append(violations, relative+": undeclared contract dependency contracts."+contractOwner) } } } return nil }) if err != nil { return err } if len(violations) > 0 { for _, v := range violations { fmt.Println("VIOLATION:", v) } return fmt.Errorf("%d architecture violation(s): %s", len(violations), strings.Join(violations, "; ")) } fmt.Println("architecture checks: OK") return nil } func preflight(args []string) error { selection, err := parseTaskSelection(args, false) if err != nil { return err } root, err := gitRepositoryRoot(".") if err != nil { return err } taskPacket, taskPath, _, err := loadSelectedTask(root, selection) if err != nil { return err } if err := validateTaskPacket(root, taskPacket, taskPath); err != nil { return err } registry, err := loadRegistry() if err != nil { return err } if err := validateTaskScope(taskPacket, registry); err != nil { return err } fmt.Printf("task preflight loaded: %s tier=%s\n", taskPacket.TaskID, taskPacket.RiskTier) return checkArchitecture() } func validateTaskScope(taskPacket TaskPacket, registry Registry) error { if taskPacket.RiskTier == "E" { return nil } for _, allowed := range taskPacket.AllowedChangeScope.Paths { allowed = filepath.ToSlash(allowed) for _, protected := range registry.ProtectedPaths { prefix := strings.TrimSuffix(filepath.ToSlash(protected), "/**") if allowed == prefix || strings.HasPrefix(allowed, prefix+"/") { return fmt.Errorf("task %s tier %s may not write protected path %s", taskPacket.TaskID, taskPacket.RiskTier, allowed) } } } return nil }