cast.go raw

   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