1use 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
53pub struct ProofGuard {
59 config: ThinkToolModuleConfig,
61 triangulation_config: TriangulationConfig,
63}
64
65impl Default for ProofGuard {
66 fn default() -> Self {
67 Self::new()
68 }
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct ProofGuardInput {
74 pub claim: String,
76 #[serde(default)]
78 pub sources: Vec<ProofGuardSource>,
79 #[serde(default)]
81 pub min_sources: Option<usize>,
82 #[serde(default)]
84 pub require_tier1: Option<bool>,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct ProofGuardSource {
90 pub name: String,
92 #[serde(default = "default_tier")]
94 pub tier: String,
95 #[serde(default = "default_source_type")]
97 pub source_type: String,
98 #[serde(default = "default_stance")]
100 pub stance: String,
101 #[serde(default)]
103 pub url: Option<String>,
104 #[serde(default)]
106 pub domain: Option<String>,
107 #[serde(default)]
109 pub author: Option<String>,
110 #[serde(default)]
112 pub quote: Option<String>,
113 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct ProofGuardOutput {
133 pub verdict: ProofGuardVerdict,
135 pub claim: String,
137 pub verification_score: f64,
139 pub is_verified: bool,
141 pub confidence_level: String,
143 pub recommendation: String,
145 pub sources: Vec<SourceSummary>,
147 pub contradictions: Vec<ContradictionInfo>,
149 pub issues: Vec<IssueInfo>,
151 pub stats: VerificationStats,
153}
154
155#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
157#[serde(rename_all = "snake_case")]
158pub enum ProofGuardVerdict {
159 Verified,
161 PartiallyVerified,
163 Contested,
165 InsufficientSources,
167 Refuted,
169 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#[derive(Debug, Clone, Serialize, Deserialize)]
188pub struct SourceSummary {
189 pub name: String,
191 pub tier: String,
193 pub weight: f64,
195 pub stance: String,
197 pub verified: bool,
199 pub effective_weight: f64,
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize)]
205pub struct ContradictionInfo {
206 pub supporting_sources: Vec<String>,
208 pub contradicting_sources: Vec<String>,
210 pub severity: String,
212 pub description: String,
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize)]
218pub struct IssueInfo {
219 pub issue_type: String,
221 pub severity: String,
223 pub description: String,
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize)]
229pub struct VerificationStats {
230 pub total_sources: usize,
232 pub supporting_count: usize,
234 pub contradicting_count: usize,
236 pub neutral_count: usize,
238 pub tier1_count: usize,
240 pub tier2_count: usize,
242 pub tier3_count: usize,
244 pub tier4_count: usize,
246 pub source_diversity: f64,
248 pub triangulation_weight: f64,
250}
251
252impl ProofGuard {
253 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 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 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 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 fn parse_input(&self, context: &ThinkToolContext) -> Result<ProofGuardInput, Error> {
309 if let Ok(input) = serde_json::from_str::<ProofGuardInput>(&context.query) {
311 return Ok(input);
312 }
313
314 Ok(ProofGuardInput {
316 claim: context.query.clone(),
317 sources: Vec::new(),
318 min_sources: None,
319 require_tier1: None,
320 })
321 }
322
323 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 fn convert_result(&self, result: &TriangulationResult) -> ProofGuardOutput {
379 let verdict = self.determine_verdict(result);
381
382 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 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 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 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 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 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 fn determine_verdict(&self, result: &TriangulationResult) -> ProofGuardVerdict {
520 if result
522 .issues
523 .iter()
524 .any(|i| i.issue_type == TriangulationIssueType::InsufficientSources)
525 {
526 return ProofGuardVerdict::InsufficientSources;
527 }
528
529 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 if result.contradict_count > 0 && result.support_count == 0 {
539 return ProofGuardVerdict::Refuted;
540 }
541
542 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 fn calculate_confidence(&self, result: &TriangulationResult) -> f64 {
565 let base_confidence = result.verification_score as f64;
567
568 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 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 let diversity_boost = result.source_diversity as f64 * 0.1;
589
590 (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 let input = self.parse_input(context)?;
603
604 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 let mut triangulator = Triangulator::with_config(config);
615 triangulator.set_claim(&input.claim);
616
617 for src in &input.sources {
619 let source = self.convert_source(src);
620 triangulator.add_source(source);
621 }
622
623 let result = triangulator.verify();
625
626 let output = self.convert_result(&result);
628
629 let confidence = self.calculate_confidence(&result);
631
632 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 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 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 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 assert_ne!(output.verdict, ProofGuardVerdict::InsufficientSources);
878 }
879}