Chaos, Record, and Replay
Chaos
Feelings.chaos samples from an answer’s probability distribution instead of always taking the
top pick, useful for simulations and load-testing prompts.
Feelings.chaos { Feelings(email).most_like(KINDS) }
Inside a chaos block, every noul and choice question draws a random number and uses it to
sample rather than always picking the highest-probability outcome.
Sampling rules: maybe band first, then sample
A chaos draw is checked against the maybe band before any sampling happens. If a probability
falls inside a maybe band (declared via a maybe branch or at_least:, per
Asking Questions), the result is "maybe" regardless of the draw; a probability
inside the band never gets sampled into yes or no. Outside the band, a noul question samples:
yes wins when the draw is less than the probability, no otherwise. A choice question without
a confidence gate samples across the whole probability spread using Feelings::Distribution.sample,
which walks the normalized cumulative probabilities and returns the first label whose cumulative
share exceeds the draw.
Feelings.random
Feelings.random is the source of chaos draws, a zero-arg callable defaulting to
-> { Kernel.rand }. Assign your own to make chaos deterministic in tests:
Feelings.random = -> { 0.4 }
Tapes
Record a real run once, then replay it in tests without ever calling a judge again.
tape = Feelings.record { Feelings(email).like?("spam") }
Feelings.replay(tape) { Feelings(email).like?("spam") } # judge is never called
Entries and the model field
Each question asked inside a Feelings.record block produces one Feelings::Tape::Entry, with
kind ("noul" or "choice"), value (the resolved wire value), question (the built
wire question), answer (the raw answer hash, minus draw), model (the model name the judge
reported), and draw (the chaos draw, if any, nil outside a chaos block). tape.entries
returns the array; Tape is Enumerable over the same entries, so tape.size, tape.count,
and tape.map all work directly on the tape.
JSON round trip
A tape round-trips through JSON:
json = tape.to_json
restored = Feelings::Tape.from_json(json)
Entry#to_h drops any nil fields (so a tape recorded outside chaos serializes without a
draw key at all) before to_json generates the array.
ReplayMismatch cases
Feelings.replay(tape) { ... } shifts entries off the tape’s queue as questions are asked, in
order, and raises Feelings::ReplayMismatch in three situations:
- A question is asked but the tape has no more entries left.
- An entry’s recorded
valueorquestiondoesn’t match what’s actually being asked this time (the tape was recorded against different state). - The block finishes but the tape still has unused entries left over.
During replay, the judge is never called; a judge that raises if invoked (as in the gem’s own tests) proves replay never reaches it.
Using tapes in a test suite
Record once against a real judge, save the JSON, then load and replay it in tests so the suite never depends on network access or nondeterministic model output:
tape = Feelings::Tape.from_json(File.read("test/fixtures/spam_check.json"))
result = Feelings.replay(tape) { Feelings(email).like?("spam") }
For unit tests that don’t need a prerecorded run at all, the Stub judge is usually simpler; tapes are for pinning the exact shape of a real judge’s response.
Next: Testing.