ricecoder_orchestration/managers/
rules_validator.rs

1//! Rules validation for workspace orchestration
2//!
3//! Validates workspace rules and compliance across all projects.
4
5use crate::error::Result;
6use crate::models::{Project, ProjectDependency, RuleType, Workspace, WorkspaceRule};
7use std::collections::{HashMap, HashSet};
8
9/// Validates workspace rules and compliance
10#[derive(Debug, Clone)]
11pub struct RulesValidator {
12    /// Workspace being validated
13    workspace: Workspace,
14}
15
16/// Result of rules validation
17#[derive(Debug, Clone)]
18pub struct ValidationResult {
19    /// Whether validation passed
20    pub passed: bool,
21
22    /// Violations found
23    pub violations: Vec<RuleViolation>,
24
25    /// Warnings
26    pub warnings: Vec<String>,
27}
28
29/// A rule violation
30#[derive(Debug, Clone)]
31pub struct RuleViolation {
32    /// Name of the violated rule
33    pub rule_name: String,
34
35    /// Type of rule
36    pub rule_type: RuleType,
37
38    /// Description of the violation
39    pub description: String,
40
41    /// Projects involved in the violation
42    pub affected_projects: Vec<String>,
43
44    /// Severity level
45    pub severity: ViolationSeverity,
46}
47
48/// Severity level of a rule violation
49#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
50pub enum ViolationSeverity {
51    /// Warning level
52    Warning,
53
54    /// Error level
55    Error,
56
57    /// Critical level
58    Critical,
59}
60
61impl RulesValidator {
62    /// Creates a new rules validator
63    pub fn new(workspace: Workspace) -> Self {
64        Self { workspace }
65    }
66
67    /// Validates all workspace rules
68    pub fn validate_all(&self) -> Result<ValidationResult> {
69        let mut violations = Vec::new();
70        let mut warnings = Vec::new();
71
72        for rule in &self.workspace.config.rules {
73            if !rule.enabled {
74                continue;
75            }
76
77            match rule.rule_type {
78                RuleType::DependencyConstraint => {
79                    if let Err(e) = self.validate_dependency_constraints(rule, &mut violations) {
80                        warnings.push(format!("Error validating dependency constraints: {}", e));
81                    }
82                }
83                RuleType::NamingConvention => {
84                    if let Err(e) = self.validate_naming_conventions(rule, &mut violations) {
85                        warnings.push(format!("Error validating naming conventions: {}", e));
86                    }
87                }
88                RuleType::ArchitecturalBoundary => {
89                    if let Err(e) = self.validate_architectural_boundaries(rule, &mut violations) {
90                        warnings.push(format!("Error validating architectural boundaries: {}", e));
91                    }
92                }
93            }
94        }
95
96        let passed = violations.iter().all(|v| v.severity == ViolationSeverity::Warning);
97
98        Ok(ValidationResult {
99            passed,
100            violations,
101            warnings,
102        })
103    }
104
105    /// Validates dependency constraint rules
106    fn validate_dependency_constraints(
107        &self,
108        rule: &WorkspaceRule,
109        violations: &mut Vec<RuleViolation>,
110    ) -> Result<()> {
111        // Check for circular dependencies
112        if rule.name == "no-circular-deps" {
113            let circular_deps = self.find_circular_dependencies();
114            for (projects, cycle) in circular_deps {
115                violations.push(RuleViolation {
116                    rule_name: rule.name.clone(),
117                    rule_type: rule.rule_type,
118                    description: format!("Circular dependency detected: {}", cycle),
119                    affected_projects: projects,
120                    severity: ViolationSeverity::Error,
121                });
122            }
123        }
124
125        Ok(())
126    }
127
128    /// Validates naming convention rules
129    fn validate_naming_conventions(
130        &self,
131        rule: &WorkspaceRule,
132        violations: &mut Vec<RuleViolation>,
133    ) -> Result<()> {
134        // Check project naming conventions
135        if rule.name == "naming-convention" {
136            for project in &self.workspace.projects {
137                // Project names should be lowercase with hyphens
138                if !self.is_valid_project_name(&project.name) {
139                    violations.push(RuleViolation {
140                        rule_name: rule.name.clone(),
141                        rule_type: rule.rule_type,
142                        description: format!(
143                            "Project name '{}' does not follow naming convention (should be lowercase with hyphens)",
144                            project.name
145                        ),
146                        affected_projects: vec![project.name.clone()],
147                        severity: ViolationSeverity::Warning,
148                    });
149                }
150            }
151        }
152
153        Ok(())
154    }
155
156    /// Validates architectural boundary rules
157    fn validate_architectural_boundaries(
158        &self,
159        rule: &WorkspaceRule,
160        violations: &mut Vec<RuleViolation>,
161    ) -> Result<()> {
162        // Check for cross-layer dependencies
163        if rule.name == "no-cross-layer-deps" {
164            let cross_layer_deps = self.find_cross_layer_dependencies();
165            for (from, to) in cross_layer_deps {
166                violations.push(RuleViolation {
167                    rule_name: rule.name.clone(),
168                    rule_type: rule.rule_type,
169                    description: format!(
170                        "Cross-layer dependency detected: {} depends on {}",
171                        from, to
172                    ),
173                    affected_projects: vec![from, to],
174                    severity: ViolationSeverity::Warning,
175                });
176            }
177        }
178
179        Ok(())
180    }
181
182    /// Finds circular dependencies in the workspace
183    fn find_circular_dependencies(&self) -> Vec<(Vec<String>, String)> {
184        let mut circular_deps = Vec::new();
185        let mut visited = HashSet::new();
186        let mut rec_stack = HashSet::new();
187
188        for project in &self.workspace.projects {
189            if !visited.contains(&project.name) {
190                if let Some(cycle) = self.find_cycle(&project.name, &mut visited, &mut rec_stack) {
191                    circular_deps.push((cycle.clone(), cycle.join(" -> ")));
192                }
193            }
194        }
195
196        circular_deps
197    }
198
199    /// Finds a cycle starting from a project
200    fn find_cycle(
201        &self,
202        project: &str,
203        visited: &mut HashSet<String>,
204        rec_stack: &mut HashSet<String>,
205    ) -> Option<Vec<String>> {
206        visited.insert(project.to_string());
207        rec_stack.insert(project.to_string());
208
209        // Get dependencies of this project
210        let deps: Vec<String> = self
211            .workspace
212            .dependencies
213            .iter()
214            .filter(|d| d.from == project)
215            .map(|d| d.to.clone())
216            .collect();
217
218        for dep in deps {
219            if !visited.contains(&dep) {
220                if let Some(mut cycle) = self.find_cycle(&dep, visited, rec_stack) {
221                    cycle.insert(0, project.to_string());
222                    return Some(cycle);
223                }
224            } else if rec_stack.contains(&dep) {
225                return Some(vec![project.to_string(), dep]);
226            }
227        }
228
229        rec_stack.remove(project);
230        None
231    }
232
233    /// Checks if a project name follows naming conventions
234    fn is_valid_project_name(&self, name: &str) -> bool {
235        // Project names should be lowercase with hyphens
236        name.chars().all(|c| c.is_ascii_lowercase() || c == '-' || c.is_ascii_digit())
237            && !name.starts_with('-')
238            && !name.ends_with('-')
239    }
240
241    /// Finds cross-layer dependencies
242    fn find_cross_layer_dependencies(&self) -> Vec<(String, String)> {
243        let mut cross_layer_deps = Vec::new();
244
245        // Determine project layers based on naming conventions
246        let layers = self.determine_project_layers();
247
248        for dep in &self.workspace.dependencies {
249            let from_layer = layers.get(&dep.from).copied().unwrap_or(0);
250            let to_layer = layers.get(&dep.to).copied().unwrap_or(0);
251
252            // Cross-layer dependency if from_layer > to_layer (depends on lower layer)
253            if from_layer > to_layer {
254                cross_layer_deps.push((dep.from.clone(), dep.to.clone()));
255            }
256        }
257
258        cross_layer_deps
259    }
260
261    /// Determines project layers based on naming conventions
262    fn determine_project_layers(&self) -> HashMap<String, u32> {
263        let mut layers = HashMap::new();
264
265        for project in &self.workspace.projects {
266            let layer = if project.name.starts_with("ricecoder-core") {
267                0
268            } else if project.name.starts_with("ricecoder-") {
269                1
270            } else {
271                2
272            };
273
274            layers.insert(project.name.clone(), layer);
275        }
276
277        layers
278    }
279
280    /// Validates a specific project against all rules
281    pub fn validate_project(&self, project: &Project) -> Result<ValidationResult> {
282        let mut violations = Vec::new();
283        let warnings = Vec::new();
284
285        for rule in &self.workspace.config.rules {
286            if !rule.enabled {
287                continue;
288            }
289
290            if rule.rule_type == RuleType::NamingConvention && !self.is_valid_project_name(&project.name) {
291                violations.push(RuleViolation {
292                    rule_name: rule.name.clone(),
293                    rule_type: rule.rule_type,
294                    description: format!(
295                        "Project name '{}' does not follow naming convention",
296                        project.name
297                    ),
298                    affected_projects: vec![project.name.clone()],
299                    severity: ViolationSeverity::Warning,
300                });
301            }
302        }
303
304        let passed = violations.iter().all(|v| v.severity == ViolationSeverity::Warning);
305
306        Ok(ValidationResult {
307            passed,
308            violations,
309            warnings,
310        })
311    }
312
313    /// Validates a dependency against all rules
314    pub fn validate_dependency(&self, dep: &ProjectDependency) -> Result<ValidationResult> {
315        let mut violations = Vec::new();
316        let warnings = Vec::new();
317
318        for rule in &self.workspace.config.rules {
319            if !rule.enabled {
320                continue;
321            }
322
323            if rule.rule_type == RuleType::DependencyConstraint {
324                // Check if dependency creates a cycle
325                if self.would_create_cycle(&dep.from, &dep.to) {
326                    violations.push(RuleViolation {
327                        rule_name: rule.name.clone(),
328                        rule_type: rule.rule_type,
329                        description: format!(
330                            "Dependency would create a cycle: {} -> {}",
331                            dep.from, dep.to
332                        ),
333                        affected_projects: vec![dep.from.clone(), dep.to.clone()],
334                        severity: ViolationSeverity::Error,
335                    });
336                }
337            }
338        }
339
340        let passed = violations.iter().all(|v| v.severity == ViolationSeverity::Warning);
341
342        Ok(ValidationResult {
343            passed,
344            violations,
345            warnings,
346        })
347    }
348
349    /// Checks if adding a dependency would create a cycle
350    fn would_create_cycle(&self, from: &str, to: &str) -> bool {
351        // Check if there's already a path from 'to' to 'from'
352        self.has_path(to, from)
353    }
354
355    /// Checks if there's a path from one project to another
356    fn has_path(&self, from: &str, to: &str) -> bool {
357        if from == to {
358            return true;
359        }
360
361        let mut visited = HashSet::new();
362        self.has_path_recursive(from, to, &mut visited)
363    }
364
365    /// Recursively checks if there's a path from one project to another
366    fn has_path_recursive(&self, from: &str, to: &str, visited: &mut HashSet<String>) -> bool {
367        if from == to {
368            return true;
369        }
370
371        if visited.contains(from) {
372            return false;
373        }
374
375        visited.insert(from.to_string());
376
377        for dep in &self.workspace.dependencies {
378            if dep.from == from && self.has_path_recursive(&dep.to, to, visited) {
379                return true;
380            }
381        }
382
383        false
384    }
385}
386
387#[cfg(test)]
388mod tests {
389    use super::*;
390    use crate::models::{ProjectStatus, WorkspaceConfig, WorkspaceMetrics};
391
392    fn create_test_workspace() -> Workspace {
393        Workspace {
394            root: std::path::PathBuf::from("/workspace"),
395            projects: vec![
396                Project {
397                    path: std::path::PathBuf::from("/workspace/project-a"),
398                    name: "project-a".to_string(),
399                    project_type: "rust".to_string(),
400                    version: "0.1.0".to_string(),
401                    status: ProjectStatus::Healthy,
402                },
403                Project {
404                    path: std::path::PathBuf::from("/workspace/project-b"),
405                    name: "project-b".to_string(),
406                    project_type: "rust".to_string(),
407                    version: "0.1.0".to_string(),
408                    status: ProjectStatus::Healthy,
409                },
410            ],
411            dependencies: vec![],
412            config: WorkspaceConfig::default(),
413            metrics: WorkspaceMetrics::default(),
414        }
415    }
416
417    #[test]
418    fn test_rules_validator_creation() {
419        let workspace = create_test_workspace();
420        let validator = RulesValidator::new(workspace);
421        assert_eq!(validator.workspace.projects.len(), 2);
422    }
423
424    #[test]
425    fn test_is_valid_project_name() {
426        let workspace = create_test_workspace();
427        let validator = RulesValidator::new(workspace);
428
429        assert!(validator.is_valid_project_name("project-a"));
430        assert!(validator.is_valid_project_name("my-project-123"));
431        assert!(!validator.is_valid_project_name("Project-A"));
432        assert!(!validator.is_valid_project_name("-project"));
433        assert!(!validator.is_valid_project_name("project-"));
434    }
435
436    #[test]
437    fn test_validate_naming_conventions() {
438        let mut workspace = create_test_workspace();
439        workspace.projects.push(Project {
440            path: std::path::PathBuf::from("/workspace/InvalidProject"),
441            name: "InvalidProject".to_string(),
442            project_type: "rust".to_string(),
443            version: "0.1.0".to_string(),
444            status: ProjectStatus::Healthy,
445        });
446
447        // Add naming convention rule to the workspace config
448        workspace.config.rules.push(WorkspaceRule {
449            name: "naming-convention".to_string(),
450            rule_type: RuleType::NamingConvention,
451            enabled: true,
452        });
453
454        let validator = RulesValidator::new(workspace);
455        let result = validator.validate_all().unwrap();
456
457        assert!(!result.violations.is_empty());
458    }
459
460    #[test]
461    fn test_find_circular_dependencies() {
462        let mut workspace = create_test_workspace();
463        workspace.dependencies = vec![
464            ProjectDependency {
465                from: "project-a".to_string(),
466                to: "project-b".to_string(),
467                dependency_type: crate::models::DependencyType::Direct,
468                version_constraint: "^0.1.0".to_string(),
469            },
470            ProjectDependency {
471                from: "project-b".to_string(),
472                to: "project-a".to_string(),
473                dependency_type: crate::models::DependencyType::Direct,
474                version_constraint: "^0.1.0".to_string(),
475            },
476        ];
477
478        let validator = RulesValidator::new(workspace);
479        let circular_deps = validator.find_circular_dependencies();
480
481        assert!(!circular_deps.is_empty());
482    }
483
484    #[test]
485    fn test_would_create_cycle() {
486        let mut workspace = create_test_workspace();
487        workspace.dependencies = vec![ProjectDependency {
488            from: "project-a".to_string(),
489            to: "project-b".to_string(),
490            dependency_type: crate::models::DependencyType::Direct,
491            version_constraint: "^0.1.0".to_string(),
492        }];
493
494        let validator = RulesValidator::new(workspace);
495
496        // Adding project-b -> project-a would create a cycle
497        assert!(validator.would_create_cycle("project-b", "project-a"));
498
499        // Adding project-b -> project-c would not create a cycle
500        assert!(!validator.would_create_cycle("project-b", "project-c"));
501    }
502
503    #[test]
504    fn test_validate_project() {
505        let workspace = create_test_workspace();
506        let validator = RulesValidator::new(workspace);
507
508        let project = Project {
509            path: std::path::PathBuf::from("/workspace/project-a"),
510            name: "project-a".to_string(),
511            project_type: "rust".to_string(),
512            version: "0.1.0".to_string(),
513            status: ProjectStatus::Healthy,
514        };
515
516        let result = validator.validate_project(&project).unwrap();
517        assert!(result.passed);
518    }
519
520    #[test]
521    fn test_validate_dependency() {
522        let workspace = create_test_workspace();
523        let validator = RulesValidator::new(workspace);
524
525        let dep = ProjectDependency {
526            from: "project-a".to_string(),
527            to: "project-b".to_string(),
528            dependency_type: crate::models::DependencyType::Direct,
529            version_constraint: "^0.1.0".to_string(),
530        };
531
532        let result = validator.validate_dependency(&dep).unwrap();
533        assert!(result.passed);
534    }
535
536    #[test]
537    fn test_has_path() {
538        let mut workspace = create_test_workspace();
539        workspace.dependencies = vec![
540            ProjectDependency {
541                from: "project-a".to_string(),
542                to: "project-b".to_string(),
543                dependency_type: crate::models::DependencyType::Direct,
544                version_constraint: "^0.1.0".to_string(),
545            },
546        ];
547
548        let validator = RulesValidator::new(workspace);
549
550        assert!(validator.has_path("project-a", "project-b"));
551        assert!(!validator.has_path("project-b", "project-a"));
552    }
553
554    #[test]
555    fn test_determine_project_layers() {
556        let mut workspace = create_test_workspace();
557        workspace.projects.push(Project {
558            path: std::path::PathBuf::from("/workspace/ricecoder-core"),
559            name: "ricecoder-core".to_string(),
560            project_type: "rust".to_string(),
561            version: "0.1.0".to_string(),
562            status: ProjectStatus::Healthy,
563        });
564
565        let validator = RulesValidator::new(workspace);
566        let layers = validator.determine_project_layers();
567
568        assert_eq!(layers.get("ricecoder-core"), Some(&0));
569        assert_eq!(layers.get("project-a"), Some(&2));
570    }
571}