diff --git a/HANDOVER.md b/HANDOVER.md index 0d10f0a..fcd7d0d 100644 --- a/HANDOVER.md +++ b/HANDOVER.md @@ -1,6 +1,6 @@ # Session Handover — 2026-02-15 -## Branch: `claude/ada-rs-consolidation-6nvNm` +## Branch: `claude/pr-123-handover-Wx4VA` ## What Was Built (Recent Sessions — Qualia Module Stack) @@ -63,6 +63,30 @@ order: system's attentional gravity map. - Verb: `VERB_VOLITION=0xFA` +#### Layer 8: Dream–Reflection Bridge (this session) +- **`dream_bridge.rs`** (~280 lines, 7 tests) — Connects ghost resonance to dream consolidation: + - `GhostRecord`: Sibling bundle packaged for dream input (branch DN, bundle, resonance, depth) + - `harvest_ghosts()`: Extract high-resonance sibling bundles from FeltPath + - `ghosts_to_records()`: Package ghosts as CogRecords (low NARS confidence, neutral frequency) + - `DreamReflectionConfig`: Ghost threshold, dream config, injection params + - `dream_reflection_cycle()`: Full integration — harvest ghosts → combine with session records → + dream consolidation → match novels against Explore nodes → XOR-inject as hydration context + - `dream_consolidate_with_ghosts()`: Lightweight variant (no injection) + - Verb: `VERB_DREAM_GHOST=0xF9` + +#### Layer 9: MUL–Reflection Bridge (this session) +- **`mul_bridge.rs`** (~630 lines, 11 tests) — MUL metacognitive state driving reflection: + - `AdaptiveThresholds`: Surprise/confidence thresholds adapted by MUL state + - Trust modulation: Crystalline→+0.05, Dissonant→-0.08 + - Homeostasis: Anxiety→conservative(+0.05), Boredom→aggressive(-0.05), Apathy→minimal(+0.08) + - False flow override: Severe→force explore (threshold=0.3) + - `mul_council_weights()`: Homeostasis-modulated council weights + (Anxiety→Guardian dominant, Boredom→Catalyst dominant) + - `reclassify_with_thresholds()`: Re-evaluate ReflectionEntries with MUL-adapted thresholds + - `mul_volitional_cycle()`: MUL-gated volitional cycle (gate check → council → reflect → reclassify) + - `reflection_to_mul_learning()`: Convert reflection outcomes → MUL PostActionLearning signal + - `mul_reflection_feedback()`: Full feedback loop — reflect, compute learning signal, feed back to MUL + ### ARCHITECTURE.md — Comprehensive Extension (commit `05010ee`) Extended from 402 → 1,649 lines. Preserved existing CAM/scent-index sections @@ -129,7 +153,25 @@ drops), structural mismatch (no legal parse). All three ARE free energy concepts — the system can't reduce surprise at the current abstraction level, so it elevates to a deeper rung. -### 6. Volition = Integrated Decision Score (Closes the Loop) +### 6. MUL State Modulates Reflection Sensitivity (New Bridge) + +MUL state IS the system's prediction about its own epistemic capacity. +Reflection measures how well the tree structure predicts content (surprise). +The bridge connects these: the system's self-assessment (MUL) modulates +how aggressively it responds to prediction errors (reflection). Adaptive +thresholds shift based on trust level, homeostasis state, and false flow. +Feedback loop: reflection outcomes → PostActionLearning → DK + trust update. + +### 7. Dream Ghosts = Cross-Hydration from Uncollapsed Context (New Bridge) + +Ghost vectors (sibling bundles from felt traversal) have high resonance +but low confidence — they're contextual but unconfirmed. Dream consolidation +prunes the weak, merges the similar, and RECOMBINES to generate creative +novels. When a dream novel matches an Explore node, it's XOR-injected as +hydration context — the system literally dreams about its unresolved thoughts +and the dreams inform its next exploration. + +### 8. Volition = Integrated Decision Score (Closes the Loop) Volition score = `free_energy × ghost_intensity × (1 - confidence) × rung_weight`. Four orthogonal signals: urgency (surprise), felt relevance (ghost resonance), @@ -156,12 +198,11 @@ The system now has a complete sense→feel→reflect→decide cycle. ### High Priority — Next Code Steps 1. ~~**Volition module**~~ — DONE (commit `75f94fa`, 8/8 tests pass) -2. **Dream consolidation integration** — Connect lingering ghosts (bighorn) to - reflection's hydration candidates. High-echo ghosts should surface during - dream processing and become hydration context. -3. **MUL → Reflection bridge** — The MUL's 10-layer snapshot should feed into - `reflect_walk()` as the query container. MUL state IS the system's prediction; - reflection measures how well it matches reality. +2. ~~**Dream consolidation integration**~~ — DONE (`dream_bridge.rs`, 7/7 tests pass) + Ghost harvesting from felt paths → dream consolidation → XOR-inject into Explore nodes. +3. ~~**MUL → Reflection bridge**~~ — DONE (`mul_bridge.rs`, 11/11 tests pass) + Adaptive thresholds from MUL state, council modulation, gated volitional cycle, + full feedback loop (reflection outcomes → MUL learning). ### Medium Priority — Wiring @@ -186,10 +227,17 @@ The system now has a complete sense→feel→reflect→decide cycle. | File | Status | What | |------|--------|------| -| `src/qualia/volition.rs` | NEW, ~600 lines | VolitionalAct, VolitionalAgenda, CouncilWeights, volitional_cycle | -| `src/qualia/reflection.rs` | NEW, 753 lines | NARS bridge, ReflectionOutcome, HydrationChain, FreeEnergySemiring | -| `src/qualia/mod.rs` | MODIFIED | Added volition + reflection wiring + re-exports | -| `ARCHITECTURE.md` | EXTENDED +1247 lines | 17 new sections covering full container substrate | +| `src/qualia/dream_bridge.rs` | NEW, ~280 lines | GhostRecord, harvest_ghosts, dream_reflection_cycle | +| `src/qualia/mul_bridge.rs` | NEW, ~630 lines | AdaptiveThresholds, mul_volitional_cycle, mul_reflection_feedback | +| `src/qualia/mod.rs` | MODIFIED | Added dream_bridge + mul_bridge wiring + re-exports | + +### Key Files (Prior Session) + +| File | Status | What | +|------|--------|------| +| `src/qualia/volition.rs` | ~600 lines | VolitionalAct, VolitionalAgenda, CouncilWeights, volitional_cycle | +| `src/qualia/reflection.rs` | 753 lines | NARS bridge, ReflectionOutcome, HydrationChain, FreeEnergySemiring | +| `ARCHITECTURE.md` | +1247 lines | 17 new sections covering full container substrate | ## Key Files To Know (Full Stack) @@ -216,6 +264,8 @@ The system now has a complete sense→feel→reflect→decide cycle. | `src/qualia/felt_traversal.rs` | FeltPath, FeltChoice, AweTriple, free energy | | `src/qualia/reflection.rs` | ReflectionOutcome, HydrationChain, FreeEnergySemiring | | `src/qualia/volition.rs` | VolitionalAct, VolitionalAgenda, CouncilWeights, volitional_cycle | +| `src/qualia/dream_bridge.rs` | Ghost harvesting, dream consolidation integration, XOR-injection | +| `src/qualia/mul_bridge.rs` | Adaptive thresholds, MUL-gated volitional cycle, feedback loop | | **Cognitive** | | | `src/cognitive/rung.rs` | RungLevel R0-R9, 3 triggers, RungState | | `src/cognitive/collapse_gate.rs` | GateState (Flow/Block) | @@ -232,21 +282,24 @@ The system now has a complete sense→feel→reflect→decide cycle. ## Cargo Status -- `cargo check` — GREEN +- `cargo check` — GREEN (only pre-existing warnings: chess VsaOps, server.rs unused assignment) +- `cargo test dream_bridge` — 7/7 PASS +- `cargo test mul_bridge` — 11/11 PASS - `cargo test qualia::volition` — 8/8 PASS - `cargo test qualia::reflection` — 13/13 PASS -- All qualia tests pass (79/80 — 1 pre-existing flaky gestalt test) +- All qualia tests pass ## Git State -All repos on branch `claude/ada-rs-consolidation-6nvNm`. Latest commits: +Branch: `claude/pr-123-handover-Wx4VA`. Latest commits (new on top): ``` -75f94fa feat(qualia): volition module — self-directed action selection via free energy + ghost resonance + council -02e95dc docs: update session handover with qualia stack + architectural insights -05010ee feat(qualia): reflection module + comprehensive architecture docs -6824bf8 feat(qualia): felt traversal — sibling superposition, awe triples, Friston free energy -e816031 feat(qualia): Gestalt I/Thou/It frame — SPO role binding, cross-perspective, collapse gate -eef6219 feat(qualia): HDR resonance, triangle council, focus mask — awareness without collapse -23e29de feat(qualia): add 48-axis meaning encoder, inner council, causal opcodes, and epiphany detector + feat(qualia): dream–reflection bridge + MUL–reflection bridge +75f94fa feat(qualia): volition module — self-directed action selection via free energy + ghost resonance + council +02e95dc docs: update session handover with qualia stack + architectural insights +05010ee feat(qualia): reflection module + comprehensive architecture docs +6824bf8 feat(qualia): felt traversal — sibling superposition, awe triples, Friston free energy +e816031 feat(qualia): Gestalt I/Thou/It frame — SPO role binding, cross-perspective, collapse gate +eef6219 feat(qualia): HDR resonance, triangle council, focus mask — awareness without collapse +23e29de feat(qualia): add 48-axis meaning encoder, inner council, causal opcodes, and epiphany detector ``` diff --git a/src/qualia/dream_bridge.rs b/src/qualia/dream_bridge.rs new file mode 100644 index 0000000..f103675 --- /dev/null +++ b/src/qualia/dream_bridge.rs @@ -0,0 +1,488 @@ +//! Dream–Reflection Bridge — Connecting Ghost Resonance to Dream Consolidation +//! +//! This module bridges two existing systems: +//! +//! 1. **Reflection** (`qualia::reflection`) identifies nodes needing attention: +//! - `Explore` nodes have sibling bundles (ghost vectors) waiting to be used +//! - `Revise` nodes have high surprise against their current beliefs +//! - Hydration chains carry context deltas between siblings +//! +//! 2. **Dream consolidation** (`learning::dream`) operates on CogRecord batches: +//! prune low-confidence, merge similar, generate creative recombinations. +//! +//! ## The Missing Link +//! +//! Ghost vectors from sibling bundles (felt traversal) should surface during +//! dream consolidation. High-echo ghosts become inputs to the dream pipeline, +//! and dream-produced novels become hydration context for Explore nodes. +//! +//! ```text +//! VolitionalAgenda → ghost vectors (high resonance siblings) +//! ↓ +//! DreamConsolidation(ghost_records + session_records) +//! ↓ +//! Dream novels → inject as hydration context for Explore nodes +//! ``` +//! +//! ## Ghost Harvesting +//! +//! During felt traversal, each branch records a sibling bundle (XOR-fold of +//! all siblings). High-resonance bundles indicate branches where the unchosen +//! paths are contextually relevant — these are the "lingering ghosts" that +//! should influence dream processing. +//! +//! Ghost harvesting extracts these high-resonance bundles as CogRecords +//! suitable for dream consolidation input. + +use crate::container::{Container, CogRecord, ContainerGeometry, CONTAINER_BITS}; +use crate::container::graph::ContainerGraph; +use crate::container::adjacency::PackedDn; +use crate::learning::dream::{DreamConfig, consolidate_with_config}; + +use super::reflection::{ + ReflectionOutcome, write_truth, +}; +use super::volition::VolitionalAgenda; +use super::felt_traversal::FeltPath; +use ladybug_contract::nars::TruthValue; + +// ============================================================================= +// CONSTANTS +// ============================================================================= + +/// Minimum bundle resonance for a ghost to be harvested. +/// Ghosts below this threshold are too weakly resonant to influence dreams. +pub const GHOST_RESONANCE_THRESHOLD: f32 = 0.4; + +/// NARS confidence assigned to ghost records (uncertain, contextual). +pub const GHOST_INITIAL_CONFIDENCE: f32 = 0.25; + +/// NARS frequency assigned to ghost records (speculative). +pub const GHOST_INITIAL_FREQUENCY: f32 = 0.5; + +/// NARS confidence assigned to dream-produced records injected as hydration context. +pub const DREAM_INJECT_CONFIDENCE: f32 = 0.3; + +/// Verb ID for "dream ghost" edges — marks nodes sourced from dream consolidation. +pub const VERB_DREAM_GHOST: u8 = 0xF9; + +// ============================================================================= +// GHOST RECORD — A sibling bundle packaged for dream input +// ============================================================================= + +/// A ghost record harvested from a felt path's sibling bundles. +#[derive(Debug, Clone)] +pub struct GhostRecord { + /// The branch DN where this ghost was observed. + pub branch_dn: PackedDn, + /// The sibling bundle (XOR-fold of all siblings at this branch). + pub bundle: Container, + /// Resonance of the query against this bundle. + pub resonance: f32, + /// Depth in the tree. + pub depth: u8, +} + +// ============================================================================= +// GHOST HARVESTING — Extract high-resonance ghosts from felt paths +// ============================================================================= + +/// Harvest ghost records from a felt path. +/// +/// Selects sibling bundles whose resonance exceeds the threshold, +/// packages them as CogRecords with low confidence (speculative), +/// ready for dream consolidation input. +pub fn harvest_ghosts(felt_path: &FeltPath, threshold: f32) -> Vec { + felt_path + .choices + .iter() + .filter(|choice| choice.bundle_resonance >= threshold) + .map(|choice| GhostRecord { + branch_dn: choice.branch_dn, + bundle: choice.sibling_bundle.clone(), + resonance: choice.bundle_resonance, + depth: choice.depth, + }) + .collect() +} + +/// Convert ghost records into CogRecords suitable for dream consolidation. +/// +/// Each ghost becomes a CogRecord with: +/// - Content = sibling bundle (the ghost vector) +/// - Geometry = Cam (compatible with standard containers) +/// - Confidence = low (speculative, from uncollapsed context) +/// - Frequency = 0.5 (neutral — neither confirmed nor denied) +pub fn ghosts_to_records(ghosts: &[GhostRecord]) -> Vec { + ghosts + .iter() + .map(|ghost| { + let mut record = CogRecord::new(ContainerGeometry::Cam); + record.content = ghost.bundle.clone(); + record.meta_view_mut().set_nars_confidence(GHOST_INITIAL_CONFIDENCE); + record.meta_view_mut().set_nars_frequency(GHOST_INITIAL_FREQUENCY); + record + }) + .collect() +} + +// ============================================================================= +// DREAM-REFLECTION INTEGRATION +// ============================================================================= + +/// Configuration for dream-reflection integration. +#[derive(Debug, Clone)] +pub struct DreamReflectionConfig { + /// Ghost resonance threshold for harvesting. + pub ghost_threshold: f32, + /// Dream consolidation config. + pub dream_config: DreamConfig, + /// Maximum number of dream novels to inject per Explore node. + pub max_inject_per_node: usize, + /// Minimum similarity between dream novel and Explore node for injection. + pub inject_similarity_threshold: f32, +} + +impl Default for DreamReflectionConfig { + fn default() -> Self { + Self { + ghost_threshold: GHOST_RESONANCE_THRESHOLD, + dream_config: DreamConfig { + prune_confidence_threshold: 0.15, // lower than default — ghosts start low + recombination_count: 8, // more novels from ghost context + ..DreamConfig::default() + }, + max_inject_per_node: 3, + inject_similarity_threshold: 0.35, + } + } +} + +/// Result of dream-reflection integration. +#[derive(Debug, Clone)] +pub struct DreamReflectionResult { + /// Ghost records harvested from the felt path. + pub ghosts_harvested: usize, + /// Total records fed to dream consolidation (session + ghosts). + pub dream_input_count: usize, + /// Records produced by dream consolidation. + pub dream_output_count: usize, + /// Novel dream records injected as hydration context. + pub injections: Vec, +} + +/// A single dream injection: a dream-produced novel matched to an Explore node. +#[derive(Debug, Clone)] +pub struct DreamInjection { + /// The Explore node that received this context. + pub target_dn: PackedDn, + /// The dream-produced novel content. + pub novel_content: Container, + /// Similarity between the novel and the Explore node's content. + pub similarity: f32, +} + +/// Run dream consolidation on session records enriched with ghost context, +/// then inject dream novels into Explore nodes as hydration context. +/// +/// This is the full dream-reflection integration cycle: +/// +/// 1. Harvest high-resonance ghosts from the volitional agenda's felt path +/// 2. Combine ghost records with session records +/// 3. Run dream consolidation on the combined set +/// 4. Match dream-produced novels against Explore candidates +/// 5. Inject matching novels as hydration context (update content + NARS) +pub fn dream_reflection_cycle( + graph: &mut ContainerGraph, + agenda: &VolitionalAgenda, + session_records: &[CogRecord], + config: &DreamReflectionConfig, +) -> DreamReflectionResult { + // Step 1: Harvest ghosts from the felt path + let ghosts = harvest_ghosts(&agenda.reflection.felt_path, config.ghost_threshold); + let ghosts_harvested = ghosts.len(); + let ghost_records = ghosts_to_records(&ghosts); + + // Step 2: Combine session records with ghost records + let mut dream_input: Vec = session_records.to_vec(); + dream_input.extend(ghost_records); + let dream_input_count = dream_input.len(); + + // Step 3: Run dream consolidation + let dream_output = consolidate_with_config(&dream_input, &config.dream_config); + let dream_output_count = dream_output.len(); + + // Step 4: Identify dream-produced novels (records not in original input) + // Novels are records whose content differs significantly from all inputs. + let novels: Vec<&CogRecord> = dream_output + .iter() + .filter(|output| { + // A record is "novel" if it doesn't closely match any input + !dream_input.iter().any(|input| { + input.content.hamming(&output.content) < (CONTAINER_BITS as u32 / 8) + }) + }) + .collect(); + + // Step 5: Match novels against Explore candidates and inject + let explore_dns: Vec = agenda + .acts + .iter() + .filter(|act| act.outcome == ReflectionOutcome::Explore) + .map(|act| act.dn) + .collect(); + + let mut injections = Vec::new(); + + for &explore_dn in &explore_dns { + let explore_fp = match graph.fingerprint(&explore_dn) { + Some(fp) => fp.clone(), + None => continue, + }; + + // Find the best-matching novels for this Explore node + let mut matches: Vec<(&CogRecord, f32)> = novels + .iter() + .map(|novel| { + let sim = explore_fp.similarity(&novel.content); + (*novel, sim) + }) + .filter(|(_, sim)| *sim >= config.inject_similarity_threshold) + .collect(); + + matches.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)); + matches.truncate(config.max_inject_per_node); + + for (novel, similarity) in matches { + // Inject: XOR the dream novel into the Explore node's content + // This is cross-hydration with dream context + if let Some(record) = graph.get_mut(&explore_dn) { + let hydrated = record.content.xor(&novel.content); + record.content = hydrated; + + // Set initial truth value: low confidence, neutral frequency + write_truth(record, &TruthValue::new(GHOST_INITIAL_FREQUENCY, DREAM_INJECT_CONFIDENCE)); + } + + // Add a dream-ghost edge for provenance tracking + graph.add_edge(&explore_dn, VERB_DREAM_GHOST, 0); + + injections.push(DreamInjection { + target_dn: explore_dn, + novel_content: novel.content.clone(), + similarity, + }); + } + } + + DreamReflectionResult { + ghosts_harvested, + dream_input_count, + dream_output_count, + injections, + } +} + +/// Lightweight variant: harvest ghosts and consolidate, but don't inject. +/// +/// Returns the consolidated dream output for external processing. +pub fn dream_consolidate_with_ghosts( + felt_path: &FeltPath, + session_records: &[CogRecord], + config: &DreamReflectionConfig, +) -> Vec { + let ghosts = harvest_ghosts(felt_path, config.ghost_threshold); + let ghost_records = ghosts_to_records(&ghosts); + + let mut dream_input: Vec = session_records.to_vec(); + dream_input.extend(ghost_records); + + consolidate_with_config(&dream_input, &config.dream_config) +} + +// ============================================================================= +// TESTS +// ============================================================================= + +#[cfg(test)] +mod tests { + use super::*; + use crate::container::graph::ContainerGraph; + use crate::cognitive::RungLevel; + use super::super::volition::{CouncilWeights, volitional_cycle}; + use super::super::reflection::write_truth; + use ladybug_contract::nars::TruthValue; + + fn build_test_tree() -> ContainerGraph { + let mut graph = ContainerGraph::new(); + + let root = PackedDn::ROOT; + let mut root_rec = CogRecord::new(ContainerGeometry::Cam); + root_rec.content = Container::random(1); + graph.insert(root, root_rec); + + for (i, seed) in [(0u8, 10u64), (1, 20), (2, 30)] { + let dn = PackedDn::new(&[i]); + let mut rec = CogRecord::new(ContainerGeometry::Cam); + rec.content = Container::random(seed); + graph.insert(dn, rec); + } + + for (i, seed) in [(0u8, 100u64), (1, 101), (2, 102)] { + let dn = PackedDn::new(&[0, i]); + let mut rec = CogRecord::new(ContainerGeometry::Cam); + rec.content = Container::random(seed); + graph.insert(dn, rec); + } + + for (i, seed) in [(0u8, 200u64), (1, 201)] { + let dn = PackedDn::new(&[1, i]); + let mut rec = CogRecord::new(ContainerGeometry::Cam); + rec.content = Container::random(seed); + graph.insert(dn, rec); + } + + graph + } + + fn seed_nars(graph: &mut ContainerGraph) { + if let Some(rec) = graph.get_mut(&PackedDn::new(&[0])) { + write_truth(rec, &TruthValue::new(0.8, 0.9)); + } + if let Some(rec) = graph.get_mut(&PackedDn::new(&[1])) { + write_truth(rec, &TruthValue::new(0.5, 0.1)); + } + if let Some(rec) = graph.get_mut(&PackedDn::new(&[0, 1])) { + write_truth(rec, &TruthValue::new(0.7, 0.5)); + } + } + + fn make_session_records(count: usize) -> Vec { + (0..count) + .map(|i| { + let mut rec = CogRecord::new(ContainerGeometry::Cam); + rec.content = Container::random(1000 + i as u64); + rec.meta_view_mut().set_nars_confidence(0.6); + rec.meta_view_mut().set_nars_frequency(0.5); + rec + }) + .collect() + } + + #[test] + fn test_harvest_ghosts_empty_path() { + let path = FeltPath { + choices: vec![], + target: PackedDn::ROOT, + total_surprise: 0.0, + mean_surprise: 0.0, + path_context: Container::zero(), + }; + let ghosts = harvest_ghosts(&path, GHOST_RESONANCE_THRESHOLD); + assert!(ghosts.is_empty()); + } + + #[test] + fn test_harvest_ghosts_from_felt_path() { + let graph = build_test_tree(); + let query = Container::random(42); + let target = PackedDn::new(&[0, 1]); + + let path = super::super::felt_traversal::felt_walk(&graph, target, &query); + + // With threshold 0.0, should harvest all branch bundles + let all_ghosts = harvest_ghosts(&path, 0.0); + assert_eq!(all_ghosts.len(), path.choices.len()); + + // With very high threshold, should harvest none or few + let strict_ghosts = harvest_ghosts(&path, 0.99); + assert!(strict_ghosts.len() <= all_ghosts.len()); + } + + #[test] + fn test_ghosts_to_records() { + let ghosts = vec![ + GhostRecord { + branch_dn: PackedDn::ROOT, + bundle: Container::random(42), + resonance: 0.7, + depth: 0, + }, + GhostRecord { + branch_dn: PackedDn::new(&[0]), + bundle: Container::random(43), + resonance: 0.5, + depth: 1, + }, + ]; + + let records = ghosts_to_records(&ghosts); + assert_eq!(records.len(), 2); + + for rec in &records { + let conf = rec.meta_view().nars_confidence(); + assert!((conf - GHOST_INITIAL_CONFIDENCE).abs() < 1e-5); + let freq = rec.meta_view().nars_frequency(); + assert!((freq - GHOST_INITIAL_FREQUENCY).abs() < 1e-5); + } + } + + #[test] + fn test_dream_consolidate_with_ghosts() { + let graph = build_test_tree(); + let query = Container::random(42); + let target = PackedDn::new(&[0, 1]); + + let path = super::super::felt_traversal::felt_walk(&graph, target, &query); + let session = make_session_records(5); + let config = DreamReflectionConfig::default(); + + let output = dream_consolidate_with_ghosts(&path, &session, &config); + // Should produce some records (at least surviving session records) + assert!(!output.is_empty(), "dream should produce output"); + } + + #[test] + fn test_dream_reflection_cycle() { + let mut graph = build_test_tree(); + seed_nars(&mut graph); + + let query = Container::random(42); + let target = PackedDn::new(&[0, 1]); + let council = CouncilWeights::default(); + + let agenda = volitional_cycle( + &mut graph, target, &query, + RungLevel::Meta, &council, + ); + + let session = make_session_records(10); + let config = DreamReflectionConfig::default(); + + let result = dream_reflection_cycle(&mut graph, &agenda, &session, &config); + + assert!(result.dream_input_count >= 10, "input should include session records"); + assert!(result.dream_output_count > 0, "should have dream output"); + } + + #[test] + fn test_dream_reflection_config_defaults() { + let config = DreamReflectionConfig::default(); + assert!((config.ghost_threshold - GHOST_RESONANCE_THRESHOLD).abs() < 1e-5); + assert_eq!(config.max_inject_per_node, 3); + assert!(config.inject_similarity_threshold > 0.0); + } + + #[test] + fn test_ghost_record_structure() { + let ghost = GhostRecord { + branch_dn: PackedDn::new(&[1, 2]), + bundle: Container::random(999), + resonance: 0.65, + depth: 2, + }; + assert_eq!(ghost.depth, 2); + assert!(ghost.resonance > 0.5); + assert!(!ghost.bundle.is_zero()); + } +} diff --git a/src/qualia/mod.rs b/src/qualia/mod.rs index 2c7b6a9..4d652a7 100644 --- a/src/qualia/mod.rs +++ b/src/qualia/mod.rs @@ -11,6 +11,8 @@ //! - `felt_traversal`: Tree walk with sibling superposition, awe triples, free energy //! - `reflection`: NARS introspection, hydration chains, free energy semiring //! - `volition`: Self-directed action selection via free energy + ghost resonance + council +//! - `dream_bridge`: Ghost harvesting + dream consolidation → hydration injection +//! - `mul_bridge`: MUL metacognitive state → adaptive reflection thresholds + feedback pub mod texture; pub mod meaning_axes; @@ -20,6 +22,8 @@ pub mod gestalt; pub mod felt_traversal; pub mod reflection; pub mod volition; +pub mod dream_bridge; +pub mod mul_bridge; pub use texture::{GraphMetrics, Texture, compute}; pub use meaning_axes::{ @@ -49,3 +53,14 @@ pub use volition::{ CouncilWeights, VolitionalAct, VolitionalAgenda, VERB_VOLITION, compute_agenda, focused_volition, volitional_cycle, volitional_gradient, }; +pub use dream_bridge::{ + DreamInjection, DreamReflectionConfig, DreamReflectionResult, + GhostRecord, GHOST_RESONANCE_THRESHOLD, VERB_DREAM_GHOST, + dream_consolidate_with_ghosts, dream_reflection_cycle, + harvest_ghosts, ghosts_to_records, +}; +pub use mul_bridge::{ + AdaptiveThresholds, MulReflectionResult, + adaptive_thresholds, mul_council_weights, mul_reflection_feedback, + mul_volitional_cycle, reclassify_with_thresholds, reflection_to_mul_learning, +}; diff --git a/src/qualia/mul_bridge.rs b/src/qualia/mul_bridge.rs new file mode 100644 index 0000000..32ea69c --- /dev/null +++ b/src/qualia/mul_bridge.rs @@ -0,0 +1,645 @@ +//! MUL–Reflection Bridge — Metacognitive State Driving Reflection +//! +//! The MUL (Meta-Uncertainty Layer) produces a 10-layer snapshot of the +//! system's epistemic state. This module bridges that snapshot into the +//! reflection/volition pipeline: +//! +//! ```text +//! MUL Snapshot (L1-L10) +//! │ +//! ├─→ Trust qualia → adjust surprise thresholds +//! │ (low trust → lower threshold → more Explore/Revise) +//! │ +//! ├─→ Homeostasis state → modulate council weights +//! │ (Anxiety → Guardian dominant, Boredom → Catalyst dominant) +//! │ +//! ├─→ Free will modifier → gate volitional cycle +//! │ (low modifier → don't act, high → full agency) +//! │ +//! └─→ Reflection outcomes → feed back to MUL via learn() +//! (mean surprise → novelty, revision count → prediction accuracy) +//! ``` +//! +//! ## The Core Insight +//! +//! MUL state IS the system's prediction about its own epistemic capacity. +//! Reflection measures how well the tree structure predicts content (surprise). +//! The bridge connects these: the system's self-assessment (MUL) modulates +//! how aggressively it responds to prediction errors (reflection). +//! +//! ## Adaptive Thresholds +//! +//! The default surprise thresholds (0.55 high, 0.45 low) assume balanced +//! metacognition. When the MUL reports abnormal states, we adjust: +//! +//! - **Low trust** → lower surprise threshold → more nodes get Revise/Explore +//! (the system doesn't trust itself, so flags more for review) +//! - **High trust** → higher threshold → more Stable outcomes +//! (the system trusts its beliefs, only attends to extreme surprise) +//! - **Anxiety** → conservative thresholds (fewer Explore, more Stable) +//! - **Boredom** → aggressive thresholds (more Explore, fewer Stable) +//! - **False flow** → force Explore on everything (break the loop) + +use crate::container::Container; +use crate::container::graph::ContainerGraph; +use crate::container::adjacency::PackedDn; +use crate::cognitive::RungLevel; +use crate::mul::{ + MetaUncertaintyLayer, MulSnapshot, HomeostasisState, TrustLevel, + FalseFlowSeverity, CompassDecision, + PostActionLearning, +}; + +use super::reflection::{ + ReflectionResult, ReflectionOutcome, ReflectionEntry, + hydrate_explorers, + SURPRISE_HIGH, SURPRISE_LOW, CONFIDENCE_HIGH, +}; +use super::volition::{ + VolitionalAgenda, CouncilWeights, volitional_cycle, +}; + +// ============================================================================= +// ADAPTIVE THRESHOLDS +// ============================================================================= + +/// Surprise and confidence thresholds adapted by MUL state. +#[derive(Debug, Clone, Copy)] +pub struct AdaptiveThresholds { + /// Threshold for "high" surprise (above → Revise or Explore). + pub surprise_high: f32, + /// Threshold for "low" surprise (below → Confirm or Stable). + pub surprise_low: f32, + /// Threshold for "high" confidence. + pub confidence_high: f32, +} + +impl Default for AdaptiveThresholds { + fn default() -> Self { + Self { + surprise_high: SURPRISE_HIGH, + surprise_low: SURPRISE_LOW, + confidence_high: CONFIDENCE_HIGH, + } + } +} + +/// Compute adaptive thresholds from a MUL snapshot. +/// +/// The thresholds shift based on the system's metacognitive state: +/// - Trust level controls the surprise sensitivity +/// - Homeostasis state biases toward caution or exploration +/// - False flow severity can force aggressive exploration +pub fn adaptive_thresholds(snapshot: &MulSnapshot) -> AdaptiveThresholds { + let mut thresholds = AdaptiveThresholds::default(); + + // Trust modulation: low trust → lower surprise threshold → more flagging + let trust_shift = match snapshot.trust_level { + TrustLevel::Crystalline => 0.05, // raise thresholds (trust self) + TrustLevel::Solid => 0.02, + TrustLevel::Fuzzy => 0.0, // default + TrustLevel::Murky => -0.03, + TrustLevel::Dissonant => -0.08, // lower thresholds (don't trust self) + }; + thresholds.surprise_high += trust_shift; + thresholds.surprise_low += trust_shift; + + // Homeostasis modulation + match snapshot.homeostasis_state { + HomeostasisState::Flow => { + // Optimal — no adjustment + } + HomeostasisState::Anxiety => { + // Conservative: raise surprise threshold (fewer flags, less exploration) + thresholds.surprise_high += 0.05; + thresholds.surprise_low += 0.03; + thresholds.confidence_high -= 0.05; // lower bar for "confident enough" + } + HomeostasisState::Boredom => { + // Aggressive: lower surprise threshold (more flags, more exploration) + thresholds.surprise_high -= 0.05; + thresholds.surprise_low -= 0.03; + } + HomeostasisState::Apathy => { + // Minimal: raise everything (conserve energy) + thresholds.surprise_high += 0.08; + thresholds.surprise_low += 0.05; + } + } + + // False flow override: if coherent but not progressing, force exploration + match snapshot.false_flow_severity { + FalseFlowSeverity::None => {} + FalseFlowSeverity::Caution => { + thresholds.surprise_high -= 0.02; + } + FalseFlowSeverity::Warning => { + thresholds.surprise_high -= 0.04; + } + FalseFlowSeverity::Severe => { + // Everything looks surprising → breaks false flow loop + thresholds.surprise_high = 0.3; + thresholds.surprise_low = 0.2; + } + } + + // Clamp to valid range + thresholds.surprise_high = thresholds.surprise_high.clamp(0.2, 0.9); + thresholds.surprise_low = thresholds.surprise_low.clamp(0.1, thresholds.surprise_high - 0.05); + thresholds.confidence_high = thresholds.confidence_high.clamp(0.2, 0.8); + + thresholds +} + +// ============================================================================= +// MUL-MODULATED COUNCIL WEIGHTS +// ============================================================================= + +/// Create council weights modulated by the MUL's homeostasis state. +/// +/// - Anxiety → Guardian-dominant (caution amplified) +/// - Boredom → Catalyst-dominant (curiosity amplified) +/// - Flow → Balanced (default) +/// - Apathy → All dampened (conservation mode) +pub fn mul_council_weights(snapshot: &MulSnapshot) -> CouncilWeights { + match snapshot.homeostasis_state { + HomeostasisState::Flow => CouncilWeights::default(), + HomeostasisState::Anxiety => CouncilWeights { + guardian_surprise_factor: 0.4, // stronger dampening + catalyst_surprise_factor: 1.1, // weaker amplification + balanced_factor: 0.8, // slight overall dampening + }, + HomeostasisState::Boredom => CouncilWeights { + guardian_surprise_factor: 0.8, // less dampening + catalyst_surprise_factor: 1.8, // stronger amplification + balanced_factor: 1.1, // slight overall boost + }, + HomeostasisState::Apathy => CouncilWeights { + guardian_surprise_factor: 0.5, // moderate dampening + catalyst_surprise_factor: 0.8, // dampened (no energy for curiosity) + balanced_factor: 0.6, // overall conservation + }, + } +} + +// ============================================================================= +// MUL-GATED REFLECTION +// ============================================================================= + +/// Result of a MUL-gated reflection cycle. +#[derive(Debug, Clone)] +pub struct MulReflectionResult { + /// Whether the MUL gate allowed reflection to proceed. + pub gate_allowed: bool, + /// The MUL snapshot used for modulation. + pub snapshot: MulSnapshot, + /// Adaptive thresholds used (for transparency). + pub thresholds: AdaptiveThresholds, + /// Council weights used (for transparency). + pub council: CouncilWeights, + /// The volitional agenda (None if gate blocked). + pub agenda: Option, + /// Reclassified entries using adaptive thresholds. + pub reclassified_count: usize, +} + +/// Reclassify reflection entries using adaptive thresholds. +/// +/// The standard `reflect_walk` uses fixed thresholds (0.55/0.45/0.5). +/// This function takes an existing ReflectionResult and reclassifies +/// each entry using MUL-adapted thresholds, returning the count of +/// entries whose outcome changed. +pub fn reclassify_with_thresholds( + result: &ReflectionResult, + thresholds: &AdaptiveThresholds, +) -> (Vec, usize) { + let mut reclassified = Vec::with_capacity(result.entries.len()); + let mut changed_count = 0; + + for entry in &result.entries { + let high_surprise = entry.surprise > thresholds.surprise_high; + let high_confidence = entry.truth_before.confidence > thresholds.confidence_high; + + let new_outcome = match (high_surprise, high_confidence) { + (true, true) => ReflectionOutcome::Revise, + (false, false) => ReflectionOutcome::Confirm, + (true, false) => ReflectionOutcome::Explore, + (false, true) => ReflectionOutcome::Stable, + }; + + if new_outcome != entry.outcome { + changed_count += 1; + } + + reclassified.push(ReflectionEntry { + dn: entry.dn, + surprise: entry.surprise, + truth_before: entry.truth_before, + truth_after: entry.truth_after, + outcome: new_outcome, + depth: entry.depth, + }); + } + + (reclassified, changed_count) +} + +/// Run a MUL-gated volitional cycle. +/// +/// This is the top-level integration point: +/// +/// 1. Evaluate MUL → get snapshot +/// 2. If gate closed → return early (no action) +/// 3. Compute adaptive thresholds from MUL state +/// 4. Compute MUL-modulated council weights +/// 5. Run volitional cycle with MUL-adjusted parameters +/// 6. Reclassify reflection entries with adaptive thresholds +pub fn mul_volitional_cycle( + graph: &mut ContainerGraph, + target: PackedDn, + query: &Container, + current_rung: RungLevel, + mul: &MetaUncertaintyLayer, + complexity_mapped: bool, +) -> MulReflectionResult { + let snapshot = mul.evaluate(complexity_mapped); + let thresholds = adaptive_thresholds(&snapshot); + let council = mul_council_weights(&snapshot); + + // Gate check: if MUL says don't proceed, return early + if !snapshot.should_proceed() { + return MulReflectionResult { + gate_allowed: false, + snapshot, + thresholds, + council, + agenda: None, + reclassified_count: 0, + }; + } + + // Run the volitional cycle with MUL-modulated council + let agenda = volitional_cycle(graph, target, query, current_rung, &council); + + // Reclassify using adaptive thresholds + let (reclassified_entries, reclassified_count) = + reclassify_with_thresholds(&agenda.reflection, &thresholds); + + // Update hydration candidates based on reclassification + let new_hydration_candidates: Vec = reclassified_entries + .iter() + .filter(|e| e.outcome == ReflectionOutcome::Explore) + .map(|e| e.dn) + .collect(); + + // If reclassification changed Explore candidates, re-hydrate + if reclassified_count > 0 && !new_hydration_candidates.is_empty() { + hydrate_explorers(graph, &new_hydration_candidates); + } + + MulReflectionResult { + gate_allowed: true, + snapshot, + thresholds, + council, + agenda: Some(agenda), + reclassified_count, + } +} + +// ============================================================================= +// MUL FEEDBACK — Reflection outcomes → MUL learning +// ============================================================================= + +/// Convert reflection results into MUL learning signals. +/// +/// Maps reflection outcomes back to MUL's `learn()` interface: +/// - Mean surprise → novelty signal for false flow detection +/// - Revision count / total → prediction accuracy for DK calibration +/// - Confidence delta → trust update +pub fn reflection_to_mul_learning( + result: &ReflectionResult, +) -> PostActionLearning { + let total = result.entries.len().max(1) as f32; + let revision_rate = result.revision_count() as f32 / total; + let _mean_confidence_delta = result.mean_confidence_delta(); + + // Map reflection outcomes to compass decision: + // - Mostly Stable/Confirm → routine decision (ExecuteWithLearning) + // - Mostly Explore → exploratory decision + // - Mostly Revise → surprising outcome (SurfaceToMeta) + let decision = if revision_rate > 0.5 { + CompassDecision::SurfaceToMeta + } else if result.hydration_candidates.len() as f32 / total > 0.3 { + CompassDecision::Exploratory + } else { + CompassDecision::ExecuteWithLearning + }; + + // Predicted confidence = mean confidence before reflection + let predicted_confidence = if result.entries.is_empty() { + 0.5 + } else { + result.entries.iter() + .map(|e| e.truth_before.confidence) + .sum::() / total + }; + + // Actual outcome = 1.0 - revision_rate (high revisions = poor prediction) + let actual_outcome = 1.0 - revision_rate; + + PostActionLearning::new(decision, predicted_confidence, actual_outcome) +} + +/// Full MUL-reflection feedback loop: reflect, then feed results back to MUL. +/// +/// 1. Run MUL-gated reflection +/// 2. If successful, compute learning signal +/// 3. Feed learning signal back to MUL +/// 4. Tick MUL with reflection-derived novelty/coherence +pub fn mul_reflection_feedback( + graph: &mut ContainerGraph, + target: PackedDn, + query: &Container, + current_rung: RungLevel, + mul: &mut MetaUncertaintyLayer, + complexity_mapped: bool, +) -> MulReflectionResult { + let result = mul_volitional_cycle( + graph, target, query, current_rung, mul, complexity_mapped, + ); + + if let Some(ref agenda) = result.agenda { + // Feed reflection outcomes back to MUL + let learning = reflection_to_mul_learning(&agenda.reflection); + mul.learn(&learning); + + // Tick MUL with reflection-derived signals: + // - coherence = 1.0 - mean surprise (low surprise = coherent) + // - novelty = fraction of Explore outcomes + let total = agenda.reflection.entries.len().max(1) as f32; + let coherence = 1.0 - agenda.reflection.felt_path.mean_surprise; + let novelty = agenda.reflection.hydration_candidates.len() as f32 / total; + // challenge/skill from rung: deeper rung = harder challenge + let challenge = current_rung.as_u8() as f32 / 9.0; + let skill = result.snapshot.free_will_modifier.value(); + + mul.tick(coherence, novelty, challenge, skill); + } + + result +} + +// ============================================================================= +// TESTS +// ============================================================================= + +#[cfg(test)] +mod tests { + use super::*; + use crate::container::{ContainerGeometry, CogRecord}; + use crate::mul::{MetaUncertaintyLayer, DKPosition}; + use super::super::reflection::{write_truth, reflect_walk}; + use ladybug_contract::nars::TruthValue; + + fn build_test_tree() -> ContainerGraph { + let mut graph = ContainerGraph::new(); + + let root = PackedDn::ROOT; + let mut root_rec = CogRecord::new(ContainerGeometry::Cam); + root_rec.content = Container::random(1); + graph.insert(root, root_rec); + + for (i, seed) in [(0u8, 10u64), (1, 20), (2, 30)] { + let dn = PackedDn::new(&[i]); + let mut rec = CogRecord::new(ContainerGeometry::Cam); + rec.content = Container::random(seed); + graph.insert(dn, rec); + } + + for (i, seed) in [(0u8, 100u64), (1, 101), (2, 102)] { + let dn = PackedDn::new(&[0, i]); + let mut rec = CogRecord::new(ContainerGeometry::Cam); + rec.content = Container::random(seed); + graph.insert(dn, rec); + } + + for (i, seed) in [(0u8, 200u64), (1, 201)] { + let dn = PackedDn::new(&[1, i]); + let mut rec = CogRecord::new(ContainerGeometry::Cam); + rec.content = Container::random(seed); + graph.insert(dn, rec); + } + + graph + } + + fn seed_nars(graph: &mut ContainerGraph) { + if let Some(rec) = graph.get_mut(&PackedDn::new(&[0])) { + write_truth(rec, &TruthValue::new(0.8, 0.9)); + } + if let Some(rec) = graph.get_mut(&PackedDn::new(&[1])) { + write_truth(rec, &TruthValue::new(0.5, 0.1)); + } + if let Some(rec) = graph.get_mut(&PackedDn::new(&[0, 1])) { + write_truth(rec, &TruthValue::new(0.7, 0.5)); + } + } + + #[test] + fn test_adaptive_thresholds_default() { + let mul = MetaUncertaintyLayer::new(); + let snapshot = mul.evaluate(true); + let thresholds = adaptive_thresholds(&snapshot); + + // Default MUL (Fuzzy trust, Flow) should produce near-default thresholds + assert!(thresholds.surprise_high > 0.4); + assert!(thresholds.surprise_high < 0.7); + assert!(thresholds.surprise_low < thresholds.surprise_high); + assert!(thresholds.confidence_high > 0.3); + } + + #[test] + fn test_adaptive_thresholds_anxiety() { + // Construct snapshot directly to isolate anxiety modulation + // (ticking MUL with anxiety patterns also triggers false flow, + // which overrides the anxiety threshold adjustment) + let snapshot = MulSnapshot { + trust_level: TrustLevel::Fuzzy, + dk_position: DKPosition::SlopeOfEnlightenment, + homeostasis_state: HomeostasisState::Anxiety, + false_flow_severity: FalseFlowSeverity::None, // isolate anxiety effect + free_will_modifier: crate::mul::FreeWillModifier::from_value(0.5), + gate_open: true, + gate_block_reason: None, + allostatic_load: 0.3, + }; + + let thresholds = adaptive_thresholds(&snapshot); + let defaults = AdaptiveThresholds::default(); + + // Anxiety should raise surprise thresholds (more conservative) + assert!(thresholds.surprise_high > defaults.surprise_high, + "anxiety surprise_high={} should exceed default={}", + thresholds.surprise_high, defaults.surprise_high); + } + + #[test] + fn test_adaptive_thresholds_false_flow() { + let mut mul = MetaUncertaintyLayer::new(); + for _ in 0..25 { + mul.tick(0.9, 0.01, 0.5, 0.5); // high coherence, no novelty → false flow + } + let snapshot = mul.evaluate(true); + + let thresholds = adaptive_thresholds(&snapshot); + // Severe false flow should dramatically lower surprise threshold + assert!(thresholds.surprise_high < 0.4, + "false flow should lower surprise_high: {}", + thresholds.surprise_high); + } + + #[test] + fn test_mul_council_weights_flow() { + let mul = MetaUncertaintyLayer::new(); + let snapshot = mul.evaluate(true); + let council = mul_council_weights(&snapshot); + + let defaults = CouncilWeights::default(); + assert!((council.guardian_surprise_factor - defaults.guardian_surprise_factor).abs() < 1e-5); + assert!((council.catalyst_surprise_factor - defaults.catalyst_surprise_factor).abs() < 1e-5); + } + + #[test] + fn test_mul_council_weights_anxiety() { + let mut mul = MetaUncertaintyLayer::new(); + for _ in 0..20 { + mul.tick(0.8, 0.1, 0.9, 0.2); + } + let snapshot = mul.evaluate(true); + let council = mul_council_weights(&snapshot); + + // Anxiety → guardian more aggressive, catalyst less + assert!(council.guardian_surprise_factor < 0.6); + assert!(council.catalyst_surprise_factor < 1.5); + } + + #[test] + fn test_mul_council_weights_boredom() { + let mut mul = MetaUncertaintyLayer::new(); + for _ in 0..20 { + mul.tick(0.8, 0.1, 0.2, 0.9); // low challenge, high skill → boredom + } + let snapshot = mul.evaluate(true); + let council = mul_council_weights(&snapshot); + + // Boredom → catalyst more aggressive + assert!(council.catalyst_surprise_factor > 1.5); + } + + #[test] + fn test_mul_volitional_cycle_gate_open() { + let mut graph = build_test_tree(); + seed_nars(&mut graph); + + let query = Container::random(42); + let target = PackedDn::new(&[0, 1]); + + // Use high-trust, low-risk MUL so modifier > 0.3 + let mut mul = MetaUncertaintyLayer::new(); + mul.trust_qualia = crate::mul::TrustQualia::uniform(0.9); + mul.risk_vector = crate::mul::RiskVector::low(); + + let result = mul_volitional_cycle( + &mut graph, target, &query, RungLevel::Meta, &mul, true, + ); + + assert!(result.gate_allowed, "high-trust MUL should allow reflection"); + assert!(result.agenda.is_some(), "should have agenda when gate open"); + } + + #[test] + fn test_mul_volitional_cycle_gate_closed() { + let mut graph = build_test_tree(); + seed_nars(&mut graph); + + let query = Container::random(42); + let target = PackedDn::new(&[0, 1]); + + // MUL with MountStupid → gate closed + let mut mul = MetaUncertaintyLayer::new(); + mul.dk_detector.position = DKPosition::MountStupid; + + let result = mul_volitional_cycle( + &mut graph, target, &query, RungLevel::Meta, &mul, true, + ); + + assert!(!result.gate_allowed, "MountStupid should block reflection"); + assert!(result.agenda.is_none(), "no agenda when gate closed"); + } + + #[test] + fn test_reflection_to_mul_learning() { + let mut graph = build_test_tree(); + seed_nars(&mut graph); + + let query = Container::random(42); + let target = PackedDn::new(&[0, 1]); + + let reflection = reflect_walk(&mut graph, target, &query); + let learning = reflection_to_mul_learning(&reflection); + + assert!(learning.predicted_confidence >= 0.0 && learning.predicted_confidence <= 1.0); + assert!(learning.actual_outcome >= 0.0 && learning.actual_outcome <= 1.0); + } + + #[test] + fn test_mul_reflection_feedback() { + let mut graph = build_test_tree(); + seed_nars(&mut graph); + + let query = Container::random(42); + let target = PackedDn::new(&[0, 1]); + + // Use high-trust, low-risk MUL so modifier > 0.3 + let mut mul = MetaUncertaintyLayer::new(); + mul.trust_qualia = crate::mul::TrustQualia::uniform(0.9); + mul.risk_vector = crate::mul::RiskVector::low(); + + let initial_tick = mul.tick_count(); + let result = mul_reflection_feedback( + &mut graph, target, &query, RungLevel::Meta, &mut mul, true, + ); + + assert!(result.gate_allowed, "high-trust MUL should allow reflection"); + // MUL should have been ticked + assert!(mul.tick_count() > initial_tick, + "MUL should have been ticked: {} > {}", + mul.tick_count(), initial_tick); + } + + #[test] + fn test_reclassify_with_thresholds() { + let mut graph = build_test_tree(); + seed_nars(&mut graph); + + let query = Container::random(42); + let target = PackedDn::new(&[0, 1]); + + let reflection = reflect_walk(&mut graph, target, &query); + let defaults = AdaptiveThresholds::default(); + + // Reclassify with same thresholds should change nothing + let (reclassified, changed) = reclassify_with_thresholds(&reflection, &defaults); + assert_eq!(reclassified.len(), reflection.entries.len()); + + // With very aggressive thresholds, more should be Explore/Revise + let aggressive = AdaptiveThresholds { + surprise_high: 0.2, // everything is "surprising" + surprise_low: 0.1, + confidence_high: 0.8, + }; + let (recl_agg, _) = reclassify_with_thresholds(&reflection, &aggressive); + assert_eq!(recl_agg.len(), reflection.entries.len()); + } +}