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