use serde::{Deserialize, Serialize};
use crate::consciousness_profile::ConsciousnessTier;
pub const LAMBDA_MIN: f64 = 0.001;
pub const LAMBDA_MAX: f64 = 0.020;
pub const MAX_DIMENSION_WEIGHT: f64 = 0.50;
pub const MIN_MATURATION_HOURS: u64 = 72;
pub const TIER_THRESHOLDS: [f64; 5] = [0.0, 0.3, 0.4, 0.6, 0.8];
pub const MIN_DIMENSIONS: usize = 2;
pub const MAX_DIMENSIONS: usize = 16;
pub const WEIGHT_SUM_TOLERANCE: f64 = 1e-6;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ConstitutionalViolation {
DecayTooLow { lambda: f64, minimum: f64 },
DecayTooHigh { lambda: f64, maximum: f64 },
DimensionWeightExceeds {
dimension_index: usize,
weight: f64,
maximum: f64,
},
WeightSumInvalid { sum: f64, expected: f64 },
MaturationTooShort { hours: u64, minimum: u64 },
TooFewDimensions { count: usize, minimum: usize },
TooManyDimensions { count: usize, maximum: usize },
InvalidWeight { dimension_index: usize, weight: f64 },
InvalidLambda { lambda: f64 },
}
impl core::fmt::Display for ConstitutionalViolation {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::DecayTooLow { lambda, minimum } => {
write!(f, "Decay rate {lambda} < constitutional minimum {minimum}")
}
Self::DecayTooHigh { lambda, maximum } => {
write!(f, "Decay rate {lambda} > constitutional maximum {maximum}")
}
Self::DimensionWeightExceeds {
dimension_index,
weight,
maximum,
} => write!(
f,
"Dimension {dimension_index} weight {weight} > constitutional maximum {maximum}"
),
Self::WeightSumInvalid { sum, expected } => {
write!(f, "Weight sum {sum} != expected {expected}")
}
Self::MaturationTooShort { hours, minimum } => {
write!(
f,
"Maturation period {hours}h < constitutional minimum {minimum}h"
)
}
Self::TooFewDimensions { count, minimum } => {
write!(f, "{count} dimensions < constitutional minimum {minimum}")
}
Self::TooManyDimensions { count, maximum } => {
write!(f, "{count} dimensions > constitutional maximum {maximum}")
}
Self::InvalidWeight {
dimension_index,
weight,
} => write!(f, "Dimension {dimension_index} has invalid weight {weight}"),
Self::InvalidLambda { lambda } => {
write!(f, "Lambda {lambda} is not a finite positive number")
}
}
}
}
pub fn validate_model(
weights: &[f64],
lambda: f64,
maturation_hours: u64,
) -> Result<(), ConstitutionalViolation> {
if !lambda.is_finite() || lambda <= 0.0 {
return Err(ConstitutionalViolation::InvalidLambda { lambda });
}
if lambda < LAMBDA_MIN {
return Err(ConstitutionalViolation::DecayTooLow {
lambda,
minimum: LAMBDA_MIN,
});
}
if lambda > LAMBDA_MAX {
return Err(ConstitutionalViolation::DecayTooHigh {
lambda,
maximum: LAMBDA_MAX,
});
}
if weights.len() < MIN_DIMENSIONS {
return Err(ConstitutionalViolation::TooFewDimensions {
count: weights.len(),
minimum: MIN_DIMENSIONS,
});
}
if weights.len() > MAX_DIMENSIONS {
return Err(ConstitutionalViolation::TooManyDimensions {
count: weights.len(),
maximum: MAX_DIMENSIONS,
});
}
for (i, &w) in weights.iter().enumerate() {
if !w.is_finite() || w < 0.0 {
return Err(ConstitutionalViolation::InvalidWeight {
dimension_index: i,
weight: w,
});
}
if w > MAX_DIMENSION_WEIGHT {
return Err(ConstitutionalViolation::DimensionWeightExceeds {
dimension_index: i,
weight: w,
maximum: MAX_DIMENSION_WEIGHT,
});
}
}
let sum: f64 = weights.iter().sum();
if (sum - 1.0).abs() > WEIGHT_SUM_TOLERANCE {
return Err(ConstitutionalViolation::WeightSumInvalid { sum, expected: 1.0 });
}
if maturation_hours < MIN_MATURATION_HOURS {
return Err(ConstitutionalViolation::MaturationTooShort {
hours: maturation_hours,
minimum: MIN_MATURATION_HOURS,
});
}
Ok(())
}
pub fn score_to_tier(score: f64) -> ConsciousnessTier {
let s = if score.is_finite() {
score.clamp(0.0, 1.0)
} else {
0.0
};
if s >= TIER_THRESHOLDS[4] {
ConsciousnessTier::Guardian
} else if s >= TIER_THRESHOLDS[3] {
ConsciousnessTier::Steward
} else if s >= TIER_THRESHOLDS[2] {
ConsciousnessTier::Citizen
} else if s >= TIER_THRESHOLDS[1] {
ConsciousnessTier::Participant
} else {
ConsciousnessTier::Observer
}
}
pub fn sanitize_score(raw: f64) -> f64 {
if raw.is_finite() {
raw.clamp(0.0, 1.0)
} else {
0.0
}
}
pub fn decay_multiplier(lambda: f64, elapsed_days: f64) -> f64 {
if !lambda.is_finite() || !elapsed_days.is_finite() || elapsed_days < 0.0 {
return 0.0;
}
let m = (-lambda * elapsed_days).exp();
if m.is_finite() {
m.clamp(0.0, 1.0)
} else {
0.0
}
}
pub fn apply_decay(score: f64, lambda: f64, elapsed_days: f64) -> f64 {
sanitize_score(score * decay_multiplier(lambda, elapsed_days))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_model_passes() {
let weights = &[0.25, 0.25, 0.30, 0.20]; assert!(validate_model(weights, 0.002, 72).is_ok());
}
#[test]
fn zero_lambda_rejected() {
let weights = &[0.50, 0.50];
let result = validate_model(weights, 0.0, 72);
assert!(matches!(
result,
Err(ConstitutionalViolation::InvalidLambda { .. })
));
}
#[test]
fn negative_lambda_rejected() {
let weights = &[0.50, 0.50];
let result = validate_model(weights, -0.001, 72);
assert!(matches!(
result,
Err(ConstitutionalViolation::InvalidLambda { .. })
));
}
#[test]
fn lambda_below_minimum_rejected() {
let weights = &[0.50, 0.50];
let result = validate_model(weights, 0.0005, 72);
assert!(matches!(
result,
Err(ConstitutionalViolation::DecayTooLow { .. })
));
}
#[test]
fn lambda_above_maximum_rejected() {
let weights = &[0.50, 0.50];
let result = validate_model(weights, 0.05, 72);
assert!(matches!(
result,
Err(ConstitutionalViolation::DecayTooHigh { .. })
));
}
#[test]
fn lambda_at_minimum_accepted() {
let weights = &[0.50, 0.50];
assert!(validate_model(weights, LAMBDA_MIN, 72).is_ok());
}
#[test]
fn lambda_at_maximum_accepted() {
let weights = &[0.50, 0.50];
assert!(validate_model(weights, LAMBDA_MAX, 72).is_ok());
}
#[test]
fn nan_lambda_rejected() {
let weights = &[0.50, 0.50];
let result = validate_model(weights, f64::NAN, 72);
assert!(matches!(
result,
Err(ConstitutionalViolation::InvalidLambda { .. })
));
}
#[test]
fn weight_exceeding_50_percent_rejected() {
let weights = &[0.60, 0.40]; let result = validate_model(weights, 0.002, 72);
assert!(matches!(
result,
Err(ConstitutionalViolation::DimensionWeightExceeds { .. })
));
}
#[test]
fn weight_at_50_percent_accepted() {
let weights = &[0.50, 0.50];
assert!(validate_model(weights, 0.002, 72).is_ok());
}
#[test]
fn negative_weight_rejected() {
let weights = &[0.50, 0.50, -0.10, 0.10];
let result = validate_model(weights, 0.002, 72);
assert!(matches!(
result,
Err(ConstitutionalViolation::InvalidWeight { .. })
));
}
#[test]
fn nan_weight_rejected() {
let weights = &[0.50, f64::NAN, 0.50];
let result = validate_model(weights, 0.002, 72);
assert!(matches!(
result,
Err(ConstitutionalViolation::InvalidWeight { .. })
));
}
#[test]
fn weights_not_summing_to_one_rejected() {
let weights = &[0.30, 0.30, 0.30]; let result = validate_model(weights, 0.002, 72);
assert!(matches!(
result,
Err(ConstitutionalViolation::WeightSumInvalid { .. })
));
}
#[test]
fn canonical_4d_weights_pass() {
let weights = &[0.25, 0.25, 0.30, 0.20]; assert!(validate_model(weights, 0.002, 72).is_ok());
}
#[test]
fn sovereign_8d_governance_weights_pass() {
let weights = &[0.15, 0.10, 0.10, 0.12, 0.18, 0.13, 0.12, 0.10];
assert!(validate_model(weights, 0.002, 72).is_ok());
}
#[test]
fn minimal_2d_model_passes() {
let weights = &[0.50, 0.50];
assert!(validate_model(weights, 0.002, 72).is_ok());
}
#[test]
fn single_dimension_rejected() {
let weights = &[1.0]; let result = validate_model(weights, 0.002, 72);
assert!(result.is_err());
}
#[test]
fn seventeen_dimensions_rejected() {
let weights: Vec<f64> = (0..17).map(|_| 1.0 / 17.0).collect();
let result = validate_model(&weights, 0.002, 72);
assert!(matches!(
result,
Err(ConstitutionalViolation::TooManyDimensions { .. })
));
}
#[test]
fn maturation_below_minimum_rejected() {
let weights = &[0.50, 0.50];
let result = validate_model(weights, 0.002, 48); assert!(matches!(
result,
Err(ConstitutionalViolation::MaturationTooShort { .. })
));
}
#[test]
fn maturation_at_minimum_accepted() {
let weights = &[0.50, 0.50];
assert!(validate_model(weights, 0.002, MIN_MATURATION_HOURS).is_ok());
}
#[test]
fn maturation_above_minimum_accepted() {
let weights = &[0.50, 0.50];
assert!(validate_model(weights, 0.002, 168).is_ok()); }
#[test]
fn tier_thresholds_are_monotonic() {
for pair in TIER_THRESHOLDS.windows(2) {
assert!(
pair[0] < pair[1],
"Tier thresholds must be strictly increasing: {} >= {}",
pair[0],
pair[1]
);
}
}
#[test]
fn score_to_tier_matches_canonical() {
assert_eq!(score_to_tier(0.0), ConsciousnessTier::Observer);
assert_eq!(score_to_tier(0.29), ConsciousnessTier::Observer);
assert_eq!(score_to_tier(0.3), ConsciousnessTier::Participant);
assert_eq!(score_to_tier(0.39), ConsciousnessTier::Participant);
assert_eq!(score_to_tier(0.4), ConsciousnessTier::Citizen);
assert_eq!(score_to_tier(0.59), ConsciousnessTier::Citizen);
assert_eq!(score_to_tier(0.6), ConsciousnessTier::Steward);
assert_eq!(score_to_tier(0.79), ConsciousnessTier::Steward);
assert_eq!(score_to_tier(0.8), ConsciousnessTier::Guardian);
assert_eq!(score_to_tier(1.0), ConsciousnessTier::Guardian);
}
#[test]
fn score_to_tier_handles_nan() {
assert_eq!(score_to_tier(f64::NAN), ConsciousnessTier::Observer);
}
#[test]
fn score_to_tier_handles_infinity() {
assert_eq!(score_to_tier(f64::INFINITY), ConsciousnessTier::Observer);
assert_eq!(
score_to_tier(f64::NEG_INFINITY),
ConsciousnessTier::Observer
);
}
#[test]
fn score_to_tier_clamps_above_one() {
assert_eq!(score_to_tier(1.5), ConsciousnessTier::Guardian);
}
#[test]
fn score_to_tier_clamps_below_zero() {
assert_eq!(score_to_tier(-0.5), ConsciousnessTier::Observer);
}
#[test]
fn decay_multiplier_at_zero_elapsed() {
let m = decay_multiplier(0.002, 0.0);
assert!((m - 1.0).abs() < 1e-10, "No elapsed time → no decay");
}
#[test]
fn decay_multiplier_decreases_over_time() {
let m1 = decay_multiplier(0.002, 30.0);
let m2 = decay_multiplier(0.002, 60.0);
assert!(m2 < m1, "Decay should increase with time");
assert!(m1 < 1.0, "30-day decay should reduce below 1.0");
}
#[test]
fn decay_multiplier_higher_lambda_decays_faster() {
let m_slow = decay_multiplier(0.001, 30.0);
let m_fast = decay_multiplier(0.010, 30.0);
assert!(
m_fast < m_slow,
"Higher lambda should decay faster: fast={}, slow={}",
m_fast,
m_slow
);
}
#[test]
fn decay_multiplier_handles_nan() {
assert_eq!(decay_multiplier(f64::NAN, 30.0), 0.0);
assert_eq!(decay_multiplier(0.002, f64::NAN), 0.0);
}
#[test]
fn decay_multiplier_handles_negative_time() {
assert_eq!(decay_multiplier(0.002, -10.0), 0.0);
}
#[test]
fn apply_decay_full_score_30_days() {
let decayed = apply_decay(1.0, 0.002, 30.0);
assert!(
decayed > 0.93 && decayed < 0.95,
"Score after 30 days at lambda=0.002 should be ~0.94, got {}",
decayed
);
}
#[test]
fn apply_decay_respects_lambda_min_half_life() {
let decayed = apply_decay(1.0, LAMBDA_MIN, 693.0);
assert!(
decayed > 0.45 && decayed < 0.55,
"At lambda_min half-life, score should be ~0.5, got {}",
decayed
);
}
#[test]
fn apply_decay_respects_lambda_max_half_life() {
let decayed = apply_decay(1.0, LAMBDA_MAX, 35.0);
assert!(
decayed > 0.45 && decayed < 0.55,
"At lambda_max half-life, score should be ~0.5, got {}",
decayed
);
}
#[test]
fn violation_display_is_readable() {
let v = ConstitutionalViolation::DecayTooLow {
lambda: 0.0005,
minimum: 0.001,
};
let s = format!("{}", v);
assert!(s.contains("0.0005"));
assert!(s.contains("0.001"));
}
#[test]
fn lambda_min_less_than_max() {
assert!(LAMBDA_MIN < LAMBDA_MAX);
}
#[test]
fn tier_thresholds_match_consciousness_tier() {
assert_eq!(
score_to_tier(TIER_THRESHOLDS[0]),
ConsciousnessTier::Observer
);
assert_eq!(
score_to_tier(TIER_THRESHOLDS[1]),
ConsciousnessTier::Participant
);
assert_eq!(
score_to_tier(TIER_THRESHOLDS[2]),
ConsciousnessTier::Citizen
);
assert_eq!(
score_to_tier(TIER_THRESHOLDS[3]),
ConsciousnessTier::Steward
);
assert_eq!(
score_to_tier(TIER_THRESHOLDS[4]),
ConsciousnessTier::Guardian
);
}
#[test]
fn max_dimension_weight_prevents_single_axis_domination() {
assert!(MAX_DIMENSION_WEIGHT <= 0.50);
}
}