use crate::math::clamp;
use crate::types::intervention::HiddenStateStats;
const CHURN_FLOOR: f64 = 0.2;
const CHURN_CEILING: f64 = 1.5;
const MAGNITUDE_WEIGHT: f64 = 0.15;
const MAGNITUDE_REFERENCE: f64 = 3.0;
pub fn arousal_from_hs(stats: &HiddenStateStats) -> Option<f64> {
if !stats.valid {
return None;
}
let base = (stats.state_churn - CHURN_FLOOR) / (CHURN_CEILING - CHURN_FLOOR);
let magnitude_factor = clamp(stats.state_magnitude / MAGNITUDE_REFERENCE, 0.0, 1.0);
let magnitude_boost = magnitude_factor * MAGNITUDE_WEIGHT;
Some(clamp(base + magnitude_boost, 0.0, 1.0))
}
pub fn resolve_arousal(hs_stats: Option<&HiddenStateStats>, regex_arousal: f64) -> f64 {
match hs_stats {
Some(stats) => arousal_from_hs(stats).unwrap_or(regex_arousal),
None => regex_arousal,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn invalid_stats_returns_none() {
let stats = HiddenStateStats {
state_churn: 0.5,
state_magnitude: 2.0,
valid: false,
};
assert!(arousal_from_hs(&stats).is_none());
}
#[test]
fn zero_churn_zero_arousal() {
let stats = HiddenStateStats {
state_churn: 0.0,
state_magnitude: 0.0,
valid: true,
};
let arousal = arousal_from_hs(&stats).unwrap();
assert_eq!(arousal, 0.0);
}
#[test]
fn below_floor_very_low_arousal() {
let stats = HiddenStateStats {
state_churn: 0.1,
state_magnitude: 1.0,
valid: true,
};
let arousal = arousal_from_hs(&stats).unwrap();
assert!(
arousal < 0.1,
"Below-floor churn should produce very low arousal, got {arousal}"
);
}
#[test]
fn mid_churn_mid_arousal() {
let stats = HiddenStateStats {
state_churn: 0.85, state_magnitude: 3.0,
valid: true,
};
let arousal = arousal_from_hs(&stats).unwrap();
assert!(
arousal > 0.3 && arousal < 0.8,
"Mid-range churn should produce mid arousal, got {arousal}"
);
}
#[test]
fn high_churn_high_arousal() {
let stats = HiddenStateStats {
state_churn: 1.2,
state_magnitude: 3.0,
valid: true,
};
let arousal = arousal_from_hs(&stats).unwrap();
assert!(
arousal > 0.5,
"High churn should produce high arousal, got {arousal}"
);
}
#[test]
fn ceiling_churn_saturates() {
let stats = HiddenStateStats {
state_churn: 2.0,
state_magnitude: 5.0,
valid: true,
};
let arousal = arousal_from_hs(&stats).unwrap();
assert_eq!(arousal, 1.0, "Above-ceiling churn should saturate at 1.0");
}
#[test]
fn magnitude_contributes_modestly() {
let low_mag = HiddenStateStats {
state_churn: 0.5,
state_magnitude: 0.5,
valid: true,
};
let high_mag = HiddenStateStats {
state_churn: 0.5,
state_magnitude: 5.0,
valid: true,
};
let a_low = arousal_from_hs(&low_mag).unwrap();
let a_high = arousal_from_hs(&high_mag).unwrap();
assert!(
a_high > a_low,
"Higher magnitude should increase arousal slightly"
);
assert!(
a_high - a_low <= MAGNITUDE_WEIGHT,
"Magnitude contribution should be bounded by MAGNITUDE_WEIGHT"
);
}
#[test]
fn resolve_uses_hs_when_valid() {
let stats = HiddenStateStats {
state_churn: 1.0,
state_magnitude: 3.0,
valid: true,
};
let result = resolve_arousal(Some(&stats), 0.0);
assert!(
result > 0.3,
"Should use hs arousal, not regex (0.0), got {result}"
);
}
#[test]
fn resolve_falls_back_on_invalid() {
let stats = HiddenStateStats {
state_churn: 1.0,
state_magnitude: 3.0,
valid: false,
};
let result = resolve_arousal(Some(&stats), 0.7);
assert_eq!(result, 0.7, "Should fall back to regex arousal on invalid hs");
}
#[test]
fn resolve_falls_back_on_none() {
let result = resolve_arousal(None, 0.5);
assert_eq!(result, 0.5, "Should fall back to regex arousal when no hs");
}
}