use super::{ThinkToolContext, ThinkToolModule, ThinkToolModuleConfig, ThinkToolOutput};
use crate::error::Error;
use crate::thinktool::triangulation::{
IssueSeverity, Source, SourceTier, SourceType, Stance, TriangulationConfig,
TriangulationIssueType, TriangulationResult, Triangulator, VerificationConfidence,
VerificationRecommendation,
};
use serde::{Deserialize, Serialize};
pub struct ProofGuard {
config: ThinkToolModuleConfig,
triangulation_config: TriangulationConfig,
}
impl Default for ProofGuard {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProofGuardInput {
pub claim: String,
#[serde(default)]
pub sources: Vec<ProofGuardSource>,
#[serde(default)]
pub min_sources: Option<usize>,
#[serde(default)]
pub require_tier1: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProofGuardSource {
pub name: String,
#[serde(default = "default_tier")]
pub tier: String,
#[serde(default = "default_source_type")]
pub source_type: String,
#[serde(default = "default_stance")]
pub stance: String,
#[serde(default)]
pub url: Option<String>,
#[serde(default)]
pub domain: Option<String>,
#[serde(default)]
pub author: Option<String>,
#[serde(default)]
pub quote: Option<String>,
#[serde(default)]
pub verified: bool,
}
fn default_tier() -> String {
"Unverified".to_string()
}
fn default_source_type() -> String {
"Documentation".to_string()
}
fn default_stance() -> String {
"Neutral".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProofGuardOutput {
pub verdict: ProofGuardVerdict,
pub claim: String,
pub verification_score: f64,
pub is_verified: bool,
pub confidence_level: String,
pub recommendation: String,
pub sources: Vec<SourceSummary>,
pub contradictions: Vec<ContradictionInfo>,
pub issues: Vec<IssueInfo>,
pub stats: VerificationStats,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProofGuardVerdict {
Verified,
PartiallyVerified,
Contested,
InsufficientSources,
Refuted,
Inconclusive,
}
impl std::fmt::Display for ProofGuardVerdict {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ProofGuardVerdict::Verified => write!(f, "Verified"),
ProofGuardVerdict::PartiallyVerified => write!(f, "Partially Verified"),
ProofGuardVerdict::Contested => write!(f, "Contested"),
ProofGuardVerdict::InsufficientSources => write!(f, "Insufficient Sources"),
ProofGuardVerdict::Refuted => write!(f, "Refuted"),
ProofGuardVerdict::Inconclusive => write!(f, "Inconclusive"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SourceSummary {
pub name: String,
pub tier: String,
pub weight: f64,
pub stance: String,
pub verified: bool,
pub effective_weight: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContradictionInfo {
pub supporting_sources: Vec<String>,
pub contradicting_sources: Vec<String>,
pub severity: String,
pub description: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IssueInfo {
pub issue_type: String,
pub severity: String,
pub description: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VerificationStats {
pub total_sources: usize,
pub supporting_count: usize,
pub contradicting_count: usize,
pub neutral_count: usize,
pub tier1_count: usize,
pub tier2_count: usize,
pub tier3_count: usize,
pub tier4_count: usize,
pub source_diversity: f64,
pub triangulation_weight: f64,
}
impl ProofGuard {
pub fn new() -> Self {
Self {
config: ThinkToolModuleConfig {
name: "ProofGuard".to_string(),
version: "2.1.0".to_string(),
description: "Triangulation-based fact verification with 3+ source requirement"
.to_string(),
confidence_weight: 0.30,
},
triangulation_config: TriangulationConfig::default(),
}
}
pub fn with_config(triangulation_config: TriangulationConfig) -> Self {
Self {
config: ThinkToolModuleConfig {
name: "ProofGuard".to_string(),
version: "2.1.0".to_string(),
description: "Triangulation-based fact verification with 3+ source requirement"
.to_string(),
confidence_weight: 0.30,
},
triangulation_config,
}
}
pub fn strict() -> Self {
let config = TriangulationConfig {
min_sources: 3,
min_tier1_sources: 2,
verification_threshold: 0.7,
require_verified_urls: true,
require_domain_diversity: true,
..Default::default()
};
Self::with_config(config)
}
pub fn relaxed() -> Self {
let config = TriangulationConfig {
min_sources: 2,
min_tier1_sources: 0,
verification_threshold: 0.5,
require_verified_urls: false,
require_domain_diversity: false,
..Default::default()
};
Self::with_config(config)
}
fn parse_input(&self, context: &ThinkToolContext) -> Result<ProofGuardInput, Error> {
if let Ok(input) = serde_json::from_str::<ProofGuardInput>(&context.query) {
return Ok(input);
}
Ok(ProofGuardInput {
claim: context.query.clone(),
sources: Vec::new(),
min_sources: None,
require_tier1: None,
})
}
fn convert_source(&self, src: &ProofGuardSource) -> Source {
let tier = match src.tier.to_lowercase().as_str() {
"primary" | "tier1" | "tier 1" => SourceTier::Primary,
"secondary" | "tier2" | "tier 2" => SourceTier::Secondary,
"independent" | "tier3" | "tier 3" => SourceTier::Independent,
_ => SourceTier::Unverified,
};
let source_type = match src.source_type.to_lowercase().as_str() {
"academic" => SourceType::Academic,
"documentation" | "docs" => SourceType::Documentation,
"news" => SourceType::News,
"expert" | "blog" => SourceType::Expert,
"government" | "gov" => SourceType::Government,
"industry" => SourceType::Industry,
"community" | "forum" => SourceType::Community,
"social" | "socialmedia" => SourceType::Social,
"primarydata" | "data" => SourceType::PrimaryData,
_ => SourceType::Documentation,
};
let stance = match src.stance.to_lowercase().as_str() {
"support" | "supports" | "supporting" => Stance::Support,
"contradict" | "contradicts" | "contradicting" | "oppose" | "against" => {
Stance::Contradict
}
"partial" | "partially" => Stance::Partial,
_ => Stance::Neutral,
};
let mut source = Source::new(&src.name, tier)
.with_type(source_type)
.with_stance(stance);
if let Some(url) = &src.url {
source = source.with_url(url);
}
if let Some(domain) = &src.domain {
source = source.with_domain(domain);
}
if let Some(author) = &src.author {
source = source.with_author(author);
}
if let Some(quote) = &src.quote {
source = source.with_quote(quote);
}
if src.verified {
source = source.verified();
}
source
}
fn convert_result(&self, result: &TriangulationResult) -> ProofGuardOutput {
let verdict = self.determine_verdict(result);
let sources: Vec<SourceSummary> = result
.sources
.iter()
.map(|s| SourceSummary {
name: s.name.clone(),
tier: s.tier.label().to_string(),
weight: s.tier.weight() as f64,
stance: format!("{:?}", s.stance),
verified: s.verified,
effective_weight: s.effective_weight() as f64,
})
.collect();
let contradictions = if result.contradict_count > 0 && result.support_count > 0 {
let supporting: Vec<String> = result
.sources
.iter()
.filter(|s| matches!(s.stance, Stance::Support | Stance::Partial))
.map(|s| s.name.clone())
.collect();
let contradicting: Vec<String> = result
.sources
.iter()
.filter(|s| matches!(s.stance, Stance::Contradict))
.map(|s| s.name.clone())
.collect();
vec![ContradictionInfo {
supporting_sources: supporting,
contradicting_sources: contradicting,
severity: if result.contradict_count >= result.support_count {
"High".to_string()
} else {
"Medium".to_string()
},
description: format!(
"{} sources support while {} sources contradict the claim",
result.support_count, result.contradict_count
),
}]
} else {
vec![]
};
let issues: Vec<IssueInfo> = result
.issues
.iter()
.map(|i| IssueInfo {
issue_type: format!("{:?}", i.issue_type),
severity: match i.severity {
IssueSeverity::Warning => "Warning".to_string(),
IssueSeverity::Error => "Error".to_string(),
IssueSeverity::Critical => "Critical".to_string(),
},
description: i.description.clone(),
})
.collect();
let tier1_count = result
.sources
.iter()
.filter(|s| s.tier == SourceTier::Primary)
.count();
let tier2_count = result
.sources
.iter()
.filter(|s| s.tier == SourceTier::Secondary)
.count();
let tier3_count = result
.sources
.iter()
.filter(|s| s.tier == SourceTier::Independent)
.count();
let tier4_count = result
.sources
.iter()
.filter(|s| s.tier == SourceTier::Unverified)
.count();
let neutral_count = result
.sources
.iter()
.filter(|s| matches!(s.stance, Stance::Neutral))
.count();
let stats = VerificationStats {
total_sources: result.sources.len(),
supporting_count: result.support_count,
contradicting_count: result.contradict_count,
neutral_count,
tier1_count,
tier2_count,
tier3_count,
tier4_count,
source_diversity: result.source_diversity as f64,
triangulation_weight: result.triangulation_weight as f64,
};
let recommendation = match &result.recommendation {
VerificationRecommendation::AcceptAsFact => "Accept as fact".to_string(),
VerificationRecommendation::AcceptWithQualifier(q) => {
format!("Accept with qualifier: {}", q)
}
VerificationRecommendation::NeedsMoreSources => {
"Need more sources before making this claim".to_string()
}
VerificationRecommendation::PresentBothSides => {
"Present both supporting and contradicting evidence".to_string()
}
VerificationRecommendation::Reject => {
"Evidence does not support this claim".to_string()
}
VerificationRecommendation::Inconclusive => {
"Unable to determine - more research needed".to_string()
}
};
ProofGuardOutput {
verdict,
claim: result.claim.clone(),
verification_score: result.verification_score as f64,
is_verified: result.is_verified,
confidence_level: format!("{:?}", result.confidence),
recommendation,
sources,
contradictions,
issues,
stats,
}
}
fn determine_verdict(&self, result: &TriangulationResult) -> ProofGuardVerdict {
if result
.issues
.iter()
.any(|i| i.issue_type == TriangulationIssueType::InsufficientSources)
{
return ProofGuardVerdict::InsufficientSources;
}
if result.contradict_count > 0 && result.support_count > 0 {
if result.contradict_count > result.support_count {
return ProofGuardVerdict::Refuted;
}
return ProofGuardVerdict::Contested;
}
if result.contradict_count > 0 && result.support_count == 0 {
return ProofGuardVerdict::Refuted;
}
match result.confidence {
VerificationConfidence::High => {
if result.is_verified {
ProofGuardVerdict::Verified
} else {
ProofGuardVerdict::PartiallyVerified
}
}
VerificationConfidence::Medium => {
if result.is_verified {
ProofGuardVerdict::PartiallyVerified
} else {
ProofGuardVerdict::Inconclusive
}
}
VerificationConfidence::Low => ProofGuardVerdict::Inconclusive,
VerificationConfidence::Unverifiable => ProofGuardVerdict::Inconclusive,
}
}
fn calculate_confidence(&self, result: &TriangulationResult) -> f64 {
let base_confidence = result.verification_score as f64;
let level_modifier = match result.confidence {
VerificationConfidence::High => 1.0,
VerificationConfidence::Medium => 0.85,
VerificationConfidence::Low => 0.6,
VerificationConfidence::Unverifiable => 0.3,
};
let issue_penalty: f64 = result
.issues
.iter()
.map(|i| match i.severity {
IssueSeverity::Critical => 0.3,
IssueSeverity::Error => 0.15,
IssueSeverity::Warning => 0.05,
})
.sum();
let diversity_boost = result.source_diversity as f64 * 0.1;
(base_confidence * level_modifier - issue_penalty + diversity_boost).clamp(0.0, 1.0)
}
}
impl ThinkToolModule for ProofGuard {
fn config(&self) -> &ThinkToolModuleConfig {
&self.config
}
fn execute(&self, context: &ThinkToolContext) -> Result<ThinkToolOutput, Error> {
let input = self.parse_input(context)?;
let mut config = self.triangulation_config.clone();
if let Some(min) = input.min_sources {
config.min_sources = min;
}
if let Some(req) = input.require_tier1 {
config.min_tier1_sources = if req { 1 } else { 0 };
}
let mut triangulator = Triangulator::with_config(config);
triangulator.set_claim(&input.claim);
for src in &input.sources {
let source = self.convert_source(src);
triangulator.add_source(source);
}
let result = triangulator.verify();
let output = self.convert_result(&result);
let confidence = self.calculate_confidence(&result);
Ok(ThinkToolOutput {
module: self.config.name.clone(),
confidence,
output: serde_json::to_value(&output).map_err(|e| {
Error::ThinkToolExecutionError(format!("Failed to serialize output: {}", e))
})?,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_proofguard_new() {
let pg = ProofGuard::new();
assert_eq!(pg.config.name, "ProofGuard");
assert_eq!(pg.config.version, "2.1.0");
}
#[test]
fn test_proofguard_default() {
let pg = ProofGuard::default();
assert_eq!(pg.config.name, "ProofGuard");
}
#[test]
fn test_proofguard_strict() {
let pg = ProofGuard::strict();
assert_eq!(pg.triangulation_config.min_tier1_sources, 2);
assert!(pg.triangulation_config.require_verified_urls);
}
#[test]
fn test_proofguard_relaxed() {
let pg = ProofGuard::relaxed();
assert_eq!(pg.triangulation_config.min_sources, 2);
assert_eq!(pg.triangulation_config.min_tier1_sources, 0);
}
#[test]
fn test_execute_with_json_input() {
let pg = ProofGuard::new();
let input_json = r#"{
"claim": "Rust is memory-safe without garbage collection",
"sources": [
{"name": "Rust Book", "tier": "Primary", "stance": "Support", "verified": true},
{"name": "ACM Paper", "tier": "Primary", "stance": "Support", "domain": "PL"},
{"name": "Tech Blog", "tier": "Secondary", "stance": "Support"}
]
}"#;
let context = ThinkToolContext {
query: input_json.to_string(),
previous_steps: vec![],
};
let result = pg.execute(&context).unwrap();
assert_eq!(result.module, "ProofGuard");
assert!(result.confidence > 0.0);
let output: ProofGuardOutput = serde_json::from_value(result.output).unwrap();
assert_eq!(
output.claim,
"Rust is memory-safe without garbage collection"
);
assert_eq!(output.stats.total_sources, 3);
assert_eq!(output.stats.tier1_count, 2);
assert!(output.is_verified);
}
#[test]
fn test_execute_with_plain_text_claim() {
let pg = ProofGuard::new();
let context = ThinkToolContext {
query: "This is a plain text claim".to_string(),
previous_steps: vec![],
};
let result = pg.execute(&context).unwrap();
let output: ProofGuardOutput = serde_json::from_value(result.output).unwrap();
assert_eq!(output.verdict, ProofGuardVerdict::InsufficientSources);
assert!(!output.is_verified);
}
#[test]
fn test_execute_with_contradictions() {
let pg = ProofGuard::new();
let input_json = r#"{
"claim": "AI will achieve AGI by 2030",
"sources": [
{"name": "Optimist Paper", "tier": "Primary", "stance": "Support"},
{"name": "Skeptic Paper", "tier": "Primary", "stance": "Contradict"},
{"name": "Neutral Review", "tier": "Secondary", "stance": "Partial"}
]
}"#;
let context = ThinkToolContext {
query: input_json.to_string(),
previous_steps: vec![],
};
let result = pg.execute(&context).unwrap();
let output: ProofGuardOutput = serde_json::from_value(result.output).unwrap();
assert_eq!(output.verdict, ProofGuardVerdict::Contested);
assert!(!output.contradictions.is_empty());
assert!(output.stats.contradicting_count > 0);
}
#[test]
fn test_execute_refuted_claim() {
let pg = ProofGuard::new();
let input_json = r#"{
"claim": "The Earth is flat",
"sources": [
{"name": "NASA", "tier": "Primary", "stance": "Contradict"},
{"name": "ESA", "tier": "Primary", "stance": "Contradict"},
{"name": "Physics Journal", "tier": "Primary", "stance": "Contradict"}
]
}"#;
let context = ThinkToolContext {
query: input_json.to_string(),
previous_steps: vec![],
};
let result = pg.execute(&context).unwrap();
let output: ProofGuardOutput = serde_json::from_value(result.output).unwrap();
assert_eq!(output.verdict, ProofGuardVerdict::Refuted);
assert!(!output.is_verified);
}
#[test]
fn test_source_tier_parsing() {
let pg = ProofGuard::new();
let test_cases = vec![
("Primary", SourceTier::Primary),
("primary", SourceTier::Primary),
("Tier1", SourceTier::Primary),
("tier 1", SourceTier::Primary),
("Secondary", SourceTier::Secondary),
("tier2", SourceTier::Secondary),
("Independent", SourceTier::Independent),
("tier 3", SourceTier::Independent),
("Unverified", SourceTier::Unverified),
("unknown", SourceTier::Unverified),
];
for (input, expected) in test_cases {
let src = ProofGuardSource {
name: "Test".to_string(),
tier: input.to_string(),
source_type: default_source_type(),
stance: default_stance(),
url: None,
domain: None,
author: None,
quote: None,
verified: false,
};
let converted = pg.convert_source(&src);
assert_eq!(converted.tier, expected, "Failed for input: {}", input);
}
}
#[test]
fn test_stance_parsing() {
let pg = ProofGuard::new();
let test_cases = vec![
("Support", Stance::Support),
("supports", Stance::Support),
("Contradict", Stance::Contradict),
("against", Stance::Contradict),
("Partial", Stance::Partial),
("Neutral", Stance::Neutral),
("unknown", Stance::Neutral),
];
for (input, expected) in test_cases {
let src = ProofGuardSource {
name: "Test".to_string(),
tier: default_tier(),
source_type: default_source_type(),
stance: input.to_string(),
url: None,
domain: None,
author: None,
quote: None,
verified: false,
};
let converted = pg.convert_source(&src);
assert_eq!(converted.stance, expected, "Failed for input: {}", input);
}
}
#[test]
fn test_verdict_display() {
assert_eq!(format!("{}", ProofGuardVerdict::Verified), "Verified");
assert_eq!(
format!("{}", ProofGuardVerdict::PartiallyVerified),
"Partially Verified"
);
assert_eq!(format!("{}", ProofGuardVerdict::Contested), "Contested");
assert_eq!(
format!("{}", ProofGuardVerdict::InsufficientSources),
"Insufficient Sources"
);
assert_eq!(format!("{}", ProofGuardVerdict::Refuted), "Refuted");
assert_eq!(
format!("{}", ProofGuardVerdict::Inconclusive),
"Inconclusive"
);
}
#[test]
fn test_min_sources_override() {
let pg = ProofGuard::new();
let input_json = r#"{
"claim": "Some claim",
"sources": [
{"name": "Source A", "tier": "Primary", "stance": "Support"}
],
"min_sources": 1
}"#;
let context = ThinkToolContext {
query: input_json.to_string(),
previous_steps: vec![],
};
let result = pg.execute(&context).unwrap();
let output: ProofGuardOutput = serde_json::from_value(result.output).unwrap();
assert_ne!(output.verdict, ProofGuardVerdict::InsufficientSources);
}
}