use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use crate::error::Result;
use crate::evidence::EvidenceParser;
use crate::github::{GitHubClient, GitHubConfig, GitHubVerifier};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VerificationResult {
pub verified: bool,
pub confidence: f64,
pub evidence_valid: bool,
pub details: String,
}
#[async_trait]
pub trait OutputVerifier: Send + Sync {
async fn verify_github_commit(
&self,
repo_url: &str,
commit_hash: &str,
expected_author: &str,
) -> Result<VerificationResult>;
async fn verify_content_url(
&self,
url: &str,
expected_content_hash: Option<&str>,
) -> Result<VerificationResult>;
}
pub struct DefaultVerifier {
github_client: Option<GitHubClient>,
}
impl Default for DefaultVerifier {
fn default() -> Self {
Self::new()
}
}
impl DefaultVerifier {
#[must_use]
pub fn new() -> Self {
Self {
github_client: None,
}
}
pub fn with_github_token(token: String) -> Result<Self> {
let config = GitHubConfig::default().with_token(token);
let client = GitHubClient::new(config)?;
Ok(Self {
github_client: Some(client),
})
}
#[must_use]
pub fn with_github_client(client: GitHubClient) -> Self {
Self {
github_client: Some(client),
}
}
}
#[async_trait]
impl OutputVerifier for DefaultVerifier {
async fn verify_github_commit(
&self,
repo_url: &str,
commit_hash: &str,
expected_author: &str,
) -> Result<VerificationResult> {
if let Some(client) = &self.github_client {
let verifier = GitHubVerifier::new(client.clone());
let parts: Vec<&str> = repo_url.trim_end_matches('/').split('/').collect();
if parts.len() >= 2 {
let owner = parts[parts.len() - 2];
let repo = parts[parts.len() - 1];
let verification = verifier.verify_commit(owner, repo, commit_hash).await?;
let author_matches = expected_author.is_empty()
|| verification
.author
.to_lowercase()
.contains(&expected_author.to_lowercase());
let verified = verification.exists && author_matches;
let confidence = if verified {
if author_matches { 0.95 } else { 0.75 }
} else {
0.3
};
let details = if verified {
format!(
"Commit {} by {} verified successfully",
&verification.sha[..7.min(verification.sha.len())],
verification.author
)
} else if !verification.exists {
"Commit not found".to_string()
} else {
format!(
"Commit exists but author mismatch (expected: {}, actual: {})",
expected_author, verification.author
)
};
return Ok(VerificationResult {
verified,
confidence,
evidence_valid: verification.exists,
details,
});
}
}
Ok(VerificationResult {
verified: false,
confidence: 0.0,
evidence_valid: false,
details: "Verification pending - manual review required (GitHub client not configured)"
.to_string(),
})
}
async fn verify_content_url(
&self,
url: &str,
_expected_content_hash: Option<&str>,
) -> Result<VerificationResult> {
let is_valid = EvidenceParser::validate_url(url);
if !is_valid {
return Ok(VerificationResult {
verified: false,
confidence: 0.0,
evidence_valid: false,
details: "Invalid URL format".to_string(),
});
}
let evidence_type = EvidenceParser::detect_evidence_type(url);
let confidence = match evidence_type {
crate::evidence::EvidenceType::GitHub => 0.9,
crate::evidence::EvidenceType::YouTube => 0.8,
crate::evidence::EvidenceType::Twitter => 0.8,
crate::evidence::EvidenceType::Website => 0.7,
crate::evidence::EvidenceType::BlogPost => 0.7,
crate::evidence::EvidenceType::Documentation => 0.8,
crate::evidence::EvidenceType::Image => 0.6,
crate::evidence::EvidenceType::Pdf => 0.7,
crate::evidence::EvidenceType::Unknown => 0.5,
};
Ok(VerificationResult {
verified: is_valid,
confidence,
evidence_valid: is_valid,
details: format!("URL validated as {evidence_type:?} evidence"),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_verify_content_url_valid() {
let verifier = DefaultVerifier::new();
let result = verifier
.verify_content_url("https://github.com/owner/repo", None)
.await
.unwrap();
assert!(result.verified);
assert!(result.confidence > 0.0);
}
#[tokio::test]
async fn test_verify_content_url_invalid() {
let verifier = DefaultVerifier::new();
let result = verifier
.verify_content_url("not-a-url", None)
.await
.unwrap();
assert!(!result.verified);
assert!(result.confidence.abs() < 1e-10);
}
#[tokio::test]
async fn test_verify_github_commit_without_client() {
let verifier = DefaultVerifier::new();
let result = verifier
.verify_github_commit("https://github.com/owner/repo", "abc123", "author")
.await
.unwrap();
assert!(!result.verified);
assert!(result.details.contains("manual review required"));
}
}