use anyhow::Result;
use colored::*;
use design::doc::state_from_directory;
use design::state::StateManager;
use std::path::PathBuf;
pub fn scan_documents(state_mgr: &mut StateManager, fix: bool, verbose: bool) -> Result<()> {
println!("\n{}\n", "Scanning documents...".bold());
let result = state_mgr.scan_for_changes()?;
if result.has_changes() {
if !result.new_files.is_empty() {
println!("{}", "New Files:".green().bold());
for num in &result.new_files {
if let Some(record) = state_mgr.state().get(*num) {
println!(" {} {:04} - {}", "+".green(), num, record.metadata.title);
}
}
println!();
}
if !result.changed.is_empty() {
println!("{}", "Modified Files:".yellow().bold());
for num in &result.changed {
if let Some(record) = state_mgr.state().get(*num) {
println!(" {} {:04} - {}", "~".yellow(), num, record.metadata.title);
}
}
println!();
}
if !result.deleted.is_empty() {
println!("{}", "Deleted Files:".red().bold());
for num in &result.deleted {
println!(" {} {:04}", "-".red(), num);
}
println!();
}
} else {
println!("{} No changes detected\n", "✓".green().bold());
}
if !result.errors.is_empty() {
println!("{}", "Errors:".red().bold());
for error in &result.errors {
println!(" {} {}", "✗".red(), error);
}
println!();
}
if verbose {
validate_consistency(state_mgr, fix)?;
}
println!(
"{} State updated: {} documents tracked\n",
"✓".green().bold(),
state_mgr.state().documents.len()
);
Ok(())
}
fn validate_consistency(state_mgr: &StateManager, fix: bool) -> Result<()> {
println!("{}", "Validating Consistency:".bold());
let mut inconsistencies = 0;
let mut fixable = Vec::new();
for record in state_mgr.state().all() {
let full_path = PathBuf::from(state_mgr.docs_dir()).join(&record.path);
if !full_path.exists() {
println!(
" {} {:04} - File not found: {}",
"✗".red(),
record.metadata.number,
record.path
);
inconsistencies += 1;
continue;
}
if let Some(dir_state) = state_from_directory(&full_path) {
if record.metadata.state != dir_state {
println!(
" {} {:04} - State mismatch: YAML='{}' Directory='{}'",
"âš ".yellow(),
record.metadata.number,
record.metadata.state.as_str(),
dir_state.as_str()
);
inconsistencies += 1;
fixable.push((record.metadata.number, full_path.clone()));
}
}
}
if inconsistencies == 0 {
println!(" {} All documents consistent", "✓".green());
} else {
println!(" {} {} inconsistencies found", "âš ".yellow(), inconsistencies);
if fix && !fixable.is_empty() {
println!("\n{}", "Fixing inconsistencies...".bold());
for (num, path) in &fixable {
println!(" Syncing {:04}: {}", num, path.display());
}
} else if !fixable.is_empty() {
println!(
"\n{} Run with {} to fix {} issue(s)",
"→".cyan(),
"--fix".cyan(),
fixable.len()
);
}
}
println!();
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use design::doc::DocState;
use std::fs;
use tempfile::TempDir;
fn create_test_doc_content(number: u32, title: &str, state: DocState) -> String {
format!(
"---\nnumber: {}\ntitle: \"{}\"\nauthor: \"Test Author\"\ncreated: 2024-01-01\nupdated: 2024-01-01\nstate: {}\n---\n\n# {}\n\nTest content",
number, title, state.as_str(), title
)
}
#[test]
fn test_scan_no_changes() {
let temp = TempDir::new().unwrap();
let content = create_test_doc_content(1, "Test Doc", DocState::Draft);
let doc_path = temp.path().join("0001-test.md");
fs::write(&doc_path, content).unwrap();
let mut state_mgr = StateManager::new(temp.path()).unwrap();
state_mgr.scan_for_changes().unwrap();
let result = scan_documents(&mut state_mgr, false, false);
assert!(result.is_ok());
}
#[test]
fn test_scan_new_file() {
let temp = TempDir::new().unwrap();
let mut state_mgr = StateManager::new(temp.path()).unwrap();
let content = create_test_doc_content(1, "New Doc", DocState::Draft);
fs::write(temp.path().join("0001-new.md"), content).unwrap();
let result = scan_documents(&mut state_mgr, false, false);
assert!(result.is_ok());
}
#[test]
fn test_scan_with_verbose() {
let temp = TempDir::new().unwrap();
let content = create_test_doc_content(1, "Test Doc", DocState::Draft);
fs::write(temp.path().join("0001-test.md"), content).unwrap();
let mut state_mgr = StateManager::new(temp.path()).unwrap();
let result = scan_documents(&mut state_mgr, false, true);
assert!(result.is_ok());
}
#[test]
fn test_scan_with_fix() {
let temp = TempDir::new().unwrap();
let content = create_test_doc_content(1, "Test Doc", DocState::Draft);
fs::write(temp.path().join("0001-test.md"), content).unwrap();
let mut state_mgr = StateManager::new(temp.path()).unwrap();
let result = scan_documents(&mut state_mgr, true, false);
assert!(result.is_ok());
}
#[test]
fn test_scan_empty_directory() {
let temp = TempDir::new().unwrap();
let mut state_mgr = StateManager::new(temp.path()).unwrap();
let result = scan_documents(&mut state_mgr, false, false);
assert!(result.is_ok());
}
#[test]
fn test_scan_multiple_states() {
let temp = TempDir::new().unwrap();
for (num, state) in [(1, DocState::Draft), (2, DocState::Final), (3, DocState::Active)] {
let content = create_test_doc_content(num, &format!("Doc {}", num), state);
fs::write(temp.path().join(format!("{:04}-test.md", num)), content).unwrap();
}
let mut state_mgr = StateManager::new(temp.path()).unwrap();
let result = scan_documents(&mut state_mgr, false, false);
assert!(result.is_ok());
}
#[test]
fn test_validate_consistency() {
let temp = TempDir::new().unwrap();
let content = create_test_doc_content(1, "Test Doc", DocState::Draft);
fs::write(temp.path().join("0001-test.md"), content).unwrap();
let state_mgr = StateManager::new(temp.path()).unwrap();
let result = validate_consistency(&state_mgr, false);
assert!(result.is_ok());
}
#[test]
fn test_validate_consistency_with_fix() {
let temp = TempDir::new().unwrap();
let content = create_test_doc_content(1, "Test Doc", DocState::Draft);
fs::write(temp.path().join("0001-test.md"), content).unwrap();
let state_mgr = StateManager::new(temp.path()).unwrap();
let result = validate_consistency(&state_mgr, true);
assert!(result.is_ok());
}
#[test]
fn test_scan_verbose_and_fix() {
let temp = TempDir::new().unwrap();
let content = create_test_doc_content(1, "Test Doc", DocState::Draft);
fs::write(temp.path().join("0001-test.md"), content).unwrap();
let mut state_mgr = StateManager::new(temp.path()).unwrap();
let result = scan_documents(&mut state_mgr, true, true);
assert!(result.is_ok());
}
#[test]
fn test_scan_with_new_files_display() {
let temp = TempDir::new().unwrap();
let draft_dir = temp.path().join("01-draft");
fs::create_dir_all(&draft_dir).unwrap();
let mut state_mgr = StateManager::new(temp.path()).unwrap();
for i in 1..=3 {
let content = create_test_doc_content(i, &format!("New Doc {}", i), DocState::Draft);
fs::write(draft_dir.join(format!("{:04}-new.md", i)), content).unwrap();
}
let result = scan_documents(&mut state_mgr, false, false);
assert!(result.is_ok());
assert_eq!(state_mgr.state().documents.len(), 3);
}
#[test]
fn test_scan_with_changed_files_display() {
let temp = TempDir::new().unwrap();
let draft_dir = temp.path().join("01-draft");
fs::create_dir_all(&draft_dir).unwrap();
let content = create_test_doc_content(1, "Original Doc", DocState::Draft);
let doc_path = draft_dir.join("0001-original.md");
fs::write(&doc_path, &content).unwrap();
let mut state_mgr = StateManager::new(temp.path()).unwrap();
state_mgr.scan_for_changes().unwrap();
let modified_content = content + "\n\nAdditional content added";
fs::write(&doc_path, modified_content).unwrap();
let result = scan_documents(&mut state_mgr, false, false);
assert!(result.is_ok());
}
#[test]
fn test_scan_with_deleted_files_display() {
let temp = TempDir::new().unwrap();
let draft_dir = temp.path().join("01-draft");
fs::create_dir_all(&draft_dir).unwrap();
for i in 1..=3 {
let content = create_test_doc_content(i, &format!("Doc {}", i), DocState::Draft);
fs::write(draft_dir.join(format!("{:04}-doc.md", i)), content).unwrap();
}
let mut state_mgr = StateManager::new(temp.path()).unwrap();
state_mgr.scan_for_changes().unwrap();
fs::remove_file(draft_dir.join("0001-doc.md")).unwrap();
fs::remove_file(draft_dir.join("0002-doc.md")).unwrap();
let result = scan_documents(&mut state_mgr, false, false);
assert!(result.is_ok());
assert_eq!(state_mgr.state().documents.len(), 1);
}
#[test]
fn test_scan_with_errors_display() {
let temp = TempDir::new().unwrap();
let draft_dir = temp.path().join("01-draft");
fs::create_dir_all(&draft_dir).unwrap();
let valid_content = create_test_doc_content(1, "Valid Doc", DocState::Draft);
fs::write(draft_dir.join("0001-valid.md"), valid_content).unwrap();
fs::write(draft_dir.join("0002-invalid.md"), "Just plain text without frontmatter")
.unwrap();
let mut state_mgr = StateManager::new(temp.path()).unwrap();
let result = scan_documents(&mut state_mgr, false, false);
assert!(result.is_ok());
assert_eq!(state_mgr.state().documents.len(), 1);
}
#[test]
fn test_scan_with_mixed_changes() {
let temp = TempDir::new().unwrap();
let draft_dir = temp.path().join("01-draft");
fs::create_dir_all(&draft_dir).unwrap();
let content1 = create_test_doc_content(1, "Existing Doc", DocState::Draft);
let doc1_path = draft_dir.join("0001-existing.md");
fs::write(&doc1_path, &content1).unwrap();
let mut state_mgr = StateManager::new(temp.path()).unwrap();
state_mgr.scan_for_changes().unwrap();
let content2 = create_test_doc_content(2, "New Doc", DocState::Draft);
fs::write(draft_dir.join("0002-new.md"), content2).unwrap();
let modified_content = content1 + "\n\nModified content";
fs::write(&doc1_path, modified_content).unwrap();
fs::write(draft_dir.join("0003-invalid.md"), "Invalid content").unwrap();
let result = scan_documents(&mut state_mgr, false, false);
assert!(result.is_ok());
}
#[test]
fn test_validate_consistency_missing_file() {
let temp = TempDir::new().unwrap();
let draft_dir = temp.path().join("01-draft");
fs::create_dir_all(&draft_dir).unwrap();
let content = create_test_doc_content(1, "Test Doc", DocState::Draft);
let doc_path = draft_dir.join("0001-test.md");
fs::write(&doc_path, content).unwrap();
let mut state_mgr = StateManager::new(temp.path()).unwrap();
state_mgr.scan_for_changes().unwrap();
fs::remove_file(&doc_path).unwrap();
let result = validate_consistency(&state_mgr, false);
assert!(result.is_ok());
}
#[test]
fn test_validate_consistency_state_mismatch() {
let temp = TempDir::new().unwrap();
let draft_dir = temp.path().join("01-draft");
let final_dir = temp.path().join("06-final");
fs::create_dir_all(&draft_dir).unwrap();
fs::create_dir_all(&final_dir).unwrap();
let content = create_test_doc_content(1, "Test Doc", DocState::Final);
fs::write(draft_dir.join("0001-test.md"), content).unwrap();
let mut state_mgr = StateManager::new(temp.path()).unwrap();
state_mgr.scan_for_changes().unwrap();
let result = validate_consistency(&state_mgr, false);
assert!(result.is_ok());
}
#[test]
fn test_validate_consistency_with_fix_mode() {
let temp = TempDir::new().unwrap();
let draft_dir = temp.path().join("01-draft");
let final_dir = temp.path().join("06-final");
fs::create_dir_all(&draft_dir).unwrap();
fs::create_dir_all(&final_dir).unwrap();
let content = create_test_doc_content(1, "Test Doc", DocState::Final);
fs::write(draft_dir.join("0001-test.md"), content).unwrap();
let mut state_mgr = StateManager::new(temp.path()).unwrap();
state_mgr.scan_for_changes().unwrap();
let result = validate_consistency(&state_mgr, true);
assert!(result.is_ok());
}
#[test]
fn test_validate_consistency_no_fix_suggestion() {
let temp = TempDir::new().unwrap();
let draft_dir = temp.path().join("01-draft");
fs::create_dir_all(&draft_dir).unwrap();
let content = create_test_doc_content(1, "Test Doc", DocState::Final);
fs::write(draft_dir.join("0001-test.md"), content).unwrap();
let mut state_mgr = StateManager::new(temp.path()).unwrap();
state_mgr.scan_for_changes().unwrap();
let result = validate_consistency(&state_mgr, false);
assert!(result.is_ok());
}
#[test]
fn test_validate_consistency_all_consistent() {
let temp = TempDir::new().unwrap();
let draft_dir = temp.path().join("01-draft");
fs::create_dir_all(&draft_dir).unwrap();
let content = create_test_doc_content(1, "Test Doc", DocState::Draft);
fs::write(draft_dir.join("0001-test.md"), content).unwrap();
let mut state_mgr = StateManager::new(temp.path()).unwrap();
state_mgr.scan_for_changes().unwrap();
let result = validate_consistency(&state_mgr, false);
assert!(result.is_ok());
}
#[test]
fn test_validate_consistency_multiple_mismatches() {
let temp = TempDir::new().unwrap();
let draft_dir = temp.path().join("01-draft");
let active_dir = temp.path().join("05-active");
fs::create_dir_all(&draft_dir).unwrap();
fs::create_dir_all(&active_dir).unwrap();
let content1 = create_test_doc_content(1, "Doc 1", DocState::Active);
fs::write(draft_dir.join("0001-doc1.md"), content1).unwrap();
let content2 = create_test_doc_content(2, "Doc 2", DocState::Draft);
fs::write(active_dir.join("0002-doc2.md"), content2).unwrap();
let mut state_mgr = StateManager::new(temp.path()).unwrap();
state_mgr.scan_for_changes().unwrap();
let result = validate_consistency(&state_mgr, false);
assert!(result.is_ok());
}
#[test]
fn test_validate_consistency_with_missing_and_mismatch() {
let temp = TempDir::new().unwrap();
let draft_dir = temp.path().join("01-draft");
fs::create_dir_all(&draft_dir).unwrap();
let content1 = create_test_doc_content(1, "Missing Doc", DocState::Draft);
let doc1_path = draft_dir.join("0001-missing.md");
fs::write(&doc1_path, content1).unwrap();
let content2 = create_test_doc_content(2, "Mismatch Doc", DocState::Final);
fs::write(draft_dir.join("0002-mismatch.md"), content2).unwrap();
let mut state_mgr = StateManager::new(temp.path()).unwrap();
state_mgr.scan_for_changes().unwrap();
fs::remove_file(&doc1_path).unwrap();
let result = validate_consistency(&state_mgr, false);
assert!(result.is_ok());
}
#[test]
fn test_scan_verbose_shows_validation() {
let temp = TempDir::new().unwrap();
let draft_dir = temp.path().join("01-draft");
fs::create_dir_all(&draft_dir).unwrap();
let content = create_test_doc_content(1, "Test Doc", DocState::Final);
fs::write(draft_dir.join("0001-test.md"), content).unwrap();
let mut state_mgr = StateManager::new(temp.path()).unwrap();
let result = scan_documents(&mut state_mgr, false, true);
assert!(result.is_ok());
}
#[test]
fn test_scan_quiet_skips_validation() {
let temp = TempDir::new().unwrap();
let draft_dir = temp.path().join("01-draft");
fs::create_dir_all(&draft_dir).unwrap();
let content = create_test_doc_content(1, "Test Doc", DocState::Final);
fs::write(draft_dir.join("0001-test.md"), content).unwrap();
let mut state_mgr = StateManager::new(temp.path()).unwrap();
let result = scan_documents(&mut state_mgr, false, false);
assert!(result.is_ok());
}
#[test]
fn test_validate_consistency_file_without_directory_state() {
let temp = TempDir::new().unwrap();
let draft_dir = temp.path().join("01-draft");
fs::create_dir_all(&draft_dir).unwrap();
let content = create_test_doc_content(1, "Test Doc", DocState::Draft);
fs::write(draft_dir.join("0001-test.md"), content).unwrap();
let mut state_mgr = StateManager::new(temp.path()).unwrap();
state_mgr.scan_for_changes().unwrap();
let result = validate_consistency(&state_mgr, false);
assert!(result.is_ok());
}
#[test]
fn test_validate_consistency_with_fix_and_no_fixable() {
let temp = TempDir::new().unwrap();
let draft_dir = temp.path().join("01-draft");
fs::create_dir_all(&draft_dir).unwrap();
let content = create_test_doc_content(1, "Test Doc", DocState::Draft);
let doc_path = draft_dir.join("0001-test.md");
fs::write(&doc_path, content).unwrap();
let mut state_mgr = StateManager::new(temp.path()).unwrap();
state_mgr.scan_for_changes().unwrap();
fs::remove_file(&doc_path).unwrap();
let result = validate_consistency(&state_mgr, true);
assert!(result.is_ok());
}
#[test]
fn test_scan_comprehensive_workflow() {
let temp = TempDir::new().unwrap();
let draft_dir = temp.path().join("01-draft");
let final_dir = temp.path().join("06-final");
fs::create_dir_all(&draft_dir).unwrap();
fs::create_dir_all(&final_dir).unwrap();
let mut state_mgr = StateManager::new(temp.path()).unwrap();
let result = scan_documents(&mut state_mgr, false, false);
assert!(result.is_ok());
let content1 = create_test_doc_content(1, "Doc 1", DocState::Draft);
fs::write(draft_dir.join("0001-doc1.md"), content1).unwrap();
let result = scan_documents(&mut state_mgr, false, false);
assert!(result.is_ok());
assert_eq!(state_mgr.state().documents.len(), 1);
let result = scan_documents(&mut state_mgr, false, true);
assert!(result.is_ok());
let content2 = create_test_doc_content(2, "Doc 2", DocState::Final);
fs::write(draft_dir.join("0002-doc2.md"), content2).unwrap();
let result = scan_documents(&mut state_mgr, true, true);
assert!(result.is_ok());
}
#[test]
fn test_validate_consistency_empty_state() {
let temp = TempDir::new().unwrap();
let state_mgr = StateManager::new(temp.path()).unwrap();
let result = validate_consistency(&state_mgr, false);
assert!(result.is_ok());
}
#[test]
fn test_scan_all_state_directories() {
let temp = TempDir::new().unwrap();
let states = vec![
(DocState::Draft, "01-draft"),
(DocState::UnderReview, "02-under-review"),
(DocState::Revised, "03-revised"),
(DocState::Accepted, "04-accepted"),
(DocState::Active, "05-active"),
(DocState::Final, "06-final"),
(DocState::Deferred, "07-deferred"),
(DocState::Rejected, "08-rejected"),
(DocState::Withdrawn, "09-withdrawn"),
(DocState::Superseded, "10-superseded"),
];
for (i, (state, dir)) in states.iter().enumerate() {
let state_dir = temp.path().join(dir);
fs::create_dir_all(&state_dir).unwrap();
let num = (i + 1) as u32;
let content = create_test_doc_content(num, &format!("Doc {}", num), *state);
fs::write(state_dir.join(format!("{:04}-doc.md", num)), content).unwrap();
}
let mut state_mgr = StateManager::new(temp.path()).unwrap();
let result = scan_documents(&mut state_mgr, false, false);
assert!(result.is_ok());
assert_eq!(state_mgr.state().documents.len(), 10);
let result = scan_documents(&mut state_mgr, false, true);
assert!(result.is_ok());
}
}