agentic_navigation_guide/
recursive.rs1use 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#[derive(Debug, Clone)]
14pub struct GuideLocation {
15 pub guide_path: PathBuf,
17 pub root_path: PathBuf,
19}
20
21#[derive(Debug)]
23pub struct GuideVerificationResult {
24 pub location: GuideLocation,
26 pub success: bool,
28 pub error: Option<String>,
30 pub ignored: bool,
32}
33
34pub 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 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 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 if path.is_file() {
62 if let Some(file_name) = path.file_name() {
63 if file_name == guide_name {
64 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
79fn 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 if globs.is_match(relative_path) {
90 return false;
91 }
92
93 let mut current_path = PathBuf::new();
95 for component in relative_path.components() {
96 current_path.push(component);
97 if globs.is_match(¤t_path) {
98 return false;
99 }
100 }
101 }
102 }
103 true
104}
105
106pub 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
121fn verify_single_guide(location: &GuideLocation, _config: &Config) -> GuideVerificationResult {
123 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 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 if guide.ignore {
153 return GuideVerificationResult {
154 location: location.clone(),
155 success: true,
156 error: None,
157 ignored: true,
158 };
159 }
160
161 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 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
194pub 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 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 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
241fn 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
262fn 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
284fn 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}