vagus.go raw

   1  package grammar
   2  
   3  import (
   4  	"time"
   5  
   6  	"git.mleku.dev/mleku/dendrite/pkg/dissolve"
   7  	"git.mleku.dev/mleku/dendrite/pkg/grow"
   8  	"git.mleku.dev/mleku/dendrite/pkg/memory"
   9  	"git.mleku.dev/mleku/dendrite/pkg/ratio"
  10  )
  11  
  12  // VagusBaseline holds the default parameters that the vagus pathway modulates.
  13  // These are the resting-state values — the parameters when no metabolic
  14  // feedback is present (generation 0, or nil digest).
  15  type VagusBaseline struct {
  16  	DissolveThreshold ratio.Ratio
  17  	DissolveHalfLife  uint8
  18  	GrowMaxSteps      int
  19  	GrowWorkers       int
  20  }
  21  
  22  // DefaultBaseline returns a reasonable resting-state configuration.
  23  func DefaultBaseline() VagusBaseline {
  24  	return VagusBaseline{
  25  		DissolveThreshold: ratio.Half,
  26  		DissolveHalfLife:  2,
  27  		GrowMaxSteps:      500,
  28  		GrowWorkers:       3,
  29  	}
  30  }
  31  
  32  // VagusSignal carries metabolic feedback from memory to growth parameters.
  33  // This is the rigid feedback pathway — deterministic, not hash-perturbed.
  34  // Named after the vagus nerve: the longest cranial nerve, carrying
  35  // parasympathetic signals from organs to brainstem.
  36  type VagusSignal struct {
  37  	DissolveThreshold ratio.Ratio
  38  	DissolveHalfLife  uint8
  39  	GrowMaxSteps      int
  40  	GrowWorkers       int
  41  	TypeAdjustments   map[string]int // -1 = shrink allocation, +1 = grow allocation
  42  }
  43  
  44  // DefaultSignal returns a VagusSignal with baseline parameters and no
  45  // type adjustments. Used when no metabolic feedback is available.
  46  func DefaultSignal() VagusSignal {
  47  	b := DefaultBaseline()
  48  	return VagusSignal{
  49  		DissolveThreshold: b.DissolveThreshold,
  50  		DissolveHalfLife:  b.DissolveHalfLife,
  51  		GrowMaxSteps:      b.GrowMaxSteps,
  52  		GrowWorkers:       b.GrowWorkers,
  53  		TypeAdjustments:   make(map[string]int),
  54  	}
  55  }
  56  
  57  // ReadVagus translates a memory.Digest into direct parameter modulations.
  58  // This replaces the hash-based perturbation with a rigid feedback pathway:
  59  // metabolic signals → parameter adjustments, deterministic and immediate.
  60  //
  61  // The threshold multipliers (7/10, 8/10, 13/10) are decimal-denominated
  62  // fractions applied to binary-structured lattice dynamics. This is an
  63  // intentional asymmetry: the vagus pathway measures in decimal (human-
  64  // readable percentages) and actuates in binary (lattice topology). The
  65  // phase drift between these domains is bounded by the epoch relationship
  66  // 10^a × 2^b (see package epoch).
  67  //
  68  // Translation rules (biology analogs):
  69  //   - Fitness falling → more aggressive dissolution (clear failing structure)
  70  //   - Fitness rising → preserve what's working (raise dissolution threshold)
  71  //   - Fitness stagnant → increase exploration (more walkers, longer walks)
  72  //   - Occupancy falling → increase growth effort (double max steps)
  73  //   - High sustain fraction → increase turnover (lower half-life)
  74  //   - High young fraction → reduce churn (raise half-life)
  75  //   - Per-tag bond rate → type allocation adjustment
  76  func ReadVagus(d *memory.Digest, baseline VagusBaseline) VagusSignal {
  77  	sig := VagusSignal{
  78  		DissolveThreshold: baseline.DissolveThreshold,
  79  		DissolveHalfLife:  baseline.DissolveHalfLife,
  80  		GrowMaxSteps:      baseline.GrowMaxSteps,
  81  		GrowWorkers:       baseline.GrowWorkers,
  82  		TypeAdjustments:   make(map[string]int),
  83  	}
  84  
  85  	if d == nil {
  86  		return sig
  87  	}
  88  
  89  	// Fitness-driven modulation.
  90  	switch d.FitnessTrend {
  91  	case memory.TrendFalling:
  92  		// Failing — clear weak structure more aggressively.
  93  		sig.DissolveThreshold = baseline.DissolveThreshold.Mul(ratio.New(7, 10))
  94  		if sig.DissolveHalfLife > 1 {
  95  			sig.DissolveHalfLife--
  96  		}
  97  	case memory.TrendRising:
  98  		// Improving — preserve what's working.
  99  		sig.DissolveThreshold = baseline.DissolveThreshold.Mul(ratio.New(13, 10))
 100  	case memory.TrendStagnant:
 101  		// Stuck — explore more.
 102  		sig.GrowMaxSteps = int(ratio.New(3, 2).ScaleInt(int64(baseline.GrowMaxSteps)))
 103  		sig.GrowWorkers = baseline.GrowWorkers + 1
 104  	}
 105  
 106  	// Occupancy-driven modulation.
 107  	if d.OccupancyTrend == memory.TrendFalling {
 108  		sig.GrowMaxSteps = max(sig.GrowMaxSteps, int(ratio.New(2, 1).ScaleInt(int64(baseline.GrowMaxSteps))))
 109  		if sig.DissolveHalfLife > 1 {
 110  			sig.DissolveHalfLife--
 111  		}
 112  	}
 113  
 114  	// OverExtended: occupancy falling + explore dominant.
 115  	// Suppress exploration, increase dissolution.
 116  	if d.OverExtended {
 117  		sig.GrowMaxSteps = int(ratio.New(1, 2).ScaleInt(int64(baseline.GrowMaxSteps)))
 118  		if sig.GrowWorkers > 2 {
 119  			sig.GrowWorkers--
 120  		}
 121  	}
 122  
 123  	// ADSR homeostasis.
 124  	// High sustain fraction = too rigid, needs turnover.
 125  	if !d.SustainFraction.IsZero() && ratio.New(8, 10).Less(d.SustainFraction) {
 126  		if sig.DissolveHalfLife > 1 {
 127  			sig.DissolveHalfLife--
 128  		}
 129  	}
 130  	// High young fraction = too much churn, stabilize.
 131  	if !d.YoungFraction.IsZero() && ratio.New(7, 10).Less(d.YoungFraction) {
 132  		sig.DissolveHalfLife++
 133  	}
 134  
 135  	// Per-tag bond rate adjustments.
 136  	// Absorbs the logic from rebalanceTypes() in cmd/dendrite/main.go.
 137  	for tag, td := range d.Types {
 138  		if td.BondRate.Less(ratio.New(1, 10)) {
 139  			sig.TypeAdjustments[tag] = -1 // bond rate < 10% → shrink
 140  		} else if ratio.Half.Less(td.BondRate) {
 141  			sig.TypeAdjustments[tag] = 1 // bond rate > 50% → grow
 142  		}
 143  	}
 144  
 145  	return sig
 146  }
 147  
 148  // DissolveConfig returns a dissolve.Config with vagus-modulated parameters.
 149  func (v VagusSignal) DissolveConfig(interval time.Duration) dissolve.Config {
 150  	return dissolve.Config{
 151  		Threshold: v.DissolveThreshold,
 152  		Interval:  interval,
 153  		HalfLife:  v.DissolveHalfLife,
 154  	}
 155  }
 156  
 157  // GrowConfig returns a grow.Config with vagus-modulated parameters.
 158  func (v VagusSignal) GrowConfig() grow.Config {
 159  	return grow.Config{
 160  		MaxSteps: v.GrowMaxSteps,
 161  		Workers:  v.GrowWorkers,
 162  	}
 163  }
 164  
 165  // AdjustCounts applies type adjustments to a base count map.
 166  // Shrink (-1) halves the count (minimum 1).
 167  // Grow (+1) increases by 50%.
 168  func (v VagusSignal) AdjustCounts(base map[string]int) map[string]int {
 169  	result := make(map[string]int, len(base))
 170  	for tag, count := range base {
 171  		adj, ok := v.TypeAdjustments[tag]
 172  		if !ok {
 173  			result[tag] = count
 174  			continue
 175  		}
 176  		switch {
 177  		case adj < 0:
 178  			result[tag] = max(1, int(ratio.New(1, 2).ScaleInt(int64(count))))
 179  		case adj > 0:
 180  			result[tag] = int(ratio.New(3, 2).ScaleInt(int64(count)))
 181  		default:
 182  			result[tag] = count
 183  		}
 184  	}
 185  	return result
 186  }
 187