// Package gap implements structural gap detection — inspecting the lattice // and spore for absences that shape the organism's information needs. // // This is pure structural analysis with no external dependencies. // DetectGaps identifies what the lattice lacks; ComposeQuery formulates // English questions from those gaps (useful for foraging or logging). package gap import ( "fmt" "sort" "git.mleku.dev/mleku/dendrite/pkg/lattice" "git.mleku.dev/mleku/dendrite/pkg/nostr" "git.mleku.dev/mleku/dendrite/pkg/ratio" "git.mleku.dev/mleku/dendrite/pkg/spore" ) // Gap describes a structural absence in the organism — something it // cannot currently handle or understand. type Gap struct { Type string // "missing_site", "orphan_event", "orphan_pubkey", "low_occupancy", "weak_region" Description string // human-readable description of the gap Severity ratio.Ratio // 0 = minor, 1 = critical Tag string // the element type or reference ID involved Count int // how many times this gap was observed } // DetectGaps inspects the lattice and spore for structural absences. // Returns gaps sorted by severity (most severe first). func DetectGaps(l *lattice.Lattice, s *spore.Spore, eg *nostr.EventGraph) []Gap { var gaps []Gap // 1. Missing sites from spore: element types that failed to bond. if s != nil && len(s.MissingSites) > 0 { for _, tc := range s.MissingSites { tag := tc.Tag count := tc.Count severity := ratio.New(int64(count), 100) if ratio.One.Less(severity) { severity = ratio.One } gaps = append(gaps, Gap{ Type: "missing_site", Description: fmt.Sprintf("element type %q rejected %d times — no matching lattice sites", tag, count), Severity: severity, Tag: tag, Count: count, }) } } // 2. Low occupancy regions from lattice health. if l != nil { health := l.Health() if health.OccupancyRate.Less(ratio.New(3, 10)) && health.NodeCount > 10 { gaps = append(gaps, Gap{ Type: "low_occupancy", Description: fmt.Sprintf("lattice occupancy is %.0f%% — most sites are vacant", health.OccupancyRate.Float64()*100), Severity: ratio.One.Sub(health.OccupancyRate), Tag: "occupancy", Count: health.Vacant, }) } if health.DissolveSoft > health.Occupied/2 && health.Occupied > 0 { gaps = append(gaps, Gap{ Type: "weak_region", Description: fmt.Sprintf("%d of %d occupied nodes have weak bonds (lock-in < 1)", health.DissolveSoft, health.Occupied), Severity: ratio.New(int64(health.DissolveSoft), int64(health.Occupied)), Tag: "lock-in", Count: health.DissolveSoft, }) } } // 3. Orphan references from event graph. if eg != nil && len(eg.Orphans) > 0 { orphanTypes := make(map[string]int) for _, o := range eg.Orphans { orphanTypes[o.Type]++ } otypes := make([]string, 0, len(orphanTypes)) for otype := range orphanTypes { otypes = append(otypes, otype) } sort.Strings(otypes) for _, otype := range otypes { count := orphanTypes[otype] severity := ratio.New(int64(count), 50) if ratio.One.Less(severity) { severity = ratio.One } gaps = append(gaps, Gap{ Type: "orphan_" + otype, Description: fmt.Sprintf("%d orphan %s references — events or pubkeys mentioned but absent", count, otype), Severity: severity, Tag: otype, Count: count, }) } } sort.Slice(gaps, func(i, j int) bool { if !gaps[i].Severity.Equal(gaps[j].Severity) { return gaps[j].Severity.Less(gaps[i].Severity) } return gaps[i].Tag < gaps[j].Tag }) return gaps } // ComposeQuery formulates an English question from a gap. // The question is shaped by the absence — it asks about what // the organism cannot currently handle. func ComposeQuery(gap Gap, context string) string { switch gap.Type { case "missing_site": return fmt.Sprintf( "I am a Go program that processes data through typed lattice elements. "+ "I am receiving elements of type %q but have no lattice sites that accept them. "+ "What kind of data produces %q elements, and what constraints should I define "+ "to process them? %s", gap.Tag, gap.Tag, context) case "orphan_event": return fmt.Sprintf( "I am processing Nostr events and found %d references to events I don't have. "+ "What strategies exist for resolving orphan event references in a Nostr relay? "+ "How can I request missing events from other relays? %s", gap.Count, context) case "orphan_pubkey": return fmt.Sprintf( "I am processing Nostr events and found %d references to pubkeys I have no events from. "+ "How should a relay handle mentions of unknown authors? "+ "Should I fetch their profiles from other relays? %s", gap.Count, context) case "low_occupancy": return fmt.Sprintf( "My lattice has %d vacant sites. Occupancy is low. "+ "What types of input might I be missing that would fill these sites? "+ "How can I increase the diversity of elements entering the lattice? %s", gap.Count, context) case "weak_region": return fmt.Sprintf( "My lattice has %d weakly-bonded elements (lock-in depth < 1). "+ "These elements satisfy only one constraint each. "+ "How can I add additional constraints to strengthen bonding, "+ "or should I allow weak bonds to dissolve? %s", gap.Count, context) default: return fmt.Sprintf( "I detected a structural gap in my lattice: %s. "+ "How should I address this? %s", gap.Description, context) } }