1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13pub enum ValidationSeverity {
14 Info,
16 Warning,
18 Error,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct Violation {
25 pub rule: String,
27 pub severity: ValidationSeverity,
29 pub message: String,
31 pub location: Option<String>,
33 pub suggestion: Option<String>,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct ValidationOutcome {
40 pub passed: bool,
42 pub violations: Vec<Violation>,
44 pub error_count: usize,
46 pub warning_count: usize,
48 pub info_count: usize,
50}
51
52impl ValidationOutcome {
53 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 pub fn has_errors(&self) -> bool {
66 self.error_count > 0
67 }
68
69 pub fn has_warnings(&self) -> bool {
71 self.warning_count > 0
72 }
73
74 pub fn errors(&self) -> impl Iterator<Item = &Violation> {
76 self.violations
77 .iter()
78 .filter(|v| v.severity == ValidationSeverity::Error)
79 }
80
81 pub fn warnings(&self) -> impl Iterator<Item = &Violation> {
83 self.violations
84 .iter()
85 .filter(|v| v.severity == ValidationSeverity::Warning)
86 }
87}
88
89pub struct SmeltValidator {
91 config: ValidationConfig,
92 rules: Vec<Box<dyn ValidationRule>>,
93}
94
95impl SmeltValidator {
96 pub fn new(config: ValidationConfig) -> Self {
98 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 pub fn default_config() -> Self {
111 Self::new(ValidationConfig::default())
112 }
113
114 pub fn strict() -> Self {
116 Self::new(ValidationConfig::strict())
117 }
118
119 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 if validator.config.architecture.check_circular_deps
126 || validator.config.architecture.enforce_layers
127 {
128 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 pub fn config(&self) -> &ValidationConfig {
141 &self.config
142 }
143
144 pub fn validate(
146 &self,
147 delta: &SemanticDelta,
148 intent: Option<&IntentRecord>,
149 ) -> ValidationOutcome {
150 let mut violations = Vec::new();
151
152 for rule in &self.rules {
154 let rule_violations = rule.validate(delta, intent);
155 violations.extend(rule_violations);
156 }
157
158 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 pub fn validate_simple(&self, delta: &SemanticDelta, intent: Option<&IntentRecord>) -> bool {
183 self.validate(delta, intent).passed
184 }
185
186 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 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, ..Default::default()
271 },
272 };
273
274 let outcome = validator.validate(&delta, None);
275 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}