1 package oracle
2 3 import (
4 "crypto/sha256"
5 "encoding/binary"
6 "encoding/json"
7 "math/rand/v2"
8 9 "git.mleku.dev/mleku/dendrite/pkg/state"
10 )
11 12 // Oracle is the I Ching oracle state machine. It maintains the chain
13 // of readings and produces strategy directives from hexagram states.
14 //
15 // Each reading chains from the previous: the resulting hexagram becomes
16 // the next primary, and 2 bits of PRNG entropy are XOR'd onto each
17 // line to evolve the state. This creates a learning trajectory with
18 // memory — continuity is structural, not random.
19 type Oracle struct {
20 // Current is the active reading being studied.
21 Current *Reading `json:"current"`
22 23 // History is the chain of all past readings (most recent last).
24 History []*Reading `json:"history"`
25 26 // Seed is the PRNG seed for line entropy generation.
27 Seed uint64 `json:"seed"`
28 }
29 30 // New creates an oracle with no reading history (abiogenesis).
31 func New(seed uint64) *Oracle {
32 return &Oracle{Seed: seed}
33 }
34 35 // FromState restores an oracle from persisted JSON state.
36 func FromState(data []byte) (*Oracle, error) {
37 o := &Oracle{}
38 if err := json.Unmarshal(data, o); err != nil {
39 return nil, err
40 }
41 return o, nil
42 }
43 44 // Marshal serializes the oracle state for persistence.
45 func (o *Oracle) Marshal() ([]byte, error) {
46 return json.Marshal(o)
47 }
48 49 // Cast produces a new reading. If a previous reading exists, the
50 // resulting hexagram becomes the primary and 2 bits of PRNG entropy
51 // are XOR'd onto each inherited line state. If no previous reading
52 // exists, a fresh cast is generated from the seed.
53 //
54 // source identifies what data source prompted this reading.
55 // adsrPhase is the dominant ADSR phase of the learning lattice.
56 // gen is the current dendrite generation number.
57 func (o *Oracle) Cast(source string, adsrPhase uint8, gen uint32) *Reading {
58 seq := uint32(0)
59 if o.Current != nil {
60 seq = o.Current.Sequence + 1
61 }
62 63 r := &Reading{
64 Generation: gen,
65 Sequence: seq,
66 Source: source,
67 }
68 69 rng := deriveRNG(o.Seed, seq, source)
70 71 if o.Current == nil {
72 // Abiogenesis: full random cast.
73 r.Primary = state.Hexagram(rng.IntN(64))
74 for i := range 6 {
75 r.Lines[i] = LineState(rng.IntN(4))
76 }
77 } else {
78 // Chain: inherit resulting hexagram, XOR entropy onto lines.
79 r.Primary = o.Current.Resulting
80 for i := range 6 {
81 prevState := o.Current.Lines[i]
82 // If prev line was changing, it has already changed in the
83 // resulting hexagram. Its new "natural" state is the post-
84 // change value: young, since the transition completed.
85 if prevState.IsChanging() {
86 prevState = prevState.Stabilize()
87 }
88 entropy := LineState(rng.IntN(4))
89 r.Lines[i] = prevState ^ entropy
90 }
91 }
92 93 // Compute resulting hexagram from changing lines.
94 r.Resulting = ApplyChangingLines(r.Primary, r.Lines)
95 96 // Generate strategy directives.
97 r.Directives = GenerateDirectives(r, source, ADSRToStyle(adsrPhase))
98 99 // Archive the old reading and install the new one.
100 if o.Current != nil {
101 o.History = append(o.History, o.Current)
102 }
103 o.Current = r
104 105 // Advance seed for next cast.
106 o.Seed = advanceSeed(o.Seed)
107 108 return r
109 }
110 111 // MarkAbsorbed marks the current reading as fully absorbed at the given
112 // generation. Call this when the stability monitor detects oscillation.
113 func (o *Oracle) MarkAbsorbed(gen uint32) {
114 if o.Current != nil {
115 o.Current.Absorbed = true
116 o.Current.AbsorbedAtGen = gen
117 }
118 }
119 120 // ApplyChangingLines computes the resulting hexagram by flipping all
121 // changing lines in the primary hexagram.
122 func ApplyChangingLines(primary state.Hexagram, lines [6]LineState) state.Hexagram {
123 result := primary
124 for i, ls := range lines {
125 if ls.IsChanging() {
126 isInner := i < 3
127 bit := uint8(i)
128 if !isInner {
129 bit = uint8(i - 3)
130 }
131 result = result.MoveLine(isInner, bit)
132 }
133 }
134 return result
135 }
136 137 // deriveRNG creates a deterministic PRNG from the oracle seed,
138 // sequence number, and source identifier. Same inputs always produce
139 // the same random stream.
140 func deriveRNG(seed uint64, seq uint32, source string) *rand.Rand {
141 h := sha256.Sum256([]byte(source))
142 seedMix := seed ^ binary.BigEndian.Uint64(h[:8]) ^ uint64(seq)
143 return rand.New(rand.NewPCG(seedMix, seedMix^0xcafebabe))
144 }
145 146 // advanceSeed advances the seed using SplitMix64.
147 func advanceSeed(prev uint64) uint64 {
148 s := prev + 0x9e3779b97f4a7c15
149 s = (s ^ (s >> 30)) * 0xbf58476d1ce4e5b9
150 s = (s ^ (s >> 27)) * 0x94d049bb133111eb
151 return s ^ (s >> 31)
152 }
153