Skip to main content

forge_agent/
verify.rs

1//! Verification engine - Post-mutation validation.
2//!
3//! This module implements the verification phase, validating that
4//! mutations meet quality and policy requirements.
5
6use crate::{AgentError, Result};
7use forge_core::Forge;
8use std::process::Command;
9use std::sync::Arc;
10
11/// Verifier for post-mutation validation.
12///
13/// The Verifier runs compile checks, tests, and graph validation.
14#[derive(Clone)]
15pub struct Verifier {
16    /// Forge SDK for graph queries
17    forge: Arc<Forge>,
18}
19
20impl Verifier {
21    /// Creates a new verifier.
22    pub fn new(forge: Forge) -> Self {
23        Self {
24            forge: Arc::new(forge),
25        }
26    }
27
28    /// Runs compile check via cargo.
29    ///
30    /// # Arguments
31    ///
32    /// * `working_dir` - Directory to check
33    pub async fn compile_check(&self, working_dir: &std::path::Path) -> Result<Vec<Diagnostic>> {
34        let output = Command::new("cargo")
35            .args(["check", "--message-format=short"])
36            .current_dir(working_dir)
37            .output()
38            .map_err(|e| AgentError::VerificationFailed(format!("Cargo check failed: {}", e)))?;
39
40        let stdout = String::from_utf8_lossy(&output.stdout);
41        let stderr = String::from_utf8_lossy(&output.stderr);
42
43        let mut diagnostics = Vec::new();
44        for line in stdout.lines().chain(stderr.lines()) {
45            if line.contains("error:") {
46                diagnostics.push(Diagnostic {
47                    level: DiagnosticLevel::Error,
48                    message: line.trim().to_string(),
49                });
50            } else if line.contains("warning:") {
51                diagnostics.push(Diagnostic {
52                    level: DiagnosticLevel::Warning,
53                    message: line.trim().to_string(),
54                });
55            }
56        }
57
58        Ok(diagnostics)
59    }
60
61    /// Runs tests via cargo.
62    ///
63    /// # Arguments
64    ///
65    /// * `working_dir` - Directory to test
66    pub async fn test_check(&self, working_dir: &std::path::Path) -> Result<Vec<Diagnostic>> {
67        let output = Command::new("cargo")
68            .args(["test", "--message-format=short"])
69            .current_dir(working_dir)
70            .output()
71            .map_err(|e| AgentError::VerificationFailed(format!("Cargo test failed: {}", e)))?;
72
73        let stdout = String::from_utf8_lossy(&output.stdout);
74        let stderr = String::from_utf8_lossy(&output.stderr);
75
76        let mut diagnostics = Vec::new();
77
78        // Parse test results
79        for line in stdout.lines().chain(stderr.lines()) {
80            if line.contains("test result:") && line.contains("FAILED") {
81                diagnostics.push(Diagnostic {
82                    level: DiagnosticLevel::Error,
83                    message: line.trim().to_string(),
84                });
85            } else if line.contains("test result:") && line.contains("ok") {
86                // Tests passed
87            }
88        }
89
90        Ok(diagnostics)
91    }
92
93    /// Validates graph consistency.
94    ///
95    /// Checks for orphan references and broken symbol links.
96    pub async fn graph_check(&self, _working_dir: &std::path::Path) -> Result<Vec<Diagnostic>> {
97        let mut diagnostics = Vec::new();
98
99        // For v0.4, this is a simplified check
100        // In production, would query graph for orphan references
101
102        // Placeholder: graph is assumed consistent
103        diagnostics.push(Diagnostic {
104            level: DiagnosticLevel::Info,
105            message: "Graph consistency check: skipped (not yet implemented)".to_string(),
106        });
107
108        Ok(diagnostics)
109    }
110
111    /// Runs full verification.
112    ///
113    /// # Arguments
114    ///
115    /// * `working_dir` - Directory to verify
116    pub async fn verify(&self, working_dir: &std::path::Path) -> Result<VerificationReport> {
117        let mut all_diagnostics = Vec::new();
118
119        // Run compile check
120        let compile_diags = self.compile_check(working_dir).await?;
121        all_diagnostics.extend(compile_diags);
122
123        // Run test check
124        let test_diags = self.test_check(working_dir).await?;
125        all_diagnostics.extend(test_diags);
126
127        // Run graph check
128        let graph_diags = self.graph_check(working_dir).await?;
129        all_diagnostics.extend(graph_diags);
130
131        let errors = all_diagnostics
132            .iter()
133            .filter(|d| d.level == DiagnosticLevel::Error)
134            .count();
135
136        let passed = errors == 0;
137
138        Ok(VerificationReport {
139            passed,
140            diagnostics: all_diagnostics,
141        })
142    }
143}
144
145/// Verification report.
146#[derive(Clone, Debug)]
147pub struct VerificationReport {
148    /// Whether verification passed
149    pub passed: bool,
150    /// Any diagnostics or errors
151    pub diagnostics: Vec<Diagnostic>,
152}
153
154/// Diagnostic message.
155#[derive(Clone, Debug)]
156pub struct Diagnostic {
157    /// Severity level
158    pub level: DiagnosticLevel,
159    /// Diagnostic message
160    pub message: String,
161}
162
163/// Diagnostic severity level.
164#[derive(Clone, Debug, PartialEq, Eq)]
165pub enum DiagnosticLevel {
166    /// Info message
167    Info,
168    /// Warning message
169    Warning,
170    /// Error message
171    Error,
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177    use tempfile::TempDir;
178
179    #[tokio::test]
180    async fn test_verifier_creation() {
181        let temp_dir = TempDir::new().unwrap();
182        let forge = Forge::open(temp_dir.path()).await.unwrap();
183        let _verifier = Verifier::new(forge);
184
185        // Should create successfully - verifier stores forge internally
186        assert!(true);
187    }
188
189    #[test]
190    fn test_diagnostic_level_equality() {
191        assert_eq!(DiagnosticLevel::Error, DiagnosticLevel::Error);
192        assert_ne!(DiagnosticLevel::Error, DiagnosticLevel::Warning);
193    }
194}