Skip to main content

smelt_validator/
validator.rs

1//! Main SmeltValidator implementation
2
3use crate::config::ValidationConfig;
4use crate::crucible::CrucibleAdapter;
5use crate::rules::{BreakingChangeChecker, ComplexityChecker, ValidationRule, VisibilityChecker};
6use crate::semantic::IntentValidator;
7use serde::{Deserialize, Serialize};
8use smelt_core::{IntentRecord, SemanticDelta};
9use std::path::Path;
10
11/// Severity of a validation violation
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13pub enum ValidationSeverity {
14    /// Informational only
15    Info,
16    /// Warning - may proceed
17    Warning,
18    /// Error - should not proceed
19    Error,
20}
21
22/// A validation violation
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct Violation {
25    /// Rule that was violated
26    pub rule: String,
27    /// Severity of the violation
28    pub severity: ValidationSeverity,
29    /// Human-readable message
30    pub message: String,
31    /// Location in code (file path)
32    pub location: Option<String>,
33    /// Suggestion for fixing
34    pub suggestion: Option<String>,
35}
36
37/// Overall validation outcome
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct ValidationOutcome {
40    /// Whether validation passed (no errors)
41    pub passed: bool,
42    /// List of violations
43    pub violations: Vec<Violation>,
44    /// Count of errors
45    pub error_count: usize,
46    /// Count of warnings
47    pub warning_count: usize,
48    /// Count of info messages
49    pub info_count: usize,
50}
51
52impl ValidationOutcome {
53    /// Create a passing outcome
54    pub fn pass() -> Self {
55        Self {
56            passed: true,
57            violations: Vec::new(),
58            error_count: 0,
59            warning_count: 0,
60            info_count: 0,
61        }
62    }
63
64    /// Check if there are any errors
65    pub fn has_errors(&self) -> bool {
66        self.error_count > 0
67    }
68
69    /// Check if there are any warnings
70    pub fn has_warnings(&self) -> bool {
71        self.warning_count > 0
72    }
73
74    /// Get all error violations
75    pub fn errors(&self) -> impl Iterator<Item = &Violation> {
76        self.violations
77            .iter()
78            .filter(|v| v.severity == ValidationSeverity::Error)
79    }
80
81    /// Get all warning violations
82    pub fn warnings(&self) -> impl Iterator<Item = &Violation> {
83        self.violations
84            .iter()
85            .filter(|v| v.severity == ValidationSeverity::Warning)
86    }
87}
88
89/// Main validator for Smelt
90pub struct SmeltValidator {
91    config: ValidationConfig,
92    rules: Vec<Box<dyn ValidationRule>>,
93}
94
95impl SmeltValidator {
96    /// Create a new validator with the given configuration
97    pub fn new(config: ValidationConfig) -> Self {
98        // Add semantic delta validators
99        let rules: Vec<Box<dyn ValidationRule>> = vec![
100            Box::new(BreakingChangeChecker::new(config.semantic.clone())),
101            Box::new(VisibilityChecker::new(config.semantic.clone())),
102            Box::new(ComplexityChecker::new(config.semantic.complexity.clone())),
103            Box::new(IntentValidator::new(config.intent.clone())),
104        ];
105
106        Self { config, rules }
107    }
108
109    /// Create a validator with default configuration
110    pub fn default_config() -> Self {
111        Self::new(ValidationConfig::default())
112    }
113
114    /// Create a validator with strict configuration
115    pub fn strict() -> Self {
116        Self::new(ValidationConfig::strict())
117    }
118
119    /// Load a validator from a smelt directory
120    pub fn from_smelt_dir(smelt_dir: &Path) -> Self {
121        let config = ValidationConfig::load_or_default(smelt_dir);
122        let mut validator = Self::new(config);
123
124        // Add Crucible adapter if architecture config enables it
125        if validator.config.architecture.check_circular_deps
126            || validator.config.architecture.enforce_layers
127        {
128            // Get project root (parent of .smelt)
129            if let Some(project_root) = smelt_dir.parent() {
130                let crucible = CrucibleAdapter::new(project_root)
131                    .with_circular_deps(validator.config.architecture.check_circular_deps);
132                validator.add_rule(Box::new(crucible));
133            }
134        }
135
136        validator
137    }
138
139    /// Get the configuration
140    pub fn config(&self) -> &ValidationConfig {
141        &self.config
142    }
143
144    /// Validate a semantic delta
145    pub fn validate(
146        &self,
147        delta: &SemanticDelta,
148        intent: Option<&IntentRecord>,
149    ) -> ValidationOutcome {
150        let mut violations = Vec::new();
151
152        // Run all validation rules
153        for rule in &self.rules {
154            let rule_violations = rule.validate(delta, intent);
155            violations.extend(rule_violations);
156        }
157
158        // Count by severity
159        let error_count = violations
160            .iter()
161            .filter(|v| v.severity == ValidationSeverity::Error)
162            .count();
163        let warning_count = violations
164            .iter()
165            .filter(|v| v.severity == ValidationSeverity::Warning)
166            .count();
167        let info_count = violations
168            .iter()
169            .filter(|v| v.severity == ValidationSeverity::Info)
170            .count();
171
172        ValidationOutcome {
173            passed: error_count == 0,
174            violations,
175            error_count,
176            warning_count,
177            info_count,
178        }
179    }
180
181    /// Validate a delta and return a simple pass/fail result
182    pub fn validate_simple(&self, delta: &SemanticDelta, intent: Option<&IntentRecord>) -> bool {
183        self.validate(delta, intent).passed
184    }
185
186    /// Add a custom validation rule
187    pub fn add_rule(&mut self, rule: Box<dyn ValidationRule>) {
188        self.rules.push(rule);
189    }
190}
191
192impl Default for SmeltValidator {
193    fn default() -> Self {
194        Self::default_config()
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201    use chrono::Utc;
202    use smelt_core::{ImpactSummary, SemanticChange};
203    use uuid::Uuid;
204
205    fn make_delta(changes: Vec<SemanticChange>) -> SemanticDelta {
206        SemanticDelta {
207            id: Uuid::new_v4(),
208            intent_id: Uuid::new_v4(),
209            timestamp: Utc::now(),
210            from_snapshot: Uuid::new_v4(),
211            to_snapshot: Uuid::new_v4(),
212            changes,
213            impact_summary: ImpactSummary::default(),
214        }
215    }
216
217    #[test]
218    fn test_empty_delta_passes() {
219        let validator = SmeltValidator::default_config();
220        let delta = make_delta(vec![]);
221
222        let outcome = validator.validate(&delta, None);
223        assert!(outcome.passed);
224        assert_eq!(outcome.error_count, 0);
225    }
226
227    #[test]
228    fn test_breaking_change_fails() {
229        let validator = SmeltValidator::default_config();
230
231        let delta = make_delta(vec![SemanticChange::FunctionRemoved {
232            name: "public_api".to_string(),
233            file: "lib.rs".to_string(),
234            was_public: true,
235        }]);
236
237        let outcome = validator.validate(&delta, None);
238        assert!(!outcome.passed);
239        assert_eq!(outcome.error_count, 1);
240    }
241
242    #[test]
243    fn test_private_removal_passes() {
244        let validator = SmeltValidator::default_config();
245
246        let delta = make_delta(vec![SemanticChange::FunctionRemoved {
247            name: "helper".to_string(),
248            file: "lib.rs".to_string(),
249            was_public: false,
250        }]);
251
252        let outcome = validator.validate(&delta, None);
253        assert!(outcome.passed);
254    }
255
256    #[test]
257    fn test_strict_validator() {
258        let validator = SmeltValidator::strict();
259
260        // Even smaller complexity increase should fail in strict mode
261        let delta = SemanticDelta {
262            id: Uuid::new_v4(),
263            intent_id: Uuid::new_v4(),
264            timestamp: Utc::now(),
265            from_snapshot: Uuid::new_v4(),
266            to_snapshot: Uuid::new_v4(),
267            changes: vec![],
268            impact_summary: ImpactSummary {
269                complexity_delta: 10, // Over strict threshold of 5
270                ..Default::default()
271            },
272        };
273
274        let outcome = validator.validate(&delta, None);
275        // Strict mode makes complexity errors
276        assert!(outcome.has_errors() || outcome.has_warnings());
277    }
278
279    #[test]
280    fn test_outcome_helpers() {
281        let outcome = ValidationOutcome {
282            passed: false,
283            violations: vec![
284                Violation {
285                    rule: "test".to_string(),
286                    severity: ValidationSeverity::Error,
287                    message: "error".to_string(),
288                    location: None,
289                    suggestion: None,
290                },
291                Violation {
292                    rule: "test".to_string(),
293                    severity: ValidationSeverity::Warning,
294                    message: "warning".to_string(),
295                    location: None,
296                    suggestion: None,
297                },
298            ],
299            error_count: 1,
300            warning_count: 1,
301            info_count: 0,
302        };
303
304        assert!(outcome.has_errors());
305        assert!(outcome.has_warnings());
306        assert_eq!(outcome.errors().count(), 1);
307        assert_eq!(outcome.warnings().count(), 1);
308    }
309}