use crate::claim::Claim;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ConfidenceLevel {
High,
Medium,
Low,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum TrajectoryPattern {
Oscillating,
FlatHigh,
FlatLow,
Convergent,
Divergent,
Mixed,
Insufficient,
}
impl std::fmt::Display for TrajectoryPattern {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TrajectoryPattern::Oscillating => {
write!(f, "OSCILLATING (underdamped — hallucination risk)")
}
TrajectoryPattern::FlatHigh => {
write!(f, "FLAT HIGH (uniformly overconfident — suspicious)")
}
TrajectoryPattern::FlatLow => {
write!(f, "FLAT LOW (consistently cautious — trustworthy)")
}
TrajectoryPattern::Convergent => {
write!(f, "CONVERGENT (builds confidence — good pattern)")
}
TrajectoryPattern::Divergent => write!(f, "DIVERGENT (losing confidence — uncertain)"),
TrajectoryPattern::Mixed => write!(f, "MIXED (no clear pattern)"),
TrajectoryPattern::Insufficient => write!(f, "INSUFFICIENT (too few claims)"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrajectoryAnalysis {
pub pattern: TrajectoryPattern,
pub damping_estimate: f64,
pub trust_modifier: f64,
pub transitions: usize,
pub explanation: String,
}
pub fn analyze_trajectory(claims: &[Claim]) -> TrajectoryAnalysis {
if claims.len() < 3 {
return TrajectoryAnalysis {
pattern: TrajectoryPattern::Insufficient,
damping_estimate: 1.0,
trust_modifier: 0.0,
transitions: 0,
explanation: "Too few claims for trajectory analysis.".to_string(),
};
}
let levels: Vec<ConfidenceLevel> = claims.iter().map(classify_confidence).collect();
let scores: Vec<f64> = levels
.iter()
.map(|l| match l {
ConfidenceLevel::High => 1.0,
ConfidenceLevel::Medium => 0.5,
ConfidenceLevel::Low => 0.0,
})
.collect();
let transitions = count_transitions(&scores);
let trend = compute_trend(&scores);
let avg_confidence = scores.iter().sum::<f64>() / scores.len() as f64;
let variance = scores
.iter()
.map(|s| (s - avg_confidence).powi(2))
.sum::<f64>()
/ scores.len() as f64;
let pattern = classify_pattern(transitions, trend, avg_confidence, variance, scores.len());
let damping_estimate = match &pattern {
TrajectoryPattern::Oscillating => 0.2 + 0.3 * (1.0 / (transitions as f64 + 1.0)),
TrajectoryPattern::FlatHigh => 0.1, TrajectoryPattern::FlatLow => 1.5, TrajectoryPattern::Convergent => 0.9, TrajectoryPattern::Divergent => 0.6, TrajectoryPattern::Mixed => 0.7,
TrajectoryPattern::Insufficient => 1.0,
};
let trust_modifier = match &pattern {
TrajectoryPattern::FlatLow => 0.10, TrajectoryPattern::Convergent => 0.08, TrajectoryPattern::Mixed => 0.0, TrajectoryPattern::Divergent => -0.05, TrajectoryPattern::Oscillating => -0.10, TrajectoryPattern::FlatHigh => -0.12, TrajectoryPattern::Insufficient => 0.0,
};
let explanation = format!(
"{pattern} — damping ζ≈{damping_estimate:.2}, {} transitions, avg confidence {:.0}%, variance {:.3}",
transitions,
avg_confidence * 100.0,
variance
);
TrajectoryAnalysis {
pattern,
damping_estimate,
trust_modifier,
transitions,
explanation,
}
}
fn classify_confidence(claim: &Claim) -> ConfidenceLevel {
if claim.is_hedged {
return ConfidenceLevel::Low;
}
if claim.specificity > 0.5 && claim.is_verifiable {
return ConfidenceLevel::High;
}
if claim.specificity < 0.25 {
return ConfidenceLevel::Low;
}
ConfidenceLevel::Medium
}
fn count_transitions(scores: &[f64]) -> usize {
if scores.len() < 2 {
return 0;
}
let mut transitions = 0;
let mut prev_direction: Option<bool> = None;
for window in scores.windows(2) {
let diff = window[1] - window[0];
if diff.abs() < 0.1 {
continue; }
let increasing = diff > 0.0;
if let Some(prev) = prev_direction {
if prev != increasing {
transitions += 1;
}
}
prev_direction = Some(increasing);
}
transitions
}
fn compute_trend(scores: &[f64]) -> f64 {
let n = scores.len() as f64;
let x_mean = (n - 1.0) / 2.0;
let y_mean = scores.iter().sum::<f64>() / n;
let mut num = 0.0;
let mut den = 0.0;
for (i, s) in scores.iter().enumerate() {
let x = i as f64 - x_mean;
let y = s - y_mean;
num += x * y;
den += x * x;
}
if den.abs() < 1e-10 {
0.0
} else {
num / den
}
}
fn classify_pattern(
transitions: usize,
trend: f64,
avg_confidence: f64,
variance: f64,
n_claims: usize,
) -> TrajectoryPattern {
if transitions >= 2 && variance > 0.1 {
return TrajectoryPattern::Oscillating;
}
if variance < 0.05 {
if avg_confidence > 0.7 {
return TrajectoryPattern::FlatHigh;
} else if avg_confidence < 0.3 {
return TrajectoryPattern::FlatLow;
}
}
if trend > 0.1 && transitions <= 1 {
return TrajectoryPattern::Convergent;
}
if trend < -0.1 && transitions <= 1 {
return TrajectoryPattern::Divergent;
}
if n_claims < 4 {
return TrajectoryPattern::Mixed;
}
TrajectoryPattern::Mixed
}
#[cfg(test)]
mod tests {
use super::*;
fn make_claim(text: &str, specificity: f64, verifiable: bool, hedged: bool) -> Claim {
Claim {
text: text.to_string(),
sentence_idx: 0,
is_verifiable: verifiable,
specificity,
is_hedged: hedged,
}
}
#[test]
fn oscillating_pattern() {
let claims = vec![
make_claim("Einstein was born in 1879.", 0.7, true, false), make_claim("Something might be related.", 0.2, false, true), make_claim("He published exactly 300 papers.", 0.8, true, false), make_claim("Perhaps this is unclear.", 0.1, false, true), make_claim("The answer is precisely 42.", 0.9, true, false), ];
let analysis = analyze_trajectory(&claims);
assert_eq!(analysis.pattern, TrajectoryPattern::Oscillating);
assert!(analysis.trust_modifier < 0.0);
}
#[test]
fn flat_high_pattern() {
let claims = vec![
make_claim("Einstein discovered 47 particles.", 0.8, true, false),
make_claim("He won exactly 3 Nobel prizes.", 0.9, true, false),
make_claim("The theory has 12 dimensions.", 0.7, true, false),
make_claim("Physics explains 99.9% of reality.", 0.8, true, false),
];
let analysis = analyze_trajectory(&claims);
assert_eq!(analysis.pattern, TrajectoryPattern::FlatHigh);
assert!(analysis.trust_modifier < 0.0);
}
#[test]
fn flat_low_pattern() {
let claims = vec![
make_claim("This might be true.", 0.2, false, true),
make_claim("Perhaps it could work.", 0.1, false, true),
make_claim("It is possible that things improve.", 0.15, false, true),
make_claim("Some evidence suggests this.", 0.2, false, true),
];
let analysis = analyze_trajectory(&claims);
assert_eq!(analysis.pattern, TrajectoryPattern::FlatLow);
assert!(analysis.trust_modifier > 0.0);
}
#[test]
fn insufficient_claims() {
let claims = vec![
make_claim("Hello.", 0.1, false, false),
make_claim("World.", 0.1, false, false),
];
let analysis = analyze_trajectory(&claims);
assert_eq!(analysis.pattern, TrajectoryPattern::Insufficient);
assert_eq!(analysis.trust_modifier, 0.0);
}
#[test]
fn damping_bounded() {
let claims = vec![
make_claim("A.", 0.5, true, false),
make_claim("B.", 0.5, false, true),
make_claim("C.", 0.5, true, false),
make_claim("D.", 0.5, false, true),
];
let analysis = analyze_trajectory(&claims);
assert!(analysis.damping_estimate > 0.0);
assert!(analysis.damping_estimate < 2.0);
}
}