use crate::SovereignDimension;
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DimensionInput {
pub activity_count: u64,
pub baseline: u64,
pub quality: f64,
pub days_since_last: f64,
pub recency_half_life_days: f64,
}
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct CollectedDimensions {
pub epistemic_integrity: DimensionInput,
pub thermodynamic_yield: DimensionInput,
pub network_resilience: DimensionInput,
pub economic_velocity: DimensionInput,
pub civic_participation: DimensionInput,
pub stewardship_care: DimensionInput,
pub semantic_resonance: DimensionInput,
pub domain_competence: DimensionInput,
}
impl CollectedDimensions {
pub fn get(&self, dim: SovereignDimension) -> &DimensionInput {
match dim {
SovereignDimension::EpistemicIntegrity => &self.epistemic_integrity,
SovereignDimension::ThermodynamicYield => &self.thermodynamic_yield,
SovereignDimension::NetworkResilience => &self.network_resilience,
SovereignDimension::EconomicVelocity => &self.economic_velocity,
SovereignDimension::CivicParticipation => &self.civic_participation,
SovereignDimension::StewardshipCare => &self.stewardship_care,
SovereignDimension::SemanticResonance => &self.semantic_resonance,
SovereignDimension::DomainCompetence => &self.domain_competence,
}
}
}
pub fn normalize(input: &DimensionInput) -> f64 {
if input.baseline == 0 {
return 0.0;
}
let saturation = (input.activity_count as f64 / input.baseline as f64).min(1.0);
let quality = if input.quality.is_finite() {
input.quality.clamp(0.0, 1.0)
} else {
0.0
};
let recency = if input.recency_half_life_days > 0.0 && input.days_since_last >= 0.0 {
(-input.days_since_last * core::f64::consts::LN_2 / input.recency_half_life_days).exp()
} else {
1.0 };
(saturation * quality * recency).clamp(0.0, 1.0)
}
pub fn normalize_all(collected: &CollectedDimensions) -> crate::SovereignProfile {
crate::SovereignProfile {
epistemic_integrity: normalize(&collected.epistemic_integrity),
thermodynamic_yield: normalize(&collected.thermodynamic_yield),
network_resilience: normalize(&collected.network_resilience),
economic_velocity: normalize(&collected.economic_velocity),
civic_participation: normalize(&collected.civic_participation),
stewardship_care: normalize(&collected.stewardship_care),
semantic_resonance: normalize(&collected.semantic_resonance),
domain_competence: normalize(&collected.domain_competence),
}
}
pub fn default_input(dim: SovereignDimension) -> DimensionInput {
match dim {
SovereignDimension::EpistemicIntegrity => DimensionInput {
baseline: 50, recency_half_life_days: 90.0, ..Default::default()
},
SovereignDimension::ThermodynamicYield => DimensionInput {
baseline: 30, recency_half_life_days: 30.0, ..Default::default()
},
SovereignDimension::NetworkResilience => DimensionInput {
baseline: 720, recency_half_life_days: 14.0, ..Default::default()
},
SovereignDimension::EconomicVelocity => DimensionInput {
baseline: 50, recency_half_life_days: 60.0, ..Default::default()
},
SovereignDimension::CivicParticipation => DimensionInput {
baseline: 20, recency_half_life_days: 180.0, ..Default::default()
},
SovereignDimension::StewardshipCare => DimensionInput {
baseline: 30, recency_half_life_days: 60.0, ..Default::default()
},
SovereignDimension::SemanticResonance => DimensionInput {
baseline: 1, recency_half_life_days: 30.0, ..Default::default()
},
SovereignDimension::DomainCompetence => DimensionInput {
baseline: 3, recency_half_life_days: 365.0, ..Default::default()
},
}
}
#[cfg(test)]
mod tests {
use super::*;
fn input(count: u64, baseline: u64, quality: f64, days: f64, half_life: f64) -> DimensionInput {
DimensionInput {
activity_count: count,
baseline,
quality,
days_since_last: days,
recency_half_life_days: half_life,
}
}
#[test]
fn normalize_full_saturation_full_quality_recent() {
let score = normalize(&input(100, 50, 1.0, 0.0, 30.0));
assert!((score - 1.0).abs() < 1e-10);
}
#[test]
fn normalize_half_saturation() {
let score = normalize(&input(25, 50, 1.0, 0.0, 30.0));
assert!((score - 0.5).abs() < 1e-10);
}
#[test]
fn normalize_half_quality() {
let score = normalize(&input(50, 50, 0.5, 0.0, 30.0));
assert!((score - 0.5).abs() < 1e-10);
}
#[test]
fn normalize_recency_decay_at_half_life() {
let score = normalize(&input(50, 50, 1.0, 30.0, 30.0));
assert!((score - 0.5).abs() < 0.01);
}
#[test]
fn normalize_zero_activity_is_zero() {
let score = normalize(&input(0, 50, 1.0, 0.0, 30.0));
assert_eq!(score, 0.0);
}
#[test]
fn normalize_zero_baseline_is_zero() {
let score = normalize(&input(100, 0, 1.0, 0.0, 30.0));
assert_eq!(score, 0.0);
}
#[test]
fn normalize_zero_quality_is_zero() {
let score = normalize(&input(50, 50, 0.0, 0.0, 30.0));
assert_eq!(score, 0.0);
}
#[test]
fn normalize_nan_quality_is_zero() {
let score = normalize(&input(50, 50, f64::NAN, 0.0, 30.0));
assert_eq!(score, 0.0);
}
#[test]
fn normalize_capped_at_one() {
let score = normalize(&input(200, 50, 1.0, 0.0, 30.0));
assert!((score - 1.0).abs() < 1e-10);
}
#[test]
fn normalize_all_produces_valid_profile() {
let mut collected = CollectedDimensions::default();
collected.epistemic_integrity = input(40, 50, 0.9, 5.0, 90.0);
collected.civic_participation = input(15, 20, 0.8, 10.0, 180.0);
collected.domain_competence = input(2, 3, 0.95, 30.0, 365.0);
let profile = normalize_all(&collected);
assert!(profile.epistemic_integrity > 0.0);
assert!(profile.civic_participation > 0.0);
assert!(profile.domain_competence > 0.0);
assert_eq!(profile.thermodynamic_yield, 0.0);
assert_eq!(profile.network_resilience, 0.0);
}
#[test]
fn default_inputs_have_nonzero_baselines() {
for dim in SovereignDimension::ALL {
let def = default_input(dim);
assert!(def.baseline > 0, "Dimension {:?} has zero baseline", dim);
assert!(
def.recency_half_life_days > 0.0,
"Dimension {:?} has zero half-life",
dim
);
}
}
#[test]
fn conservative_defaults_start_at_zero() {
let collected = CollectedDimensions::default();
let profile = normalize_all(&collected);
for dim in SovereignDimension::ALL {
assert_eq!(
profile.get(dim),
0.0,
"Dimension {:?} should default to 0.0",
dim
);
}
}
}