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