1 // Package gap implements structural gap detection — inspecting the lattice
2 // and spore for absences that shape the organism's information needs.
3 //
4 // This is pure structural analysis with no external dependencies.
5 // DetectGaps identifies what the lattice lacks; ComposeQuery formulates
6 // English questions from those gaps (useful for foraging or logging).
7 package gap
8 9 import (
10 "fmt"
11 "sort"
12 13 "git.mleku.dev/mleku/dendrite/pkg/lattice"
14 "git.mleku.dev/mleku/dendrite/pkg/nostr"
15 "git.mleku.dev/mleku/dendrite/pkg/ratio"
16 "git.mleku.dev/mleku/dendrite/pkg/spore"
17 )
18 19 // Gap describes a structural absence in the organism — something it
20 // cannot currently handle or understand.
21 type Gap struct {
22 Type string // "missing_site", "orphan_event", "orphan_pubkey", "low_occupancy", "weak_region"
23 Description string // human-readable description of the gap
24 Severity ratio.Ratio // 0 = minor, 1 = critical
25 Tag string // the element type or reference ID involved
26 Count int // how many times this gap was observed
27 }
28 29 // DetectGaps inspects the lattice and spore for structural absences.
30 // Returns gaps sorted by severity (most severe first).
31 func DetectGaps(l *lattice.Lattice, s *spore.Spore, eg *nostr.EventGraph) []Gap {
32 var gaps []Gap
33 34 // 1. Missing sites from spore: element types that failed to bond.
35 if s != nil && len(s.MissingSites) > 0 {
36 for _, tc := range s.MissingSites {
37 tag := tc.Tag
38 count := tc.Count
39 severity := ratio.New(int64(count), 100)
40 if ratio.One.Less(severity) {
41 severity = ratio.One
42 }
43 gaps = append(gaps, Gap{
44 Type: "missing_site",
45 Description: fmt.Sprintf("element type %q rejected %d times — no matching lattice sites", tag, count),
46 Severity: severity,
47 Tag: tag,
48 Count: count,
49 })
50 }
51 }
52 53 // 2. Low occupancy regions from lattice health.
54 if l != nil {
55 health := l.Health()
56 if health.OccupancyRate.Less(ratio.New(3, 10)) && health.NodeCount > 10 {
57 gaps = append(gaps, Gap{
58 Type: "low_occupancy",
59 Description: fmt.Sprintf("lattice occupancy is %.0f%% — most sites are vacant", health.OccupancyRate.Float64()*100),
60 Severity: ratio.One.Sub(health.OccupancyRate),
61 Tag: "occupancy",
62 Count: health.Vacant,
63 })
64 }
65 if health.DissolveSoft > health.Occupied/2 && health.Occupied > 0 {
66 gaps = append(gaps, Gap{
67 Type: "weak_region",
68 Description: fmt.Sprintf("%d of %d occupied nodes have weak bonds (lock-in < 1)", health.DissolveSoft, health.Occupied),
69 Severity: ratio.New(int64(health.DissolveSoft), int64(health.Occupied)),
70 Tag: "lock-in",
71 Count: health.DissolveSoft,
72 })
73 }
74 }
75 76 // 3. Orphan references from event graph.
77 if eg != nil && len(eg.Orphans) > 0 {
78 orphanTypes := make(map[string]int)
79 for _, o := range eg.Orphans {
80 orphanTypes[o.Type]++
81 }
82 otypes := make([]string, 0, len(orphanTypes))
83 for otype := range orphanTypes {
84 otypes = append(otypes, otype)
85 }
86 sort.Strings(otypes)
87 for _, otype := range otypes {
88 count := orphanTypes[otype]
89 severity := ratio.New(int64(count), 50)
90 if ratio.One.Less(severity) {
91 severity = ratio.One
92 }
93 gaps = append(gaps, Gap{
94 Type: "orphan_" + otype,
95 Description: fmt.Sprintf("%d orphan %s references — events or pubkeys mentioned but absent", count, otype),
96 Severity: severity,
97 Tag: otype,
98 Count: count,
99 })
100 }
101 }
102 103 sort.Slice(gaps, func(i, j int) bool {
104 if !gaps[i].Severity.Equal(gaps[j].Severity) {
105 return gaps[j].Severity.Less(gaps[i].Severity)
106 }
107 return gaps[i].Tag < gaps[j].Tag
108 })
109 110 return gaps
111 }
112 113 // ComposeQuery formulates an English question from a gap.
114 // The question is shaped by the absence — it asks about what
115 // the organism cannot currently handle.
116 func ComposeQuery(gap Gap, context string) string {
117 switch gap.Type {
118 case "missing_site":
119 return fmt.Sprintf(
120 "I am a Go program that processes data through typed lattice elements. "+
121 "I am receiving elements of type %q but have no lattice sites that accept them. "+
122 "What kind of data produces %q elements, and what constraints should I define "+
123 "to process them? %s",
124 gap.Tag, gap.Tag, context)
125 126 case "orphan_event":
127 return fmt.Sprintf(
128 "I am processing Nostr events and found %d references to events I don't have. "+
129 "What strategies exist for resolving orphan event references in a Nostr relay? "+
130 "How can I request missing events from other relays? %s",
131 gap.Count, context)
132 133 case "orphan_pubkey":
134 return fmt.Sprintf(
135 "I am processing Nostr events and found %d references to pubkeys I have no events from. "+
136 "How should a relay handle mentions of unknown authors? "+
137 "Should I fetch their profiles from other relays? %s",
138 gap.Count, context)
139 140 case "low_occupancy":
141 return fmt.Sprintf(
142 "My lattice has %d vacant sites. Occupancy is low. "+
143 "What types of input might I be missing that would fill these sites? "+
144 "How can I increase the diversity of elements entering the lattice? %s",
145 gap.Count, context)
146 147 case "weak_region":
148 return fmt.Sprintf(
149 "My lattice has %d weakly-bonded elements (lock-in depth < 1). "+
150 "These elements satisfy only one constraint each. "+
151 "How can I add additional constraints to strengthen bonding, "+
152 "or should I allow weak bonds to dissolve? %s",
153 gap.Count, context)
154 155 default:
156 return fmt.Sprintf(
157 "I detected a structural gap in my lattice: %s. "+
158 "How should I address this? %s",
159 gap.Description, context)
160 }
161 }
162