Skip to main content

smelt_validator/semantic/
intent.rs

1//! Intent validation - validate delta matches intent constraints
2
3use crate::config::IntentConfig;
4use crate::rules::ValidationRule;
5use crate::validator::{ValidationSeverity, Violation};
6use smelt_core::{IntentRecord, SemanticDelta};
7
8/// Validates that semantic deltas match intent constraints
9pub struct IntentValidator {
10    config: IntentConfig,
11}
12
13impl IntentValidator {
14    /// Create a new intent validator
15    pub fn new(config: IntentConfig) -> Self {
16        Self { config }
17    }
18
19    /// Check if the delta scope is reasonable for the intent
20    fn check_scope(&self, delta: &SemanticDelta, intent: &IntentRecord) -> Vec<Violation> {
21        let mut violations = Vec::new();
22
23        // Check if change is too large without rationale
24        if self.config.require_rationale_for_large_changes
25            && delta.impact_summary.files_affected >= self.config.large_change_threshold
26            && intent.rationale.is_none()
27        {
28            violations.push(Violation {
29                rule: "large-change-rationale".to_string(),
30                severity: ValidationSeverity::Warning,
31                message: format!(
32                    "Large change ({} files affected) without rationale",
33                    delta.impact_summary.files_affected
34                ),
35                location: None,
36                suggestion: Some(format!(
37                    "Provide rationale for changes affecting {} or more files",
38                    self.config.large_change_threshold
39                )),
40            });
41        }
42
43        // Check intent constraints
44        for constraint in &intent.constraints {
45            if constraint.required {
46                // TODO: Implement constraint validation based on constraint type
47                // For now, just acknowledge the constraint exists
48                tracing::debug!(
49                    "Checking constraint: {} = {}",
50                    constraint.name,
51                    constraint.value
52                );
53            }
54        }
55
56        violations
57    }
58
59    /// Check if breaking changes are allowed by intent
60    fn check_breaking_allowed(
61        &self,
62        delta: &SemanticDelta,
63        intent: &IntentRecord,
64    ) -> Vec<Violation> {
65        let mut violations = Vec::new();
66
67        // Check if intent explicitly allows breaking changes
68        let breaking_allowed = intent
69            .constraints
70            .iter()
71            .any(|c| c.name == "allow_breaking_changes" && c.value.to_lowercase() == "true");
72
73        if delta.impact_summary.breaking_changes > 0 && !breaking_allowed {
74            violations.push(Violation {
75                rule: "breaking-not-allowed".to_string(),
76                severity: ValidationSeverity::Error,
77                message: format!(
78                    "{} breaking change(s) detected but intent does not allow breaking changes",
79                    delta.impact_summary.breaking_changes
80                ),
81                location: None,
82                suggestion: Some(
83                    "Add constraint 'allow_breaking_changes: true' to intent if intentional"
84                        .to_string(),
85                ),
86            });
87        }
88
89        violations
90    }
91}
92
93impl ValidationRule for IntentValidator {
94    fn name(&self) -> &'static str {
95        "intent"
96    }
97
98    fn validate(&self, delta: &SemanticDelta, intent: Option<&IntentRecord>) -> Vec<Violation> {
99        let Some(intent) = intent else {
100            return Vec::new();
101        };
102
103        if !self.config.validate_scope {
104            return Vec::new();
105        }
106
107        let mut violations = Vec::new();
108        violations.extend(self.check_scope(delta, intent));
109        violations.extend(self.check_breaking_allowed(delta, intent));
110
111        violations
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    use chrono::Utc;
119    use smelt_core::{Author, AuthorType, Constraint, ContextLinks, ImpactSummary, IntentStatus};
120    use uuid::Uuid;
121
122    fn make_intent(rationale: Option<String>, constraints: Vec<Constraint>) -> IntentRecord {
123        IntentRecord {
124            id: Uuid::new_v4(),
125            created_at: Utc::now(),
126            author: Author {
127                name: "Test".to_string(),
128                email: "test@test.com".to_string(),
129                author_type: AuthorType::Human,
130            },
131            goal: "Test goal".to_string(),
132            rationale,
133            constraints,
134            context_links: ContextLinks::default(),
135            status: IntentStatus::InProgress,
136            baseline_snapshot_id: None,
137        }
138    }
139
140    fn make_delta(files_affected: usize, breaking_changes: usize) -> SemanticDelta {
141        SemanticDelta {
142            id: Uuid::new_v4(),
143            intent_id: Uuid::new_v4(),
144            timestamp: Utc::now(),
145            from_snapshot: Uuid::new_v4(),
146            to_snapshot: Uuid::new_v4(),
147            changes: Vec::new(),
148            impact_summary: ImpactSummary {
149                files_affected,
150                breaking_changes,
151                ..Default::default()
152            },
153        }
154    }
155
156    #[test]
157    fn test_large_change_with_rationale_ok() {
158        let config = IntentConfig {
159            require_rationale_for_large_changes: true,
160            large_change_threshold: 5,
161            ..Default::default()
162        };
163
164        let validator = IntentValidator::new(config);
165        let intent = make_intent(Some("Major refactoring".to_string()), vec![]);
166        let delta = make_delta(10, 0);
167
168        let violations = validator.validate(&delta, Some(&intent));
169        assert!(violations.is_empty());
170    }
171
172    #[test]
173    fn test_large_change_without_rationale() {
174        let config = IntentConfig {
175            require_rationale_for_large_changes: true,
176            large_change_threshold: 5,
177            ..Default::default()
178        };
179
180        let validator = IntentValidator::new(config);
181        let intent = make_intent(None, vec![]);
182        let delta = make_delta(10, 0);
183
184        let violations = validator.validate(&delta, Some(&intent));
185        assert_eq!(violations.len(), 1);
186        assert_eq!(violations[0].rule, "large-change-rationale");
187    }
188
189    #[test]
190    fn test_breaking_changes_not_allowed() {
191        let config = IntentConfig::default();
192
193        let validator = IntentValidator::new(config);
194        let intent = make_intent(None, vec![]);
195        let delta = make_delta(1, 2);
196
197        let violations = validator.validate(&delta, Some(&intent));
198        assert_eq!(violations.len(), 1);
199        assert_eq!(violations[0].rule, "breaking-not-allowed");
200    }
201
202    #[test]
203    fn test_breaking_changes_allowed_by_constraint() {
204        let config = IntentConfig::default();
205
206        let validator = IntentValidator::new(config);
207        let intent = make_intent(
208            None,
209            vec![Constraint {
210                name: "allow_breaking_changes".to_string(),
211                value: "true".to_string(),
212                required: false,
213            }],
214        );
215        let delta = make_delta(1, 2);
216
217        let violations = validator.validate(&delta, Some(&intent));
218        assert!(violations.is_empty());
219    }
220}