main.go raw

   1  // Command mindsicle is the bootstrapper — thaws a frozen lattice, emits Go
   2  // source, compiles, and runs the result.
   3  //
   4  // Usage:
   5  //
   6  //	mindsicle frozen.json              thaw → emit → repair → compile → run
   7  //	mindsicle -emit frozen.json        thaw → emit only (no compile/run)
   8  //	mindsicle -out DIR frozen.json     set output directory
   9  //	mindsicle -auto -repo .            autonomous mode: iterate until walk exhausts repo
  10  package main
  11  
  12  import (
  13  	"bytes"
  14  	"context"
  15  	"flag"
  16  	"fmt"
  17  	"os"
  18  	"os/exec"
  19  	"os/signal"
  20  	"path/filepath"
  21  	"strings"
  22  	"time"
  23  
  24  	"git.mleku.dev/mleku/dendrite/pkg/axiom"
  25  	"git.mleku.dev/mleku/dendrite/pkg/emit"
  26  	"git.mleku.dev/mleku/dendrite/pkg/enzyme"
  27  	"git.mleku.dev/mleku/dendrite/pkg/ewma"
  28  	"git.mleku.dev/mleku/dendrite/pkg/fitness"
  29  	"git.mleku.dev/mleku/dendrite/pkg/grow"
  30  	"git.mleku.dev/mleku/dendrite/pkg/hexagram"
  31  	"git.mleku.dev/mleku/dendrite/pkg/lattice"
  32  	"git.mleku.dev/mleku/dendrite/pkg/memory"
  33  	"git.mleku.dev/mleku/dendrite/pkg/mindsicle"
  34  	"git.mleku.dev/mleku/dendrite/pkg/ratio"
  35  	"git.mleku.dev/mleku/dendrite/pkg/spore"
  36  	"git.mleku.dev/mleku/dendrite/pkg/walk"
  37  )
  38  
  39  // tagConstraint is the simplest constraint: admits elements with matching type.
  40  type tagConstraint struct{ tag string }
  41  
  42  func (c tagConstraint) Tag() string                { return c.tag }
  43  func (c tagConstraint) Admits(e axiom.Element) bool { return e.Type() == c.tag }
  44  
  45  func constraintFactory(tag string) axiom.Constraint { return tagConstraint{tag} }
  46  
  47  var goRoot string
  48  
  49  func init() {
  50  	goRoot = os.Getenv("GOROOT")
  51  	if goRoot == "" {
  52  		goRoot = "/home/mleku/go"
  53  	}
  54  }
  55  
  56  func main() {
  57  	// Shared flags.
  58  	outDir := flag.String("out", "_output", "output directory")
  59  
  60  	// Bootstrap mode flags.
  61  	emitOnly := flag.Bool("emit", false, "emit Go source without compiling or running")
  62  	maxPasses := flag.Int("passes", 5, "max compile-repair passes")
  63  
  64  	// Autonomous mode flags.
  65  	autoMode := flag.Bool("auto", false, "autonomous mode: iterate until ergodic walk exhausts repo")
  66  	repoDir := flag.String("repo", ".", "repository root for ergodic walk")
  67  	ewmaWindow := flag.Int("ewma-window", 10, "EWMA smoothing window")
  68  	crossings := flag.Int("crossings", 6, "reversal threshold for oscillation detection")
  69  	seed := flag.Uint64("seed", 0, "PRNG seed for ergodic walk (0 = time-based)")
  70  	memDir := flag.String("memory-dir", "", "persistent memory database (default: <out>/memory)")
  71  	feedPct := flag.Int("feed-pct", 50, "feed-back threshold percentage")
  72  	maxEpochs := flag.Int("max-epochs", 0, "maximum epochs (0 = unlimited)")
  73  
  74  	flag.Parse()
  75  
  76  	if *autoMode {
  77  		runAuto(*repoDir, *outDir, *memDir, *ewmaWindow, *crossings, *seed, *feedPct, *maxEpochs)
  78  		return
  79  	}
  80  
  81  	// --- Bootstrap mode ---
  82  	args := flag.Args()
  83  	if len(args) < 1 {
  84  		fmt.Fprintf(os.Stderr, "usage: mindsicle [flags] frozen.json\n")
  85  		fmt.Fprintf(os.Stderr, "       mindsicle -auto -repo .\n")
  86  		os.Exit(1)
  87  	}
  88  	runBootstrap(args[0], *outDir, *emitOnly, *maxPasses)
  89  }
  90  
  91  func runBootstrap(inputFile, outDir string, emitOnly bool, maxPasses int) {
  92  	// Read the mindsicle.
  93  	f, err := os.Open(inputFile)
  94  	if err != nil {
  95  		fmt.Fprintf(os.Stderr, "open: %v\n", err)
  96  		os.Exit(1)
  97  	}
  98  	m, err := mindsicle.ReadMindsicle(f)
  99  	f.Close()
 100  	if err != nil {
 101  		fmt.Fprintf(os.Stderr, "read mindsicle: %v\n", err)
 102  		os.Exit(1)
 103  	}
 104  	fmt.Printf("mindsicle: %d nodes, version %d, frozen at %s\n",
 105  		len(m.Nodes), m.Version, m.FrozenAt.Format("2006-01-02 15:04:05"))
 106  
 107  	// Thaw.
 108  	l := m.Thaw(constraintFactory)
 109  	fmt.Printf("thaw: %d nodes live\n", l.Size())
 110  
 111  	// Harvest and emit.
 112  	files := emit.Harvest(l)
 113  	var allFrags []emit.Fragment
 114  	for _, frags := range files {
 115  		allFrags = append(allFrags, frags...)
 116  	}
 117  	fmt.Printf("harvest: %d fragments\n", len(allFrags))
 118  
 119  	var source strings.Builder
 120  	if err := emit.EmitGo(allFrags, &source); err != nil {
 121  		fmt.Fprintf(os.Stderr, "emit: %v\n", err)
 122  		os.Exit(1)
 123  	}
 124  
 125  	os.MkdirAll(outDir, 0o755)
 126  
 127  	base := filepath.Base(inputFile)
 128  	base = strings.TrimSuffix(base, filepath.Ext(base))
 129  	goFile := filepath.Join(outDir, base+".go")
 130  
 131  	if emitOnly {
 132  		if err := os.WriteFile(goFile, []byte(source.String()), 0o644); err != nil {
 133  			fmt.Fprintf(os.Stderr, "write: %v\n", err)
 134  			os.Exit(1)
 135  		}
 136  		fmt.Printf("emit: %s (%d bytes)\n", goFile, source.Len())
 137  		return
 138  	}
 139  
 140  	repaired, err := emit.CompileAndRepair(source.String(), goRoot, maxPasses)
 141  	if err != nil {
 142  		fmt.Fprintf(os.Stderr, "repair: %v\n", err)
 143  		os.WriteFile(goFile, []byte(source.String()), 0o644)
 144  		fmt.Fprintf(os.Stderr, "unrepaired source written to %s\n", goFile)
 145  		os.Exit(1)
 146  	}
 147  
 148  	if err := os.WriteFile(goFile, []byte(repaired), 0o644); err != nil {
 149  		fmt.Fprintf(os.Stderr, "write: %v\n", err)
 150  		os.Exit(1)
 151  	}
 152  	fmt.Printf("repair: %s (%d bytes)\n", goFile, len(repaired))
 153  
 154  	binFile := filepath.Join(outDir, base)
 155  	if err := fitness.CompileTo(goFile, binFile, goRoot); err != nil {
 156  		fmt.Fprintf(os.Stderr, "compile: %v\n", err)
 157  		os.Exit(1)
 158  	}
 159  	fmt.Printf("compile: %s\n", binFile)
 160  
 161  	fmt.Println("--- running offspring ---")
 162  	cmd := exec.Command(binFile)
 163  	cmd.Stdout = os.Stdout
 164  	cmd.Stderr = os.Stderr
 165  	if err := cmd.Run(); err != nil {
 166  		fmt.Fprintf(os.Stderr, "run: %v\n", err)
 167  		os.Exit(1)
 168  	}
 169  	fmt.Println("--- offspring done ---")
 170  }
 171  
 172  // runAuto implements the autonomous driver loop.
 173  func runAuto(repoDir, outDir, memDir string, ewmaWindow, crossingThreshold int, prngSeed uint64, feedPct, maxEpochs int) {
 174  	if memDir == "" {
 175  		memDir = filepath.Join(outDir, "memory")
 176  	}
 177  	os.MkdirAll(outDir, 0o755)
 178  	os.MkdirAll(memDir, 0o755)
 179  
 180  	// Open persistent memory.
 181  	mem, err := memory.Open(memDir)
 182  	if err != nil {
 183  		fmt.Fprintf(os.Stderr, "memory open: %v\n", err)
 184  		os.Exit(1)
 185  	}
 186  	defer mem.Close()
 187  
 188  	// PRNG seed.
 189  	if prngSeed == 0 {
 190  		prngSeed = uint64(time.Now().UnixNano())
 191  	}
 192  	fmt.Printf("seed: %d\n", prngSeed)
 193  
 194  	// Try resuming walker state from checkpoint.
 195  	var walker *walk.Walker
 196  	var epochNum uint32
 197  	var genNum uint32
 198  	currentSeed := prngSeed
 199  
 200  	if cp, err := mem.LoadWalkerCheckpoint(); err == nil {
 201  		manifest := &walk.Manifest{Files: cp.Files, Root: cp.Root, Seed: cp.Seed}
 202  		walker = walk.Resume(manifest, cp.Position)
 203  		epochNum = cp.Epoch
 204  		genNum = cp.GenNum
 205  		currentSeed = cp.Seed
 206  		fmt.Printf("resume: epoch %d, position %d/%d, gen %d\n",
 207  			epochNum, cp.Position, len(cp.Files), genNum)
 208  	} else {
 209  		// Build ergodic walk manifest.
 210  		manifest, err := walk.Build(repoDir, prngSeed, walk.DefaultExclude)
 211  		if err != nil {
 212  			fmt.Fprintf(os.Stderr, "walk build: %v\n", err)
 213  			os.Exit(1)
 214  		}
 215  		fmt.Printf("manifest: %d files in %s\n", len(manifest.Files), repoDir)
 216  		if len(manifest.Files) == 0 {
 217  			fmt.Fprintf(os.Stderr, "no source files found\n")
 218  			os.Exit(1)
 219  		}
 220  		walker = walk.NewWalker(manifest)
 221  	}
 222  
 223  	// Try to resume lattice state.
 224  	var l *lattice.Lattice
 225  
 226  	// Check for existing mindsicle in args or memory.
 227  	args := flag.Args()
 228  	if len(args) > 0 {
 229  		// Thaw from provided mindsicle file.
 230  		f, err := os.Open(args[0])
 231  		if err != nil {
 232  			fmt.Fprintf(os.Stderr, "open mindsicle: %v\n", err)
 233  			os.Exit(1)
 234  		}
 235  		m, err := mindsicle.ReadMindsicle(f)
 236  		f.Close()
 237  		if err != nil {
 238  			fmt.Fprintf(os.Stderr, "read mindsicle: %v\n", err)
 239  			os.Exit(1)
 240  		}
 241  		l = m.Thaw(constraintFactory)
 242  		fmt.Printf("thaw: %d nodes from %s\n", l.Size(), args[0])
 243  	} else {
 244  		// Try loading latest mindsicle from memory.
 245  		gen, data, err := mem.LatestMindsicle()
 246  		if err == nil && len(data) > 0 {
 247  			m, err := mindsicle.ReadMindsicle(bytes.NewReader(data))
 248  			if err == nil {
 249  				l = m.Thaw(constraintFactory)
 250  				fmt.Printf("thaw: %d nodes from memory gen %d\n", l.Size(), gen)
 251  			}
 252  		}
 253  	}
 254  
 255  	if l == nil {
 256  		// Abiogenesis — start with empty lattice with code-aware sites.
 257  		l = abiogenesis()
 258  		fmt.Printf("abiogenesis: %d nodes\n", l.Size())
 259  	}
 260  
 261  	// Create oscillation detector.
 262  	detector := ewma.NewDetector(ewmaWindow, 0, crossingThreshold)
 263  
 264  	// Try restoring detector state.
 265  	_, ewmaData, err := mem.LoadLatestEWMAState()
 266  	if err == nil && len(ewmaData) > 0 {
 267  		restored, err := ewma.UnmarshalDetector(ewmaData)
 268  		if err == nil {
 269  			detector = restored
 270  			fmt.Printf("ewma: restored detector state\n")
 271  		}
 272  	}
 273  
 274  	// Set up signal handler for graceful shutdown.
 275  	sigCh := make(chan os.Signal, 1)
 276  	signal.Notify(sigCh, os.Interrupt)
 277  
 278  	feedThreshold := ratio.New(int64(feedPct), 100)
 279  
 280  	// Feed first file and track which file is being processed.
 281  	solution := make(chan axiom.Element, 1024)
 282  	currentFile := feedFile(walker, solution)
 283  
 284  	// Stability-triggered feeding state.
 285  	const stabilityCooldown uint32 = 5
 286  	var lastStabilityFeedGen uint32
 287  	stabilityMinSustain := ratio.New(9, 10) // 90%
 288  	stabilityMaxYoung := ratio.New(5, 100)  // 5%
 289  
 290  	// Per-epoch generation counter for safety limit.
 291  	epochStartGen := genNum
 292  
 293  	fmt.Printf("=== autonomous loop (epoch %d) ===\n", epochNum)
 294  
 295  	for {
 296  		select {
 297  		case <-sigCh:
 298  			fmt.Println("\ninterrupt — freezing state...")
 299  			freezeState(l, genNum, outDir, mem, detector, walker, epochNum)
 300  			return
 301  		default:
 302  		}
 303  
 304  		genNum++
 305  		fmt.Printf("\n--- gen %d (epoch %d, walk: %s, %d remaining) ---\n",
 306  			genNum, epochNum, walker.Progress(), walker.Remaining())
 307  
 308  		// Run one generation. Oscillation is now endogenous — the engine
 309  		// breathes on its own schedule. The detector's state is used only
 310  		// for diagnostic logging.
 311  		rawCount, accretedCount, adsrDist := runAutoGeneration(l, solution, genNum, mem)
 312  
 313  		fmt.Printf("gen %d: raw=%d accreted=%d ratio=%.1f%%\n",
 314  			genNum, rawCount, accretedCount,
 315  			float64(accretedCount)*100/max64(float64(rawCount), 1))
 316  
 317  		// Record per-file accretion score.
 318  		if currentFile != "" && mem != nil {
 319  			mem.RecordFileScore(currentFile, rawCount, accretedCount)
 320  		}
 321  
 322  		// Feed into oscillation detector.
 323  		oscillating := detector.Observe(rawCount, accretedCount)
 324  
 325  		if oscillating {
 326  			fmt.Printf("oscillation detected (reversals=%d) — ", detector.Reversals)
 327  			detector.Reset()
 328  
 329  			// Check if we should feed lattice back into itself.
 330  			if rawCount > 0 {
 331  				accretedRatio := ratio.New(accretedCount, rawCount)
 332  				if accretedRatio.Greater(feedThreshold) || accretedRatio.Equal(feedThreshold) {
 333  					fmt.Printf("self-feeding (accreted %.0f%% >= %d%%)\n",
 334  						accretedRatio.Float64()*100, feedPct)
 335  					feedLatticeIntoItself(l, solution)
 336  				} else {
 337  					fmt.Println("below feed threshold")
 338  				}
 339  			}
 340  
 341  			// Feed next file — start new epoch if walk exhausted.
 342  			if walker.Done() {
 343  				epochNum++
 344  				if maxEpochs > 0 && epochNum >= uint32(maxEpochs) {
 345  					fmt.Println("max epochs reached")
 346  					break
 347  				}
 348  				walker, currentSeed = startNewEpoch(repoDir, currentSeed, mem)
 349  				fmt.Printf("\n=== epoch %d (%d files, seed=%d) ===\n",
 350  					epochNum, len(walker.Manifest.Files), currentSeed)
 351  				epochStartGen = genNum
 352  				detector.Reset()
 353  			}
 354  			currentFile = feedFile(walker, solution)
 355  			lastStabilityFeedGen = genNum // reset stability cooldown
 356  		} else if genNum >= lastStabilityFeedGen+stabilityCooldown {
 357  			// Stability-triggered feeding: material absorbed, ready for more.
 358  			if checkStability(adsrDist, mem, stabilityMinSustain, stabilityMaxYoung) {
 359  				fmt.Println("stability reached — feeding next file")
 360  				if walker.Done() {
 361  					epochNum++
 362  					if maxEpochs > 0 && epochNum >= uint32(maxEpochs) {
 363  						fmt.Println("max epochs reached")
 364  						break
 365  					}
 366  					walker, currentSeed = startNewEpoch(repoDir, currentSeed, mem)
 367  					fmt.Printf("\n=== epoch %d (%d files, seed=%d) ===\n",
 368  						epochNum, len(walker.Manifest.Files), currentSeed)
 369  					epochStartGen = genNum
 370  				}
 371  				currentFile = feedFile(walker, solution)
 372  				lastStabilityFeedGen = genNum
 373  				detector.Reset() // new material invalidates EWMA history
 374  			}
 375  		}
 376  
 377  		// Periodic freeze (every 10 generations).
 378  		if genNum%10 == 0 {
 379  			freezeState(l, genNum, outDir, mem, detector, walker, epochNum)
 380  		}
 381  
 382  		// Per-epoch safety limit: force epoch transition if stuck.
 383  		if walker.Done() && !oscillating {
 384  			if genNum-epochStartGen > uint32(len(walker.Manifest.Files)*20) {
 385  				epochNum++
 386  				if maxEpochs > 0 && epochNum >= uint32(maxEpochs) {
 387  					fmt.Println("max epochs reached (safety limit)")
 388  					break
 389  				}
 390  				fmt.Println("epoch safety limit — starting new epoch")
 391  				walker, currentSeed = startNewEpoch(repoDir, currentSeed, mem)
 392  				fmt.Printf("\n=== epoch %d (%d files, seed=%d) ===\n",
 393  					epochNum, len(walker.Manifest.Files), currentSeed)
 394  				epochStartGen = genNum
 395  				detector.Reset()
 396  				currentFile = feedFile(walker, solution)
 397  				lastStabilityFeedGen = genNum
 398  			}
 399  		}
 400  	}
 401  
 402  	// Final freeze.
 403  	fmt.Println("\n=== final freeze ===")
 404  	freezeState(l, genNum, outDir, mem, detector, walker, epochNum)
 405  	fmt.Printf("autonomous run complete: %d generations, %d epochs, %d/%d files in current epoch\n",
 406  		genNum, epochNum+1, walker.Position, len(walker.Manifest.Files))
 407  }
 408  
 409  // abiogenesis creates an initial lattice with code-aware constraint sites.
 410  func abiogenesis() *lattice.Lattice {
 411  	l := lattice.New()
 412  	codeTags := map[string]int{
 413  		"literal": 16, "ident": 12, "func": 12, "method": 12, "type": 12,
 414  		"field": 8, "import": 8, "package": 8,
 415  		"struct": 6, "interface": 6, "comment": 6, "file": 6,
 416  		"assign": 6, "return": 6, "if": 6, "for": 6,
 417  		"word": 16, "punct": 4,
 418  		"select": 4, "switch": 4, "go": 4, "send": 4,
 419  		"expr": 6, "defer": 4, "decl": 4, "branch": 3, "case": 4, "comm": 3,
 420  		"directive": 3, "var": 6,
 421  	}
 422  
 423  	var allNodes []*lattice.Node
 424  	for tag, count := range codeTags {
 425  		for range count {
 426  			n := l.AddNode([]axiom.Constraint{tagConstraint{tag}})
 427  			n.SetEnergy(true)
 428  			allNodes = append(allNodes, n)
 429  		}
 430  	}
 431  
 432  	// Connect in a ring with cross-links.
 433  	for i, n := range allNodes {
 434  		l.Connect(n, allNodes[(i+1)%len(allNodes)])
 435  		if i%4 == 0 && i+7 < len(allNodes) {
 436  			l.Connect(n, allNodes[i+7])
 437  		}
 438  	}
 439  
 440  	return l
 441  }
 442  
 443  // runAutoGeneration runs one generation on the live lattice.
 444  // Returns raw element count and accreted (bonded) count.
 445  // oscillating indicates whether the EWMA detector sees sustained reversals;
 446  // when true, the hexagram engine halves the sustain threshold so more
 447  // Sustain-phase nodes destabilize into Release.
 448  func runAutoGeneration(l *lattice.Lattice, solution chan axiom.Element, genNum uint32, mem *memory.DB) (rawCount, accretedCount int64, adsrDist [4]int) {
 449  	growDuration := 3 * time.Second
 450  	engineDuration := 2 * time.Second
 451  
 452  	// === GROWTH ===
 453  	ctx, cancel := context.WithTimeout(context.Background(), growDuration)
 454  	defer cancel()
 455  
 456  	growEvents := make(chan grow.Event, 512)
 457  	go func() {
 458  		grow.Run(ctx, l, solution, grow.Config{
 459  			MaxSteps: 500,
 460  			Workers:  4,
 461  		}, growEvents)
 462  		close(growEvents)
 463  	}()
 464  
 465  	var bondRecords []memory.BondRecord
 466  	for ev := range growEvents {
 467  		rawCount++ // every event is one element attempt
 468  		switch ev.Type {
 469  		case grow.EventBonded:
 470  			accretedCount++
 471  			if mem != nil && ev.Element != nil {
 472  				bondRecords = append(bondRecords, memory.BondRecord{
 473  					Tag:    ev.Element.Type(),
 474  					SiteID: uint32(ev.NodeID),
 475  				})
 476  			}
 477  		}
 478  	}
 479  
 480  	// === SELF-GOVERNANCE ===
 481  	engineCtx, engineCancel := context.WithTimeout(context.Background(), engineDuration)
 482  	defer engineCancel()
 483  
 484  	engineEvents := make(chan hexagram.Event, 512)
 485  	go func() {
 486  		hexagram.RunEngine(engineCtx, l, hexagram.EngineConfig{
 487  			Interval:         16 * time.Millisecond, // 2^4 ms — epoch-aligns with dissolve at 10^2 ms
 488  			Solution:         solution,
 489  			MaxNewSites:      32,
 490  			MinOccupancy:     ratio.New(2, 5),
 491  			SustainThreshold: ratio.New(4, 10),
 492  			Oscillating:      false,
 493  		}, engineEvents)
 494  		close(engineEvents)
 495  	}()
 496  
 497  	opCounts := make(map[hexagram.Op]int)
 498  	for ev := range engineEvents {
 499  		opCounts[ev.Op]++
 500  		if ev.Op == hexagram.OpAccrete {
 501  			accretedCount++
 502  		}
 503  	}
 504  
 505  	// Report engine ops.
 506  	opNames := map[hexagram.Op]string{
 507  		hexagram.OpNone: "none", hexagram.OpAccrete: "accrete",
 508  		hexagram.OpDissolve: "dissolve", hexagram.OpNucleate: "nucleate",
 509  		hexagram.OpPrune: "prune", hexagram.OpStrengthen: "strengthen",
 510  		hexagram.OpExplore: "explore", hexagram.OpCollapse: "collapse",
 511  		hexagram.OpRecycle: "recycle",
 512  	}
 513  	fmt.Print("  engine: ")
 514  	for op, count := range opCounts {
 515  		name := opNames[op]
 516  		if name == "" {
 517  			name = fmt.Sprintf("op_%d", op)
 518  		}
 519  		fmt.Printf("%s=%d ", name, count)
 520  	}
 521  	fmt.Println()
 522  
 523  	// Record to memory.
 524  	if mem != nil {
 525  		mem.RecordBonds(genNum, bondRecords)
 526  		hexOps := make(map[byte]uint32, len(opCounts))
 527  		for op, count := range opCounts {
 528  			hexOps[byte(op)] = uint32(count)
 529  		}
 530  		mem.RecordHexagramOps(genNum, hexOps)
 531  
 532  		// Health snapshot.
 533  		occupied := 0
 534  		total := l.Size()
 535  		for i := range total {
 536  			if l.Node(lattice.NodeID(i)).Occupied() {
 537  				occupied++
 538  			}
 539  		}
 540  		mem.RecordHealth(genNum, uint32(occupied), uint32(total), ratio.Zero)
 541  	}
 542  
 543  	// Report ADSR phase distribution.
 544  	total := l.Size()
 545  	for i := range total {
 546  		n := l.Node(lattice.NodeID(i))
 547  		if n.Occupied() {
 548  			age := n.Age()
 549  			if age < 4 {
 550  				adsrDist[age]++
 551  			}
 552  		}
 553  	}
 554  	fmt.Printf("  adsr: A=%d D=%d S=%d R=%d\n",
 555  		adsrDist[0], adsrDist[1], adsrDist[2], adsrDist[3])
 556  
 557  	// Record ADSR to memory.
 558  	if mem != nil {
 559  		var u32 [4]uint32
 560  		for i := range 4 {
 561  			u32[i] = uint32(adsrDist[i])
 562  		}
 563  		mem.RecordADSR(genNum, u32)
 564  	}
 565  
 566  	// Sporulate.
 567  	sp := spore.Extract(l)
 568  	if sp != nil {
 569  		fmt.Printf("  spore: occupied=%d/%d types=%d\n",
 570  			sp.Occupied, sp.TotalNodes, len(sp.TypeSignature))
 571  	}
 572  
 573  	return rawCount, accretedCount, adsrDist
 574  }
 575  
 576  // feedFile opens the next file from the walker and feeds its elements
 577  // into the solution channel. Returns the relative path of the file fed,
 578  // or "" if the walker is exhausted.
 579  func feedFile(walker *walk.Walker, solution chan axiom.Element) string {
 580  	ch, ok := walker.DigestNext()
 581  	if !ok {
 582  		return ""
 583  	}
 584  	var filePath string
 585  	if walker.Position > 0 && walker.Position <= len(walker.Manifest.Files) {
 586  		filePath = walker.Manifest.Files[walker.Position-1]
 587  	}
 588  	go func() {
 589  		for elem := range ch {
 590  			solution <- elem
 591  		}
 592  	}()
 593  	if filePath != "" {
 594  		fmt.Printf("  feed: %s\n", filePath)
 595  	}
 596  	return filePath
 597  }
 598  
 599  // feedLatticeIntoItself harvests occupied elements from the lattice,
 600  // decomposes their string values through Text enzyme, and feeds
 601  // the resulting tokens back into the solution channel.
 602  func feedLatticeIntoItself(l *lattice.Lattice, solution chan axiom.Element) {
 603  	var texts []string
 604  	for i := range l.Size() {
 605  		n := l.Node(lattice.NodeID(i))
 606  		if !n.Occupied() {
 607  			continue
 608  		}
 609  		val := n.Occupant().Value()
 610  		if s, ok := val.(string); ok && len(s) > 0 {
 611  			texts = append(texts, s)
 612  		}
 613  	}
 614  
 615  	if len(texts) == 0 {
 616  		return
 617  	}
 618  
 619  	combined := strings.Join(texts, " ")
 620  	fmt.Printf("  self-feed: %d elements, %d bytes\n", len(texts), len(combined))
 621  
 622  	go func() {
 623  		ch := enzyme.Text{}.Digest(strings.NewReader(combined))
 624  		for elem := range ch {
 625  			if elem.Type() == "space" {
 626  				continue
 627  			}
 628  			solution <- elem
 629  		}
 630  	}()
 631  }
 632  
 633  // startNewEpoch re-scans the repository with a new seed and weighted
 634  // permutation based on historical file accretion scores.
 635  func startNewEpoch(repoDir string, prevSeed uint64, mem *memory.DB) (*walk.Walker, uint64) {
 636  	newSeed := walk.DeriveNextSeed(prevSeed)
 637  
 638  	scoresByHash, _ := mem.LoadFileScores()
 639  	if len(scoresByHash) > 0 {
 640  		// Do a plain scan first to get the file list for weight computation.
 641  		plain, err := walk.Build(repoDir, newSeed, walk.DefaultExclude)
 642  		if err == nil && len(plain.Files) > 0 {
 643  			weights := computeFileWeights(plain.Files, scoresByHash)
 644  			weighted, err := walk.BuildWeighted(repoDir, newSeed, walk.DefaultExclude, weights, 2.0)
 645  			if err == nil {
 646  				return walk.NewWalker(weighted), newSeed
 647  			}
 648  		}
 649  	}
 650  
 651  	// Fallback: uniform shuffle.
 652  	manifest, _ := walk.Build(repoDir, newSeed, walk.DefaultExclude)
 653  	return walk.NewWalker(manifest), newSeed
 654  }
 655  
 656  // computeFileWeights converts per-file accretion scores into weights for
 657  // the Efraimidis-Spirakis weighted permutation.
 658  // Weight = 2.0 - accretionRate, range [1.0, 2.0].
 659  // Files with no history get 2.0 (maximum priority).
 660  func computeFileWeights(files []string, scoresByHash map[[8]byte][2]int64) map[string]float64 {
 661  	weights := make(map[string]float64, len(files))
 662  	for _, f := range files {
 663  		h := memory.TagHash(f)
 664  		scores, ok := scoresByHash[h]
 665  		if !ok || scores[1] == 0 {
 666  			weights[f] = 2.0
 667  			continue
 668  		}
 669  		rate := float64(scores[0]) / float64(scores[1])
 670  		if rate > 1.0 {
 671  			rate = 1.0
 672  		}
 673  		if rate < 0.0 {
 674  			rate = 0.0
 675  		}
 676  		weights[f] = 2.0 - rate
 677  	}
 678  	return weights
 679  }
 680  
 681  // checkStability returns true when the lattice has fully absorbed its current
 682  // material: high Sustain fraction, low young (Attack+Decay) fraction, and
 683  // neither occupancy nor fitness still rising.
 684  func checkStability(adsrDist [4]int, mem *memory.DB,
 685  	minSustain, maxYoung ratio.Ratio) bool {
 686  
 687  	occupied := adsrDist[0] + adsrDist[1] + adsrDist[2] + adsrDist[3]
 688  	if occupied == 0 {
 689  		return false
 690  	}
 691  
 692  	sustainFrac := ratio.New(int64(adsrDist[2]), int64(occupied))
 693  	youngFrac := ratio.New(int64(adsrDist[0]+adsrDist[1]), int64(occupied))
 694  
 695  	if sustainFrac.Less(minSustain) {
 696  		return false
 697  	}
 698  	if maxYoung.Less(youngFrac) {
 699  		return false
 700  	}
 701  
 702  	// Check cross-generational trends from memory.
 703  	if mem == nil {
 704  		return true
 705  	}
 706  	digest := mem.WalkDigest(nil, 5)
 707  	if digest == nil {
 708  		return true // <2 gens of data — trust ADSR alone
 709  	}
 710  	if digest.OccupancyTrend == memory.TrendRising {
 711  		return false
 712  	}
 713  	if digest.FitnessTrend == memory.TrendRising {
 714  		return false
 715  	}
 716  
 717  	return true
 718  }
 719  
 720  // freezeState persists the current lattice, detector, and walker state.
 721  func freezeState(l *lattice.Lattice, genNum uint32, outDir string, mem *memory.DB,
 722  	detector *ewma.OscillationDetector, walker *walk.Walker, epochNum uint32) {
 723  
 724  	sp := spore.Extract(l)
 725  	m := mindsicle.Freeze(l, sp)
 726  
 727  	// Write to file.
 728  	fileName := filepath.Join(outDir, fmt.Sprintf("dendrite.gen%d.mindsicle", genNum))
 729  	var buf bytes.Buffer
 730  	m.WriteTo(&buf)
 731  	os.WriteFile(fileName, buf.Bytes(), 0o644)
 732  	fmt.Printf("  freeze: %s (%d bytes)\n", fileName, buf.Len())
 733  
 734  	// Write to memory DB.
 735  	if mem != nil {
 736  		var mbuf bytes.Buffer
 737  		m.WriteTo(&mbuf)
 738  		mem.RecordMindsicle(genNum, mbuf.Bytes())
 739  
 740  		// Persist detector state.
 741  		detectorData, err := detector.Marshal()
 742  		if err == nil {
 743  			mem.RecordEWMAState(genNum, detectorData)
 744  		}
 745  
 746  		// Persist walker checkpoint for exact resume.
 747  		mem.RecordWalkerCheckpoint(memory.WalkerCheckpoint{
 748  			Epoch:    epochNum,
 749  			Seed:     walker.Manifest.Seed,
 750  			Position: walker.Position,
 751  			GenNum:   genNum,
 752  			Files:    walker.Manifest.Files,
 753  			Root:     walker.Manifest.Root,
 754  		})
 755  	}
 756  }
 757  
 758  func max64(a, b float64) float64 {
 759  	if a > b {
 760  		return a
 761  	}
 762  	return b
 763  }
 764