Skip to main content

batuta/comply/
mod.rs

1//! Stack Compliance Engine - Cross-project consistency enforcement
2//!
3//! Ensures consistent patterns across the Sovereign AI Stack:
4//! - Makefile target consistency
5//! - Cargo.toml parity
6//! - CI workflow alignment
7//! - Code duplication detection (MinHash+LSH)
8//!
9//! # Toyota Production System Principles
10//!
11//! - **Heijunka**: Standardized targets across all projects
12//! - **Poka-Yoke**: Configuration validation prevents drift
13//! - **Jidoka**: Stop-on-error for critical violations
14//! - **Kaizen**: Continuous improvement via compliance tracking
15
16// Allow unused for public API items not yet consumed
17pub mod config;
18pub mod report;
19pub mod rule;
20pub mod rules;
21
22pub use config::ComplyConfig;
23#[allow(unused_imports)]
24pub use config::ProjectOverride;
25pub use report::{ComplyReport, ComplyReportFormat};
26pub use rule::StackComplianceRule;
27#[allow(unused_imports)]
28pub use rule::{FixResult, RuleResult};
29
30use crate::stack::PAIML_CRATES;
31use std::path::{Path, PathBuf};
32
33/// Stack Compliance Engine
34///
35/// Orchestrates compliance checks across all PAIML stack projects.
36#[derive(Debug)]
37pub struct StackComplyEngine {
38    /// Compliance configuration
39    config: ComplyConfig,
40    /// Registered compliance rules
41    rules: Vec<Box<dyn StackComplianceRule>>,
42    /// Project discovery cache
43    discovered_projects: Vec<ProjectInfo>,
44}
45
46/// Information about a discovered project
47#[derive(Debug, Clone)]
48pub struct ProjectInfo {
49    /// Project name (crate name)
50    pub name: String,
51    /// Path to project root
52    pub path: PathBuf,
53    /// Whether it's a PAIML stack crate
54    pub is_paiml_crate: bool,
55}
56
57impl StackComplyEngine {
58    /// Create a new compliance engine with default rules
59    pub fn new(config: ComplyConfig) -> Self {
60        let mut engine = Self { config, rules: Vec::new(), discovered_projects: Vec::new() };
61
62        // Register default rules
63        engine.register_rule(Box::new(rules::MakefileRule::new()));
64        engine.register_rule(Box::new(rules::CargoTomlRule::new()));
65        engine.register_rule(Box::new(rules::CiWorkflowRule::new()));
66        engine.register_rule(Box::new(rules::DuplicationRule::new()));
67
68        engine
69    }
70
71    /// Create engine with default configuration
72    pub fn default_for_workspace(workspace: &Path) -> Self {
73        Self::new(ComplyConfig::default_for_workspace(workspace))
74    }
75
76    /// Register a custom compliance rule
77    pub fn register_rule(&mut self, rule: Box<dyn StackComplianceRule>) {
78        self.rules.push(rule);
79    }
80
81    /// Discover projects in the workspace
82    pub fn discover_projects(&mut self, workspace: &Path) -> anyhow::Result<&[ProjectInfo]> {
83        self.discovered_projects.clear();
84
85        // Walk workspace looking for Cargo.toml files
86        for entry in
87            walkdir::WalkDir::new(workspace).max_depth(2).into_iter().filter_map(|e| e.ok())
88        {
89            let path = entry.path();
90            if path.file_name() == Some(std::ffi::OsStr::new("Cargo.toml")) {
91                if let Some(project) = self.parse_project(path)? {
92                    self.discovered_projects.push(project);
93                }
94            }
95        }
96
97        Ok(&self.discovered_projects)
98    }
99
100    /// Parse a project from its Cargo.toml
101    fn parse_project(&self, cargo_toml: &Path) -> anyhow::Result<Option<ProjectInfo>> {
102        let content = std::fs::read_to_string(cargo_toml)?;
103        let toml: toml::Value = toml::from_str(&content)?;
104
105        let name = toml
106            .get("package")
107            .and_then(|p| p.get("name"))
108            .and_then(|n| n.as_str())
109            .map(String::from);
110
111        match name {
112            Some(name) => {
113                let path = cargo_toml.parent().unwrap_or(Path::new(".")).to_path_buf();
114                let is_paiml_crate = PAIML_CRATES.contains(&name.as_str());
115
116                Ok(Some(ProjectInfo { name, path, is_paiml_crate }))
117            }
118            None => Ok(None),
119        }
120    }
121
122    /// Run all compliance checks
123    pub fn check_all(&self) -> ComplyReport {
124        let mut report = ComplyReport::new();
125
126        for project in &self.discovered_projects {
127            // Skip non-PAIML crates unless explicitly included
128            if !project.is_paiml_crate && !self.config.include_external {
129                continue;
130            }
131
132            for rule in &self.rules {
133                // Check if rule is enabled
134                if !self.is_rule_enabled(rule.id()) {
135                    continue;
136                }
137
138                // Check for project-specific override
139                if self.has_rule_exemption(&project.name, rule.id()) {
140                    report.add_exemption(&project.name, rule.id());
141                    continue;
142                }
143
144                match rule.check(&project.path) {
145                    Ok(result) => {
146                        report.add_result(&project.name, rule.id(), result);
147                    }
148                    Err(e) => {
149                        report.add_error(&project.name, rule.id(), e.to_string());
150                    }
151                }
152            }
153        }
154
155        report.finalize();
156        report
157    }
158
159    /// Run a specific rule
160    pub fn check_rule(&self, rule_id: &str) -> ComplyReport {
161        let mut report = ComplyReport::new();
162
163        let rule = match self.rules.iter().find(|r| r.id() == rule_id) {
164            Some(r) => r,
165            None => {
166                report.add_global_error(format!("Unknown rule: {}", rule_id));
167                return report;
168            }
169        };
170
171        for project in &self.discovered_projects {
172            if !project.is_paiml_crate && !self.config.include_external {
173                continue;
174            }
175
176            if self.has_rule_exemption(&project.name, rule_id) {
177                report.add_exemption(&project.name, rule_id);
178                continue;
179            }
180
181            match rule.check(&project.path) {
182                Ok(result) => {
183                    report.add_result(&project.name, rule_id, result);
184                }
185                Err(e) => {
186                    report.add_error(&project.name, rule_id, e.to_string());
187                }
188            }
189        }
190
191        report.finalize();
192        report
193    }
194
195    /// Attempt to fix violations
196    pub fn fix_all(&self, dry_run: bool) -> ComplyReport {
197        let mut report = ComplyReport::new();
198
199        for project in &self.discovered_projects {
200            if !project.is_paiml_crate && !self.config.include_external {
201                continue;
202            }
203
204            for rule in &self.rules {
205                if !self.is_rule_enabled(rule.id()) || !rule.can_fix() {
206                    continue;
207                }
208
209                if self.has_rule_exemption(&project.name, rule.id()) {
210                    continue;
211                }
212
213                // First check if there are violations to fix
214                let check_result = match rule.check(&project.path) {
215                    Ok(r) => r,
216                    Err(e) => {
217                        report.add_error(&project.name, rule.id(), e.to_string());
218                        continue;
219                    }
220                };
221
222                if check_result.passed {
223                    report.add_result(&project.name, rule.id(), check_result);
224                    continue;
225                }
226
227                // Attempt fix
228                if dry_run {
229                    report.add_dry_run_fix(&project.name, rule.id(), &check_result.violations);
230                } else {
231                    match rule.fix(&project.path) {
232                        Ok(fix_result) => {
233                            report.add_fix_result(&project.name, rule.id(), fix_result);
234                        }
235                        Err(e) => {
236                            report.add_error(&project.name, rule.id(), e.to_string());
237                        }
238                    }
239                }
240            }
241        }
242
243        report.finalize();
244        report
245    }
246
247    /// Check if a rule is enabled
248    fn is_rule_enabled(&self, rule_id: &str) -> bool {
249        if self.config.enabled_rules.is_empty() {
250            // All rules enabled by default
251            !self.config.disabled_rules.contains(&rule_id.to_string())
252        } else {
253            self.config.enabled_rules.contains(&rule_id.to_string())
254        }
255    }
256
257    /// Check if a project has an exemption for a rule
258    fn has_rule_exemption(&self, project_name: &str, rule_id: &str) -> bool {
259        self.config
260            .project_overrides
261            .get(project_name)
262            .map(|o| o.exempt_rules.contains(&rule_id.to_string()))
263            .unwrap_or(false)
264    }
265
266    /// Get list of available rules
267    pub fn available_rules(&self) -> Vec<(&str, &str)> {
268        self.rules.iter().map(|r| (r.id(), r.description())).collect()
269    }
270
271    /// Get list of discovered projects
272    pub fn projects(&self) -> &[ProjectInfo] {
273        &self.discovered_projects
274    }
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280
281    #[test]
282    fn test_comply_engine_creation() {
283        let config = ComplyConfig::default();
284        let engine = StackComplyEngine::new(config);
285        assert!(!engine.rules.is_empty());
286    }
287
288    #[test]
289    fn test_available_rules() {
290        let engine = StackComplyEngine::new(ComplyConfig::default());
291        let rules = engine.available_rules();
292        assert!(rules.iter().any(|(id, _)| *id == "makefile-targets"));
293        assert!(rules.iter().any(|(id, _)| *id == "cargo-toml-consistency"));
294        assert!(rules.iter().any(|(id, _)| *id == "ci-workflow-parity"));
295        assert!(rules.iter().any(|(id, _)| *id == "code-duplication"));
296    }
297
298    #[test]
299    fn test_rule_enabled_check() {
300        let mut config = ComplyConfig::default();
301        config.disabled_rules.push("makefile-targets".to_string());
302        let engine = StackComplyEngine::new(config);
303        assert!(!engine.is_rule_enabled("makefile-targets"));
304        assert!(engine.is_rule_enabled("cargo-toml-consistency"));
305    }
306
307    #[test]
308    fn test_project_exemption() {
309        let mut config = ComplyConfig::default();
310        let mut override_config = ProjectOverride::default();
311        override_config.exempt_rules.push("makefile-targets".to_string());
312        config.project_overrides.insert("test-project".to_string(), override_config);
313
314        let engine = StackComplyEngine::new(config);
315        assert!(engine.has_rule_exemption("test-project", "makefile-targets"));
316        assert!(!engine.has_rule_exemption("test-project", "cargo-toml-consistency"));
317        assert!(!engine.has_rule_exemption("other-project", "makefile-targets"));
318    }
319
320    #[test]
321    fn test_default_for_workspace() {
322        let engine = StackComplyEngine::default_for_workspace(Path::new("."));
323        assert!(!engine.rules.is_empty());
324    }
325
326    #[test]
327    fn test_projects_empty_initially() {
328        let engine = StackComplyEngine::new(ComplyConfig::default());
329        assert!(engine.projects().is_empty());
330    }
331
332    #[test]
333    fn test_enabled_rules_explicit() {
334        let mut config = ComplyConfig::default();
335        config.enabled_rules.push("makefile-targets".to_string());
336        let engine = StackComplyEngine::new(config);
337        assert!(engine.is_rule_enabled("makefile-targets"));
338        assert!(!engine.is_rule_enabled("cargo-toml-consistency"));
339    }
340
341    #[test]
342    fn test_project_info_fields() {
343        let info = ProjectInfo {
344            name: "test-project".to_string(),
345            path: PathBuf::from("/path/to/project"),
346            is_paiml_crate: true,
347        };
348        assert_eq!(info.name, "test-project");
349        assert_eq!(info.path, PathBuf::from("/path/to/project"));
350        assert!(info.is_paiml_crate);
351    }
352
353    #[test]
354    fn test_check_rule_unknown() {
355        let engine = StackComplyEngine::new(ComplyConfig::default());
356        let report = engine.check_rule("nonexistent-rule");
357        assert!(!report.errors.is_empty());
358    }
359
360    #[test]
361    fn test_check_all_empty_projects() {
362        let engine = StackComplyEngine::new(ComplyConfig::default());
363        let report = engine.check_all();
364        // With no discovered projects, should return empty report
365        assert_eq!(report.summary.total_projects, 0);
366    }
367
368    #[test]
369    fn test_fix_all_empty_projects() {
370        let engine = StackComplyEngine::new(ComplyConfig::default());
371        let report = engine.fix_all(true);
372        assert_eq!(report.summary.total_projects, 0);
373    }
374
375    #[test]
376    fn test_fix_all_dry_run() {
377        let engine = StackComplyEngine::new(ComplyConfig::default());
378        let report = engine.fix_all(true); // dry_run = true
379        assert_eq!(report.summary.total_projects, 0);
380    }
381
382    #[test]
383    fn test_fix_all_actual_run() {
384        let engine = StackComplyEngine::new(ComplyConfig::default());
385        let report = engine.fix_all(false); // dry_run = false
386        assert_eq!(report.summary.total_projects, 0);
387    }
388
389    #[test]
390    fn test_register_custom_rule() {
391        use crate::comply::rules::MakefileRule;
392        let mut engine = StackComplyEngine::new(ComplyConfig::default());
393        let initial_count = engine.rules.len();
394        engine.register_rule(Box::new(MakefileRule::new()));
395        assert_eq!(engine.rules.len(), initial_count + 1);
396    }
397
398    #[test]
399    fn test_project_info_clone() {
400        let info = ProjectInfo {
401            name: "test-project".to_string(),
402            path: PathBuf::from("/path/to/project"),
403            is_paiml_crate: true,
404        };
405        let cloned = info.clone();
406        assert_eq!(cloned.name, info.name);
407        assert_eq!(cloned.path, info.path);
408        assert_eq!(cloned.is_paiml_crate, info.is_paiml_crate);
409    }
410
411    #[test]
412    fn test_project_info_non_paiml() {
413        let info = ProjectInfo {
414            name: "third-party-lib".to_string(),
415            path: PathBuf::from("/external/lib"),
416            is_paiml_crate: false,
417        };
418        assert!(!info.is_paiml_crate);
419    }
420
421    #[test]
422    fn test_discover_projects_in_tempdir() {
423        let tempdir = tempfile::tempdir().expect("tempdir creation failed");
424        let project_dir = tempdir.path().join("my-project");
425        std::fs::create_dir_all(&project_dir).expect("mkdir failed");
426
427        // Create a minimal Cargo.toml
428        let cargo_toml = r#"
429[package]
430name = "my-project"
431version = "0.1.0"
432"#;
433        std::fs::write(project_dir.join("Cargo.toml"), cargo_toml).expect("fs write failed");
434
435        let mut engine = StackComplyEngine::new(ComplyConfig::default());
436        let projects = engine.discover_projects(tempdir.path()).expect("unexpected failure");
437
438        assert_eq!(projects.len(), 1);
439        assert_eq!(projects[0].name, "my-project");
440        assert!(!projects[0].is_paiml_crate);
441    }
442
443    #[test]
444    fn test_discover_projects_paiml_crate() {
445        let tempdir = tempfile::tempdir().expect("tempdir creation failed");
446        let project_dir = tempdir.path().join("trueno");
447        std::fs::create_dir_all(&project_dir).expect("mkdir failed");
448
449        // Create Cargo.toml with PAIML crate name
450        let cargo_toml = r#"
451[package]
452name = "trueno"
453version = "0.1.0"
454"#;
455        std::fs::write(project_dir.join("Cargo.toml"), cargo_toml).expect("fs write failed");
456
457        let mut engine = StackComplyEngine::new(ComplyConfig::default());
458        let projects = engine.discover_projects(tempdir.path()).expect("unexpected failure");
459
460        assert_eq!(projects.len(), 1);
461        assert!(projects[0].is_paiml_crate);
462    }
463
464    #[test]
465    fn test_discover_projects_multiple() {
466        let tempdir = tempfile::tempdir().expect("tempdir creation failed");
467
468        // Create two projects
469        for name in &["proj-a", "proj-b"] {
470            let project_dir = tempdir.path().join(name);
471            std::fs::create_dir_all(&project_dir).expect("mkdir failed");
472            let cargo_toml = format!(
473                r#"
474[package]
475name = "{}"
476version = "0.1.0"
477"#,
478                name
479            );
480            std::fs::write(project_dir.join("Cargo.toml"), cargo_toml).expect("fs write failed");
481        }
482
483        let mut engine = StackComplyEngine::new(ComplyConfig::default());
484        let projects = engine.discover_projects(tempdir.path()).expect("unexpected failure");
485
486        assert_eq!(projects.len(), 2);
487    }
488
489    #[test]
490    fn test_discover_projects_no_name() {
491        let tempdir = tempfile::tempdir().expect("tempdir creation failed");
492        let project_dir = tempdir.path().join("unnamed");
493        std::fs::create_dir_all(&project_dir).expect("mkdir failed");
494
495        // Cargo.toml without name
496        let cargo_toml = r#"
497[package]
498version = "0.1.0"
499"#;
500        std::fs::write(project_dir.join("Cargo.toml"), cargo_toml).expect("fs write failed");
501
502        let mut engine = StackComplyEngine::new(ComplyConfig::default());
503        let projects = engine.discover_projects(tempdir.path()).expect("unexpected failure");
504
505        assert_eq!(projects.len(), 0);
506    }
507
508    #[test]
509    fn test_discover_projects_clears_cache() {
510        let tempdir = tempfile::tempdir().expect("tempdir creation failed");
511        let project_dir = tempdir.path().join("proj");
512        std::fs::create_dir_all(&project_dir).expect("mkdir failed");
513        std::fs::write(
514            project_dir.join("Cargo.toml"),
515            "[package]\nname = \"proj\"\nversion = \"0.1.0\"",
516        )
517        .expect("unexpected failure");
518
519        let mut engine = StackComplyEngine::new(ComplyConfig::default());
520
521        // First discovery
522        engine.discover_projects(tempdir.path()).expect("unexpected failure");
523        assert_eq!(engine.projects().len(), 1);
524
525        // Second discovery on empty dir should clear
526        let empty_dir = tempfile::tempdir().expect("tempdir creation failed");
527        engine.discover_projects(empty_dir.path()).expect("unexpected failure");
528        assert_eq!(engine.projects().len(), 0);
529    }
530
531    #[test]
532    fn test_check_all_with_include_external() {
533        let config = ComplyConfig { include_external: true, ..Default::default() };
534
535        let engine = StackComplyEngine::new(config);
536        let report = engine.check_all();
537        assert_eq!(report.summary.total_projects, 0);
538    }
539
540    #[test]
541    fn test_check_rule_with_discovered_projects() {
542        let tempdir = tempfile::tempdir().expect("tempdir creation failed");
543        let project_dir = tempdir.path().join("trueno");
544        std::fs::create_dir_all(&project_dir).expect("mkdir failed");
545        std::fs::write(
546            project_dir.join("Cargo.toml"),
547            "[package]\nname = \"trueno\"\nversion = \"0.1.0\"",
548        )
549        .expect("unexpected failure");
550
551        let mut engine = StackComplyEngine::new(ComplyConfig::default());
552        engine.discover_projects(tempdir.path()).expect("unexpected failure");
553
554        let report = engine.check_rule("makefile-targets");
555        // Report should have processed the project
556        assert!(!report.results.is_empty() || !report.errors.is_empty());
557    }
558
559    #[test]
560    fn test_available_rules_descriptions() {
561        let engine = StackComplyEngine::new(ComplyConfig::default());
562        let rules = engine.available_rules();
563
564        for (id, desc) in &rules {
565            assert!(!id.is_empty());
566            assert!(!desc.is_empty());
567        }
568    }
569
570    #[test]
571    fn test_engine_debug() {
572        let engine = StackComplyEngine::new(ComplyConfig::default());
573        let debug_str = format!("{:?}", engine);
574        assert!(debug_str.contains("StackComplyEngine"));
575    }
576
577    #[test]
578    fn test_project_info_debug() {
579        let info = ProjectInfo {
580            name: "test".to_string(),
581            path: PathBuf::from("/test"),
582            is_paiml_crate: false,
583        };
584        let debug_str = format!("{:?}", info);
585        assert!(debug_str.contains("ProjectInfo"));
586    }
587
588    // =========================================================================
589    // Coverage: check_all with discovered PAIML projects
590    // =========================================================================
591
592    #[test]
593    fn test_check_all_with_paiml_project_no_makefile() {
594        let tempdir = tempfile::tempdir().expect("tempdir creation failed");
595        let project_dir = tempdir.path().join("trueno");
596        std::fs::create_dir_all(&project_dir).expect("mkdir failed");
597        std::fs::write(
598            project_dir.join("Cargo.toml"),
599            "[package]\nname = \"trueno\"\nversion = \"0.1.0\"",
600        )
601        .expect("unexpected failure");
602
603        let mut engine = StackComplyEngine::new(ComplyConfig::default());
604        engine.discover_projects(tempdir.path()).expect("unexpected failure");
605
606        let report = engine.check_all();
607        // trueno is a PAIML crate, so it should be checked
608        assert_eq!(report.summary.total_projects, 1);
609        assert!(report.summary.total_checks > 0);
610    }
611
612    #[test]
613    fn test_check_all_skips_non_paiml_without_include_external() {
614        let tempdir = tempfile::tempdir().expect("tempdir creation failed");
615        let project_dir = tempdir.path().join("some-lib");
616        std::fs::create_dir_all(&project_dir).expect("mkdir failed");
617        std::fs::write(
618            project_dir.join("Cargo.toml"),
619            "[package]\nname = \"some-lib\"\nversion = \"0.1.0\"",
620        )
621        .expect("unexpected failure");
622
623        let config = ComplyConfig::default(); // include_external is false
624        let mut engine = StackComplyEngine::new(config);
625        engine.discover_projects(tempdir.path()).expect("unexpected failure");
626
627        let report = engine.check_all();
628        // Non-PAIML crate should be skipped
629        assert_eq!(report.summary.total_projects, 0);
630    }
631
632    #[test]
633    fn test_check_all_includes_non_paiml_with_include_external() {
634        let tempdir = tempfile::tempdir().expect("tempdir creation failed");
635        let project_dir = tempdir.path().join("external-lib");
636        std::fs::create_dir_all(&project_dir).expect("mkdir failed");
637        std::fs::write(
638            project_dir.join("Cargo.toml"),
639            "[package]\nname = \"external-lib\"\nversion = \"0.1.0\"",
640        )
641        .expect("unexpected failure");
642
643        let config = ComplyConfig { include_external: true, ..Default::default() };
644        let mut engine = StackComplyEngine::new(config);
645        engine.discover_projects(tempdir.path()).expect("unexpected failure");
646
647        let report = engine.check_all();
648        // External crate should now be included
649        assert_eq!(report.summary.total_projects, 1);
650    }
651
652    #[test]
653    fn test_check_all_with_disabled_rule() {
654        let tempdir = tempfile::tempdir().expect("tempdir creation failed");
655        let project_dir = tempdir.path().join("trueno");
656        std::fs::create_dir_all(&project_dir).expect("mkdir failed");
657        std::fs::write(
658            project_dir.join("Cargo.toml"),
659            "[package]\nname = \"trueno\"\nversion = \"0.1.0\"",
660        )
661        .expect("unexpected failure");
662
663        let mut config = ComplyConfig::default();
664        config.disabled_rules.push("makefile-targets".to_string());
665        let mut engine = StackComplyEngine::new(config);
666        engine.discover_projects(tempdir.path()).expect("unexpected failure");
667
668        let report = engine.check_all();
669        // makefile-targets rule should be skipped
670        let trueno_results = report.results.get("trueno");
671        if let Some(results) = trueno_results {
672            assert!(!results.contains_key("makefile-targets"));
673        }
674    }
675
676    #[test]
677    fn test_check_all_with_exemption() {
678        let tempdir = tempfile::tempdir().expect("tempdir creation failed");
679        let project_dir = tempdir.path().join("trueno");
680        std::fs::create_dir_all(&project_dir).expect("mkdir failed");
681        std::fs::write(
682            project_dir.join("Cargo.toml"),
683            "[package]\nname = \"trueno\"\nversion = \"0.1.0\"",
684        )
685        .expect("unexpected failure");
686
687        let mut config = ComplyConfig::default();
688        let mut override_cfg = ProjectOverride::default();
689        override_cfg.exempt_rules.push("makefile-targets".to_string());
690        config.project_overrides.insert("trueno".to_string(), override_cfg);
691
692        let mut engine = StackComplyEngine::new(config);
693        engine.discover_projects(tempdir.path()).expect("unexpected failure");
694
695        let report = engine.check_all();
696        // Should have exemption recorded
697        assert!(report
698            .exemptions
699            .iter()
700            .any(|e| e.project == "trueno" && e.rule == "makefile-targets"));
701    }
702
703    // =========================================================================
704    // Coverage: fix_all with discovered projects
705    // =========================================================================
706
707    #[test]
708    fn test_fix_all_dry_run_with_paiml_project() {
709        let tempdir = tempfile::tempdir().expect("tempdir creation failed");
710        let project_dir = tempdir.path().join("trueno");
711        std::fs::create_dir_all(&project_dir).expect("mkdir failed");
712        std::fs::write(
713            project_dir.join("Cargo.toml"),
714            "[package]\nname = \"trueno\"\nversion = \"0.1.0\"",
715        )
716        .expect("unexpected failure");
717        // No Makefile -> makefile-targets rule will have violations
718
719        let mut engine = StackComplyEngine::new(ComplyConfig::default());
720        engine.discover_projects(tempdir.path()).expect("unexpected failure");
721
722        let report = engine.fix_all(true); // dry_run = true
723                                           // Should have processed the project
724        assert_eq!(report.summary.total_projects, 1);
725    }
726
727    #[test]
728    fn test_fix_all_actual_with_paiml_project() {
729        let tempdir = tempfile::tempdir().expect("tempdir creation failed");
730        let project_dir = tempdir.path().join("trueno");
731        std::fs::create_dir_all(&project_dir).expect("mkdir failed");
732        std::fs::write(
733            project_dir.join("Cargo.toml"),
734            "[package]\nname = \"trueno\"\nversion = \"0.1.0\"",
735        )
736        .expect("unexpected failure");
737
738        let mut engine = StackComplyEngine::new(ComplyConfig::default());
739        engine.discover_projects(tempdir.path()).expect("unexpected failure");
740
741        let report = engine.fix_all(false); // actual fix
742        assert_eq!(report.summary.total_projects, 1);
743    }
744
745    #[test]
746    fn test_fix_all_skips_non_paiml() {
747        let tempdir = tempfile::tempdir().expect("tempdir creation failed");
748        let project_dir = tempdir.path().join("external");
749        std::fs::create_dir_all(&project_dir).expect("mkdir failed");
750        std::fs::write(
751            project_dir.join("Cargo.toml"),
752            "[package]\nname = \"external\"\nversion = \"0.1.0\"",
753        )
754        .expect("unexpected failure");
755
756        let mut engine = StackComplyEngine::new(ComplyConfig::default());
757        engine.discover_projects(tempdir.path()).expect("unexpected failure");
758
759        let report = engine.fix_all(false);
760        // Non-PAIML skipped
761        assert_eq!(report.summary.total_projects, 0);
762    }
763
764    #[test]
765    fn test_fix_all_skips_disabled_rules() {
766        let tempdir = tempfile::tempdir().expect("tempdir creation failed");
767        let project_dir = tempdir.path().join("trueno");
768        std::fs::create_dir_all(&project_dir).expect("mkdir failed");
769        std::fs::write(
770            project_dir.join("Cargo.toml"),
771            "[package]\nname = \"trueno\"\nversion = \"0.1.0\"",
772        )
773        .expect("unexpected failure");
774
775        let mut config = ComplyConfig::default();
776        // Disable all rules
777        config.disabled_rules.push("makefile-targets".to_string());
778        config.disabled_rules.push("cargo-toml-consistency".to_string());
779        config.disabled_rules.push("ci-workflow-parity".to_string());
780        config.disabled_rules.push("code-duplication".to_string());
781        let mut engine = StackComplyEngine::new(config);
782        engine.discover_projects(tempdir.path()).expect("unexpected failure");
783
784        let report = engine.fix_all(false);
785        // All rules disabled -> no checks run
786        assert_eq!(report.summary.total_checks, 0);
787    }
788
789    #[test]
790    fn test_fix_all_with_exemption() {
791        let tempdir = tempfile::tempdir().expect("tempdir creation failed");
792        let project_dir = tempdir.path().join("trueno");
793        std::fs::create_dir_all(&project_dir).expect("mkdir failed");
794        std::fs::write(
795            project_dir.join("Cargo.toml"),
796            "[package]\nname = \"trueno\"\nversion = \"0.1.0\"",
797        )
798        .expect("unexpected failure");
799
800        let mut config = ComplyConfig::default();
801        let mut override_cfg = ProjectOverride::default();
802        override_cfg.exempt_rules.push("makefile-targets".to_string());
803        config.project_overrides.insert("trueno".to_string(), override_cfg);
804
805        let mut engine = StackComplyEngine::new(config);
806        engine.discover_projects(tempdir.path()).expect("unexpected failure");
807
808        let report = engine.fix_all(false);
809        // fix_all only processes rules where can_fix() is true.
810        // Only makefile-targets has can_fix()=true, and it's exempt here.
811        // So zero fixable rules run -> total_projects is 0 in the report.
812        assert_eq!(report.summary.total_projects, 0);
813    }
814
815    #[test]
816    fn test_fix_all_passing_project_with_makefile() {
817        let tempdir = tempfile::tempdir().expect("tempdir creation failed");
818        let project_dir = tempdir.path().join("trueno");
819        std::fs::create_dir_all(&project_dir).expect("mkdir failed");
820        std::fs::write(
821            project_dir.join("Cargo.toml"),
822            "[package]\nname = \"trueno\"\nversion = \"0.1.0\"",
823        )
824        .expect("unexpected failure");
825
826        // Create a Makefile with required targets so makefile-targets passes
827        let makefile = r#"test-fast:
828	cargo nextest run --lib
829
830test:
831	cargo nextest run
832
833lint:
834	cargo clippy
835
836fmt:
837	cargo fmt
838
839coverage:
840	cargo llvm-cov
841"#;
842        std::fs::write(project_dir.join("Makefile"), makefile).expect("fs write failed");
843
844        let mut engine = StackComplyEngine::new(ComplyConfig::default());
845        engine.discover_projects(tempdir.path()).expect("unexpected failure");
846
847        let report = engine.fix_all(false);
848        // makefile-targets should pass (nothing to fix for that rule)
849        assert_eq!(report.summary.total_projects, 1);
850    }
851
852    // =========================================================================
853    // Coverage: check_rule with discovered projects
854    // =========================================================================
855
856    #[test]
857    fn test_check_rule_skips_non_paiml() {
858        let tempdir = tempfile::tempdir().expect("tempdir creation failed");
859        let project_dir = tempdir.path().join("external");
860        std::fs::create_dir_all(&project_dir).expect("mkdir failed");
861        std::fs::write(
862            project_dir.join("Cargo.toml"),
863            "[package]\nname = \"external\"\nversion = \"0.1.0\"",
864        )
865        .expect("unexpected failure");
866
867        let mut engine = StackComplyEngine::new(ComplyConfig::default());
868        engine.discover_projects(tempdir.path()).expect("unexpected failure");
869
870        let report = engine.check_rule("makefile-targets");
871        // Non-PAIML should be skipped
872        assert_eq!(report.summary.total_projects, 0);
873    }
874
875    #[test]
876    fn test_check_rule_with_exemption() {
877        let tempdir = tempfile::tempdir().expect("tempdir creation failed");
878        let project_dir = tempdir.path().join("trueno");
879        std::fs::create_dir_all(&project_dir).expect("mkdir failed");
880        std::fs::write(
881            project_dir.join("Cargo.toml"),
882            "[package]\nname = \"trueno\"\nversion = \"0.1.0\"",
883        )
884        .expect("unexpected failure");
885
886        let mut config = ComplyConfig::default();
887        let mut override_cfg = ProjectOverride::default();
888        override_cfg.exempt_rules.push("makefile-targets".to_string());
889        config.project_overrides.insert("trueno".to_string(), override_cfg);
890
891        let mut engine = StackComplyEngine::new(config);
892        engine.discover_projects(tempdir.path()).expect("unexpected failure");
893
894        let report = engine.check_rule("makefile-targets");
895        assert!(report
896            .exemptions
897            .iter()
898            .any(|e| e.project == "trueno" && e.rule == "makefile-targets"));
899    }
900
901    #[test]
902    fn test_check_rule_includes_non_paiml_with_external() {
903        let tempdir = tempfile::tempdir().expect("tempdir creation failed");
904        let project_dir = tempdir.path().join("ext-proj");
905        std::fs::create_dir_all(&project_dir).expect("mkdir failed");
906        std::fs::write(
907            project_dir.join("Cargo.toml"),
908            "[package]\nname = \"ext-proj\"\nversion = \"0.1.0\"",
909        )
910        .expect("unexpected failure");
911
912        let config = ComplyConfig { include_external: true, ..Default::default() };
913        let mut engine = StackComplyEngine::new(config);
914        engine.discover_projects(tempdir.path()).expect("unexpected failure");
915
916        let report = engine.check_rule("makefile-targets");
917        // With include_external, non-PAIML projects should be checked
918        assert!(report.summary.total_projects > 0 || !report.results.is_empty());
919    }
920
921    // =========================================================================
922    // Coverage: parse_project edge cases
923    // =========================================================================
924
925    #[test]
926    fn test_parse_project_workspace_toml() {
927        // A workspace Cargo.toml has no [package] section
928        let tempdir = tempfile::tempdir().expect("tempdir creation failed");
929        let project_dir = tempdir.path().join("workspace");
930        std::fs::create_dir_all(&project_dir).expect("mkdir failed");
931        let cargo_toml = r#"
932[workspace]
933members = ["crate-a", "crate-b"]
934"#;
935        std::fs::write(project_dir.join("Cargo.toml"), cargo_toml).expect("fs write failed");
936
937        let mut engine = StackComplyEngine::new(ComplyConfig::default());
938        let projects = engine.discover_projects(tempdir.path()).expect("unexpected failure");
939        // Workspace toml has no package name, should be skipped
940        assert_eq!(projects.len(), 0);
941    }
942
943    #[test]
944    fn test_check_all_reports_rule_errors() {
945        // Use a PAIML crate dir that causes rule check to produce an error
946        // (e.g., invalid Makefile or missing directories)
947        let tempdir = tempfile::tempdir().expect("tempdir creation failed");
948        let project_dir = tempdir.path().join("trueno");
949        std::fs::create_dir_all(&project_dir).expect("mkdir failed");
950        std::fs::write(
951            project_dir.join("Cargo.toml"),
952            "[package]\nname = \"trueno\"\nversion = \"0.1.0\"",
953        )
954        .expect("unexpected failure");
955
956        let mut engine = StackComplyEngine::new(ComplyConfig::default());
957        engine.discover_projects(tempdir.path()).expect("unexpected failure");
958
959        let report = engine.check_all();
960        // Some rules may fail (no Makefile, no CI, etc.) but should produce results
961        assert_eq!(report.summary.total_projects, 1);
962        assert!(report.summary.total_checks > 0);
963    }
964}