use serde::{Deserialize, Serialize};
const PSEUDOCOUNT: f64 = 10.0;
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ExtractionContext {
Explicit, Inferred, Speculative, Authoritative, }
impl ExtractionContext {
#[must_use]
pub fn prior(self) -> f64 {
match self {
Self::Authoritative => 1.0,
Self::Explicit => 0.9,
Self::Inferred => 0.6,
Self::Speculative => 0.3,
}
}
}
impl std::str::FromStr for ExtractionContext {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"explicit" => Ok(Self::Explicit),
"inferred" => Ok(Self::Inferred),
"speculative" => Ok(Self::Speculative),
"authoritative" => Ok(Self::Authoritative),
other => Err(format!("unknown extraction context: {other}")),
}
}
}
#[must_use]
pub fn bayesian_update(current_confidence: f64, corroborate: bool) -> f64 {
let alpha = current_confidence * PSEUDOCOUNT;
let beta = PSEUDOCOUNT - alpha;
if corroborate {
(alpha + 1.0) / (alpha + beta + 1.0)
} else {
alpha / (alpha + beta + 1.0)
}
}
pub const DEFAULT_HALF_LIFE_DAYS: f64 = 90.0;
pub const DECAY_FLOOR: f64 = 0.05;
#[must_use]
pub fn temporal_decay(
stored_confidence: f64,
days_since_reinforced: f64,
half_life_days: f64,
) -> f64 {
if days_since_reinforced <= 0.0 {
return stored_confidence;
}
let decay_factor = 0.5_f64.powf(days_since_reinforced / half_life_days);
let effective = stored_confidence * decay_factor;
effective.max(DECAY_FLOOR)
}
pub fn effective_confidence(
stored_confidence: f64,
last_reinforced: Option<&serde_json::Value>,
valid_from: &serde_json::Value,
now: &chrono::DateTime<chrono::Utc>,
) -> f64 {
let anchor = last_reinforced
.and_then(parse_datetime_value)
.or_else(|| parse_datetime_value(valid_from));
match anchor {
Some(dt) => {
let days = (*now - dt).num_hours() as f64 / 24.0;
temporal_decay(stored_confidence, days, DEFAULT_HALF_LIFE_DAYS)
}
None => stored_confidence, }
}
use super::util::parse_datetime as parse_datetime_value;
#[must_use]
pub fn path_confidence(edge_confidences: &[f64]) -> f64 {
edge_confidences.iter().product()
}
#[cfg(test)]
mod tests {
use super::*;
fn approx_eq(a: f64, b: f64) -> bool {
(a - b).abs() < 0.001
}
#[test]
fn bayesian_update_corroborate_0_6() {
let result = bayesian_update(0.6, true);
assert!(approx_eq(result, 0.636), "got {}", result);
}
#[test]
fn bayesian_update_contradict_0_6() {
let result = bayesian_update(0.6, false);
assert!(approx_eq(result, 0.545), "got {}", result);
}
#[test]
fn bayesian_update_corroborate_0_9() {
let result = bayesian_update(0.9, true);
assert!(approx_eq(result, 0.909), "got {}", result);
}
#[test]
fn bayesian_update_contradict_0_9() {
let result = bayesian_update(0.9, false);
assert!(approx_eq(result, 0.818), "got {}", result);
}
#[test]
fn bayesian_update_corroborate_0_3() {
let result = bayesian_update(0.3, true);
assert!(approx_eq(result, 0.364), "got {}", result);
}
#[test]
fn path_confidence_two_edges() {
let result = path_confidence(&[0.8, 0.7]);
assert!(approx_eq(result, 0.56), "got {}", result);
}
#[test]
fn path_confidence_empty() {
assert_eq!(path_confidence(&[]), 1.0);
}
#[test]
fn extraction_context_priors() {
assert_eq!(ExtractionContext::Authoritative.prior(), 1.0);
assert_eq!(ExtractionContext::Explicit.prior(), 0.9);
assert_eq!(ExtractionContext::Inferred.prior(), 0.6);
assert_eq!(ExtractionContext::Speculative.prior(), 0.3);
}
#[test]
fn temporal_decay_zero_days() {
let result = temporal_decay(0.9, 0.0, 90.0);
assert!(approx_eq(result, 0.9), "got {}", result);
}
#[test]
fn temporal_decay_one_half_life() {
let result = temporal_decay(0.6, 90.0, 90.0);
assert!(approx_eq(result, 0.3), "got {}", result);
}
#[test]
fn temporal_decay_two_half_lives() {
let result = temporal_decay(0.8, 180.0, 90.0);
assert!(approx_eq(result, 0.2), "got {}", result);
}
#[test]
fn temporal_decay_floor() {
let result = temporal_decay(0.3, 900.0, 90.0);
assert!(approx_eq(result, DECAY_FLOOR), "got {}", result);
}
#[test]
fn temporal_decay_negative_days() {
let result = temporal_decay(0.7, -5.0, 90.0);
assert!(approx_eq(result, 0.7), "got {}", result);
}
#[test]
fn temporal_decay_high_confidence_still_decays() {
let result = temporal_decay(1.0, 90.0, 90.0);
assert!(approx_eq(result, 0.5), "got {}", result);
}
#[test]
fn effective_confidence_with_last_reinforced() {
let now = chrono::Utc::now();
let ninety_days_ago = (now - chrono::Duration::days(90)).to_rfc3339();
let valid_from_long_ago = (now - chrono::Duration::days(365)).to_rfc3339();
let last_reinforced = serde_json::Value::String(ninety_days_ago);
let valid_from = serde_json::Value::String(valid_from_long_ago);
let result = effective_confidence(0.6, Some(&last_reinforced), &valid_from, &now);
assert!(
approx_eq(result, 0.3),
"got {} (expected ~0.3, one half-life from last_reinforced)",
result
);
}
#[test]
fn effective_confidence_falls_back_to_valid_from() {
let now = chrono::Utc::now();
let ninety_days_ago = (now - chrono::Duration::days(90)).to_rfc3339();
let valid_from = serde_json::Value::String(ninety_days_ago);
let result = effective_confidence(0.6, None, &valid_from, &now);
assert!(
approx_eq(result, 0.3),
"got {} (expected ~0.3, one half-life from valid_from)",
result
);
}
#[test]
fn effective_confidence_no_parseable_date() {
let now = chrono::Utc::now();
let bad_date = serde_json::Value::String("not-a-date".to_string());
let result = effective_confidence(0.8, None, &bad_date, &now);
assert!(approx_eq(result, 0.8), "got {}", result);
}
#[test]
fn extraction_context_from_str() {
assert_eq!(
"explicit".parse::<ExtractionContext>().unwrap(),
ExtractionContext::Explicit
);
assert_eq!(
"inferred".parse::<ExtractionContext>().unwrap(),
ExtractionContext::Inferred
);
assert_eq!(
"speculative".parse::<ExtractionContext>().unwrap(),
ExtractionContext::Speculative
);
assert_eq!(
"authoritative".parse::<ExtractionContext>().unwrap(),
ExtractionContext::Authoritative
);
assert!("unknown".parse::<ExtractionContext>().is_err());
}
}