Skip to main content

instruction_files/
lib.rs

1//! Discovery, auditing, and sync for AGENTS.md/CLAUDE.md instruction files.
2
3mod audit;
4mod discovery;
5pub use agent_runbooks as runbooks;
6#[cfg(feature = "ontology")]
7pub mod ontology;
8#[cfg(feature = "spec-audit")]
9pub mod spec_audit;
10mod types;
11
12pub use audit::{
13    check_actionable, check_context_invariant, check_line_budget, check_staleness,
14    check_tree_paths,
15};
16pub use discovery::{find_instruction_files, find_root};
17#[cfg(feature = "ontology")]
18pub use ontology::check_ontology_terms;
19pub use runbooks::init_runbooks;
20pub use types::{AuditConfig, Issue, is_agent_file};
21
22use agent_kit::audit_common::LINE_BUDGET;
23use anyhow::Result;
24use std::path::Path;
25
26/// Run the full audit with the given configuration.
27///
28/// Returns `Ok(())` on success, calls `std::process::exit(1)` on issues found.
29///
30/// When the `ontology` feature is enabled and `ontology_dir` is provided,
31/// instruction files are also scanned for `[term:Name]` annotations and
32/// each term is verified against the ontology directory.
33pub fn run(
34    config: &AuditConfig,
35    root_override: Option<&Path>,
36    #[cfg(feature = "ontology")] ontology_dir: Option<&Path>,
37) -> Result<()> {
38    println!("Auditing docs...\n");
39
40    let root = match root_override {
41        Some(p) => p.to_path_buf(),
42        None => find_root(config),
43    };
44    let files = find_instruction_files(&root, config);
45    let mut issues: Vec<Issue> = Vec::new();
46
47    for doc in &files {
48        let rel = doc
49            .strip_prefix(&root)
50            .unwrap_or(doc)
51            .to_string_lossy()
52            .to_string();
53        if let Ok(content) = std::fs::read_to_string(doc) {
54            issues.extend(check_tree_paths(&rel, &content, &root));
55            issues.extend(check_actionable(&rel, &content, config));
56            issues.extend(check_context_invariant(&rel, &content, config));
57            #[cfg(feature = "ontology")]
58            if let Some(onto_dir) = ontology_dir {
59                issues.extend(check_ontology_terms(&rel, &content, onto_dir));
60            }
61        }
62    }
63
64    let (budget_issues, counts, total) = check_line_budget(&files, &root, config);
65    issues.extend(budget_issues);
66    issues.extend(check_staleness(&files, &root, config));
67
68    for issue in &issues {
69        let mut loc = format!("  {}", issue.file);
70        if issue.line > 0 {
71            if issue.end_line > issue.line {
72                loc.push_str(&format!(":{}-{}", issue.line, issue.end_line));
73            } else {
74                loc.push_str(&format!(":{}", issue.line));
75            }
76        }
77        let marker = if issue.warning { "\u{26a0}" } else { "\u{2717}" };
78        println!("{:<50} {} {}", loc, marker, issue.message);
79    }
80
81    let mark = if total <= LINE_BUDGET {
82        "\u{2713}"
83    } else {
84        "\u{2717}"
85    };
86    println!(
87        "\nCombined instruction files: {} lines (budget: {}) {}",
88        total, LINE_BUDGET, mark
89    );
90    for (name, n) in &counts {
91        println!("  {}: {}", name, n);
92    }
93
94    let n = issues.len();
95    if n > 0 {
96        println!("\nFound {} issue(s)", n);
97        std::process::exit(1);
98    } else {
99        println!("\nNo issues found \u{2713}");
100    }
101
102    Ok(())
103}