reasonkit/thinktool/modules/
proofguard.rs

1//! ProofGuard Module - Multi-Source Verification
2//!
3//! Triangulates claims across 3+ independent sources to verify factual accuracy.
4//!
5//! ## Core Features
6//!
7//! - **3+ Source Requirement**: Enforces triangulation protocol (CONS-006)
8//! - **Contradiction Detection**: Identifies conflicting evidence
9//! - **Source Tier Ranking**: Weights evidence by source quality
10//! - **Confidence Scoring**: Produces calibrated verification scores
11//!
12//! ## Source Tiers
13//!
14//! | Tier | Weight | Examples |
15//! |------|--------|----------|
16//! | Tier 1 (Primary) | 1.0 | Official docs, peer-reviewed papers, primary sources |
17//! | Tier 2 (Secondary) | 0.7 | Reputable news, expert blogs, industry reports |
18//! | Tier 3 (Independent) | 0.4 | Community content, forums |
19//! | Tier 4 (Unverified) | 0.2 | Social media, unknown sources |
20//!
21//! ## Usage
22//!
23//! ```rust,ignore
24//! use reasonkit::thinktool::modules::{ProofGuard, ThinkToolContext, ThinkToolModule};
25//!
26//! let proofguard = ProofGuard::new();
27//!
28//! // Context with claim and sources (JSON format)
29//! let context = ThinkToolContext {
30//!     query: r#"{
31//!         "claim": "Rust is memory-safe without a garbage collector",
32//!         "sources": [
33//!             {"name": "Rust Book", "tier": "Primary", "stance": "Support"},
34//!             {"name": "ACM Paper", "tier": "Primary", "stance": "Support"},
35//!             {"name": "Tech Blog", "tier": "Secondary", "stance": "Support"}
36//!         ]
37//!     }"#.to_string(),
38//!     previous_steps: vec![],
39//! };
40//!
41//! let result = proofguard.execute(&context)?;
42//! ```
43
44use super::{ThinkToolContext, ThinkToolModule, ThinkToolModuleConfig, ThinkToolOutput};
45use crate::error::Error;
46use crate::thinktool::triangulation::{
47    IssueSeverity, Source, SourceTier, SourceType, Stance, TriangulationConfig,
48    TriangulationIssueType, TriangulationResult, Triangulator, VerificationConfidence,
49    VerificationRecommendation,
50};
51use serde::{Deserialize, Serialize};
52
53/// ProofGuard reasoning module for fact verification.
54///
55/// Verifies claims using triangulated evidence from multiple sources.
56/// Implements the three-source rule (CONS-006) and provides structured
57/// verification output with confidence scoring.
58pub struct ProofGuard {
59    /// Module configuration
60    config: ThinkToolModuleConfig,
61    /// Triangulation configuration
62    triangulation_config: TriangulationConfig,
63}
64
65impl Default for ProofGuard {
66    fn default() -> Self {
67        Self::new()
68    }
69}
70
71/// Input format for ProofGuard verification
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct ProofGuardInput {
74    /// The claim to verify
75    pub claim: String,
76    /// Sources supporting or contradicting the claim
77    #[serde(default)]
78    pub sources: Vec<ProofGuardSource>,
79    /// Optional: Minimum number of sources required (default: 3)
80    #[serde(default)]
81    pub min_sources: Option<usize>,
82    /// Optional: Require at least one Tier 1 source (default: true)
83    #[serde(default)]
84    pub require_tier1: Option<bool>,
85}
86
87/// Source input for ProofGuard
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct ProofGuardSource {
90    /// Source name/title
91    pub name: String,
92    /// Source tier (Primary, Secondary, Independent, Unverified)
93    #[serde(default = "default_tier")]
94    pub tier: String,
95    /// Source type (Academic, Documentation, News, Expert, Government, Industry, Community, Social, PrimaryData)
96    #[serde(default = "default_source_type")]
97    pub source_type: String,
98    /// Source stance (Support, Contradict, Neutral, Partial)
99    #[serde(default = "default_stance")]
100    pub stance: String,
101    /// URL if available
102    #[serde(default)]
103    pub url: Option<String>,
104    /// Domain/field
105    #[serde(default)]
106    pub domain: Option<String>,
107    /// Author/organization
108    #[serde(default)]
109    pub author: Option<String>,
110    /// Direct quote supporting/refuting the claim
111    #[serde(default)]
112    pub quote: Option<String>,
113    /// Whether URL has been verified accessible
114    #[serde(default)]
115    pub verified: bool,
116}
117
118fn default_tier() -> String {
119    "Unverified".to_string()
120}
121
122fn default_source_type() -> String {
123    "Documentation".to_string()
124}
125
126fn default_stance() -> String {
127    "Neutral".to_string()
128}
129
130/// Output from ProofGuard verification
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct ProofGuardOutput {
133    /// Overall verification verdict
134    pub verdict: ProofGuardVerdict,
135    /// Claim being verified
136    pub claim: String,
137    /// Verification score (0.0-1.0)
138    pub verification_score: f64,
139    /// Whether the claim is verified
140    pub is_verified: bool,
141    /// Confidence level
142    pub confidence_level: String,
143    /// Recommendation for how to treat this claim
144    pub recommendation: String,
145    /// Sources analyzed
146    pub sources: Vec<SourceSummary>,
147    /// Detected contradictions
148    pub contradictions: Vec<ContradictionInfo>,
149    /// Issues found during verification
150    pub issues: Vec<IssueInfo>,
151    /// Statistics
152    pub stats: VerificationStats,
153}
154
155/// Verification verdict
156#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
157#[serde(rename_all = "snake_case")]
158pub enum ProofGuardVerdict {
159    /// Claim verified with high confidence
160    Verified,
161    /// Claim partially verified, needs qualifier
162    PartiallyVerified,
163    /// Conflicting evidence found
164    Contested,
165    /// Insufficient sources
166    InsufficientSources,
167    /// Claim contradicted by evidence
168    Refuted,
169    /// Unable to determine
170    Inconclusive,
171}
172
173impl std::fmt::Display for ProofGuardVerdict {
174    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
175        match self {
176            ProofGuardVerdict::Verified => write!(f, "Verified"),
177            ProofGuardVerdict::PartiallyVerified => write!(f, "Partially Verified"),
178            ProofGuardVerdict::Contested => write!(f, "Contested"),
179            ProofGuardVerdict::InsufficientSources => write!(f, "Insufficient Sources"),
180            ProofGuardVerdict::Refuted => write!(f, "Refuted"),
181            ProofGuardVerdict::Inconclusive => write!(f, "Inconclusive"),
182        }
183    }
184}
185
186/// Summary of a source used in verification
187#[derive(Debug, Clone, Serialize, Deserialize)]
188pub struct SourceSummary {
189    /// Source name
190    pub name: String,
191    /// Source tier label
192    pub tier: String,
193    /// Tier weight (0.0-1.0)
194    pub weight: f64,
195    /// Stance on the claim
196    pub stance: String,
197    /// Whether verified
198    pub verified: bool,
199    /// Effective weight after modifiers
200    pub effective_weight: f64,
201}
202
203/// Information about detected contradictions
204#[derive(Debug, Clone, Serialize, Deserialize)]
205pub struct ContradictionInfo {
206    /// Sources that support the claim
207    pub supporting_sources: Vec<String>,
208    /// Sources that contradict the claim
209    pub contradicting_sources: Vec<String>,
210    /// Severity of the contradiction
211    pub severity: String,
212    /// Description
213    pub description: String,
214}
215
216/// Information about verification issues
217#[derive(Debug, Clone, Serialize, Deserialize)]
218pub struct IssueInfo {
219    /// Issue type
220    pub issue_type: String,
221    /// Severity (Warning, Error, Critical)
222    pub severity: String,
223    /// Description
224    pub description: String,
225}
226
227/// Statistics from verification
228#[derive(Debug, Clone, Serialize, Deserialize)]
229pub struct VerificationStats {
230    /// Total sources analyzed
231    pub total_sources: usize,
232    /// Number of supporting sources
233    pub supporting_count: usize,
234    /// Number of contradicting sources
235    pub contradicting_count: usize,
236    /// Number of neutral sources
237    pub neutral_count: usize,
238    /// Number of Tier 1 sources
239    pub tier1_count: usize,
240    /// Number of Tier 2 sources
241    pub tier2_count: usize,
242    /// Number of Tier 3 sources
243    pub tier3_count: usize,
244    /// Number of Tier 4 sources
245    pub tier4_count: usize,
246    /// Source diversity score (0.0-1.0)
247    pub source_diversity: f64,
248    /// Triangulation weight (support - contradict)
249    pub triangulation_weight: f64,
250}
251
252impl ProofGuard {
253    /// Create a new ProofGuard module instance.
254    pub fn new() -> Self {
255        Self {
256            config: ThinkToolModuleConfig {
257                name: "ProofGuard".to_string(),
258                version: "2.1.0".to_string(),
259                description: "Triangulation-based fact verification with 3+ source requirement"
260                    .to_string(),
261                confidence_weight: 0.30,
262            },
263            triangulation_config: TriangulationConfig::default(),
264        }
265    }
266
267    /// Create with custom triangulation configuration.
268    pub fn with_config(triangulation_config: TriangulationConfig) -> Self {
269        Self {
270            config: ThinkToolModuleConfig {
271                name: "ProofGuard".to_string(),
272                version: "2.1.0".to_string(),
273                description: "Triangulation-based fact verification with 3+ source requirement"
274                    .to_string(),
275                confidence_weight: 0.30,
276            },
277            triangulation_config,
278        }
279    }
280
281    /// Create with strict configuration (requires 2 Tier 1 sources).
282    pub fn strict() -> Self {
283        let config = TriangulationConfig {
284            min_sources: 3,
285            min_tier1_sources: 2,
286            verification_threshold: 0.7,
287            require_verified_urls: true,
288            require_domain_diversity: true,
289            ..Default::default()
290        };
291        Self::with_config(config)
292    }
293
294    /// Create with relaxed configuration (1 Tier 1 source sufficient).
295    pub fn relaxed() -> Self {
296        let config = TriangulationConfig {
297            min_sources: 2,
298            min_tier1_sources: 0,
299            verification_threshold: 0.5,
300            require_verified_urls: false,
301            require_domain_diversity: false,
302            ..Default::default()
303        };
304        Self::with_config(config)
305    }
306
307    /// Parse the input from context.
308    fn parse_input(&self, context: &ThinkToolContext) -> Result<ProofGuardInput, Error> {
309        // First try to parse as JSON
310        if let Ok(input) = serde_json::from_str::<ProofGuardInput>(&context.query) {
311            return Ok(input);
312        }
313
314        // If not JSON, treat as plain text claim with no sources
315        Ok(ProofGuardInput {
316            claim: context.query.clone(),
317            sources: Vec::new(),
318            min_sources: None,
319            require_tier1: None,
320        })
321    }
322
323    /// Convert ProofGuardSource to triangulation Source.
324    fn convert_source(&self, src: &ProofGuardSource) -> Source {
325        let tier = match src.tier.to_lowercase().as_str() {
326            "primary" | "tier1" | "tier 1" => SourceTier::Primary,
327            "secondary" | "tier2" | "tier 2" => SourceTier::Secondary,
328            "independent" | "tier3" | "tier 3" => SourceTier::Independent,
329            _ => SourceTier::Unverified,
330        };
331
332        let source_type = match src.source_type.to_lowercase().as_str() {
333            "academic" => SourceType::Academic,
334            "documentation" | "docs" => SourceType::Documentation,
335            "news" => SourceType::News,
336            "expert" | "blog" => SourceType::Expert,
337            "government" | "gov" => SourceType::Government,
338            "industry" => SourceType::Industry,
339            "community" | "forum" => SourceType::Community,
340            "social" | "socialmedia" => SourceType::Social,
341            "primarydata" | "data" => SourceType::PrimaryData,
342            _ => SourceType::Documentation,
343        };
344
345        let stance = match src.stance.to_lowercase().as_str() {
346            "support" | "supports" | "supporting" => Stance::Support,
347            "contradict" | "contradicts" | "contradicting" | "oppose" | "against" => {
348                Stance::Contradict
349            }
350            "partial" | "partially" => Stance::Partial,
351            _ => Stance::Neutral,
352        };
353
354        let mut source = Source::new(&src.name, tier)
355            .with_type(source_type)
356            .with_stance(stance);
357
358        if let Some(url) = &src.url {
359            source = source.with_url(url);
360        }
361        if let Some(domain) = &src.domain {
362            source = source.with_domain(domain);
363        }
364        if let Some(author) = &src.author {
365            source = source.with_author(author);
366        }
367        if let Some(quote) = &src.quote {
368            source = source.with_quote(quote);
369        }
370        if src.verified {
371            source = source.verified();
372        }
373
374        source
375    }
376
377    /// Convert triangulation result to ProofGuard output.
378    fn convert_result(&self, result: &TriangulationResult) -> ProofGuardOutput {
379        // Determine verdict
380        let verdict = self.determine_verdict(result);
381
382        // Build source summaries
383        let sources: Vec<SourceSummary> = result
384            .sources
385            .iter()
386            .map(|s| SourceSummary {
387                name: s.name.clone(),
388                tier: s.tier.label().to_string(),
389                weight: s.tier.weight() as f64,
390                stance: format!("{:?}", s.stance),
391                verified: s.verified,
392                effective_weight: s.effective_weight() as f64,
393            })
394            .collect();
395
396        // Build contradiction info if present
397        let contradictions = if result.contradict_count > 0 && result.support_count > 0 {
398            let supporting: Vec<String> = result
399                .sources
400                .iter()
401                .filter(|s| matches!(s.stance, Stance::Support | Stance::Partial))
402                .map(|s| s.name.clone())
403                .collect();
404            let contradicting: Vec<String> = result
405                .sources
406                .iter()
407                .filter(|s| matches!(s.stance, Stance::Contradict))
408                .map(|s| s.name.clone())
409                .collect();
410
411            vec![ContradictionInfo {
412                supporting_sources: supporting,
413                contradicting_sources: contradicting,
414                severity: if result.contradict_count >= result.support_count {
415                    "High".to_string()
416                } else {
417                    "Medium".to_string()
418                },
419                description: format!(
420                    "{} sources support while {} sources contradict the claim",
421                    result.support_count, result.contradict_count
422                ),
423            }]
424        } else {
425            vec![]
426        };
427
428        // Build issue info
429        let issues: Vec<IssueInfo> = result
430            .issues
431            .iter()
432            .map(|i| IssueInfo {
433                issue_type: format!("{:?}", i.issue_type),
434                severity: match i.severity {
435                    IssueSeverity::Warning => "Warning".to_string(),
436                    IssueSeverity::Error => "Error".to_string(),
437                    IssueSeverity::Critical => "Critical".to_string(),
438                },
439                description: i.description.clone(),
440            })
441            .collect();
442
443        // Calculate tier counts
444        let tier1_count = result
445            .sources
446            .iter()
447            .filter(|s| s.tier == SourceTier::Primary)
448            .count();
449        let tier2_count = result
450            .sources
451            .iter()
452            .filter(|s| s.tier == SourceTier::Secondary)
453            .count();
454        let tier3_count = result
455            .sources
456            .iter()
457            .filter(|s| s.tier == SourceTier::Independent)
458            .count();
459        let tier4_count = result
460            .sources
461            .iter()
462            .filter(|s| s.tier == SourceTier::Unverified)
463            .count();
464        let neutral_count = result
465            .sources
466            .iter()
467            .filter(|s| matches!(s.stance, Stance::Neutral))
468            .count();
469
470        // Build stats
471        let stats = VerificationStats {
472            total_sources: result.sources.len(),
473            supporting_count: result.support_count,
474            contradicting_count: result.contradict_count,
475            neutral_count,
476            tier1_count,
477            tier2_count,
478            tier3_count,
479            tier4_count,
480            source_diversity: result.source_diversity as f64,
481            triangulation_weight: result.triangulation_weight as f64,
482        };
483
484        // Format recommendation
485        let recommendation = match &result.recommendation {
486            VerificationRecommendation::AcceptAsFact => "Accept as fact".to_string(),
487            VerificationRecommendation::AcceptWithQualifier(q) => {
488                format!("Accept with qualifier: {}", q)
489            }
490            VerificationRecommendation::NeedsMoreSources => {
491                "Need more sources before making this claim".to_string()
492            }
493            VerificationRecommendation::PresentBothSides => {
494                "Present both supporting and contradicting evidence".to_string()
495            }
496            VerificationRecommendation::Reject => {
497                "Evidence does not support this claim".to_string()
498            }
499            VerificationRecommendation::Inconclusive => {
500                "Unable to determine - more research needed".to_string()
501            }
502        };
503
504        ProofGuardOutput {
505            verdict,
506            claim: result.claim.clone(),
507            verification_score: result.verification_score as f64,
508            is_verified: result.is_verified,
509            confidence_level: format!("{:?}", result.confidence),
510            recommendation,
511            sources,
512            contradictions,
513            issues,
514            stats,
515        }
516    }
517
518    /// Determine the verdict based on triangulation result.
519    fn determine_verdict(&self, result: &TriangulationResult) -> ProofGuardVerdict {
520        // Check for critical issues first
521        if result
522            .issues
523            .iter()
524            .any(|i| i.issue_type == TriangulationIssueType::InsufficientSources)
525        {
526            return ProofGuardVerdict::InsufficientSources;
527        }
528
529        // Check for contradictions
530        if result.contradict_count > 0 && result.support_count > 0 {
531            if result.contradict_count > result.support_count {
532                return ProofGuardVerdict::Refuted;
533            }
534            return ProofGuardVerdict::Contested;
535        }
536
537        // Check if refuted (only contradictions, no support)
538        if result.contradict_count > 0 && result.support_count == 0 {
539            return ProofGuardVerdict::Refuted;
540        }
541
542        // Check verification status
543        match result.confidence {
544            VerificationConfidence::High => {
545                if result.is_verified {
546                    ProofGuardVerdict::Verified
547                } else {
548                    ProofGuardVerdict::PartiallyVerified
549                }
550            }
551            VerificationConfidence::Medium => {
552                if result.is_verified {
553                    ProofGuardVerdict::PartiallyVerified
554                } else {
555                    ProofGuardVerdict::Inconclusive
556                }
557            }
558            VerificationConfidence::Low => ProofGuardVerdict::Inconclusive,
559            VerificationConfidence::Unverifiable => ProofGuardVerdict::Inconclusive,
560        }
561    }
562
563    /// Calculate overall confidence for ThinkToolOutput.
564    fn calculate_confidence(&self, result: &TriangulationResult) -> f64 {
565        // Base confidence from verification score
566        let base_confidence = result.verification_score as f64;
567
568        // Adjust based on confidence level
569        let level_modifier = match result.confidence {
570            VerificationConfidence::High => 1.0,
571            VerificationConfidence::Medium => 0.85,
572            VerificationConfidence::Low => 0.6,
573            VerificationConfidence::Unverifiable => 0.3,
574        };
575
576        // Penalize for issues
577        let issue_penalty: f64 = result
578            .issues
579            .iter()
580            .map(|i| match i.severity {
581                IssueSeverity::Critical => 0.3,
582                IssueSeverity::Error => 0.15,
583                IssueSeverity::Warning => 0.05,
584            })
585            .sum();
586
587        // Boost for source diversity
588        let diversity_boost = result.source_diversity as f64 * 0.1;
589
590        // Calculate final confidence - return directly
591        (base_confidence * level_modifier - issue_penalty + diversity_boost).clamp(0.0, 1.0)
592    }
593}
594
595impl ThinkToolModule for ProofGuard {
596    fn config(&self) -> &ThinkToolModuleConfig {
597        &self.config
598    }
599
600    fn execute(&self, context: &ThinkToolContext) -> Result<ThinkToolOutput, Error> {
601        // Parse input
602        let input = self.parse_input(context)?;
603
604        // Apply any input-level configuration overrides
605        let mut config = self.triangulation_config.clone();
606        if let Some(min) = input.min_sources {
607            config.min_sources = min;
608        }
609        if let Some(req) = input.require_tier1 {
610            config.min_tier1_sources = if req { 1 } else { 0 };
611        }
612
613        // Create triangulator
614        let mut triangulator = Triangulator::with_config(config);
615        triangulator.set_claim(&input.claim);
616
617        // Convert and add sources
618        for src in &input.sources {
619            let source = self.convert_source(src);
620            triangulator.add_source(source);
621        }
622
623        // Execute verification
624        let result = triangulator.verify();
625
626        // Convert to ProofGuard output
627        let output = self.convert_result(&result);
628
629        // Calculate confidence
630        let confidence = self.calculate_confidence(&result);
631
632        // Return structured output
633        Ok(ThinkToolOutput {
634            module: self.config.name.clone(),
635            confidence,
636            output: serde_json::to_value(&output).map_err(|e| {
637                Error::ThinkToolExecutionError(format!("Failed to serialize output: {}", e))
638            })?,
639        })
640    }
641}
642
643#[cfg(test)]
644mod tests {
645    use super::*;
646
647    #[test]
648    fn test_proofguard_new() {
649        let pg = ProofGuard::new();
650        assert_eq!(pg.config.name, "ProofGuard");
651        assert_eq!(pg.config.version, "2.1.0");
652    }
653
654    #[test]
655    fn test_proofguard_default() {
656        let pg = ProofGuard::default();
657        assert_eq!(pg.config.name, "ProofGuard");
658    }
659
660    #[test]
661    fn test_proofguard_strict() {
662        let pg = ProofGuard::strict();
663        assert_eq!(pg.triangulation_config.min_tier1_sources, 2);
664        assert!(pg.triangulation_config.require_verified_urls);
665    }
666
667    #[test]
668    fn test_proofguard_relaxed() {
669        let pg = ProofGuard::relaxed();
670        assert_eq!(pg.triangulation_config.min_sources, 2);
671        assert_eq!(pg.triangulation_config.min_tier1_sources, 0);
672    }
673
674    #[test]
675    fn test_execute_with_json_input() {
676        let pg = ProofGuard::new();
677        let input_json = r#"{
678            "claim": "Rust is memory-safe without garbage collection",
679            "sources": [
680                {"name": "Rust Book", "tier": "Primary", "stance": "Support", "verified": true},
681                {"name": "ACM Paper", "tier": "Primary", "stance": "Support", "domain": "PL"},
682                {"name": "Tech Blog", "tier": "Secondary", "stance": "Support"}
683            ]
684        }"#;
685
686        let context = ThinkToolContext {
687            query: input_json.to_string(),
688            previous_steps: vec![],
689        };
690
691        let result = pg.execute(&context).unwrap();
692
693        assert_eq!(result.module, "ProofGuard");
694        assert!(result.confidence > 0.0);
695
696        // Parse the output
697        let output: ProofGuardOutput = serde_json::from_value(result.output).unwrap();
698        assert_eq!(
699            output.claim,
700            "Rust is memory-safe without garbage collection"
701        );
702        assert_eq!(output.stats.total_sources, 3);
703        assert_eq!(output.stats.tier1_count, 2);
704        assert!(output.is_verified);
705    }
706
707    #[test]
708    fn test_execute_with_plain_text_claim() {
709        let pg = ProofGuard::new();
710        let context = ThinkToolContext {
711            query: "This is a plain text claim".to_string(),
712            previous_steps: vec![],
713        };
714
715        let result = pg.execute(&context).unwrap();
716
717        // Should return insufficient sources
718        let output: ProofGuardOutput = serde_json::from_value(result.output).unwrap();
719        assert_eq!(output.verdict, ProofGuardVerdict::InsufficientSources);
720        assert!(!output.is_verified);
721    }
722
723    #[test]
724    fn test_execute_with_contradictions() {
725        let pg = ProofGuard::new();
726        let input_json = r#"{
727            "claim": "AI will achieve AGI by 2030",
728            "sources": [
729                {"name": "Optimist Paper", "tier": "Primary", "stance": "Support"},
730                {"name": "Skeptic Paper", "tier": "Primary", "stance": "Contradict"},
731                {"name": "Neutral Review", "tier": "Secondary", "stance": "Partial"}
732            ]
733        }"#;
734
735        let context = ThinkToolContext {
736            query: input_json.to_string(),
737            previous_steps: vec![],
738        };
739
740        let result = pg.execute(&context).unwrap();
741        let output: ProofGuardOutput = serde_json::from_value(result.output).unwrap();
742
743        assert_eq!(output.verdict, ProofGuardVerdict::Contested);
744        assert!(!output.contradictions.is_empty());
745        assert!(output.stats.contradicting_count > 0);
746    }
747
748    #[test]
749    fn test_execute_refuted_claim() {
750        let pg = ProofGuard::new();
751        let input_json = r#"{
752            "claim": "The Earth is flat",
753            "sources": [
754                {"name": "NASA", "tier": "Primary", "stance": "Contradict"},
755                {"name": "ESA", "tier": "Primary", "stance": "Contradict"},
756                {"name": "Physics Journal", "tier": "Primary", "stance": "Contradict"}
757            ]
758        }"#;
759
760        let context = ThinkToolContext {
761            query: input_json.to_string(),
762            previous_steps: vec![],
763        };
764
765        let result = pg.execute(&context).unwrap();
766        let output: ProofGuardOutput = serde_json::from_value(result.output).unwrap();
767
768        assert_eq!(output.verdict, ProofGuardVerdict::Refuted);
769        assert!(!output.is_verified);
770    }
771
772    #[test]
773    fn test_source_tier_parsing() {
774        let pg = ProofGuard::new();
775
776        // Test various tier formats
777        let test_cases = vec![
778            ("Primary", SourceTier::Primary),
779            ("primary", SourceTier::Primary),
780            ("Tier1", SourceTier::Primary),
781            ("tier 1", SourceTier::Primary),
782            ("Secondary", SourceTier::Secondary),
783            ("tier2", SourceTier::Secondary),
784            ("Independent", SourceTier::Independent),
785            ("tier 3", SourceTier::Independent),
786            ("Unverified", SourceTier::Unverified),
787            ("unknown", SourceTier::Unverified),
788        ];
789
790        for (input, expected) in test_cases {
791            let src = ProofGuardSource {
792                name: "Test".to_string(),
793                tier: input.to_string(),
794                source_type: default_source_type(),
795                stance: default_stance(),
796                url: None,
797                domain: None,
798                author: None,
799                quote: None,
800                verified: false,
801            };
802            let converted = pg.convert_source(&src);
803            assert_eq!(converted.tier, expected, "Failed for input: {}", input);
804        }
805    }
806
807    #[test]
808    fn test_stance_parsing() {
809        let pg = ProofGuard::new();
810
811        let test_cases = vec![
812            ("Support", Stance::Support),
813            ("supports", Stance::Support),
814            ("Contradict", Stance::Contradict),
815            ("against", Stance::Contradict),
816            ("Partial", Stance::Partial),
817            ("Neutral", Stance::Neutral),
818            ("unknown", Stance::Neutral),
819        ];
820
821        for (input, expected) in test_cases {
822            let src = ProofGuardSource {
823                name: "Test".to_string(),
824                tier: default_tier(),
825                source_type: default_source_type(),
826                stance: input.to_string(),
827                url: None,
828                domain: None,
829                author: None,
830                quote: None,
831                verified: false,
832            };
833            let converted = pg.convert_source(&src);
834            assert_eq!(converted.stance, expected, "Failed for input: {}", input);
835        }
836    }
837
838    #[test]
839    fn test_verdict_display() {
840        assert_eq!(format!("{}", ProofGuardVerdict::Verified), "Verified");
841        assert_eq!(
842            format!("{}", ProofGuardVerdict::PartiallyVerified),
843            "Partially Verified"
844        );
845        assert_eq!(format!("{}", ProofGuardVerdict::Contested), "Contested");
846        assert_eq!(
847            format!("{}", ProofGuardVerdict::InsufficientSources),
848            "Insufficient Sources"
849        );
850        assert_eq!(format!("{}", ProofGuardVerdict::Refuted), "Refuted");
851        assert_eq!(
852            format!("{}", ProofGuardVerdict::Inconclusive),
853            "Inconclusive"
854        );
855    }
856
857    #[test]
858    fn test_min_sources_override() {
859        let pg = ProofGuard::new();
860        let input_json = r#"{
861            "claim": "Some claim",
862            "sources": [
863                {"name": "Source A", "tier": "Primary", "stance": "Support"}
864            ],
865            "min_sources": 1
866        }"#;
867
868        let context = ThinkToolContext {
869            query: input_json.to_string(),
870            previous_steps: vec![],
871        };
872
873        let result = pg.execute(&context).unwrap();
874        let output: ProofGuardOutput = serde_json::from_value(result.output).unwrap();
875
876        // With min_sources=1, should not be InsufficientSources
877        assert_ne!(output.verdict, ProofGuardVerdict::InsufficientSources);
878    }
879}