use anyhow::{bail, Result};
use colored::Colorize;
use std::collections::BTreeMap;
use std::path::PathBuf;
use crate::utils;
use crate::validation::{self, Severity, ValidationIssue};
pub fn run(
path: &str,
fix: bool,
staged: bool,
include_charters: bool,
check_pending_reviews: bool,
max_pending_days: i64,
) -> Result<()> {
let resolved = match utils::resolve_project_root(path) {
Some(r) => r,
None => {
let target = PathBuf::from(path)
.canonicalize()
.unwrap_or_else(|_| PathBuf::from(path));
utils::info(&format!(
"StrayMark is not installed in {}",
target.display()
));
utils::info("Run 'straymark init' to initialize StrayMark in this directory.");
return Ok(());
}
};
if resolved.is_fallback {
utils::info(&format!(
"Using StrayMark installation at repo root: {}",
resolved.path.display()
));
}
let target = resolved.path;
let straymark_dir = target.join(".straymark");
if staged {
return run_staged(&target, &straymark_dir);
}
println!();
println!(" {}", "StrayMark Validate".bold().cyan());
println!(" {}", target.display().to_string().dimmed());
println!();
let (mut result, mut doc_count) = validation::validate_all(&straymark_dir);
if include_charters {
let (charter_result, charter_count) =
validation::validate_charters(&target, &straymark_dir);
result.merge(charter_result);
doc_count += charter_count;
}
if check_pending_reviews {
for issue in validation::check_pending_reviews(&straymark_dir, max_pending_days) {
result.warnings.push(issue);
}
}
if doc_count == 0 {
utils::info("No documents found to validate.");
println!(
" {} Create documents with {} or {}",
"→".blue().bold(),
"straymark new".cyan(),
"/straymark-new".cyan()
);
println!();
return Ok(());
}
if fix {
apply_fixes(&straymark_dir);
let (mut result, mut doc_count) = validation::validate_all(&straymark_dir);
if include_charters {
let (charter_result, charter_count) =
validation::validate_charters(&target, &straymark_dir);
result.merge(charter_result);
doc_count += charter_count;
}
if check_pending_reviews {
for issue in validation::check_pending_reviews(&straymark_dir, max_pending_days) {
result.warnings.push(issue);
}
}
print_results(&result, doc_count);
return exit_with_code(&result);
}
print_results(&result, doc_count);
exit_with_code(&result)
}
fn run_staged(project_root: &std::path::Path, straymark_dir: &std::path::Path) -> Result<()> {
let output = std::process::Command::new("git")
.args(["diff", "--cached", "--name-only"])
.current_dir(project_root)
.output();
let output = match output {
Ok(o) if o.status.success() => o,
_ => {
bail!("Not a git repository or git is not available. --staged requires a git repo.");
}
};
let stdout = String::from_utf8_lossy(&output.stdout);
let staged_paths: Vec<PathBuf> = stdout
.lines()
.filter(|line| line.starts_with(".straymark/") && line.ends_with(".md"))
.map(|line| project_root.join(line))
.collect();
if staged_paths.is_empty() {
println!(
" {} No staged documentation to validate.",
"✓".green().bold()
);
return Ok(());
}
println!();
println!(" {}", "StrayMark Validate (staged)".bold().cyan());
println!(
" {} file(s)",
staged_paths.len().to_string().dimmed()
);
println!();
let (result, doc_count) = validation::validate_paths(&staged_paths, straymark_dir);
if doc_count == 0 {
println!(
" {} No StrayMark documents among staged files.",
"✓".green().bold()
);
return Ok(());
}
print_results(&result, doc_count);
exit_with_code(&result)
}
fn apply_fixes(straymark_dir: &std::path::Path) {
let paths = crate::document::discover_documents(straymark_dir);
let mut fixed_count = 0;
for path in &paths {
if let Ok(doc) = crate::document::parse_document(path) {
if let Some(new_content) = validation::apply_fixes(&doc) {
if std::fs::write(path, new_content).is_ok() {
println!(
" {} Fixed: {}",
"✓".green().bold(),
path.file_name()
.and_then(|n| n.to_str())
.unwrap_or("?")
);
fixed_count += 1;
}
}
}
}
if fixed_count > 0 {
println!();
println!(
" {} {} file(s) fixed automatically",
"→".blue().bold(),
fixed_count
);
println!();
}
}
fn print_results(result: &validation::ValidationResult, doc_count: usize) {
let all_issues: Vec<&ValidationIssue> = result
.errors
.iter()
.chain(result.warnings.iter())
.collect();
if all_issues.is_empty() {
println!(
" {} All {} document(s) passed validation",
"✓".green().bold(),
doc_count
);
println!();
return;
}
let mut by_file: BTreeMap<&PathBuf, Vec<&ValidationIssue>> = BTreeMap::new();
for issue in &all_issues {
by_file.entry(&issue.file).or_default().push(issue);
}
for (file, issues) in &by_file {
let filename = file
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("?");
println!(" {}", filename.bold());
for issue in issues {
let severity_label = match issue.severity {
Severity::Error => "error".red().bold(),
Severity::Warning => "warn".yellow().bold(),
};
println!(
" {} [{}] {}",
severity_label, issue.rule, issue.message
);
if let Some(hint) = &issue.fix_hint {
println!(" {} {}", "hint:".dimmed(), hint.dimmed());
}
}
println!();
}
let error_count = result.errors.len();
let warning_count = result.warnings.len();
let summary = format!(
" {} error(s), {} warning(s) in {} document(s)",
error_count, warning_count, doc_count
);
if error_count > 0 {
println!("{}", summary.red().bold());
} else {
println!("{}", summary.yellow());
}
println!();
}
fn exit_with_code(result: &validation::ValidationResult) -> Result<()> {
if result.errors.is_empty() {
Ok(())
} else {
std::process::exit(1);
}
}