use super::sources::{SourceQuality, SourceTier};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum VerificationStatus {
Verified,
PartiallyVerified,
Conflicting,
Unverified,
Refuted,
#[default]
Pending,
}
impl VerificationStatus {
pub fn is_success(&self) -> bool {
matches!(self, Self::Verified | Self::PartiallyVerified)
}
pub fn is_problem(&self) -> bool {
matches!(self, Self::Conflicting | Self::Refuted)
}
pub fn description(&self) -> &'static str {
match self {
Self::Verified => "Verified by 3+ independent sources",
Self::PartiallyVerified => "Partially verified (fewer sources or minor discrepancies)",
Self::Conflicting => "Sources conflict - claim disputed",
Self::Unverified => "Could not verify - insufficient sources",
Self::Refuted => "Claim appears false based on sources",
Self::Pending => "Verification in progress",
}
}
pub fn emoji(&self) -> &'static str {
match self {
Self::Verified => "\u{2705}", Self::PartiallyVerified => "\u{26a0}", Self::Conflicting => "\u{274c}", Self::Unverified => "\u{2753}", Self::Refuted => "\u{1f6ab}", Self::Pending => "\u{23f3}", }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VerifiedSource {
pub url: String,
pub title: Option<String>,
pub quality: SourceQuality,
pub content_snippet: Option<String>,
pub supports_claim: Option<bool>,
pub relevance_score: f64,
pub accessed_at: DateTime<Utc>,
pub access_errors: Vec<String>,
pub http_status: Option<u16>,
}
impl VerifiedSource {
pub fn new(url: String, quality: SourceQuality) -> Self {
Self {
url,
title: None,
quality,
content_snippet: None,
supports_claim: None,
relevance_score: 0.0,
accessed_at: Utc::now(),
access_errors: Vec::new(),
http_status: None,
}
}
pub fn is_usable(&self) -> bool {
self.access_errors.is_empty()
&& self
.http_status
.map(|s| (200..400).contains(&s))
.unwrap_or(true)
&& self.quality.tier != SourceTier::Unknown
}
pub fn weighted_confidence(&self) -> f64 {
self.quality.tier.weight() * self.relevance_score
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Evidence {
pub source_url: String,
pub quote: String,
pub supports: bool,
pub confidence: f64,
pub position: Option<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VerificationMetrics {
pub total_sources: usize,
pub accessible_sources: usize,
pub supporting_sources: usize,
pub refuting_sources: usize,
pub neutral_sources: usize,
pub tier1_count: usize,
pub tier2_count: usize,
pub tier3_count: usize,
pub average_confidence: f64,
pub verification_time_ms: u64,
}
impl VerificationMetrics {
pub fn new() -> Self {
Self {
total_sources: 0,
accessible_sources: 0,
supporting_sources: 0,
refuting_sources: 0,
neutral_sources: 0,
tier1_count: 0,
tier2_count: 0,
tier3_count: 0,
average_confidence: 0.0,
verification_time_ms: 0,
}
}
pub fn from_sources(sources: &[VerifiedSource], time_ms: u64) -> Self {
let accessible: Vec<&VerifiedSource> = sources.iter().filter(|s| s.is_usable()).collect();
let supporting = accessible
.iter()
.filter(|s| s.supports_claim == Some(true))
.count();
let refuting = accessible
.iter()
.filter(|s| s.supports_claim == Some(false))
.count();
let neutral = accessible
.iter()
.filter(|s| s.supports_claim.is_none())
.count();
let tier1 = accessible
.iter()
.filter(|s| s.quality.tier == SourceTier::Tier1)
.count();
let tier2 = accessible
.iter()
.filter(|s| s.quality.tier == SourceTier::Tier2)
.count();
let tier3 = accessible
.iter()
.filter(|s| s.quality.tier == SourceTier::Tier3)
.count();
let avg_conf = if !accessible.is_empty() {
accessible
.iter()
.map(|s| s.weighted_confidence())
.sum::<f64>()
/ accessible.len() as f64
} else {
0.0
};
Self {
total_sources: sources.len(),
accessible_sources: accessible.len(),
supporting_sources: supporting,
refuting_sources: refuting,
neutral_sources: neutral,
tier1_count: tier1,
tier2_count: tier2,
tier3_count: tier3,
average_confidence: avg_conf,
verification_time_ms: time_ms,
}
}
pub fn meets_triangulation(&self) -> bool {
self.accessible_sources >= 3 && (self.tier1_count + self.tier2_count) >= 2
}
pub fn determine_status(&self) -> VerificationStatus {
if !self.meets_triangulation() {
return VerificationStatus::Unverified;
}
let agreement_ratio = if self.accessible_sources > 0 {
self.supporting_sources as f64 / self.accessible_sources as f64
} else {
0.0
};
let refutation_ratio = if self.accessible_sources > 0 {
self.refuting_sources as f64 / self.accessible_sources as f64
} else {
0.0
};
let conflict_level = f64::min(agreement_ratio, refutation_ratio);
if conflict_level > 0.33 {
return VerificationStatus::Conflicting;
}
if refutation_ratio > 0.5 {
return VerificationStatus::Refuted;
}
if agreement_ratio >= 0.67 {
return if self.average_confidence >= 0.7 {
VerificationStatus::Verified
} else {
VerificationStatus::PartiallyVerified
};
}
if agreement_ratio > 0.5 {
return VerificationStatus::PartiallyVerified;
}
VerificationStatus::Unverified
}
}
impl Default for VerificationMetrics {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_verification_status_success() {
assert!(VerificationStatus::Verified.is_success());
assert!(VerificationStatus::PartiallyVerified.is_success());
assert!(!VerificationStatus::Conflicting.is_success());
assert!(!VerificationStatus::Unverified.is_success());
}
#[test]
fn test_verification_status_problem() {
assert!(!VerificationStatus::Verified.is_problem());
assert!(VerificationStatus::Conflicting.is_problem());
assert!(VerificationStatus::Refuted.is_problem());
}
#[test]
fn test_verified_source_usable() {
let mut source = VerifiedSource::new(
"https://example.com".to_string(),
SourceQuality {
tier: SourceTier::Tier1,
..Default::default()
},
);
source.http_status = Some(200);
assert!(source.is_usable());
source.access_errors.push("timeout".to_string());
assert!(!source.is_usable());
}
#[test]
fn test_metrics_triangulation() {
let mut metrics = VerificationMetrics::new();
metrics.accessible_sources = 3;
metrics.tier1_count = 1;
metrics.tier2_count = 2;
assert!(metrics.meets_triangulation());
metrics.tier1_count = 0;
metrics.tier2_count = 1;
metrics.tier3_count = 2;
assert!(!metrics.meets_triangulation()); }
#[test]
fn test_metrics_determine_status() {
let mut metrics = VerificationMetrics::new();
metrics.accessible_sources = 4;
metrics.tier1_count = 2;
metrics.tier2_count = 2;
metrics.supporting_sources = 4;
metrics.average_confidence = 0.8;
assert_eq!(metrics.determine_status(), VerificationStatus::Verified);
metrics.supporting_sources = 2;
metrics.refuting_sources = 2;
assert_eq!(metrics.determine_status(), VerificationStatus::Conflicting);
}
}