Skip to main content

smelt_validator/crucible/
adapter.rs

1//! Adapter for integrating Crucible's architectural validation
2
3use crate::rules::ValidationRule;
4use crate::validator::{ValidationSeverity, Violation};
5use crucible_core::validator::{ValidationIssue, ValidationResult, Validator};
6use crucible_core::{Parser, Project};
7use smelt_core::{IntentRecord, SemanticDelta};
8use std::path::{Path, PathBuf};
9
10/// Adapter that bridges Crucible's validation with SmeltValidator
11pub struct CrucibleAdapter {
12    /// Root path for the project
13    project_root: PathBuf,
14    /// Whether architectural validation is enabled
15    enabled: bool,
16    /// Whether to check for circular dependencies
17    check_circular_deps: bool,
18    /// Whether to validate type references
19    check_types: bool,
20    /// Whether to validate call targets
21    check_calls: bool,
22}
23
24impl CrucibleAdapter {
25    /// Create a new Crucible adapter
26    pub fn new(project_root: &Path) -> Self {
27        Self {
28            project_root: project_root.to_path_buf(),
29            enabled: true,
30            check_circular_deps: true,
31            check_types: true,
32            check_calls: true,
33        }
34    }
35
36    /// Disable the adapter
37    pub fn disabled() -> Self {
38        Self {
39            project_root: PathBuf::new(),
40            enabled: false,
41            check_circular_deps: false,
42            check_types: false,
43            check_calls: false,
44        }
45    }
46
47    /// Set whether to check circular dependencies
48    pub fn with_circular_deps(mut self, check: bool) -> Self {
49        self.check_circular_deps = check;
50        self
51    }
52
53    /// Set whether to check type references
54    pub fn with_type_checks(mut self, check: bool) -> Self {
55        self.check_types = check;
56        self
57    }
58
59    /// Set whether to check call targets
60    pub fn with_call_checks(mut self, check: bool) -> Self {
61        self.check_calls = check;
62        self
63    }
64
65    /// Check if a Crucible project exists at the root
66    fn has_crucible_project(&self) -> bool {
67        self.project_root.join("crucible.json").exists()
68            || self.project_root.join("crucible.yaml").exists()
69    }
70
71    /// Parse the Crucible project
72    fn parse_project(&self) -> Option<Project> {
73        if !self.has_crucible_project() {
74            return None;
75        }
76
77        let parser = Parser::new(&self.project_root);
78        parser.parse_project().ok()
79    }
80
81    /// Run Crucible validation and convert results
82    fn run_crucible_validation(&self) -> Vec<Violation> {
83        let Some(project) = self.parse_project() else {
84            return Vec::new();
85        };
86
87        let validator = Validator::new(project);
88        let result = validator.validate();
89
90        self.convert_result(&result)
91    }
92
93    /// Convert Crucible ValidationResult to our Violations
94    fn convert_result(&self, result: &ValidationResult) -> Vec<Violation> {
95        let mut violations = Vec::new();
96
97        // Convert errors
98        for issue in &result.errors {
99            if self.should_include_issue(issue) {
100                violations.push(self.convert_issue(issue, ValidationSeverity::Error));
101            }
102        }
103
104        // Convert warnings
105        for issue in &result.warnings {
106            if self.should_include_issue(issue) {
107                violations.push(self.convert_issue(issue, ValidationSeverity::Warning));
108            }
109        }
110
111        // Convert info
112        for issue in &result.info {
113            if self.should_include_issue(issue) {
114                violations.push(self.convert_issue(issue, ValidationSeverity::Info));
115            }
116        }
117
118        violations
119    }
120
121    /// Check if an issue should be included based on our config
122    fn should_include_issue(&self, issue: &ValidationIssue) -> bool {
123        match issue.rule.as_str() {
124            r if r.contains("circular") || r.contains("cycle") => self.check_circular_deps,
125            r if r.contains("type") => self.check_types,
126            r if r.contains("call") => self.check_calls,
127            _ => true,
128        }
129    }
130
131    /// Convert a Crucible ValidationIssue to our Violation
132    fn convert_issue(&self, issue: &ValidationIssue, severity: ValidationSeverity) -> Violation {
133        let mut message = issue.message.clone();
134
135        // Add found/expected context if available
136        if let (Some(found), Some(expected)) = (&issue.found, &issue.expected) {
137            message = format!("{} (found: {}, expected: {})", message, found, expected);
138        }
139
140        Violation {
141            rule: format!("crucible:{}", issue.rule),
142            severity,
143            message,
144            location: issue.location.clone(),
145            suggestion: issue.suggestion.clone(),
146        }
147    }
148}
149
150impl ValidationRule for CrucibleAdapter {
151    fn name(&self) -> &'static str {
152        "crucible"
153    }
154
155    fn validate(&self, _delta: &SemanticDelta, _intent: Option<&IntentRecord>) -> Vec<Violation> {
156        if !self.enabled {
157            return Vec::new();
158        }
159
160        self.run_crucible_validation()
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167    use tempfile::tempdir;
168
169    #[test]
170    fn test_disabled_adapter() {
171        let adapter = CrucibleAdapter::disabled();
172        assert!(!adapter.enabled);
173    }
174
175    #[test]
176    fn test_no_crucible_project() {
177        let dir = tempdir().unwrap();
178        let adapter = CrucibleAdapter::new(dir.path());
179        assert!(!adapter.has_crucible_project());
180    }
181
182    #[test]
183    fn test_config_builder() {
184        let dir = tempdir().unwrap();
185        let adapter = CrucibleAdapter::new(dir.path())
186            .with_circular_deps(false)
187            .with_type_checks(false)
188            .with_call_checks(true);
189
190        assert!(!adapter.check_circular_deps);
191        assert!(!adapter.check_types);
192        assert!(adapter.check_calls);
193    }
194}