gap.go raw

   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