profile.go raw

   1  // Package profile records and analyzes walk/bond statistics during lattice
   2  // growth. The statistical signature of how text bonds into a lattice is the
   3  // detection mechanism: human text and AI text produce measurably different
   4  // profiles on a human-trained lattice.
   5  //
   6  // All derived statistics use exact rational arithmetic (ratio.Ratio) to
   7  // guarantee deterministic, platform-independent results.
   8  package profile
   9  
  10  import (
  11  	"encoding/json"
  12  	"fmt"
  13  	"strings"
  14  
  15  	"git.mleku.dev/mleku/dendrite/pkg/lattice"
  16  )
  17  
  18  // Profile captures raw statistics from a growth session.
  19  type Profile struct {
  20  	// Counters.
  21  	TokensIngested int64 `json:"tokens_ingested"`
  22  	BondEvents     int64 `json:"bond_events"`
  23  	RejectEvents   int64 `json:"reject_events"`
  24  	ExpireEvents   int64 `json:"expire_events"`
  25  	NewVertices    int64 `json:"new_vertices"`
  26  
  27  	// PathFreq counts how many times each node was the bonding site.
  28  	PathFreq map[lattice.NodeID]int64 `json:"path_freq"`
  29  
  30  	// BondDist counts bonds per constraint tag.
  31  	BondDist map[string]int64 `json:"bond_dist"`
  32  
  33  	// WalkDistHist is a histogram of walk steps before bonding.
  34  	// Key is the step count, value is how many bonds occurred at that distance.
  35  	WalkDistHist map[int]int64 `json:"walk_dist_hist"`
  36  
  37  	// TransitionFreq counts (previous_element_tag, bonded_element_tag) pairs.
  38  	// Captures sequential structure without prescribing grammar.
  39  	// Not directly JSON-serializable; use Marshal/Unmarshal methods.
  40  	TransitionFreq map[[2]string]int64 `json:"-"`
  41  }
  42  
  43  // New creates a zero-valued Profile with initialized maps.
  44  func New() *Profile {
  45  	return &Profile{
  46  		PathFreq:       make(map[lattice.NodeID]int64),
  47  		BondDist:       make(map[string]int64),
  48  		WalkDistHist:   make(map[int]int64),
  49  		TransitionFreq: make(map[[2]string]int64),
  50  	}
  51  }
  52  
  53  // Clone returns a deep copy of the profile.
  54  func (p *Profile) Clone() *Profile {
  55  	c := &Profile{
  56  		TokensIngested: p.TokensIngested,
  57  		BondEvents:     p.BondEvents,
  58  		RejectEvents:   p.RejectEvents,
  59  		ExpireEvents:   p.ExpireEvents,
  60  		NewVertices:    p.NewVertices,
  61  		PathFreq:       make(map[lattice.NodeID]int64, len(p.PathFreq)),
  62  		BondDist:       make(map[string]int64, len(p.BondDist)),
  63  		WalkDistHist:   make(map[int]int64, len(p.WalkDistHist)),
  64  		TransitionFreq: make(map[[2]string]int64, len(p.TransitionFreq)),
  65  	}
  66  	for k, v := range p.PathFreq {
  67  		c.PathFreq[k] = v
  68  	}
  69  	for k, v := range p.BondDist {
  70  		c.BondDist[k] = v
  71  	}
  72  	for k, v := range p.WalkDistHist {
  73  		c.WalkDistHist[k] = v
  74  	}
  75  	for k, v := range p.TransitionFreq {
  76  		c.TransitionFreq[k] = v
  77  	}
  78  	return c
  79  }
  80  
  81  // profileJSON is the JSON-serializable form of Profile.
  82  // TransitionFreq keys are encoded as "tag1\x00tag2" strings.
  83  type profileJSON struct {
  84  	TokensIngested int64                    `json:"tokens_ingested"`
  85  	BondEvents     int64                    `json:"bond_events"`
  86  	RejectEvents   int64                    `json:"reject_events"`
  87  	ExpireEvents   int64                    `json:"expire_events"`
  88  	NewVertices    int64                    `json:"new_vertices"`
  89  	PathFreq       map[lattice.NodeID]int64 `json:"path_freq"`
  90  	BondDist       map[string]int64         `json:"bond_dist"`
  91  	WalkDistHist   map[int]int64            `json:"walk_dist_hist"`
  92  	TransitionFreq map[string]int64         `json:"transition_freq"`
  93  }
  94  
  95  // transitionKey encodes a [2]string as a single string for JSON map keys.
  96  func transitionKey(pair [2]string) string {
  97  	return fmt.Sprintf("%s\x00%s", pair[0], pair[1])
  98  }
  99  
 100  // parseTransitionKey decodes a transition key back to [2]string.
 101  func parseTransitionKey(key string) [2]string {
 102  	parts := strings.SplitN(key, "\x00", 2)
 103  	if len(parts) == 2 {
 104  		return [2]string{parts[0], parts[1]}
 105  	}
 106  	return [2]string{key, ""}
 107  }
 108  
 109  // Marshal serializes the profile to JSON.
 110  func (p *Profile) Marshal() ([]byte, error) {
 111  	j := profileJSON{
 112  		TokensIngested: p.TokensIngested,
 113  		BondEvents:     p.BondEvents,
 114  		RejectEvents:   p.RejectEvents,
 115  		ExpireEvents:   p.ExpireEvents,
 116  		NewVertices:    p.NewVertices,
 117  		PathFreq:       p.PathFreq,
 118  		BondDist:       p.BondDist,
 119  		WalkDistHist:   p.WalkDistHist,
 120  		TransitionFreq: make(map[string]int64, len(p.TransitionFreq)),
 121  	}
 122  	for k, v := range p.TransitionFreq {
 123  		j.TransitionFreq[transitionKey(k)] = v
 124  	}
 125  	return json.Marshal(j)
 126  }
 127  
 128  // Unmarshal deserializes a profile from JSON.
 129  func Unmarshal(data []byte) (*Profile, error) {
 130  	var j profileJSON
 131  	if err := json.Unmarshal(data, &j); err != nil {
 132  		return nil, err
 133  	}
 134  	p := &Profile{
 135  		TokensIngested: j.TokensIngested,
 136  		BondEvents:     j.BondEvents,
 137  		RejectEvents:   j.RejectEvents,
 138  		ExpireEvents:   j.ExpireEvents,
 139  		NewVertices:    j.NewVertices,
 140  		PathFreq:       j.PathFreq,
 141  		BondDist:       j.BondDist,
 142  		WalkDistHist:   j.WalkDistHist,
 143  		TransitionFreq: make(map[[2]string]int64, len(j.TransitionFreq)),
 144  	}
 145  	if p.PathFreq == nil {
 146  		p.PathFreq = make(map[lattice.NodeID]int64)
 147  	}
 148  	if p.BondDist == nil {
 149  		p.BondDist = make(map[string]int64)
 150  	}
 151  	if p.WalkDistHist == nil {
 152  		p.WalkDistHist = make(map[int]int64)
 153  	}
 154  	for k, v := range j.TransitionFreq {
 155  		p.TransitionFreq[parseTransitionKey(k)] = v
 156  	}
 157  	return p, nil
 158  }
 159