1use super::sources::{SourceQuality, SourceTier};
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum VerificationStatus {
13 Verified,
15 PartiallyVerified,
17 Conflicting,
19 Unverified,
21 Refuted,
23 #[default]
25 Pending,
26}
27
28impl VerificationStatus {
29 pub fn is_success(&self) -> bool {
31 matches!(self, Self::Verified | Self::PartiallyVerified)
32 }
33
34 pub fn is_problem(&self) -> bool {
36 matches!(self, Self::Conflicting | Self::Refuted)
37 }
38
39 pub fn description(&self) -> &'static str {
41 match self {
42 Self::Verified => "Verified by 3+ independent sources",
43 Self::PartiallyVerified => "Partially verified (fewer sources or minor discrepancies)",
44 Self::Conflicting => "Sources conflict - claim disputed",
45 Self::Unverified => "Could not verify - insufficient sources",
46 Self::Refuted => "Claim appears false based on sources",
47 Self::Pending => "Verification in progress",
48 }
49 }
50
51 pub fn emoji(&self) -> &'static str {
53 match self {
54 Self::Verified => "\u{2705}", Self::PartiallyVerified => "\u{26a0}", Self::Conflicting => "\u{274c}", Self::Unverified => "\u{2753}", Self::Refuted => "\u{1f6ab}", Self::Pending => "\u{23f3}", }
61 }
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct VerifiedSource {
67 pub url: String,
69 pub title: Option<String>,
71 pub quality: SourceQuality,
73 pub content_snippet: Option<String>,
75 pub supports_claim: Option<bool>,
77 pub relevance_score: f64,
79 pub accessed_at: DateTime<Utc>,
81 pub access_errors: Vec<String>,
83 pub http_status: Option<u16>,
85}
86
87impl VerifiedSource {
88 pub fn new(url: String, quality: SourceQuality) -> Self {
90 Self {
91 url,
92 title: None,
93 quality,
94 content_snippet: None,
95 supports_claim: None,
96 relevance_score: 0.0,
97 accessed_at: Utc::now(),
98 access_errors: Vec::new(),
99 http_status: None,
100 }
101 }
102
103 pub fn is_usable(&self) -> bool {
105 self.access_errors.is_empty()
106 && self
107 .http_status
108 .map(|s| (200..400).contains(&s))
109 .unwrap_or(true)
110 && self.quality.tier != SourceTier::Unknown
111 }
112
113 pub fn weighted_confidence(&self) -> f64 {
115 self.quality.tier.weight() * self.relevance_score
116 }
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct Evidence {
122 pub source_url: String,
124 pub quote: String,
126 pub supports: bool,
128 pub confidence: f64,
130 pub position: Option<usize>,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct VerificationMetrics {
137 pub total_sources: usize,
139 pub accessible_sources: usize,
141 pub supporting_sources: usize,
143 pub refuting_sources: usize,
145 pub neutral_sources: usize,
147 pub tier1_count: usize,
149 pub tier2_count: usize,
151 pub tier3_count: usize,
153 pub average_confidence: f64,
155 pub verification_time_ms: u64,
157}
158
159impl VerificationMetrics {
160 pub fn new() -> Self {
162 Self {
163 total_sources: 0,
164 accessible_sources: 0,
165 supporting_sources: 0,
166 refuting_sources: 0,
167 neutral_sources: 0,
168 tier1_count: 0,
169 tier2_count: 0,
170 tier3_count: 0,
171 average_confidence: 0.0,
172 verification_time_ms: 0,
173 }
174 }
175
176 pub fn from_sources(sources: &[VerifiedSource], time_ms: u64) -> Self {
178 let accessible: Vec<&VerifiedSource> = sources.iter().filter(|s| s.is_usable()).collect();
179
180 let supporting = accessible
181 .iter()
182 .filter(|s| s.supports_claim == Some(true))
183 .count();
184 let refuting = accessible
185 .iter()
186 .filter(|s| s.supports_claim == Some(false))
187 .count();
188 let neutral = accessible
189 .iter()
190 .filter(|s| s.supports_claim.is_none())
191 .count();
192
193 let tier1 = accessible
194 .iter()
195 .filter(|s| s.quality.tier == SourceTier::Tier1)
196 .count();
197 let tier2 = accessible
198 .iter()
199 .filter(|s| s.quality.tier == SourceTier::Tier2)
200 .count();
201 let tier3 = accessible
202 .iter()
203 .filter(|s| s.quality.tier == SourceTier::Tier3)
204 .count();
205
206 let avg_conf = if !accessible.is_empty() {
207 accessible
208 .iter()
209 .map(|s| s.weighted_confidence())
210 .sum::<f64>()
211 / accessible.len() as f64
212 } else {
213 0.0
214 };
215
216 Self {
217 total_sources: sources.len(),
218 accessible_sources: accessible.len(),
219 supporting_sources: supporting,
220 refuting_sources: refuting,
221 neutral_sources: neutral,
222 tier1_count: tier1,
223 tier2_count: tier2,
224 tier3_count: tier3,
225 average_confidence: avg_conf,
226 verification_time_ms: time_ms,
227 }
228 }
229
230 pub fn meets_triangulation(&self) -> bool {
232 self.accessible_sources >= 3 && (self.tier1_count + self.tier2_count) >= 2
234 }
235
236 pub fn determine_status(&self) -> VerificationStatus {
244 if !self.meets_triangulation() {
245 return VerificationStatus::Unverified;
246 }
247
248 let agreement_ratio = if self.accessible_sources > 0 {
249 self.supporting_sources as f64 / self.accessible_sources as f64
250 } else {
251 0.0
252 };
253
254 let refutation_ratio = if self.accessible_sources > 0 {
255 self.refuting_sources as f64 / self.accessible_sources as f64
256 } else {
257 0.0
258 };
259
260 let conflict_level = f64::min(agreement_ratio, refutation_ratio);
262 if conflict_level > 0.33 {
263 return VerificationStatus::Conflicting;
264 }
265
266 if refutation_ratio > 0.5 {
268 return VerificationStatus::Refuted;
269 }
270
271 if agreement_ratio >= 0.67 {
274 return if self.average_confidence >= 0.7 {
276 VerificationStatus::Verified
277 } else {
278 VerificationStatus::PartiallyVerified
279 };
280 }
281
282 if agreement_ratio > 0.5 {
284 return VerificationStatus::PartiallyVerified;
285 }
286
287 VerificationStatus::Unverified
288 }
289}
290
291impl Default for VerificationMetrics {
292 fn default() -> Self {
293 Self::new()
294 }
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300
301 #[test]
302 fn test_verification_status_success() {
303 assert!(VerificationStatus::Verified.is_success());
304 assert!(VerificationStatus::PartiallyVerified.is_success());
305 assert!(!VerificationStatus::Conflicting.is_success());
306 assert!(!VerificationStatus::Unverified.is_success());
307 }
308
309 #[test]
310 fn test_verification_status_problem() {
311 assert!(!VerificationStatus::Verified.is_problem());
312 assert!(VerificationStatus::Conflicting.is_problem());
313 assert!(VerificationStatus::Refuted.is_problem());
314 }
315
316 #[test]
317 fn test_verified_source_usable() {
318 let mut source = VerifiedSource::new(
319 "https://example.com".to_string(),
320 SourceQuality {
321 tier: SourceTier::Tier1,
322 ..Default::default()
323 },
324 );
325 source.http_status = Some(200);
326
327 assert!(source.is_usable());
328
329 source.access_errors.push("timeout".to_string());
330 assert!(!source.is_usable());
331 }
332
333 #[test]
334 fn test_metrics_triangulation() {
335 let mut metrics = VerificationMetrics::new();
336 metrics.accessible_sources = 3;
337 metrics.tier1_count = 1;
338 metrics.tier2_count = 2;
339
340 assert!(metrics.meets_triangulation());
341
342 metrics.tier1_count = 0;
343 metrics.tier2_count = 1;
344 metrics.tier3_count = 2;
345
346 assert!(!metrics.meets_triangulation()); }
348
349 #[test]
350 fn test_metrics_determine_status() {
351 let mut metrics = VerificationMetrics::new();
352 metrics.accessible_sources = 4;
353 metrics.tier1_count = 2;
354 metrics.tier2_count = 2;
355 metrics.supporting_sources = 4;
356 metrics.average_confidence = 0.8;
357
358 assert_eq!(metrics.determine_status(), VerificationStatus::Verified);
359
360 metrics.supporting_sources = 2;
361 metrics.refuting_sources = 2;
362
363 assert_eq!(metrics.determine_status(), VerificationStatus::Conflicting);
364 }
365}