1 package grow
2 3 import (
4 "context"
5 "sync"
6 7 "git.mleku.dev/mleku/dendrite/pkg/axiom"
8 "git.mleku.dev/mleku/dendrite/pkg/lattice"
9 )
10 11 // ProbeEvent records what happened when an element was probed against a
12 // trained lattice. Unlike growth events which bond elements to empty sites,
13 // probe events measure how well the element fits the existing occupants.
14 type ProbeEvent struct {
15 // Type: EventBonded means a matching occupant was found (type match
16 // at a constrained site). EventExpired means no match within MaxSteps.
17 Type EventType
18 19 // NodeID is the node where a match was found (if matched).
20 NodeID lattice.NodeID
21 22 // Element is the probed element.
23 Element axiom.Element
24 25 // Steps is how many walk steps before finding a match (or expiring).
26 Steps int
27 }
28 29 // Probe walks elements through a trained (saturated) lattice without bonding.
30 // For each element, it walks the lattice and checks if visited nodes have
31 // occupants matching the element's type. A match means the lattice has
32 // "seen" this kind of element at this position — the text fits the trained
33 // topology.
34 //
35 // This is the inference counterpart to Run: Run grows the lattice,
36 // Probe reads it.
37 func Probe(ctx context.Context, l *lattice.Lattice, solution <-chan axiom.Element, cfg Config, events chan<- ProbeEvent) {
38 var wg sync.WaitGroup
39 40 for range cfg.Workers {
41 wg.Add(1)
42 go func() {
43 defer wg.Done()
44 for {
45 select {
46 case <-ctx.Done():
47 return
48 case elem, ok := <-solution:
49 if !ok {
50 return
51 }
52 ev := probeWalk(ctx, l, elem, cfg.MaxSteps)
53 select {
54 case events <- ev:
55 case <-ctx.Done():
56 return
57 }
58 }
59 }
60 }()
61 }
62 63 wg.Wait()
64 }
65 66 // probeWalk walks the lattice checking for type-matching occupants.
67 // It uses the same directed start and chemotaxis as growth walks,
68 // but instead of bonding, it checks occupant compatibility.
69 func probeWalk(ctx context.Context, l *lattice.Lattice, elem axiom.Element, maxSteps int) ProbeEvent {
70 // Start at a random node (can't use VacantByTag — lattice is full).
71 current := l.RandomNode()
72 if current == nil {
73 return ProbeEvent{Type: EventExpired, Element: elem}
74 }
75 76 tag := elem.Type()
77 78 for step := 0; step < maxSteps; step++ {
79 select {
80 case <-ctx.Done():
81 return ProbeEvent{Type: EventExpired, Element: elem, Steps: step}
82 default:
83 }
84 85 // Check if this node's occupant matches the element's type.
86 occ := current.Occupant()
87 if occ != nil && occ.Type() == tag {
88 return ProbeEvent{
89 Type: EventBonded, // "matched" — reuse the type
90 NodeID: current.ID(),
91 Element: elem,
92 Steps: step,
93 }
94 }
95 96 // Walk toward matching occupants via neighbor checking.
97 neighbors := current.Neighbors()
98 if len(neighbors) == 0 {
99 break
100 }
101 102 // Check neighbors for immediate match.
103 for _, nb := range neighbors {
104 occ := nb.Occupant()
105 if occ != nil && occ.Type() == tag {
106 return ProbeEvent{
107 Type: EventBonded,
108 NodeID: nb.ID(),
109 Element: elem,
110 Steps: step,
111 }
112 }
113 }
114 115 // No immediate match — step to a random neighbor.
116 next := lattice.RandomNeighbor(current)
117 if next == nil {
118 break
119 }
120 current = next
121 }
122 123 return ProbeEvent{Type: EventExpired, Element: elem, Steps: maxSteps}
124 }
125