|
| 1 | +//! **Materialized awareness** — the closed `F → 34 → F` dispatch loop. |
| 2 | +//! |
| 3 | +//! The 34 reasoning tactics ([`crate::recipe_kernels`]) are *dispatch targets*; this |
| 4 | +//! module supplies the **missing wire**: a selector that maps the live awareness |
| 5 | +//! state to one of the 34, and a loop driver that runs it, folds the outcome back, |
| 6 | +//! and re-dispatches until the gate settles. That closure is what makes awareness |
| 7 | +//! **materialize** rather than sit inert. |
| 8 | +//! |
| 9 | +//! ## The materialization criterion (falsifiable) |
| 10 | +//! |
| 11 | +//! Awareness *materializes* iff it is **causal in dispatch** — the encoded awareness |
| 12 | +//! changes *which tactic fires*. If perturbing the awareness state leaves the |
| 13 | +//! dispatched tactic invariant, the awareness is a **dead label** (the |
| 14 | +//! "awareness that can never materialize" failure). [`awareness_is_causal`] is the |
| 15 | +//! predicate; [`select_tactic`] makes `free_energy` (surprise) the **primary** axis |
| 16 | +//! exactly so that perturbing it crosses a band boundary and changes the dispatch. |
| 17 | +//! |
| 18 | +//! ## The loop (active inference, not a metaphor) |
| 19 | +//! |
| 20 | +//! ```text |
| 21 | +//! awareness state ──select_tactic──► one of the 34 ──run──► fold delta_conf |
| 22 | +//! ▲ │ settle gate (sd↓, dissonance↓) |
| 23 | +//! └────────────── recompute free-energy ◄──────────────────┘ |
| 24 | +//! rest when the CollapseGate enters FLOW (sd < SD_FLOW) — surprise resolved. |
| 25 | +//! ``` |
| 26 | +//! |
| 27 | +//! Awareness is not *read by* a controller that decides to think; it *is* the |
| 28 | +//! gradient that selects the next tactic. The loop rests when the gate settles — |
| 29 | +//! guaranteed, because attending decays dispersion each fired step. |
| 30 | +//! |
| 31 | +//! Zero-dep, deterministic, offline-tested. This is the reduction-to-practice for |
| 32 | +//! the 2³-rung → NARS-candidate → 34-tactic doctrine; persisting the dispatch trace |
| 33 | +//! into a SoA EdgeColumn / version-diff log (the "what fired and why" provenance) is |
| 34 | +//! the separate driver-side wire. |
| 35 | +
|
| 36 | +use crate::recipe_kernels::{kernel, GateState, ThoughtCtx, SD_BLOCK, SD_FLOW}; |
| 37 | +use crate::recipes::{recipe, Bucket, Mechanism, Tier}; |
| 38 | + |
| 39 | +/// Homeostasis floor mirroring `grammar::free_energy` (0.2): below this residual |
| 40 | +/// surprise the loop is considered at rest. (The loop's *termination* uses the |
| 41 | +/// CollapseGate FLOW transition, which is guaranteed by dispersion decay; this |
| 42 | +/// constant is the reported-surprise rest threshold.) |
| 43 | +pub const HOMEOSTASIS_FLOOR: f32 = 0.2; |
| 44 | + |
| 45 | +/// Per-fired-step dispersion settle factor — attending reduces gate dispersion, |
| 46 | +/// guaranteeing the loop reaches FLOW (rest) in `log_{1/0.85}(sd0/SD_FLOW)` steps. |
| 47 | +const SETTLE_SD: f32 = 0.85; |
| 48 | +/// Per-fired-step contradiction relaxation — engaging a tactic reconciles split. |
| 49 | +const SETTLE_DISSONANCE: f32 = 0.6; |
| 50 | + |
| 51 | +/// Re-derive the CollapseGate state from dispersion (`ThoughtCtx::gate_state` is |
| 52 | +/// private; the thresholds `SD_FLOW`/`SD_BLOCK` are public). |
| 53 | +fn gate_of(sd: f32) -> GateState { |
| 54 | + if sd < SD_FLOW { |
| 55 | + GateState::Flow |
| 56 | + } else if sd <= SD_BLOCK { |
| 57 | + GateState::Hold |
| 58 | + } else { |
| 59 | + GateState::Block |
| 60 | + } |
| 61 | +} |
| 62 | + |
| 63 | +/// **The selector** — map the awareness state to one of the 34 tactic ids (1..=34). |
| 64 | +/// |
| 65 | +/// **`free_energy` (surprise) is the primary axis** — this is what makes awareness |
| 66 | +/// *causal* in dispatch (the materialization criterion): a `free_energy` change that |
| 67 | +/// crosses a band boundary changes the chosen mechanism, hence the tactic. |
| 68 | +/// `dissonance` (contradiction → reconcile), `sd` (gate → execution bucket), and |
| 69 | +/// `rung` (depth → difficulty tier) are secondary modulators. Deterministic; scores |
| 70 | +/// every recipe by metadata match and takes the lowest id on a tie. |
| 71 | +pub fn select_tactic(ctx: &ThoughtCtx) -> u8 { |
| 72 | + // What kind of reasoning does this awareness state call for? |
| 73 | + let want_mech = if ctx.dissonance >= 0.5 { |
| 74 | + Mechanism::TruthAwareInference // a contradiction wants revision/abduction |
| 75 | + } else if ctx.free_energy >= 0.66 { |
| 76 | + Mechanism::StructuralDivergence // high surprise wants a creative leap |
| 77 | + } else if ctx.free_energy >= 0.33 { |
| 78 | + Mechanism::TruthAwareInference // mid surprise wants inference |
| 79 | + } else { |
| 80 | + Mechanism::ParallelIndependence // low surprise: routine parallel work |
| 81 | + }; |
| 82 | + // Where should it execute? (the gate picks the hardware bucket) |
| 83 | + let want_bucket = match gate_of(ctx.sd) { |
| 84 | + GateState::Block => Bucket::Gate, |
| 85 | + GateState::Hold => Bucket::Control, |
| 86 | + GateState::Flow => Bucket::Datapath, |
| 87 | + }; |
| 88 | + // How hard is the rung? (depth picks the difficulty tier) |
| 89 | + let want_tier = if ctx.rung >= 7 { |
| 90 | + Tier::ExtremelyHard |
| 91 | + } else if ctx.rung >= 4 { |
| 92 | + Tier::Hard |
| 93 | + } else { |
| 94 | + Tier::CrossTier |
| 95 | + }; |
| 96 | + |
| 97 | + let mut best_score = i32::MIN; |
| 98 | + let mut best_id = 1u8; |
| 99 | + for id in 1..=34u8 { |
| 100 | + if let Some(r) = recipe(id) { |
| 101 | + let mut score = 0; |
| 102 | + if r.mechanism == want_mech { |
| 103 | + score += 3; |
| 104 | + } |
| 105 | + if r.bucket == want_bucket { |
| 106 | + score += 2; |
| 107 | + } |
| 108 | + if r.tier == want_tier { |
| 109 | + score += 1; |
| 110 | + } |
| 111 | + if score > best_score { |
| 112 | + best_score = score; |
| 113 | + best_id = id; |
| 114 | + } |
| 115 | + } |
| 116 | + } |
| 117 | + best_id |
| 118 | +} |
| 119 | + |
| 120 | +/// One dispatch step: the tactic the awareness state selected, and what it did. |
| 121 | +#[derive(Debug, Clone, Copy, PartialEq)] |
| 122 | +pub struct Step { |
| 123 | + /// The selected tactic id (1..=34). |
| 124 | + pub tactic_id: u8, |
| 125 | + /// Did the tactic's gate let it fire? |
| 126 | + pub fired: bool, |
| 127 | + /// Confidence delta the tactic applied. |
| 128 | + pub delta_conf: f32, |
| 129 | +} |
| 130 | + |
| 131 | +/// Recompute free energy (surprise) from the resolved state — the loop closure. |
| 132 | +/// Surprise falls as confidence rises and as the gate (`sd`) and contradiction |
| 133 | +/// (`dissonance`) settle. Reported for the rest check; the loop *terminates* on the |
| 134 | +/// gate reaching FLOW (guaranteed by dispersion decay). |
| 135 | +pub fn recompute_free_energy(ctx: &ThoughtCtx) -> f32 { |
| 136 | + ((1.0 - ctx.confidence) * 0.4 + ctx.dissonance * 0.3 + ctx.sd.clamp(0.0, 1.0) * 0.3) |
| 137 | + .clamp(0.0, 1.0) |
| 138 | +} |
| 139 | + |
| 140 | +/// The trace of a materialized-awareness run — the "what fired and why" provenance. |
| 141 | +#[derive(Debug, Clone, PartialEq)] |
| 142 | +pub struct Trace { |
| 143 | + /// The ordered dispatch steps. |
| 144 | + pub steps: Vec<Step>, |
| 145 | + /// Did the loop settle into FLOW (rest), vs hit `max_steps`? |
| 146 | + pub rested: bool, |
| 147 | + /// Confidence at rest. |
| 148 | + pub final_confidence: f32, |
| 149 | + /// Residual surprise at rest. |
| 150 | + pub final_free_energy: f32, |
| 151 | +} |
| 152 | + |
| 153 | +/// **The closed `F → 34 → F` loop.** Each step: if the gate is in FLOW the loop |
| 154 | +/// rests (surprise resolved); else select a tactic from the awareness state, run it |
| 155 | +/// (folding `delta_conf` into confidence), settle the gate (dispersion + contradiction |
| 156 | +/// decay — attending reconciles), and recompute surprise. `max_steps` bounds the run; |
| 157 | +/// rest is *guaranteed* within `~log_{1/SETTLE_SD}(sd/SD_FLOW)` fired steps because |
| 158 | +/// dispersion decays monotonically into FLOW. |
| 159 | +pub fn materialize(ctx: &mut ThoughtCtx, max_steps: usize) -> Trace { |
| 160 | + let mut steps = Vec::with_capacity(max_steps); |
| 161 | + for _ in 0..max_steps { |
| 162 | + if gate_of(ctx.sd) == GateState::Flow { |
| 163 | + break; // settled — the shader rests |
| 164 | + } |
| 165 | + let id = select_tactic(ctx); |
| 166 | + let Some(tactic) = kernel(id) else { |
| 167 | + break; // unreachable: id is always 1..=34 |
| 168 | + }; |
| 169 | + let out = tactic.run(ctx); // folds out.delta_conf into ctx.confidence |
| 170 | + ctx.sd *= SETTLE_SD; // attending settles dispersion → toward FLOW |
| 171 | + ctx.dissonance *= SETTLE_DISSONANCE; |
| 172 | + ctx.free_energy = recompute_free_energy(ctx); |
| 173 | + steps.push(Step { |
| 174 | + tactic_id: id, |
| 175 | + fired: out.fired, |
| 176 | + delta_conf: out.delta_conf, |
| 177 | + }); |
| 178 | + } |
| 179 | + Trace { |
| 180 | + rested: gate_of(ctx.sd) == GateState::Flow, |
| 181 | + final_confidence: ctx.confidence, |
| 182 | + final_free_energy: ctx.free_energy, |
| 183 | + steps, |
| 184 | + } |
| 185 | +} |
| 186 | + |
| 187 | +/// **The materialization predicate.** Does perturbing `free_energy` change the |
| 188 | +/// dispatched tactic? `true` ⇒ awareness is causal in dispatch (materialized); |
| 189 | +/// `false` ⇒ the awareness encoding is inert for this base state. The falsifier the |
| 190 | +/// whole doctrine rests on. |
| 191 | +pub fn awareness_is_causal(base: &ThoughtCtx, lo_f: f32, hi_f: f32) -> bool { |
| 192 | + let mut a = base.clone(); |
| 193 | + a.free_energy = lo_f; |
| 194 | + let mut b = base.clone(); |
| 195 | + b.free_energy = hi_f; |
| 196 | + select_tactic(&a) != select_tactic(&b) |
| 197 | +} |
| 198 | + |
| 199 | +#[cfg(test)] |
| 200 | +mod tests { |
| 201 | + use super::*; |
| 202 | + use std::collections::BTreeSet; |
| 203 | + |
| 204 | + fn base() -> ThoughtCtx { |
| 205 | + // Hold gate (sd in (FLOW, BLOCK]), no contradiction, shallow rung — so |
| 206 | + // free_energy is the lone moving part for the materialization probe. |
| 207 | + let mut c = ThoughtCtx::new(vec![0.9, 0.6, 0.3]); |
| 208 | + c.sd = 0.25; |
| 209 | + c.dissonance = 0.0; |
| 210 | + c.rung = 1; |
| 211 | + c |
| 212 | + } |
| 213 | + |
| 214 | + #[test] |
| 215 | + fn awareness_free_energy_is_causal_in_dispatch() { |
| 216 | + // The materialization criterion: perturbing surprise changes the tactic. |
| 217 | + let b = base(); |
| 218 | + assert!( |
| 219 | + awareness_is_causal(&b, 0.1, 0.9), |
| 220 | + "free_energy must steer dispatch — else awareness is a dead label" |
| 221 | + ); |
| 222 | + // Sweep free_energy: dispatch must take ≥ 2 distinct tactics (not stuck). |
| 223 | + let ids: BTreeSet<u8> = (0..=10) |
| 224 | + .map(|i| { |
| 225 | + let mut c = base(); |
| 226 | + c.free_energy = i as f32 / 10.0; |
| 227 | + select_tactic(&c) |
| 228 | + }) |
| 229 | + .collect(); |
| 230 | + assert!( |
| 231 | + ids.len() >= 2, |
| 232 | + "free_energy sweep must vary the tactic, got {ids:?}" |
| 233 | + ); |
| 234 | + } |
| 235 | + |
| 236 | + #[test] |
| 237 | + fn non_awareness_fields_are_inert() { |
| 238 | + // Specificity: fields the selector does NOT read (candidates, beliefs) must |
| 239 | + // NOT change dispatch — awareness drives it, not arbitrary state noise. |
| 240 | + let a = base(); |
| 241 | + let mut b = base(); |
| 242 | + b.candidates = vec![0.01, 0.99, 0.5, 0.5, 0.2]; |
| 243 | + b.beliefs = vec![(7, 0.9, 0.8), (7, 0.1, 0.7)]; |
| 244 | + assert_eq!( |
| 245 | + select_tactic(&a), |
| 246 | + select_tactic(&b), |
| 247 | + "candidates/beliefs are not awareness — must not steer dispatch" |
| 248 | + ); |
| 249 | + } |
| 250 | + |
| 251 | + #[test] |
| 252 | + fn selector_ranges_over_the_34() { |
| 253 | + // Across a state sweep the selector must reach a variety of the 34 (it is |
| 254 | + // not a degenerate constant) — and every id it returns is a real kernel. |
| 255 | + let mut seen = BTreeSet::new(); |
| 256 | + for &fe in &[0.05f32, 0.4, 0.8] { |
| 257 | + for &diss in &[0.0f32, 0.7] { |
| 258 | + for &sd in &[0.10f32, 0.25, 0.45] { |
| 259 | + for &rung in &[1u8, 5, 8] { |
| 260 | + let mut c = base(); |
| 261 | + c.free_energy = fe; |
| 262 | + c.dissonance = diss; |
| 263 | + c.sd = sd; |
| 264 | + c.rung = rung; |
| 265 | + let id = select_tactic(&c); |
| 266 | + assert!((1..=34).contains(&id) && kernel(id).is_some()); |
| 267 | + seen.insert(id); |
| 268 | + } |
| 269 | + } |
| 270 | + } |
| 271 | + } |
| 272 | + assert!( |
| 273 | + seen.len() >= 4, |
| 274 | + "selector must range over the 34, got {seen:?}" |
| 275 | + ); |
| 276 | + } |
| 277 | + |
| 278 | + #[test] |
| 279 | + fn loop_rests_when_the_gate_settles() { |
| 280 | + // Hot start: high surprise, low confidence, a contradiction. The loop must |
| 281 | + // dispatch real tactics and settle into FLOW (rest) within a few steps. |
| 282 | + let mut c = base(); |
| 283 | + c.sd = 0.32; // Hold, near Block |
| 284 | + c.free_energy = 0.9; |
| 285 | + c.confidence = 0.1; |
| 286 | + c.dissonance = 0.5; |
| 287 | + let trace = materialize(&mut c, 64); |
| 288 | + assert!(trace.rested, "loop must reach FLOW, got {trace:?}"); |
| 289 | + assert!( |
| 290 | + !trace.steps.is_empty(), |
| 291 | + "a hot start must dispatch at least once" |
| 292 | + ); |
| 293 | + assert!( |
| 294 | + trace.steps.len() <= 12, |
| 295 | + "settles fast, got {}", |
| 296 | + trace.steps.len() |
| 297 | + ); |
| 298 | + for s in &trace.steps { |
| 299 | + assert!((1..=34).contains(&s.tactic_id) && kernel(s.tactic_id).is_some()); |
| 300 | + } |
| 301 | + } |
| 302 | + |
| 303 | + #[test] |
| 304 | + fn loop_is_deterministic() { |
| 305 | + let (mut a, mut b) = (base(), base()); |
| 306 | + for c in [&mut a, &mut b] { |
| 307 | + c.sd = 0.32; |
| 308 | + c.free_energy = 0.9; |
| 309 | + c.confidence = 0.1; |
| 310 | + c.dissonance = 0.5; |
| 311 | + } |
| 312 | + assert_eq!(materialize(&mut a, 64), materialize(&mut b, 64)); |
| 313 | + } |
| 314 | + |
| 315 | + #[test] |
| 316 | + fn already_at_rest_dispatches_nothing() { |
| 317 | + // FLOW on entry (sd < SD_FLOW) ⇒ no surprise ⇒ no dispatch (the shader rests). |
| 318 | + let mut c = base(); |
| 319 | + c.sd = 0.05; |
| 320 | + let trace = materialize(&mut c, 64); |
| 321 | + assert!(trace.rested && trace.steps.is_empty()); |
| 322 | + } |
| 323 | +} |
0 commit comments