use crate::models::{ConfidenceSignals, ConfidenceSource, Memory};
pub mod calibrate;
pub mod decay;
pub mod shadow;
pub const ENV_AUTO_CONFIDENCE: &str = "AI_MEMORY_AUTO_CONFIDENCE";
#[must_use]
pub fn auto_confidence_enabled() -> bool {
std::env::var(ENV_AUTO_CONFIDENCE).is_ok_and(|v| v == "1")
}
#[derive(Debug, Clone, Copy)]
pub struct DeriveContext {
pub source_age_days: f64,
pub atom_derivation: bool,
pub prior_corroboration_count: i64,
pub baseline_per_source: f64,
pub half_life_days: f64,
}
impl Default for DeriveContext {
fn default() -> Self {
Self {
source_age_days: 0.0,
atom_derivation: false,
prior_corroboration_count: 0,
baseline_per_source: 0.5,
half_life_days: 30.0,
}
}
}
pub const DEFAULT_HALF_LIFE_DAYS: f64 = 30.0;
#[must_use]
pub fn derive(_memory: &Memory, ctx: &DeriveContext) -> (f64, ConfidenceSignals, ConfidenceSource) {
let age = ctx.source_age_days.max(0.0);
let half_life = ctx.half_life_days.max(f64::EPSILON);
let decay_rate = std::f64::consts::LN_2 / half_life;
let freshness_factor = (-age * std::f64::consts::LN_2 / half_life)
.exp()
.clamp(0.0, 1.0);
let atom_bump = if ctx.atom_derivation { 0.1 } else { 0.0 };
let corroboration_bump = 0.05
* (1.0_f64 + ctx.prior_corroboration_count.max(0) as f64)
.log10()
.max(0.0);
let age_penalty = 0.02 * age * decay_rate;
let raw_base = 0.5 + atom_bump + corroboration_bump - age_penalty;
let clamped_base = raw_base.clamp(0.0, 1.0);
let baseline = ctx.baseline_per_source.clamp(0.0, 1.0);
let blended = clamped_base.mul_add(freshness_factor, baseline * (1.0 - freshness_factor));
let value = blended.clamp(0.0, 1.0);
let signals = ConfidenceSignals {
source_age_days: age,
atom_derivation: ctx.atom_derivation,
prior_corroboration_count: ctx.prior_corroboration_count,
freshness_factor,
baseline_per_source: baseline,
};
(value, signals, ConfidenceSource::AutoDerived)
}
#[cfg(test)]
mod tests {
use super::*;
fn mem() -> Memory {
Memory {
id: "m1".into(),
..Memory::default()
}
}
#[test]
fn derive_atom_bump_lifts_score() {
let ctx_no_atom = DeriveContext {
atom_derivation: false,
..Default::default()
};
let ctx_atom = DeriveContext {
atom_derivation: true,
..Default::default()
};
let (no_atom, _, _) = derive(&mem(), &ctx_no_atom);
let (atom, _, _) = derive(&mem(), &ctx_atom);
assert!(
atom > no_atom,
"atom-derivation should raise confidence: {atom} vs {no_atom}"
);
}
#[test]
fn derive_corroboration_lifts_score_sublinearly() {
let (low, _, _) = derive(
&mem(),
&DeriveContext {
prior_corroboration_count: 1,
..Default::default()
},
);
let (high, _, _) = derive(
&mem(),
&DeriveContext {
prior_corroboration_count: 100,
..Default::default()
},
);
assert!(high > low, "corroboration should monotonically raise score");
}
#[test]
fn derive_age_reduces_score() {
let (fresh, _, _) = derive(
&mem(),
&DeriveContext {
source_age_days: 0.0,
..Default::default()
},
);
let (old, _, _) = derive(
&mem(),
&DeriveContext {
source_age_days: 365.0,
..Default::default()
},
);
assert!(
fresh > old,
"older sources should have lower confidence: {fresh} vs {old}"
);
}
#[test]
fn derive_clamps_to_unit_interval() {
let ctx = DeriveContext {
source_age_days: 10_000.0,
atom_derivation: false,
prior_corroboration_count: 0,
baseline_per_source: 0.0,
half_life_days: 30.0,
};
let (value, _, _) = derive(&mem(), &ctx);
assert!((0.0..=1.0).contains(&value), "value out of range: {value}");
}
#[test]
fn derive_returns_signals_envelope_matching_inputs() {
let ctx = DeriveContext {
source_age_days: 15.0,
atom_derivation: true,
prior_corroboration_count: 5,
baseline_per_source: 0.6,
half_life_days: 30.0,
};
let (_value, signals, source) = derive(&mem(), &ctx);
assert_eq!(source, ConfidenceSource::AutoDerived);
assert!((signals.source_age_days - 15.0).abs() < f64::EPSILON);
assert!(signals.atom_derivation);
assert_eq!(signals.prior_corroboration_count, 5);
assert!((signals.baseline_per_source - 0.6).abs() < f64::EPSILON);
assert!((signals.freshness_factor - 0.7071).abs() < 0.01);
}
#[test]
fn derive_is_deterministic() {
let ctx = DeriveContext {
source_age_days: 7.5,
atom_derivation: false,
prior_corroboration_count: 3,
baseline_per_source: 0.55,
half_life_days: 30.0,
};
let (a, _, _) = derive(&mem(), &ctx);
let (b, _, _) = derive(&mem(), &ctx);
assert!(
(a - b).abs() < f64::EPSILON,
"derive must be deterministic for fixed inputs: {a} vs {b}"
);
}
#[test]
fn derive_never_returns_one_for_default_context() {
let (value, _, _) = derive(&mem(), &DeriveContext::default());
assert!((value - 0.5).abs() < 0.05);
}
#[test]
fn auto_confidence_env_gating_default_off() {
unsafe { std::env::remove_var(ENV_AUTO_CONFIDENCE) };
assert!(!auto_confidence_enabled());
unsafe { std::env::set_var(ENV_AUTO_CONFIDENCE, "0") };
assert!(!auto_confidence_enabled());
unsafe { std::env::set_var(ENV_AUTO_CONFIDENCE, "1") };
assert!(auto_confidence_enabled());
unsafe { std::env::remove_var(ENV_AUTO_CONFIDENCE) };
}
}