Skip to main content

reasonkit_web/research/
verification.rs

1//! Verification Result Types
2//!
3//! Defines the outcome structures for triangulated verification.
4
5use super::sources::{SourceQuality, SourceTier};
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9/// Overall verification status
10#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum VerificationStatus {
13    /// Claim verified by 3+ independent sources with agreement
14    Verified,
15    /// Claim partially verified (2 sources, or minor discrepancies)
16    PartiallyVerified,
17    /// Sources conflict - claim disputed
18    Conflicting,
19    /// Could not verify - insufficient sources
20    Unverified,
21    /// Claim appears to be false based on sources
22    Refuted,
23    /// Verification in progress
24    #[default]
25    Pending,
26}
27
28impl VerificationStatus {
29    /// Check if the status represents a successful verification
30    pub fn is_success(&self) -> bool {
31        matches!(self, Self::Verified | Self::PartiallyVerified)
32    }
33
34    /// Check if the status indicates a problem
35    pub fn is_problem(&self) -> bool {
36        matches!(self, Self::Conflicting | Self::Refuted)
37    }
38
39    /// Get a human-readable description
40    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    /// Get an emoji indicator
52    pub fn emoji(&self) -> &'static str {
53        match self {
54            Self::Verified => "\u{2705}",          // Green checkmark
55            Self::PartiallyVerified => "\u{26a0}", // Warning
56            Self::Conflicting => "\u{274c}",       // Red X
57            Self::Unverified => "\u{2753}",        // Question mark
58            Self::Refuted => "\u{1f6ab}",          // No entry
59            Self::Pending => "\u{23f3}",           // Hourglass
60        }
61    }
62}
63
64/// A verified source with extracted information
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct VerifiedSource {
67    /// Original URL
68    pub url: String,
69    /// Page title
70    pub title: Option<String>,
71    /// Source quality assessment
72    pub quality: SourceQuality,
73    /// Extracted relevant content (snippet)
74    pub content_snippet: Option<String>,
75    /// Does this source support the claim?
76    pub supports_claim: Option<bool>,
77    /// Confidence in this source's relevance (0.0 - 1.0)
78    pub relevance_score: f64,
79    /// When this source was accessed
80    pub accessed_at: DateTime<Utc>,
81    /// Any errors encountered while accessing
82    pub access_errors: Vec<String>,
83    /// HTTP status code if available
84    pub http_status: Option<u16>,
85}
86
87impl VerifiedSource {
88    /// Create a new verified source
89    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    /// Check if this source is usable for verification
104    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    /// Get weighted confidence (tier weight * relevance)
114    pub fn weighted_confidence(&self) -> f64 {
115        self.quality.tier.weight() * self.relevance_score
116    }
117}
118
119/// Evidence supporting or refuting a claim
120#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct Evidence {
122    /// The source providing this evidence
123    pub source_url: String,
124    /// Extracted quote or content
125    pub quote: String,
126    /// Does this evidence support (true) or refute (false) the claim?
127    pub supports: bool,
128    /// Confidence in this evidence (0.0 - 1.0)
129    pub confidence: f64,
130    /// Position in source content where found
131    pub position: Option<usize>,
132}
133
134/// Aggregated verification metrics
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct VerificationMetrics {
137    /// Total sources consulted
138    pub total_sources: usize,
139    /// Sources that were successfully accessed
140    pub accessible_sources: usize,
141    /// Sources that support the claim
142    pub supporting_sources: usize,
143    /// Sources that refute the claim
144    pub refuting_sources: usize,
145    /// Sources that are neutral/unclear
146    pub neutral_sources: usize,
147    /// Count by tier
148    pub tier1_count: usize,
149    /// Count by tier
150    pub tier2_count: usize,
151    /// Count by tier
152    pub tier3_count: usize,
153    /// Average weighted confidence
154    pub average_confidence: f64,
155    /// Total verification time in milliseconds
156    pub verification_time_ms: u64,
157}
158
159impl VerificationMetrics {
160    /// Create new empty metrics
161    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    /// Calculate metrics from verified sources
177    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    /// Check if triangulation requirement is met (3+ quality sources)
231    pub fn meets_triangulation(&self) -> bool {
232        // Need at least 3 accessible sources with at least 2 being Tier 1 or 2
233        self.accessible_sources >= 3 && (self.tier1_count + self.tier2_count) >= 2
234    }
235
236    /// Determine verification status based on metrics
237    ///
238    /// Uses a multi-step approach:
239    /// 1. Check if triangulation requirements are met
240    /// 2. Detect genuine conflicts (roughly even split)
241    /// 3. Determine majority status (agreement vs refutation)
242    /// 4. Apply confidence as a modifier within categories
243    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        // Step 1: Detect genuine conflict (roughly 1/3 on each side)
261        let conflict_level = f64::min(agreement_ratio, refutation_ratio);
262        if conflict_level > 0.33 {
263            return VerificationStatus::Conflicting;
264        }
265
266        // Step 2: Check for clear refutation (majority refute)
267        if refutation_ratio > 0.5 {
268            return VerificationStatus::Refuted;
269        }
270
271        // Step 3: Check for verification (2/3+ agreement is strong consensus)
272        // Use 0.67 threshold (2/3) which is more statistically robust for n>=3
273        if agreement_ratio >= 0.67 {
274            // Confidence modulates certainty WITHIN the verified category
275            return if self.average_confidence >= 0.7 {
276                VerificationStatus::Verified
277            } else {
278                VerificationStatus::PartiallyVerified
279            };
280        }
281
282        // Step 4: Partial verification (simple majority)
283        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()); // Only 1 Tier1/2 source
347    }
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}