agentic_navigation_guide/
recursive.rs

1//! Recursive navigation guide discovery and verification
2
3use crate::errors::{ErrorFormatter, Result};
4use crate::parser::Parser;
5use crate::types::{Config, ExecutionMode, LogLevel};
6use crate::validator::Validator;
7use crate::verifier::Verifier;
8use globset::{Glob, GlobSet, GlobSetBuilder};
9use std::path::{Path, PathBuf};
10use walkdir::WalkDir;
11
12/// Represents a single guide file to be verified
13#[derive(Debug, Clone)]
14pub struct GuideLocation {
15    /// Path to the guide file
16    pub guide_path: PathBuf,
17    /// Root directory for verification (parent of guide file)
18    pub root_path: PathBuf,
19}
20
21/// Result of verifying a single guide
22#[derive(Debug)]
23pub struct GuideVerificationResult {
24    /// The guide that was verified
25    pub location: GuideLocation,
26    /// Whether verification succeeded
27    pub success: bool,
28    /// Error message if verification failed
29    pub error: Option<String>,
30    /// Whether the guide was ignored (has ignore=true)
31    pub ignored: bool,
32}
33
34/// Recursively find all navigation guide files
35pub fn find_guides(
36    root: &Path,
37    guide_name: &str,
38    exclude_patterns: &[String],
39) -> Result<Vec<GuideLocation>> {
40    let mut guides = Vec::new();
41
42    // Build exclusion glob set
43    let exclude_globs = if exclude_patterns.is_empty() {
44        None
45    } else {
46        let mut builder = GlobSetBuilder::new();
47        for pattern in exclude_patterns {
48            builder.add(Glob::new(pattern)?);
49        }
50        Some(builder.build()?)
51    };
52
53    // Walk directory tree
54    let walker = WalkDir::new(root).follow_links(false).into_iter();
55
56    for entry in walker.filter_entry(|e| should_include_entry(e, root, &exclude_globs)) {
57        let entry = entry?;
58        let path = entry.path();
59
60        // Check if this is a guide file
61        if path.is_file() {
62            if let Some(file_name) = path.file_name() {
63                if file_name == guide_name {
64                    // The root for this guide is its parent directory
65                    let root_path = path.parent().unwrap_or(root).to_path_buf();
66
67                    guides.push(GuideLocation {
68                        guide_path: path.to_path_buf(),
69                        root_path,
70                    });
71                }
72            }
73        }
74    }
75
76    Ok(guides)
77}
78
79/// Check if a directory entry should be included in the walk
80fn should_include_entry(
81    entry: &walkdir::DirEntry,
82    root: &Path,
83    exclude_globs: &Option<GlobSet>,
84) -> bool {
85    if let Some(globs) = exclude_globs {
86        let path = entry.path();
87        if let Ok(relative_path) = path.strip_prefix(root) {
88            // Check the full relative path
89            if globs.is_match(relative_path) {
90                return false;
91            }
92
93            // For directories, check if any parent component matches
94            let mut current_path = PathBuf::new();
95            for component in relative_path.components() {
96                current_path.push(component);
97                if globs.is_match(&current_path) {
98                    return false;
99                }
100            }
101        }
102    }
103    true
104}
105
106/// Verify multiple guides and collect results
107pub fn verify_guides(
108    guides: &[GuideLocation],
109    config: &Config,
110) -> Result<Vec<GuideVerificationResult>> {
111    let mut results = Vec::new();
112
113    for location in guides {
114        let result = verify_single_guide(location, config);
115        results.push(result);
116    }
117
118    Ok(results)
119}
120
121/// Verify a single guide and return the result
122fn verify_single_guide(location: &GuideLocation, _config: &Config) -> GuideVerificationResult {
123    // Read the file
124    let content = match std::fs::read_to_string(&location.guide_path) {
125        Ok(content) => content,
126        Err(e) => {
127            return GuideVerificationResult {
128                location: location.clone(),
129                success: false,
130                error: Some(format!("Error reading file: {e}")),
131                ignored: false,
132            };
133        }
134    };
135
136    // Parse the guide
137    let parser = Parser::new();
138    let guide = match parser.parse(&content) {
139        Ok(guide) => guide,
140        Err(e) => {
141            let formatted = ErrorFormatter::format_with_context(&e, Some(&content));
142            return GuideVerificationResult {
143                location: location.clone(),
144                success: false,
145                error: Some(formatted),
146                ignored: false,
147            };
148        }
149    };
150
151    // Check if the guide should be ignored
152    if guide.ignore {
153        return GuideVerificationResult {
154            location: location.clone(),
155            success: true,
156            error: None,
157            ignored: true,
158        };
159    }
160
161    // Validate syntax
162    let validator = Validator::new();
163    if let Err(e) = validator.validate_syntax(&guide) {
164        let formatted = ErrorFormatter::format_with_context(&e, Some(&content));
165        return GuideVerificationResult {
166            location: location.clone(),
167            success: false,
168            error: Some(formatted),
169            ignored: false,
170        };
171    }
172
173    // Verify against filesystem
174    let verifier = Verifier::new(&location.root_path);
175    match verifier.verify(&guide) {
176        Ok(()) => GuideVerificationResult {
177            location: location.clone(),
178            success: true,
179            error: None,
180            ignored: false,
181        },
182        Err(e) => {
183            let formatted = ErrorFormatter::format_with_context(&e, Some(&content));
184            GuideVerificationResult {
185                location: location.clone(),
186                success: false,
187                error: Some(formatted),
188                ignored: false,
189            }
190        }
191    }
192}
193
194/// Format and display verification results
195pub fn display_results(results: &[GuideVerificationResult], config: &Config) -> bool {
196    let total = results.len();
197    let passed = results.iter().filter(|r| r.success && !r.ignored).count();
198    let ignored = results.iter().filter(|r| r.ignored).count();
199    let failed = results.iter().filter(|r| !r.success).count();
200
201    // Display individual results based on execution mode
202    match config.execution_mode {
203        ExecutionMode::GitHubActions => {
204            display_github_actions_results(results, config);
205        }
206        ExecutionMode::PostToolUse => {
207            display_post_tool_use_results(results, config);
208        }
209        _ => {
210            display_default_results(results, config);
211        }
212    }
213
214    // Display summary (unless in quiet mode)
215    if config.log_level != LogLevel::Quiet {
216        match config.execution_mode {
217            ExecutionMode::GitHubActions => {
218                if failed == 0 {
219                    println!("✓ All navigation guides verified ({total} total)");
220                } else {
221                    eprintln!("❌ Navigation guide verification failed: {passed} passed, {failed} failed, {ignored} ignored");
222                }
223            }
224            _ => {
225                if failed == 0 {
226                    println!("✓ All navigation guides are valid and match filesystem");
227                    println!("  Total: {total}, Passed: {passed}, Ignored: {ignored}");
228                } else {
229                    eprintln!("✗ Some navigation guides failed verification");
230                    eprintln!(
231                        "  Total: {total}, Passed: {passed}, Failed: {failed}, Ignored: {ignored}"
232                    );
233                }
234            }
235        }
236    }
237
238    failed == 0
239}
240
241/// Display results for GitHub Actions mode
242fn display_github_actions_results(results: &[GuideVerificationResult], config: &Config) {
243    for result in results {
244        if result.ignored {
245            if config.log_level != LogLevel::Quiet {
246                eprintln!(
247                    "⚠️  Skipping verification: guide at {} has ignore=true",
248                    result.location.guide_path.display()
249                );
250            }
251        } else if result.success {
252            if config.log_level != LogLevel::Quiet {
253                println!("✓ {}: verified", result.location.guide_path.display());
254            }
255        } else if let Some(error) = &result.error {
256            eprintln!("❌ {}:", result.location.guide_path.display());
257            eprintln!("{error}");
258        }
259    }
260}
261
262/// Display results for post-tool-use mode
263fn display_post_tool_use_results(results: &[GuideVerificationResult], config: &Config) {
264    for result in results {
265        if result.ignored {
266            if config.log_level != LogLevel::Quiet {
267                eprintln!(
268                    "Warning: Skipping verification of {} (marked with ignore=true)",
269                    result.location.guide_path.display()
270                );
271            }
272        } else if !result.success {
273            if let Some(error) = &result.error {
274                eprintln!(
275                    "The agentic navigation guide at {} has errors:\n\n{}",
276                    result.location.guide_path.display(),
277                    error
278                );
279            }
280        }
281    }
282}
283
284/// Display results for default mode
285fn display_default_results(results: &[GuideVerificationResult], config: &Config) {
286    for result in results {
287        if result.ignored {
288            if config.log_level != LogLevel::Quiet {
289                eprintln!(
290                    "Warning: Skipping verification of {} (marked with ignore=true)",
291                    result.location.guide_path.display()
292                );
293            }
294        } else if result.success {
295            if config.log_level == LogLevel::Verbose {
296                println!("✓ {}: valid", result.location.guide_path.display());
297            }
298        } else if let Some(error) = &result.error {
299            eprintln!("✗ {}:", result.location.guide_path.display());
300            eprintln!("{error}");
301            eprintln!();
302        }
303    }
304}