// Package integrate handles source tree manipulation for self-modification // (Stage 9). Generated code is always a new file — never modify existing // files. Rollback is deletion. The organism grows by addition, then dissolves // what doesn't work. package integrate import ( "fmt" "go/ast" "go/parser" "go/token" "os" "os/exec" "path/filepath" "strings" ) // Plan describes a code integration: what file to add and where. type Plan struct { SourceFile string // target path relative to project root (e.g., "lattice/count_nodes.go") GoSource string // the Go source code to write Package string // package name (extracted from source) Description string // English description for audit trail } // VerifyResult captures the outcome of a verification pass. type VerifyResult struct { Compiles bool // go build ./... succeeded TestsPass bool // go test ./... succeeded Error string // first error encountered } // TargetFile determines where generated code should be placed based on // its package declaration and first declared type/function name. func TargetFile(goSource, projectRoot string) string { fset := token.NewFileSet() f, err := parser.ParseFile(fset, "", goSource, parser.SkipObjectResolution) if err != nil { return filepath.Join(projectRoot, "generated.go") } pkgName := "main" if f.Name != nil { pkgName = f.Name.Name } // Find a directory that matches the package name. dir := pkgName if pkgName == "main" { dir = filepath.Join("cmd", "dendrite") } // Use the first declared type or function name for the filename. baseName := "generated" for _, d := range f.Decls { switch decl := d.(type) { case *ast.FuncDecl: baseName = toSnake(decl.Name.Name) goto named case *ast.GenDecl: for _, spec := range decl.Specs { if ts, ok := spec.(*ast.TypeSpec); ok { baseName = toSnake(ts.Name.Name) goto named } } } } named: return filepath.Join(projectRoot, dir, baseName+".go") } // Verify checks that integration would succeed by building in a temp copy. // Does not modify the actual project. func Verify(plan Plan, projectRoot, goRoot string) *VerifyResult { result := &VerifyResult{} // Create a temp copy of the project. tmpDir, err := os.MkdirTemp("", "integrate-verify-*") if err != nil { result.Error = fmt.Sprintf("create temp dir: %v", err) return result } defer os.RemoveAll(tmpDir) // Copy the project to temp. if err := copyDir(projectRoot, tmpDir); err != nil { result.Error = fmt.Sprintf("copy project: %v", err) return result } // Write the new file in the temp copy. targetPath := plan.SourceFile if filepath.IsAbs(targetPath) { // Make relative. rel, err := filepath.Rel(projectRoot, targetPath) if err == nil { targetPath = rel } } fullPath := filepath.Join(tmpDir, targetPath) os.MkdirAll(filepath.Dir(fullPath), 0o755) if err := os.WriteFile(fullPath, []byte(plan.GoSource), 0o644); err != nil { result.Error = fmt.Sprintf("write file: %v", err) return result } // Build. goBin := filepath.Join(goRoot, "bin", "go") buildCmd := exec.Command(goBin, "build", "./...") buildCmd.Dir = tmpDir buildCmd.Env = cleanGoEnv(goRoot) if out, err := buildCmd.CombinedOutput(); err != nil { result.Error = fmt.Sprintf("build: %s", string(out)) return result } result.Compiles = true // Test. testCmd := exec.Command(goBin, "test", "./...") testCmd.Dir = tmpDir testCmd.Env = cleanGoEnv(goRoot) if out, err := testCmd.CombinedOutput(); err != nil { result.Error = fmt.Sprintf("test: %s", string(out)) // Build passed but tests failed. return result } result.TestsPass = true return result } // Apply writes the new file to the actual project and verifies it builds. // If the build fails, the file is removed (rollback). func Apply(plan Plan, projectRoot, goRoot string) error { fullPath := plan.SourceFile if !filepath.IsAbs(fullPath) { fullPath = filepath.Join(projectRoot, fullPath) } // Ensure the directory exists. os.MkdirAll(filepath.Dir(fullPath), 0o755) // Write the file. if err := os.WriteFile(fullPath, []byte(plan.GoSource), 0o644); err != nil { return fmt.Errorf("write %s: %w", fullPath, err) } // Verify build. goBin := filepath.Join(goRoot, "bin", "go") cmd := exec.Command(goBin, "build", "./...") cmd.Dir = projectRoot cmd.Env = cleanGoEnv(goRoot) if out, err := cmd.CombinedOutput(); err != nil { // Rollback. os.Remove(fullPath) return fmt.Errorf("build failed (rolled back): %s", string(out)) } return nil } // Rollback removes an integrated file. func Rollback(plan Plan, projectRoot string) error { fullPath := plan.SourceFile if !filepath.IsAbs(fullPath) { fullPath = filepath.Join(projectRoot, fullPath) } return os.Remove(fullPath) } // copyDir copies a Go project directory, skipping .git, _output, vendor, // and other non-essential dirs to keep it fast. func copyDir(src, dst string) error { return filepath.Walk(src, func(path string, info os.FileInfo, err error) error { if err != nil { return err } rel, err := filepath.Rel(src, path) if err != nil { return err } // Skip directories that aren't needed for build. if info.IsDir() { base := filepath.Base(path) switch base { case ".git", "_output", "node_modules", "vendor", ".svelte-kit", ".next", "dist", "build", "coverage": return filepath.SkipDir } return os.MkdirAll(filepath.Join(dst, rel), info.Mode()) } // Only copy Go source and module files. ext := filepath.Ext(path) if ext != ".go" && ext != ".mod" && ext != ".sum" { return nil } data, err := os.ReadFile(path) if err != nil { return err } return os.WriteFile(filepath.Join(dst, rel), data, info.Mode()) }) } // cleanGoEnv returns an environment with GOROOT, PATH, and GOTOOLCHAIN set. func cleanGoEnv(root string) []string { env := os.Environ() clean := make([]string, 0, len(env)+3) for _, e := range env { if strings.HasPrefix(e, "GOROOT=") || strings.HasPrefix(e, "GOTOOLCHAIN=") || strings.HasPrefix(e, "PATH=") { continue } clean = append(clean, e) } clean = append(clean, "GOROOT="+root, "GOTOOLCHAIN=local", "PATH="+filepath.Join(root, "bin")+":"+os.Getenv("PATH"), ) return clean } // toSnake converts a CamelCase name to snake_case for filenames. func toSnake(s string) string { var b strings.Builder for i, r := range s { if r >= 'A' && r <= 'Z' { if i > 0 { b.WriteByte('_') } b.WriteRune(r + ('a' - 'A')) } else { b.WriteRune(r) } } return b.String() }