use agentic_navigation_guide::errors::{ErrorFormatter, Result};
use agentic_navigation_guide::parser::Parser;
use agentic_navigation_guide::types::{Config, ExecutionMode, LogLevel};
use agentic_navigation_guide::validator::Validator;
use agentic_navigation_guide::verifier::Verifier;
use clap::Args;
use std::path::{Path, PathBuf};
#[derive(Args, Debug)]
pub struct VerifyArgs {
#[arg(
short,
long,
env = "AGENTIC_NAVIGATION_GUIDE_PATH",
conflicts_with = "recursive"
)]
pub guide: Option<PathBuf>,
#[arg(short, long, env = "AGENTIC_NAVIGATION_GUIDE_ROOT")]
pub root: Option<PathBuf>,
#[arg(long, conflicts_with = "guide")]
pub recursive: bool,
#[arg(long, requires = "recursive", env = "AGENTIC_NAVIGATION_GUIDE_NAME")]
pub guide_name: Option<String>,
#[arg(long = "exclude", requires = "recursive")]
pub exclude_patterns: Vec<String>,
#[arg(long, conflicts_with_all = ["execution_mode", "pre_commit_hook", "github_actions_check"])]
pub post_tool_use_hook: bool,
#[arg(long, conflicts_with_all = ["execution_mode", "post_tool_use_hook", "github_actions_check"])]
pub pre_commit_hook: bool,
#[arg(long, conflicts_with_all = ["execution_mode", "post_tool_use_hook", "pre_commit_hook"])]
pub github_actions_check: bool,
}
impl VerifyArgs {
pub fn execute(self, config: &mut Config) -> Result<()> {
if self.post_tool_use_hook {
config.execution_mode = ExecutionMode::PostToolUse;
} else if self.pre_commit_hook {
config.execution_mode = ExecutionMode::PreCommitHook;
} else if self.github_actions_check {
config.execution_mode = ExecutionMode::GitHubActions;
}
if self.recursive {
return self.execute_recursive(config);
}
config.original_guide_path = self
.guide
.as_ref()
.map(|p| p.display().to_string())
.or_else(|| std::env::var("AGENTIC_NAVIGATION_GUIDE_NAME").ok());
config.original_root_path = self.root.as_ref().map(|p| p.display().to_string());
let guide_path = self
.guide
.or_else(|| {
std::env::var("AGENTIC_NAVIGATION_GUIDE_NAME")
.ok()
.map(|name| std::env::current_dir().unwrap().join(name))
})
.unwrap_or_else(|| {
std::env::current_dir()
.unwrap()
.join("AGENTIC_NAVIGATION_GUIDE.md")
});
let root_path = self
.root
.unwrap_or_else(|| std::env::current_dir().expect("Failed to get current directory"));
log::debug!(
"Verifying navigation guide: {} against root: {}",
guide_path.display(),
root_path.display()
);
let content = match std::fs::read_to_string(&guide_path) {
Ok(content) => content,
Err(e) => {
eprintln!("Error reading file {}: {}", guide_path.display(), e);
return Err(e.into());
}
};
let parser = Parser::new();
let guide = match parser.parse(&content) {
Ok(guide) => guide,
Err(e) => {
if config.execution_mode == ExecutionMode::PostToolUse {
let formatted = format_post_tool_use_error(
&e,
&guide_path,
&root_path,
config,
Some(&content),
);
eprintln!("{formatted}");
} else if config.execution_mode == ExecutionMode::GitHubActions {
let formatted =
format_github_actions_error(&e, &guide_path, Some(&content), config);
eprintln!("{formatted}");
} else {
let formatted = ErrorFormatter::format_with_context(&e, Some(&content));
eprintln!("{formatted}");
}
return Err(e);
}
};
if guide.ignore {
let display_path = config
.original_guide_path
.as_deref()
.unwrap_or("AGENTIC_NAVIGATION_GUIDE.md");
if config.log_level != LogLevel::Quiet {
match config.execution_mode {
ExecutionMode::GitHubActions => {
eprintln!(
"⚠️ Skipping verification: guide at {display_path} has ignore=true"
);
}
_ => {
eprintln!("Warning: Skipping verification of {display_path} (marked with ignore=true)");
}
}
if guide_path
.file_name()
.and_then(|n| n.to_str())
.map(|n| n == "AGENTIC_NAVIGATION_GUIDE.md")
.unwrap_or(false)
{
eprintln!(
"Note: Standalone guide file is marked with ignore=true. This may be intentional for examples."
);
}
}
return Ok(());
}
let validator = Validator::new();
if let Err(e) = validator.validate_syntax(&guide) {
if config.execution_mode == ExecutionMode::PostToolUse {
let formatted =
format_post_tool_use_error(&e, &guide_path, &root_path, config, Some(&content));
eprintln!("{formatted}");
} else if config.execution_mode == ExecutionMode::GitHubActions {
let formatted =
format_github_actions_error(&e, &guide_path, Some(&content), config);
eprintln!("{formatted}");
} else {
let formatted = ErrorFormatter::format_with_context(&e, Some(&content));
eprintln!("{formatted}");
}
return Err(e);
}
let verifier = Verifier::new(&root_path);
match verifier.verify(&guide) {
Ok(()) => {
if config.log_level != LogLevel::Quiet {
match config.execution_mode {
ExecutionMode::GitHubActions => {
println!("✓ Navigation guide verified");
}
_ => {
println!("✓ Navigation guide is valid and matches filesystem");
}
}
}
Ok(())
}
Err(e) => {
if config.execution_mode == ExecutionMode::PostToolUse {
let formatted = format_post_tool_use_error(
&e,
&guide_path,
&root_path,
config,
Some(&content),
);
eprintln!("{formatted}");
} else if config.execution_mode == ExecutionMode::GitHubActions {
let formatted =
format_github_actions_error(&e, &guide_path, Some(&content), config);
eprintln!("{formatted}");
} else {
let formatted = ErrorFormatter::format_with_context(&e, Some(&content));
eprintln!("{formatted}");
}
Err(e)
}
}
}
fn execute_recursive(self, config: &Config) -> Result<()> {
use agentic_navigation_guide::recursive;
let search_root = self
.root
.unwrap_or_else(|| std::env::current_dir().expect("Failed to get current directory"));
let guide_name = self
.guide_name
.unwrap_or_else(|| "AGENTIC_NAVIGATION_GUIDE.md".to_string());
log::debug!(
"Recursively searching for '{}' guides in {}",
guide_name,
search_root.display()
);
let guides = recursive::find_guides(&search_root, &guide_name, &self.exclude_patterns)?;
if guides.is_empty() {
if config.log_level != LogLevel::Quiet {
eprintln!(
"No navigation guide files named '{}' found in {}",
guide_name,
search_root.display()
);
}
return Ok(());
}
if config.log_level != LogLevel::Quiet {
match config.execution_mode {
ExecutionMode::GitHubActions => {
println!("Found {} navigation guide(s)", guides.len());
}
_ => {
println!(
"Found {} navigation guide(s) to verify in {}",
guides.len(),
search_root.display()
);
}
}
}
let results = recursive::verify_guides(&guides, config)?;
let all_passed = recursive::display_results(&results, config);
if all_passed {
Ok(())
} else {
Err(agentic_navigation_guide::errors::AppError::Other(
"Some guides failed verification".to_string(),
))
}
}
}
fn format_post_tool_use_error(
error: &agentic_navigation_guide::errors::AppError,
_guide_path: &Path,
_root_path: &Path,
config: &Config,
file_content: Option<&str>,
) -> String {
use agentic_navigation_guide::errors::AppError;
let display_guide_path = config
.original_guide_path
.as_deref()
.unwrap_or("AGENTIC_NAVIGATION_GUIDE.md");
let display_root_path = config.original_root_path.as_deref().unwrap_or("./");
match error {
AppError::Syntax(_) => {
let error_detail = ErrorFormatter::format_with_context(error, file_content);
format!(
"The agentic navigation guide at {display_guide_path} has a syntax error:\n\n{error_detail}"
)
}
AppError::Semantic(semantic_error) => {
let error_detail = semantic_error.to_string();
format!(
"The agentic navigation guide has become out-of-date vis-a-vis the current state of the file system.\n\n\
- guide: {display_guide_path}\n\
- root: {display_root_path}\n\
- details:\n - {error_detail}"
)
}
_ => {
ErrorFormatter::format_with_context(error, file_content)
}
}
}
fn format_github_actions_error(
error: &agentic_navigation_guide::errors::AppError,
_guide_path: &Path,
file_content: Option<&str>,
config: &Config,
) -> String {
use agentic_navigation_guide::errors::AppError;
let display_guide_path = config
.original_guide_path
.as_deref()
.unwrap_or("AGENTIC_NAVIGATION_GUIDE.md");
let mut output = String::new();
output.push_str("❌ Navigation guide verification failed\n\n");
let line_num = match error {
AppError::Syntax(e) => e.line_number(),
AppError::Semantic(e) => Some(e.line_number()),
_ => None,
};
if let Some(line_num) = line_num {
output.push_str(&format!("{display_guide_path}:{line_num}: "));
output.push_str(&error.to_string());
output.push('\n');
if let Some(content) = file_content {
if let Some(line) = content.lines().nth(line_num.saturating_sub(1)) {
let trimmed_line = line.trim_end();
output.push_str(&format!(" {trimmed_line}\n"));
}
}
} else {
output.push_str(&format!("{display_guide_path}: {error}\n"));
}
output
}