1use epistemic_core::gate::{EpistemicGate, GateConfig, GateDecision};
27use epistemic_core::graph::ReasoningGraph;
28use epistemic_core::rules::InferenceRule;
29
30use crate::emotional_episode::PadState;
31
32#[derive(Debug, Clone)]
36pub struct EmotionalGateConfig {
37 pub base: GateConfig,
39 pub arousal_reasoning_tighten: f64,
42 pub valence_narrative_tighten: f64,
45 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 pub fn modulate(&self, pad: PadState) -> GateConfig {
79 let mut config = self.base.clone();
80
81 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 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 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
107pub 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 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#[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, ®);
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 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 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 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 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 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); 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}