Skip to main content

microscope_memory/
emotional_gate.rs

1//! Emotional Gate — emotion-modulated epistemic gate.
2//!
3//! The emotional state of the system modulates the epistemic gate's
4//! thresholds. This is Arm 2 of the emotional memory overhaul:
5//!
6//! - **High arousal → tighten reasoning threshold**: emotional claims
7//!   need *more* proof, not less. When the system is aroused, it's
8//!   more likely to make sloppy inferences, so the gate demands
9//!   stronger reasoning.
10//!
11//! - **High positive valence → tighten narrative threshold**: positive
12//!   emotions make the system more likely to over-interpret (rosy
13//!   retrospection). The gate tightens the narrative dimension.
14//!
15//! - **High emotional intensity → relieve FunctionalToPhenomenological
16//!   penalty**: when there's genuine structural evidence of emotional
17//!   processing (multiple episodes, strong evidence), the penalty for
18//!   inferring phenomenological experience from functional state is
19//!   reduced. This doesn't remove the penalty — it acknowledges that
20//!   strong structural evidence makes the inference less of a leap.
21//!
22//! The key insight: emotions don't *override* the epistemic gate.
23//! They *modulate* it. The gate still blocks — but it knows the
24//! emotional context and adjusts its strictness accordingly.
25
26use epistemic_core::gate::{EpistemicGate, GateConfig, GateDecision};
27use epistemic_core::graph::ReasoningGraph;
28use epistemic_core::rules::InferenceRule;
29
30use crate::emotional_episode::PadState;
31
32// ─── Emotional Gate Config ──────────────────────────
33
34/// Configuration for emotion-modulated gating.
35#[derive(Debug, Clone)]
36pub struct EmotionalGateConfig {
37    /// Base gate config (unmodified thresholds).
38    pub base: GateConfig,
39    /// How much arousal tightens the reasoning threshold (0.0–0.3).
40    /// At 0.2, full arousal (1.0) adds 0.20 to the reasoning threshold.
41    pub arousal_reasoning_tighten: f64,
42    /// How much positive valence tightens the narrative threshold (0.0–0.3).
43    /// At 0.15, full positive valence (+1.0) adds 0.15 to the narrative threshold.
44    pub valence_narrative_tighten: f64,
45    /// How much emotional intensity relieves the FunctionalToPhenomenological
46    /// penalty (0.0–0.3). At 0.20, full intensity reduces the penalty by 0.20.
47    /// Example: penalty 0.45 → 0.45 - 0.20 = 0.25 (less harsh).
48    pub intensity_functional_relief: f64,
49}
50
51impl Default for EmotionalGateConfig {
52    fn default() -> Self {
53        Self {
54            base: GateConfig::default(),
55            arousal_reasoning_tighten: 0.15,
56            valence_narrative_tighten: 0.10,
57            intensity_functional_relief: 0.15,
58        }
59    }
60}
61
62impl EmotionalGateConfig {
63    pub fn new(
64        base: GateConfig,
65        arousal_reasoning_tighten: f64,
66        valence_narrative_tighten: f64,
67        intensity_functional_relief: f64,
68    ) -> Self {
69        Self {
70            base,
71            arousal_reasoning_tighten: arousal_reasoning_tighten.clamp(0.0, 0.3),
72            valence_narrative_tighten: valence_narrative_tighten.clamp(0.0, 0.3),
73            intensity_functional_relief: intensity_functional_relief.clamp(0.0, 0.3),
74        }
75    }
76
77    /// Compute the modulated gate config given the current PAD state.
78    pub fn modulate(&self, pad: PadState) -> GateConfig {
79        let mut config = self.base.clone();
80
81        // Arousal tightens reasoning threshold
82        let arousal_boost = self.arousal_reasoning_tighten * pad.arousal;
83        config.min_reasoning = (config.min_reasoning + arousal_boost).clamp(0.0, 1.0);
84
85        // Positive valence tightens narrative threshold
86        // (negative valence also tightens — strong emotion in either direction
87        // makes over-interpretation more likely)
88        let valence_mag = pad.pleasure.abs();
89        let valence_boost = self.valence_narrative_tighten * valence_mag;
90        config.min_narrative = (config.min_narrative + valence_boost).clamp(0.0, 1.0);
91
92        // Intensity relieves FunctionalToPhenomenological penalty
93        let intensity = pad.intensity();
94        let relief = self.intensity_functional_relief * intensity;
95        let current_penalty = config
96            .registry
97            .penalty(InferenceRule::FunctionalToPhenomenological);
98        let new_penalty = (current_penalty + relief).clamp(0.0, 1.0);
99        config
100            .registry
101            .set_penalty(InferenceRule::FunctionalToPhenomenological, new_penalty);
102
103        config
104    }
105}
106
107// ─── Emotional Gate ─────────────────────────────────
108
109/// An epistemic gate that is modulated by the system's emotional state.
110pub struct EmotionalGate {
111    config: EmotionalGateConfig,
112}
113
114impl EmotionalGate {
115    pub fn new(config: EmotionalGateConfig) -> Self {
116        Self { config }
117    }
118
119    pub fn with_default() -> Self {
120        Self::new(EmotionalGateConfig::default())
121    }
122
123    /// Evaluate a claim with emotional modulation.
124    ///
125    /// Returns the gate decision *and* the modulated config that was used,
126    /// so the caller can see how emotions affected the thresholds.
127    pub fn evaluate(
128        &self,
129        pad: PadState,
130        evidence_confidence: f64,
131        counterevidence_confidence: f64,
132        reasoning_graph: &ReasoningGraph,
133        narrative_confidence: f64,
134    ) -> (GateDecision, GateConfig) {
135        let modulated = self.config.modulate(pad);
136        let gate = EpistemicGate::new(modulated.clone());
137        let decision = gate.evaluate(
138            evidence_confidence,
139            counterevidence_confidence,
140            reasoning_graph,
141            narrative_confidence,
142        );
143        (decision, modulated)
144    }
145
146    pub fn config(&self) -> &EmotionalGateConfig {
147        &self.config
148    }
149}
150
151// ─── Tests ──────────────────────────────────────────
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156    use epistemic_core::gate::GateDecision;
157    use epistemic_core::graph::ReasoningGraph;
158    use epistemic_core::rules::{InferenceRule, RuleRegistry};
159    use epistemic_core::types::ConfidenceDimension;
160
161    fn make_graph(rule: InferenceRule, raw: f64) -> ReasoningGraph {
162        let mut g = ReasoningGraph::new();
163        let reg = RuleRegistry::new();
164        let e = g.add_evidence(1);
165        let root = g.add_conclusion(1, "test");
166        g.add_step(e, rule, root, raw, &reg);
167        g.set_root(root);
168        g
169    }
170
171    #[test]
172    fn high_arousal_tightens_reasoning() {
173        let eg = EmotionalGate::with_default();
174
175        // Calm state: arousal=0.0
176        let calm = PadState::new(0.0, 0.0, 0.5);
177        let g = make_graph(InferenceRule::ConvergentEvidence, 0.55);
178
179        let (calm_dec, calm_cfg) = eg.evaluate(calm, 0.80, 0.80, &g, 0.70);
180        assert!(matches!(calm_dec, GateDecision::Pass { .. }));
181
182        // Aroused state: arousal=1.0 — tightens reasoning by 0.15
183        let aroused = PadState::new(0.0, 1.0, 0.5);
184        let (_aroused_dec, aroused_cfg) = eg.evaluate(aroused, 0.80, 0.80, &g, 0.70);
185
186        // Threshold should be higher when aroused
187        assert!(aroused_cfg.min_reasoning > calm_cfg.min_reasoning);
188    }
189
190    #[test]
191    fn high_arousal_can_block_passing_claim() {
192        let eg = EmotionalGate::with_default();
193
194        // A claim that barely passes when calm
195        let g = make_graph(InferenceRule::ConvergentEvidence, 0.52);
196        let calm = PadState::neutral();
197        let (calm_dec, _) = eg.evaluate(calm, 0.80, 0.80, &g, 0.70);
198        assert!(matches!(calm_dec, GateDecision::Pass { .. }));
199
200        // Same claim when highly aroused — reasoning 0.52 < 0.50+0.15=0.65
201        let aroused = PadState::new(0.0, 1.0, 0.5);
202        let (aroused_dec, _) = eg.evaluate(aroused, 0.80, 0.80, &g, 0.70);
203        match aroused_dec {
204            GateDecision::Blocked { failed, .. } => {
205                assert!(failed
206                    .iter()
207                    .any(|f| f.dimension == ConfidenceDimension::Reasoning));
208            }
209            _ => panic!("high arousal should block marginal claim"),
210        }
211    }
212
213    #[test]
214    fn positive_valence_tightens_narrative() {
215        let eg = EmotionalGate::with_default();
216
217        let neutral = PadState::neutral();
218        let positive = PadState::new(0.9, 0.5, 0.5);
219
220        let g = make_graph(InferenceRule::ConvergentEvidence, 0.90);
221
222        let (_, neutral_cfg) = eg.evaluate(neutral, 0.80, 0.80, &g, 0.45);
223        let (_, positive_cfg) = eg.evaluate(positive, 0.80, 0.80, &g, 0.45);
224
225        assert!(
226            positive_cfg.min_narrative > neutral_cfg.min_narrative,
227            "positive valence should tighten narrative threshold"
228        );
229    }
230
231    #[test]
232    fn intensity_relieves_functional_penalty() {
233        let eg = EmotionalGate::with_default();
234
235        let calm = PadState::neutral();
236        let intense = PadState::new(0.6, 0.8, 0.6); // high intensity
237
238        let g = make_graph(InferenceRule::FunctionalToPhenomenological, 0.80);
239
240        let (_, calm_cfg) = eg.evaluate(calm, 0.80, 0.80, &g, 0.60);
241        let (_, intense_cfg) = eg.evaluate(intense, 0.80, 0.80, &g, 0.60);
242
243        let calm_penalty = calm_cfg
244            .registry
245            .penalty(InferenceRule::FunctionalToPhenomenological);
246        let intense_penalty = intense_cfg
247            .registry
248            .penalty(InferenceRule::FunctionalToPhenomenological);
249
250        assert!(
251            intense_penalty > calm_penalty,
252            "intense emotion should relieve (increase) the penalty factor, \
253            calm={calm_penalty}, intense={intense_penalty}"
254        );
255    }
256
257    #[test]
258    fn neutral_pad_does_not_modify_base_config() {
259        let eg = EmotionalGate::with_default();
260        let neutral = PadState::neutral();
261        let modulated = eg.config().modulate(neutral);
262
263        assert!((modulated.min_reasoning - eg.config().base.min_reasoning).abs() < 0.001);
264        assert!((modulated.min_narrative - eg.config().base.min_narrative).abs() < 0.001);
265    }
266}