use crate::errors::{ErrorFormatter, Result};
use crate::parser::Parser;
use crate::types::{Config, ExecutionMode, LogLevel};
use crate::validator::Validator;
use crate::verifier::Verifier;
use globset::{Glob, GlobSet, GlobSetBuilder};
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
#[derive(Debug, Clone)]
pub struct GuideLocation {
pub guide_path: PathBuf,
pub root_path: PathBuf,
}
#[derive(Debug)]
pub struct GuideVerificationResult {
pub location: GuideLocation,
pub success: bool,
pub error: Option<String>,
pub ignored: bool,
}
pub fn find_guides(
root: &Path,
guide_name: &str,
exclude_patterns: &[String],
) -> Result<Vec<GuideLocation>> {
let mut guides = Vec::new();
let exclude_globs = if exclude_patterns.is_empty() {
None
} else {
let mut builder = GlobSetBuilder::new();
for pattern in exclude_patterns {
builder.add(Glob::new(pattern)?);
}
Some(builder.build()?)
};
let walker = WalkDir::new(root).follow_links(false).into_iter();
for entry in walker.filter_entry(|e| should_include_entry(e, root, &exclude_globs)) {
let entry = entry?;
let path = entry.path();
if path.is_file() {
if let Some(file_name) = path.file_name() {
if file_name == guide_name {
let root_path = path.parent().unwrap_or(root).to_path_buf();
guides.push(GuideLocation {
guide_path: path.to_path_buf(),
root_path,
});
}
}
}
}
Ok(guides)
}
fn should_include_entry(
entry: &walkdir::DirEntry,
root: &Path,
exclude_globs: &Option<GlobSet>,
) -> bool {
if let Some(globs) = exclude_globs {
let path = entry.path();
if let Ok(relative_path) = path.strip_prefix(root) {
if globs.is_match(relative_path) {
return false;
}
let mut current_path = PathBuf::new();
for component in relative_path.components() {
current_path.push(component);
if globs.is_match(¤t_path) {
return false;
}
}
}
}
true
}
pub fn verify_guides(
guides: &[GuideLocation],
config: &Config,
) -> Result<Vec<GuideVerificationResult>> {
let mut results = Vec::new();
for location in guides {
let result = verify_single_guide(location, config);
results.push(result);
}
Ok(results)
}
fn verify_single_guide(location: &GuideLocation, _config: &Config) -> GuideVerificationResult {
let content = match std::fs::read_to_string(&location.guide_path) {
Ok(content) => content,
Err(e) => {
return GuideVerificationResult {
location: location.clone(),
success: false,
error: Some(format!("Error reading file: {e}")),
ignored: false,
};
}
};
let parser = Parser::new();
let guide = match parser.parse(&content) {
Ok(guide) => guide,
Err(e) => {
let formatted = ErrorFormatter::format_with_context(&e, Some(&content));
return GuideVerificationResult {
location: location.clone(),
success: false,
error: Some(formatted),
ignored: false,
};
}
};
if guide.ignore {
return GuideVerificationResult {
location: location.clone(),
success: true,
error: None,
ignored: true,
};
}
let validator = Validator::new();
if let Err(e) = validator.validate_syntax(&guide) {
let formatted = ErrorFormatter::format_with_context(&e, Some(&content));
return GuideVerificationResult {
location: location.clone(),
success: false,
error: Some(formatted),
ignored: false,
};
}
let verifier = Verifier::new(&location.root_path);
match verifier.verify(&guide) {
Ok(()) => GuideVerificationResult {
location: location.clone(),
success: true,
error: None,
ignored: false,
},
Err(e) => {
let formatted = ErrorFormatter::format_with_context(&e, Some(&content));
GuideVerificationResult {
location: location.clone(),
success: false,
error: Some(formatted),
ignored: false,
}
}
}
}
pub fn display_results(results: &[GuideVerificationResult], config: &Config) -> bool {
let total = results.len();
let passed = results.iter().filter(|r| r.success && !r.ignored).count();
let ignored = results.iter().filter(|r| r.ignored).count();
let failed = results.iter().filter(|r| !r.success).count();
match config.execution_mode {
ExecutionMode::GitHubActions => {
display_github_actions_results(results, config);
}
ExecutionMode::PostToolUse => {
display_post_tool_use_results(results, config);
}
_ => {
display_default_results(results, config);
}
}
if config.log_level != LogLevel::Quiet {
match config.execution_mode {
ExecutionMode::GitHubActions => {
if failed == 0 {
println!("✓ All navigation guides verified ({total} total)");
} else {
eprintln!("❌ Navigation guide verification failed: {passed} passed, {failed} failed, {ignored} ignored");
}
}
_ => {
if failed == 0 {
println!("✓ All navigation guides are valid and match filesystem");
println!(" Total: {total}, Passed: {passed}, Ignored: {ignored}");
} else {
eprintln!("✗ Some navigation guides failed verification");
eprintln!(
" Total: {total}, Passed: {passed}, Failed: {failed}, Ignored: {ignored}"
);
}
}
}
}
failed == 0
}
fn display_github_actions_results(results: &[GuideVerificationResult], config: &Config) {
for result in results {
if result.ignored {
if config.log_level != LogLevel::Quiet {
eprintln!(
"⚠️ Skipping verification: guide at {} has ignore=true",
result.location.guide_path.display()
);
}
} else if result.success {
if config.log_level != LogLevel::Quiet {
println!("✓ {}: verified", result.location.guide_path.display());
}
} else if let Some(error) = &result.error {
eprintln!("❌ {}:", result.location.guide_path.display());
eprintln!("{error}");
}
}
}
fn display_post_tool_use_results(results: &[GuideVerificationResult], config: &Config) {
for result in results {
if result.ignored {
if config.log_level != LogLevel::Quiet {
eprintln!(
"Warning: Skipping verification of {} (marked with ignore=true)",
result.location.guide_path.display()
);
}
} else if !result.success {
if let Some(error) = &result.error {
eprintln!(
"The agentic navigation guide at {} has errors:\n\n{}",
result.location.guide_path.display(),
error
);
}
}
}
}
fn display_default_results(results: &[GuideVerificationResult], config: &Config) {
for result in results {
if result.ignored {
if config.log_level != LogLevel::Quiet {
eprintln!(
"Warning: Skipping verification of {} (marked with ignore=true)",
result.location.guide_path.display()
);
}
} else if result.success {
if config.log_level == LogLevel::Verbose {
println!("✓ {}: valid", result.location.guide_path.display());
}
} else if let Some(error) = &result.error {
eprintln!("✗ {}:", result.location.guide_path.display());
eprintln!("{error}");
eprintln!();
}
}
}