describe.go raw

   1  // Package describe implements the English→Go code generation pipeline.
   2  //
   3  // This package provides structural utilities: extracting Go source from
   4  // markdown, extracting self-knowledge from a lattice, composing code queries,
   5  // and evaluating generated source against descriptions. The actual code
   6  // generation (formerly via LLM oracle) has been removed — the organism
   7  // relies on its own deterministic lattice dynamics for growth.
   8  package describe
   9  
  10  import (
  11  	"fmt"
  12  	"go/ast"
  13  	"go/parser"
  14  	"go/token"
  15  	"os"
  16  	"path/filepath"
  17  	"regexp"
  18  	"strings"
  19  
  20  	"os/exec"
  21  
  22  	"git.mleku.dev/mleku/dendrite/pkg/emit"
  23  	"git.mleku.dev/mleku/dendrite/pkg/lattice"
  24  	"git.mleku.dev/mleku/dendrite/pkg/ratio"
  25  )
  26  
  27  // Description is an English description of desired Go code.
  28  type Description struct {
  29  	Text       string   // what the code should do
  30  	TargetPkg  string   // target package (empty = inferred from oracle response)
  31  	TestInputs []string // optional: expected behaviors to verify
  32  }
  33  
  34  // Result captures the output of description-driven code generation.
  35  type Result struct {
  36  	Description  Description
  37  	GoSource     string   // the produced Go code
  38  	Compiles     bool     // whether it compiled successfully
  39  	CompileError string   // compiler output on failure
  40  	Declarations []string // what was declared (e.g., "func:CountNodes", "type:Summary")
  41  	BondRatio    ratio.Ratio // how well the oracle response aligned with the self-lattice
  42  	Fitness      ratio.Ratio // overall description fitness score
  43  }
  44  
  45  // ExtractGoSource finds Go source code inside a markdown-formatted oracle
  46  // response. It looks for content between ```go and ``` markers. If multiple
  47  // code blocks exist, returns the longest one. Falls back to the entire
  48  // response if no code blocks are found.
  49  func ExtractGoSource(answer string) string {
  50  	// Match ```go ... ``` blocks.
  51  	re := regexp.MustCompile("(?s)```go\\s*\n(.*?)```")
  52  	matches := re.FindAllStringSubmatch(answer, -1)
  53  
  54  	if len(matches) == 0 {
  55  		// Fallback: try plain ``` blocks.
  56  		re = regexp.MustCompile("(?s)```\\s*\n(.*?)```")
  57  		matches = re.FindAllStringSubmatch(answer, -1)
  58  	}
  59  
  60  	if len(matches) == 0 {
  61  		// No code blocks at all. If the response starts with "package",
  62  		// treat the whole thing as source.
  63  		trimmed := strings.TrimSpace(answer)
  64  		if strings.HasPrefix(trimmed, "package ") {
  65  			return trimmed
  66  		}
  67  		return ""
  68  	}
  69  
  70  	// Return the longest code block (most likely the complete source).
  71  	best := ""
  72  	for _, m := range matches {
  73  		if len(m) > 1 && len(m[1]) > len(best) {
  74  			best = m[1]
  75  		}
  76  	}
  77  
  78  	return strings.TrimSpace(best)
  79  }
  80  
  81  // ExtractSelfKnowledge reads the lattice's bonded elements and produces
  82  // a compact summary of the organism's current structure. This summary is
  83  // sent to the oracle as context so it can match existing patterns.
  84  func ExtractSelfKnowledge(l *lattice.Lattice) string {
  85  	if l == nil {
  86  		return ""
  87  	}
  88  
  89  	files := emit.Harvest(l)
  90  
  91  	// Collect unique declarations across all file groups.
  92  	types := make(map[string]bool)
  93  	funcs := make(map[string]bool)
  94  	methods := make(map[string]bool)
  95  	imports := make(map[string]bool)
  96  	var pkgName string
  97  
  98  	for _, fragments := range files {
  99  		for _, f := range fragments {
 100  			switch f.Type {
 101  			case "type":
 102  				if f.Value != "" {
 103  					types[f.Value] = true
 104  				}
 105  			case "func":
 106  				if f.Value != "" {
 107  					funcs[f.Value] = true
 108  				}
 109  			case "method":
 110  				if f.Value != "" {
 111  					methods[f.Value] = true
 112  				}
 113  			case "import":
 114  				if f.Value != "" {
 115  					imports[f.Value] = true
 116  				}
 117  			case "package":
 118  				if f.Value != "" {
 119  					pkgName = f.Value
 120  				}
 121  			}
 122  		}
 123  	}
 124  
 125  	var b strings.Builder
 126  
 127  	if pkgName != "" {
 128  		fmt.Fprintf(&b, "Package: %s\n", pkgName)
 129  	}
 130  	// Cap each category to avoid overwhelming the oracle with context.
 131  	const maxPerCategory = 10
 132  	if len(types) > 0 {
 133  		fmt.Fprintf(&b, "Types: %s\n", joinKeysLimited(types, maxPerCategory))
 134  	}
 135  	if len(funcs) > 0 {
 136  		fmt.Fprintf(&b, "Functions: %s\n", joinKeysLimited(funcs, maxPerCategory))
 137  	}
 138  	if len(methods) > 0 {
 139  		fmt.Fprintf(&b, "Methods: %s\n", joinKeysLimited(methods, maxPerCategory))
 140  	}
 141  	if len(imports) > 0 {
 142  		fmt.Fprintf(&b, "Imports: %s\n", joinKeysLimited(imports, maxPerCategory))
 143  	}
 144  
 145  	return b.String()
 146  }
 147  
 148  // ComposeCodeQuery builds an oracle prompt from a description and
 149  // the organism's self-knowledge.
 150  func ComposeCodeQuery(desc Description, selfKnowledge string) string {
 151  	var b strings.Builder
 152  
 153  	if selfKnowledge != "" {
 154  		b.WriteString("Context (for reference only — do NOT include these in your output):\n")
 155  		b.WriteString("The codebase already has these declarations:\n")
 156  		b.WriteString(selfKnowledge)
 157  		b.WriteString("\n")
 158  	}
 159  
 160  	b.WriteString("Generate ONLY the following:\n\n")
 161  	b.WriteString(desc.Text)
 162  	b.WriteString("\n\nRequirements:\n")
 163  	b.WriteString("- The code MUST compile as a standalone file\n")
 164  	b.WriteString("- Generate ONLY the requested function/type — do NOT reproduce or stub existing declarations\n")
 165  	b.WriteString("- Use only standard library imports unless the description specifies otherwise\n")
 166  	if desc.TargetPkg != "" {
 167  		fmt.Fprintf(&b, "- Use package %s\n", desc.TargetPkg)
 168  	} else {
 169  		b.WriteString("- Use package main if no package is specified in the description\n")
 170  	}
 171  	b.WriteString("- Include all necessary imports\n")
 172  	b.WriteString("- Return the complete Go source inside a single ```go code block\n")
 173  
 174  	return b.String()
 175  }
 176  
 177  
 178  // EvaluateDescription computes a fitness score for how well generated code
 179  // fulfills a description.
 180  //
 181  // Weights:
 182  //   - 0.50: compiles (binary pass/fail)
 183  //   - 0.30: declares expected types/functions from the description
 184  //   - 0.20: bond ratio (alignment with existing organism structure)
 185  func EvaluateDescription(goSource string, desc Description, bondRatio ratio.Ratio, goRoot string) ratio.Ratio {
 186  	score := ratio.Zero
 187  
 188  	// Compile check — vet (type-check), not build.
 189  	tmpDir, err := os.MkdirTemp("", "describe-fitness-*")
 190  	if err != nil {
 191  		return ratio.New(1, 5).Mul(bondRatio)
 192  	}
 193  	defer os.RemoveAll(tmpDir)
 194  
 195  	if vetCheck(goSource, tmpDir, goRoot) == nil {
 196  		score = score.Add(ratio.Half)
 197  	}
 198  
 199  	// Declaration match.
 200  	expected := extractExpectedNames(desc.Text)
 201  	if len(expected) > 0 {
 202  		actual := extractDeclarations(goSource)
 203  		actualSet := make(map[string]bool)
 204  		for _, d := range actual {
 205  			// Match both full ("func:CountNodes") and name-only ("CountNodes").
 206  			actualSet[d] = true
 207  			parts := strings.SplitN(d, ":", 2)
 208  			if len(parts) == 2 {
 209  				actualSet[parts[1]] = true
 210  			}
 211  		}
 212  		matches := 0
 213  		for _, name := range expected {
 214  			if actualSet[name] {
 215  				matches++
 216  			}
 217  		}
 218  		score = score.Add(ratio.New(3, 10).Mul(ratio.New(int64(matches), int64(len(expected)))))
 219  	} else {
 220  		// No expected names found in description — give partial credit if it compiles.
 221  		score = score.Add(ratio.New(3, 20))
 222  	}
 223  
 224  	// Bond ratio.
 225  	score = score.Add(ratio.New(1, 5).Mul(bondRatio))
 226  
 227  	return score
 228  }
 229  
 230  // extractDeclarations parses Go source and returns declared names.
 231  func extractDeclarations(src string) []string {
 232  	fset := token.NewFileSet()
 233  	f, err := parser.ParseFile(fset, "", src, parser.SkipObjectResolution)
 234  	if err != nil {
 235  		return nil
 236  	}
 237  
 238  	var decls []string
 239  	if f.Name != nil {
 240  		decls = append(decls, "package:"+f.Name.Name)
 241  	}
 242  	for _, d := range f.Decls {
 243  		switch decl := d.(type) {
 244  		case *ast.FuncDecl:
 245  			if decl.Recv != nil && len(decl.Recv.List) > 0 {
 246  				decls = append(decls, "method:"+decl.Name.Name)
 247  			} else {
 248  				decls = append(decls, "func:"+decl.Name.Name)
 249  			}
 250  		case *ast.GenDecl:
 251  			for _, spec := range decl.Specs {
 252  				if ts, ok := spec.(*ast.TypeSpec); ok {
 253  					decls = append(decls, "type:"+ts.Name.Name)
 254  				}
 255  			}
 256  		}
 257  	}
 258  	return decls
 259  }
 260  
 261  // extractExpectedNames looks for function and type names mentioned
 262  // in a description. Patterns like "function named X", "type called Y",
 263  // "function X", "type Y" are recognized.
 264  func extractExpectedNames(desc string) []string {
 265  	var names []string
 266  	seen := make(map[string]bool)
 267  
 268  	patterns := []*regexp.Regexp{
 269  		regexp.MustCompile(`(?i)function\s+(?:named|called)\s+(\w+)`),
 270  		regexp.MustCompile(`(?i)func(?:tion)?\s+(\w+)\s+that`),
 271  		regexp.MustCompile(`(?i)(?:a\s+)?type\s+(?:named|called|for)?\s*(\w+)`),
 272  		regexp.MustCompile(`(?i)method\s+(?:named|called)\s+(\w+)`),
 273  		regexp.MustCompile(`(?i)struct\s+(?:named|called)\s+(\w+)`),
 274  	}
 275  
 276  	for _, p := range patterns {
 277  		for _, m := range p.FindAllStringSubmatch(desc, -1) {
 278  			if len(m) > 1 {
 279  				name := m[1]
 280  				if !seen[name] {
 281  					seen[name] = true
 282  					names = append(names, name)
 283  				}
 284  			}
 285  		}
 286  	}
 287  
 288  	return names
 289  }
 290  
 291  // vetCheck writes Go source to a temp module and runs `go vet` to verify it
 292  // type-checks. This doesn't require func main() — it just checks that the code
 293  // is valid Go. tmpDir must exist; a subdirectory is created inside it.
 294  func vetCheck(goSource, tmpDir, goRoot string) error {
 295  	modDir := filepath.Join(tmpDir, "vetmod")
 296  	os.MkdirAll(modDir, 0o755)
 297  
 298  	// Write go.mod.
 299  	os.WriteFile(filepath.Join(modDir, "go.mod"), []byte("module vetcheck\n\ngo 1.24\n"), 0o644)
 300  
 301  	// Write source file.
 302  	if err := os.WriteFile(filepath.Join(modDir, "code.go"), []byte(goSource), 0o644); err != nil {
 303  		return err
 304  	}
 305  
 306  	goBin := filepath.Join(goRoot, "bin", "go")
 307  	cmd := exec.Command(goBin, "vet", "./...")
 308  	cmd.Dir = modDir
 309  	cmd.Env = cleanGoEnv(goRoot)
 310  	out, err := cmd.CombinedOutput()
 311  	if err != nil {
 312  		return fmt.Errorf("%s", string(out))
 313  	}
 314  	return nil
 315  }
 316  
 317  // cleanGoEnv returns an environment with GOROOT, PATH, and GOTOOLCHAIN set.
 318  func cleanGoEnv(root string) []string {
 319  	env := os.Environ()
 320  	clean := make([]string, 0, len(env)+3)
 321  	for _, e := range env {
 322  		if strings.HasPrefix(e, "GOROOT=") ||
 323  			strings.HasPrefix(e, "GOTOOLCHAIN=") ||
 324  			strings.HasPrefix(e, "PATH=") {
 325  			continue
 326  		}
 327  		clean = append(clean, e)
 328  	}
 329  	clean = append(clean,
 330  		"GOROOT="+root,
 331  		"GOTOOLCHAIN=local",
 332  		"PATH="+filepath.Join(root, "bin")+":"+os.Getenv("PATH"),
 333  	)
 334  	return clean
 335  }
 336  
 337  // joinKeys returns sorted, comma-separated keys from a string-bool map.
 338  func joinKeys(m map[string]bool) string {
 339  	return joinKeysLimited(m, 0)
 340  }
 341  
 342  // joinKeysLimited returns sorted, comma-separated keys from a string-bool map.
 343  // If limit > 0 and there are more keys, it truncates and appends "... (N more)".
 344  func joinKeysLimited(m map[string]bool, limit int) string {
 345  	keys := make([]string, 0, len(m))
 346  	for k := range m {
 347  		keys = append(keys, k)
 348  	}
 349  	// Simple sort for deterministic output.
 350  	for i := range keys {
 351  		for j := i + 1; j < len(keys); j++ {
 352  			if keys[j] < keys[i] {
 353  				keys[i], keys[j] = keys[j], keys[i]
 354  			}
 355  		}
 356  	}
 357  	if limit > 0 && len(keys) > limit {
 358  		return strings.Join(keys[:limit], ", ") + fmt.Sprintf(" ... (%d more)", len(keys)-limit)
 359  	}
 360  	return strings.Join(keys, ", ")
 361  }
 362