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