acp/commands/
check.rs

1//! @acp:module "Check Command"
2//! @acp:summary "Check guardrails for a file"
3//! @acp:domain cli
4//! @acp:layer handler
5
6use std::path::PathBuf;
7
8use anyhow::Result;
9use console::style;
10
11use crate::cache::Cache;
12
13/// Options for the check command
14#[derive(Debug, Clone)]
15pub struct CheckOptions {
16    /// File to check
17    pub file: PathBuf,
18    /// Cache file
19    pub cache: PathBuf,
20}
21
22/// Execute the check command
23pub fn execute_check(options: CheckOptions) -> Result<()> {
24    let cache_data = Cache::from_json(&options.cache)?;
25
26    // If path is ".", show all files with constraints
27    let file_str = options.file.to_string_lossy().to_string();
28    if file_str == "." {
29        return show_all_constraints(&cache_data);
30    }
31
32    // Try multiple path formats to find the file
33    let file_entry = cache_data
34        .files
35        .get(&file_str)
36        .or_else(|| cache_data.files.get(&format!("./{}", file_str)))
37        .or_else(|| {
38            let stripped = file_str.strip_prefix("./").unwrap_or(&file_str);
39            cache_data.files.get(stripped)
40        });
41
42    if let Some(file_entry) = file_entry {
43        println!("{} File found in cache", style("✓").green());
44        println!("  Path: {}", file_entry.path);
45        println!("  Lines: {}", file_entry.lines);
46        println!("  Language: {:?}", file_entry.language);
47
48        if let Some(stability) = &file_entry.stability {
49            println!("  Stability: {:?}", stability);
50        }
51
52        if !file_entry.ai_hints.is_empty() {
53            println!("  AI hints: {}", file_entry.ai_hints.join(", "));
54        }
55
56        // Check constraints if available
57        if let Some(ref constraints) = cache_data.constraints {
58            let file_constraints = constraints
59                .by_file
60                .get(&file_entry.path)
61                .or_else(|| constraints.by_file.get(&format!("./{}", file_entry.path)))
62                .or_else(|| {
63                    let stripped = file_entry
64                        .path
65                        .strip_prefix("./")
66                        .unwrap_or(&file_entry.path);
67                    constraints.by_file.get(stripped)
68                });
69
70            if let Some(file_constraints) = file_constraints {
71                if let Some(mutation) = &file_constraints.mutation {
72                    println!("  Lock level: {:?}", mutation.level);
73                    if mutation.requires_approval {
74                        println!("  {} Requires approval", style("⚠").yellow());
75                    }
76                    if mutation.requires_tests {
77                        println!("  {} Requires tests", style("⚠").yellow());
78                    }
79                    if mutation.requires_docs {
80                        println!("  {} Requires documentation", style("⚠").yellow());
81                    }
82                }
83            }
84        }
85    } else {
86        eprintln!(
87            "{} File not in cache: {}",
88            style("✗").red(),
89            options.file.display()
90        );
91    }
92
93    Ok(())
94}
95
96/// Show all files with constraints
97fn show_all_constraints(cache_data: &Cache) -> Result<()> {
98    let constraints = match &cache_data.constraints {
99        Some(c) => c,
100        None => {
101            println!("{} No constraints found in cache", style("•").dim());
102            return Ok(());
103        }
104    };
105
106    if constraints.by_file.is_empty() {
107        println!("{} No file constraints defined", style("•").dim());
108        return Ok(());
109    }
110
111    println!("{} Files with constraints:\n", style("→").cyan());
112
113    // Group by lock level
114    let mut by_level: std::collections::HashMap<String, Vec<&String>> =
115        std::collections::HashMap::new();
116
117    for (path, file_constraint) in &constraints.by_file {
118        if let Some(ref mutation) = file_constraint.mutation {
119            let level = format!("{:?}", mutation.level);
120            by_level.entry(level).or_default().push(path);
121        }
122    }
123
124    // Sort by severity (frozen first)
125    let level_order = [
126        "Frozen",
127        "Restricted",
128        "ApprovalRequired",
129        "TestsRequired",
130        "DocsRequired",
131        "ReviewRequired",
132        "Normal",
133        "Experimental",
134    ];
135
136    for level in level_order {
137        if let Some(files) = by_level.get(level) {
138            let color = match level {
139                "Frozen" => style(level).red().bold(),
140                "Restricted" => style(level).red(),
141                "ApprovalRequired" | "TestsRequired" | "DocsRequired" | "ReviewRequired" => {
142                    style(level).yellow()
143                }
144                _ => style(level).dim(),
145            };
146            println!("  {} ({} files)", color, files.len());
147            for path in files.iter().take(10) {
148                println!("    {}", path);
149            }
150            if files.len() > 10 {
151                println!("    ... and {} more", files.len() - 10);
152            }
153            println!();
154        }
155    }
156
157    Ok(())
158}