use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::mood::Emotion;
use crate::salience::SalienceScore;
use crate::traits::{PersonalityProfile, TraitKind};
const TRUST_GATE_THRESHOLD: f32 = 0.15;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum SignalSource {
MemoryActivation,
SomaticMarker,
MicroExpressionLeak,
EmotionalComplexity,
PerceptualSensitivity,
AestheticSensitivity,
}
impl SignalSource {
pub const ALL: &'static [SignalSource] = &[
Self::MemoryActivation,
Self::SomaticMarker,
Self::MicroExpressionLeak,
Self::EmotionalComplexity,
Self::PerceptualSensitivity,
Self::AestheticSensitivity,
];
}
impl_display!(SignalSource {
MemoryActivation => "memory_activation",
SomaticMarker => "somatic_marker",
MicroExpressionLeak => "micro_expression_leak",
EmotionalComplexity => "emotional_complexity",
PerceptualSensitivity => "perceptual_sensitivity",
AestheticSensitivity => "aesthetic_sensitivity",
});
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
#[non_exhaustive]
pub enum KnowingLayer {
Instinct,
Conditioning,
Belief,
Intuition,
Insight,
}
impl KnowingLayer {
pub const ALL: &'static [KnowingLayer] = &[
Self::Instinct,
Self::Conditioning,
Self::Belief,
Self::Intuition,
Self::Insight,
];
#[must_use]
#[inline]
pub fn characteristics(self) -> LayerCharacteristics {
match self {
Self::Instinct => LayerCharacteristics {
speed: 1.0,
accuracy: 0.6,
explainability: 0.0,
},
Self::Conditioning => LayerCharacteristics {
speed: 0.8,
accuracy: 0.7,
explainability: 0.4,
},
Self::Belief => LayerCharacteristics {
speed: 0.5,
accuracy: 0.8,
explainability: 0.9,
},
Self::Intuition => LayerCharacteristics {
speed: 0.7,
accuracy: 0.85,
explainability: 0.2,
},
Self::Insight => LayerCharacteristics {
speed: 0.1,
accuracy: 1.0,
explainability: 0.0,
},
}
}
}
impl_display!(KnowingLayer {
Instinct => "instinct",
Conditioning => "conditioning",
Belief => "belief",
Intuition => "intuition",
Insight => "insight",
});
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct LayerCharacteristics {
pub speed: f32,
pub accuracy: f32,
pub explainability: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IntuitiveSignal {
pub tag: String,
pub valence: f32,
pub strength: f32,
pub sources: Vec<SignalSource>,
pub confidence_gap: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IntuitionProfile {
pub sensitivity: f32,
pub integration_depth: f32,
pub trust_in_intuition: f32,
}
impl Default for IntuitionProfile {
fn default() -> Self {
Self {
sensitivity: 0.5,
integration_depth: 0.5,
trust_in_intuition: 0.5,
}
}
}
impl IntuitionProfile {
#[must_use]
#[inline]
pub fn new() -> Self {
Self::default()
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
#[must_use]
pub fn from_personality(profile: &PersonalityProfile) -> Self {
let empathy = (profile.get_trait(TraitKind::Empathy).normalized() + 1.0) / 2.0;
let curiosity = (profile.get_trait(TraitKind::Curiosity).normalized() + 1.0) / 2.0;
let skepticism = (profile.get_trait(TraitKind::Skepticism).normalized() + 1.0) / 2.0;
let warmth = (profile.get_trait(TraitKind::Warmth).normalized() + 1.0) / 2.0;
let confidence = (profile.get_trait(TraitKind::Confidence).normalized() + 1.0) / 2.0;
Self {
sensitivity: ((empathy + curiosity + (1.0 - skepticism)) / 3.0).clamp(0.0, 1.0),
integration_depth: ((empathy + warmth) / 2.0).clamp(0.0, 1.0),
trust_in_intuition: (((1.0 - skepticism) + confidence) / 2.0).clamp(0.0, 1.0),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ActivationSignals {
pub entries: Vec<(String, f64)>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SalienceSignals {
pub entries: Vec<(String, SalienceScore)>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct MicroExpressionSignals {
pub entries: Vec<(String, f32, Emotion)>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AffectiveSignals {
pub entries: Vec<(String, f32)>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PerceptionSignals {
pub entries: Vec<(String, f32)>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AestheticSignals {
pub entries: Vec<(String, f32)>,
}
#[inline]
fn sigmoid(x: f64) -> f32 {
(1.0 / (1.0 + (-x).exp())) as f32
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
#[must_use]
pub fn synthesize_intuition(
activations: &ActivationSignals,
salience: &SalienceSignals,
micro_expressions: &MicroExpressionSignals,
affective: &AffectiveSignals,
perception: &PerceptionSignals,
aesthetic: &AestheticSignals,
profile: &IntuitionProfile,
) -> Vec<IntuitiveSignal> {
let mut signals: HashMap<String, Vec<(SignalSource, f32, f32)>> = HashMap::new();
let threshold = (1.0 - profile.sensitivity) * 0.5;
for (tag, activation) in &activations.entries {
let strength = sigmoid(*activation);
if strength >= threshold {
signals.entry(tag.clone()).or_default().push((
SignalSource::MemoryActivation,
strength,
0.0,
));
}
}
for (tag, score) in &salience.entries {
let strength = score.magnitude();
if strength >= threshold {
let valence_hint = if score.urgency > score.importance {
-0.5
} else {
0.0
};
signals.entry(tag.clone()).or_default().push((
SignalSource::SomaticMarker,
strength,
valence_hint,
));
}
}
for (tag, leak_ratio, emotion) in µ_expressions.entries {
if *leak_ratio >= threshold {
let valence_hint = match emotion {
Emotion::Joy | Emotion::Trust | Emotion::Interest => 0.5,
Emotion::Frustration => -0.8,
Emotion::Arousal => -0.3,
Emotion::Dominance => -0.2,
};
signals.entry(tag.clone()).or_default().push((
SignalSource::MicroExpressionLeak,
*leak_ratio,
valence_hint,
));
}
}
for (tag, anomaly) in &affective.entries {
if *anomaly >= threshold {
signals.entry(tag.clone()).or_default().push((
SignalSource::EmotionalComplexity,
*anomaly,
0.0,
));
}
}
for (tag, strength) in &perception.entries {
if *strength >= threshold {
signals.entry(tag.clone()).or_default().push((
SignalSource::PerceptualSensitivity,
*strength,
0.0,
));
}
}
for (tag, strength) in &aesthetic.entries {
if *strength >= threshold {
signals.entry(tag.clone()).or_default().push((
SignalSource::AestheticSensitivity,
*strength,
0.0,
));
}
}
let mut results = Vec::new();
for (tag, tag_signals) in &signals {
let mut seen_sources: Vec<SignalSource> = Vec::new();
let mut strengths: Vec<f32> = Vec::new();
let mut valence_sum = 0.0f32;
let mut valence_weight = 0.0f32;
for (source, strength, valence_hint) in tag_signals {
if !seen_sources.contains(source) {
seen_sources.push(*source);
strengths.push(*strength);
}
if *valence_hint != 0.0 {
valence_sum += valence_hint * strength;
valence_weight += strength;
}
}
let n_sources = seen_sources.len();
strengths.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
let max_single = strengths.first().copied().unwrap_or(0.0);
let combined = match n_sources {
0 => continue,
1 => max_single * 0.3,
2 => max_single * 0.6,
n => {
let count = strengths.len().min(3);
let product: f32 = strengths.iter().take(count).product();
let geo_mean = product.powf(1.0 / count as f32);
let convergence_bonus = (1.0 + 0.2 * (n as f32 - 3.0)).min(2.0);
geo_mean * convergence_bonus
}
};
let confidence_gap = (combined - max_single).max(0.0);
let final_strength = (combined * (0.5 + profile.integration_depth * 0.5)).clamp(0.0, 1.0);
let valence = if valence_weight > 0.0 {
(valence_sum / valence_weight).clamp(-1.0, 1.0)
} else {
0.0
};
if final_strength * profile.trust_in_intuition < TRUST_GATE_THRESHOLD {
continue;
}
results.push(IntuitiveSignal {
tag: tag.clone(),
valence,
strength: final_strength,
sources: seen_sources,
confidence_gap,
});
}
results.sort_by(|a, b| {
b.strength
.partial_cmp(&a.strength)
.unwrap_or(std::cmp::Ordering::Equal)
});
results
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
#[must_use]
pub fn active_layer(
arousal: f32,
belief_conviction: f32,
intuition_strength: f32,
cosmic_understanding: f32,
) -> KnowingLayer {
let arousal = arousal.clamp(0.0, 1.0);
let belief = belief_conviction.clamp(0.0, 1.0);
let intuition = intuition_strength.clamp(0.0, 1.0);
let cosmic = cosmic_understanding.clamp(0.0, 1.0);
let instinct_score = arousal * arousal;
let conditioning_score = 0.3; let belief_score = belief * 0.8;
let intuition_score = intuition;
let insight_score = if cosmic > 0.8 {
cosmic * 1.5
} else {
cosmic * 0.5
};
let scores = [
(KnowingLayer::Instinct, instinct_score),
(KnowingLayer::Conditioning, conditioning_score),
(KnowingLayer::Belief, belief_score),
(KnowingLayer::Intuition, intuition_score),
(KnowingLayer::Insight, insight_score),
];
scores
.iter()
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
.map(|(layer, _)| *layer)
.unwrap_or(KnowingLayer::Conditioning)
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
#[must_use]
pub fn shadow_belief_signals(beliefs: &[(String, f32, f32)]) -> Vec<(String, f32, f32)> {
beliefs
.iter()
.filter(|(_, _, depth)| *depth > 0.1)
.map(|(tag, valence, depth)| (tag.clone(), *valence, depth * 0.7))
.collect()
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
#[must_use]
#[inline]
pub fn should_override_reasoning(
signal: &IntuitiveSignal,
arousal: f32,
reasoning_confidence: f32,
) -> bool {
signal.strength > reasoning_confidence.clamp(0.0, 1.0)
&& (signal.strength > 0.7 || arousal.clamp(0.0, 1.0) > 0.6)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::salience::SalienceScore;
use crate::traits::{PersonalityProfile, TraitKind, TraitLevel};
#[test]
fn test_signal_source_serde() {
for &source in SignalSource::ALL {
let json = serde_json::to_string(&source).unwrap();
let s2: SignalSource = serde_json::from_str(&json).unwrap();
assert_eq!(source, s2);
}
}
#[test]
fn test_intuitive_signal_serde() {
let signal = IntuitiveSignal {
tag: "alice".to_owned(),
valence: -0.5,
strength: 0.8,
sources: vec![SignalSource::SomaticMarker, SignalSource::MemoryActivation],
confidence_gap: 0.15,
};
let json = serde_json::to_string(&signal).unwrap();
let s2: IntuitiveSignal = serde_json::from_str(&json).unwrap();
assert_eq!(s2.tag, "alice");
assert!((s2.strength - 0.8).abs() < 0.001);
}
#[test]
fn test_intuition_profile_serde() {
let profile = IntuitionProfile::new();
let json = serde_json::to_string(&profile).unwrap();
let p2: IntuitionProfile = serde_json::from_str(&json).unwrap();
assert!((p2.sensitivity - 0.5).abs() < 0.001);
}
#[test]
fn test_knowing_layer_order() {
assert!(KnowingLayer::Instinct < KnowingLayer::Conditioning);
assert!(KnowingLayer::Conditioning < KnowingLayer::Belief);
assert!(KnowingLayer::Belief < KnowingLayer::Intuition);
assert!(KnowingLayer::Intuition < KnowingLayer::Insight);
}
#[test]
fn test_layer_characteristics() {
let instinct = KnowingLayer::Instinct.characteristics();
let belief = KnowingLayer::Belief.characteristics();
let intuition = KnowingLayer::Intuition.characteristics();
let insight = KnowingLayer::Insight.characteristics();
assert!(instinct.speed > belief.speed);
assert!(belief.explainability > intuition.explainability);
assert!(insight.accuracy >= intuition.accuracy);
}
#[test]
fn test_active_layer_high_arousal() {
assert_eq!(active_layer(0.95, 0.3, 0.3, 0.0), KnowingLayer::Instinct);
}
#[test]
fn test_active_layer_strong_belief() {
assert_eq!(active_layer(0.2, 0.9, 0.3, 0.0), KnowingLayer::Belief);
}
#[test]
fn test_active_layer_cosmic() {
assert_eq!(active_layer(0.2, 0.3, 0.3, 0.9), KnowingLayer::Insight);
}
#[test]
fn test_active_layer_default_conditioning() {
assert_eq!(active_layer(0.2, 0.2, 0.2, 0.2), KnowingLayer::Conditioning);
}
#[test]
fn test_active_layer_strong_intuition() {
assert_eq!(active_layer(0.2, 0.3, 0.8, 0.0), KnowingLayer::Intuition);
}
#[test]
fn test_profile_from_personality_high_empathy() {
let mut profile = PersonalityProfile::new("empath");
profile.set_trait(TraitKind::Empathy, TraitLevel::Highest);
profile.set_trait(TraitKind::Curiosity, TraitLevel::High);
profile.set_trait(TraitKind::Skepticism, TraitLevel::Lowest);
let ip = IntuitionProfile::from_personality(&profile);
assert!(
ip.sensitivity > 0.7,
"High empathy + curiosity + low skepticism should give high sensitivity, got {}",
ip.sensitivity
);
assert!(
ip.trust_in_intuition > 0.6,
"Low skepticism should give high trust, got {}",
ip.trust_in_intuition
);
}
#[test]
fn test_profile_from_personality_skeptic() {
let mut profile = PersonalityProfile::new("skeptic");
profile.set_trait(TraitKind::Skepticism, TraitLevel::Highest);
profile.set_trait(TraitKind::Empathy, TraitLevel::Low);
let ip = IntuitionProfile::from_personality(&profile);
assert!(
ip.sensitivity < 0.5,
"High skepticism should reduce sensitivity, got {}",
ip.sensitivity
);
assert!(
ip.trust_in_intuition < 0.4,
"High skepticism should reduce trust, got {}",
ip.trust_in_intuition
);
}
#[test]
fn test_synthesize_empty_inputs() {
let profile = IntuitionProfile::new();
let result = synthesize_intuition(
&ActivationSignals::default(),
&SalienceSignals::default(),
&MicroExpressionSignals::default(),
&AffectiveSignals::default(),
&PerceptionSignals::default(),
&AestheticSignals::default(),
&profile,
);
assert!(result.is_empty());
}
#[test]
fn test_synthesize_single_source_weak() {
let profile = IntuitionProfile {
sensitivity: 0.9,
integration_depth: 0.8,
trust_in_intuition: 0.9,
};
let activations = ActivationSignals {
entries: vec![("danger".to_owned(), 2.0)], };
let result = synthesize_intuition(
&activations,
&SalienceSignals::default(),
&MicroExpressionSignals::default(),
&AffectiveSignals::default(),
&PerceptionSignals::default(),
&AestheticSignals::default(),
&profile,
);
if !result.is_empty() {
assert!(
result[0].strength < 0.5,
"Single source should be weak, got {}",
result[0].strength
);
assert_eq!(result[0].sources.len(), 1);
}
}
#[test]
fn test_synthesize_two_sources_moderate() {
let profile = IntuitionProfile {
sensitivity: 0.9,
integration_depth: 0.8,
trust_in_intuition: 0.9,
};
let activations = ActivationSignals {
entries: vec![("danger".to_owned(), 2.0)],
};
let salience = SalienceSignals {
entries: vec![("danger".to_owned(), SalienceScore::new(0.8, 0.7))],
};
let result = synthesize_intuition(
&activations,
&salience,
&MicroExpressionSignals::default(),
&AffectiveSignals::default(),
&PerceptionSignals::default(),
&AestheticSignals::default(),
&profile,
);
assert!(!result.is_empty());
let sig = &result[0];
assert_eq!(sig.tag, "danger");
assert_eq!(sig.sources.len(), 2);
assert!(sig.strength > 0.3, "Two sources should be moderate");
}
#[test]
fn test_synthesize_three_sources_convergence() {
let profile = IntuitionProfile {
sensitivity: 0.9,
integration_depth: 0.9,
trust_in_intuition: 0.9,
};
let activations = ActivationSignals {
entries: vec![("liar".to_owned(), 2.0)],
};
let salience = SalienceSignals {
entries: vec![("liar".to_owned(), SalienceScore::new(0.7, 0.6))],
};
let micro = MicroExpressionSignals {
entries: vec![("liar".to_owned(), 0.8, Emotion::Frustration)],
};
let result = synthesize_intuition(
&activations,
&salience,
µ,
&AffectiveSignals::default(),
&PerceptionSignals::default(),
&AestheticSignals::default(),
&profile,
);
assert!(!result.is_empty());
let sig = &result[0];
assert_eq!(sig.tag, "liar");
assert!(
sig.sources.len() >= 3,
"Should have 3+ sources, got {}",
sig.sources.len()
);
assert!(
sig.strength > 0.5,
"Three converging sources should produce strong signal, got {}",
sig.strength
);
assert!(
sig.valence < 0.0,
"Frustration leak should produce negative valence"
);
assert!(
sig.confidence_gap >= 0.0,
"Convergence should create confidence gap"
);
}
#[test]
fn test_sensitivity_filters_weak_signals() {
let profile = IntuitionProfile {
sensitivity: 0.1, integration_depth: 0.5,
trust_in_intuition: 0.9,
};
let activations = ActivationSignals {
entries: vec![("weak".to_owned(), 0.0)], };
let salience = SalienceSignals {
entries: vec![("weak".to_owned(), SalienceScore::new(0.3, 0.3))], };
let result = synthesize_intuition(
&activations,
&salience,
&MicroExpressionSignals::default(),
&AffectiveSignals::default(),
&PerceptionSignals::default(),
&AestheticSignals::default(),
&profile,
);
assert!(
result.is_empty(),
"Low sensitivity should filter weak signals"
);
}
#[test]
fn test_trust_gate_filters_output() {
let profile = IntuitionProfile {
sensitivity: 0.9,
integration_depth: 0.5,
trust_in_intuition: 0.05, };
let activations = ActivationSignals {
entries: vec![("something".to_owned(), 1.0)],
};
let result = synthesize_intuition(
&activations,
&SalienceSignals::default(),
&MicroExpressionSignals::default(),
&AffectiveSignals::default(),
&PerceptionSignals::default(),
&AestheticSignals::default(),
&profile,
);
assert!(
result.is_empty(),
"Very low trust should filter all weak signals"
);
}
#[test]
fn test_shadow_belief_signals() {
let beliefs = vec![
("fear:loss".to_owned(), -0.7, 0.8),
("hope:future".to_owned(), 0.5, 0.05), ("anger:betrayal".to_owned(), -0.9, 0.6),
];
let signals = shadow_belief_signals(&beliefs);
assert_eq!(signals.len(), 2);
let fear = signals.iter().find(|(t, _, _)| t == "fear:loss").unwrap();
assert!((fear.1 - (-0.7)).abs() < 0.01); assert!((fear.2 - 0.56).abs() < 0.01); }
#[test]
fn test_should_override_strong_signal_high_arousal() {
let signal = IntuitiveSignal {
tag: "danger".to_owned(),
valence: -0.8,
strength: 0.85,
sources: vec![
SignalSource::SomaticMarker,
SignalSource::MemoryActivation,
SignalSource::MicroExpressionLeak,
],
confidence_gap: 0.2,
};
assert!(should_override_reasoning(&signal, 0.7, 0.5));
}
#[test]
fn test_should_not_override_weak_signal() {
let signal = IntuitiveSignal {
tag: "maybe".to_owned(),
valence: 0.3,
strength: 0.3,
sources: vec![SignalSource::PerceptualSensitivity],
confidence_gap: 0.0,
};
assert!(!should_override_reasoning(&signal, 0.3, 0.5));
}
#[test]
fn test_should_override_very_strong_signal_low_arousal() {
let signal = IntuitiveSignal {
tag: "important".to_owned(),
valence: 0.9,
strength: 0.8,
sources: vec![
SignalSource::SomaticMarker,
SignalSource::PerceptualSensitivity,
SignalSource::MemoryActivation,
],
confidence_gap: 0.25,
};
assert!(should_override_reasoning(&signal, 0.2, 0.5));
}
#[test]
fn test_knowing_layer_serde() {
for &layer in KnowingLayer::ALL {
let json = serde_json::to_string(&layer).unwrap();
let l2: KnowingLayer = serde_json::from_str(&json).unwrap();
assert_eq!(layer, l2);
}
}
}