Skip to main content

instruction_files/
types.rs

1//! Core types for instruction file auditing.
2
3use std::path::Path;
4
5/// Configuration for instruction file discovery and auditing.
6///
7/// Different projects can customize behavior by providing different configs.
8#[derive(Debug, Clone)]
9pub struct AuditConfig {
10    /// Project root marker files, checked in order.
11    /// agent-doc uses many (Cargo.toml, package.json, etc.); corky uses only Cargo.toml.
12    pub root_markers: Vec<&'static str>,
13
14    /// Whether to include CLAUDE.md in root-level discovery and agent file checks.
15    /// agent-doc: true, corky: false.
16    pub include_claude_md: bool,
17
18    /// Source file extensions to check for staleness comparison.
19    /// agent-doc: broad (rs, ts, py, etc.); corky: just "rs".
20    pub source_extensions: Vec<&'static str>,
21
22    /// Source directories to scan for staleness.
23    /// agent-doc: ["src", "lib", "app", ...]; corky: just ["src"].
24    pub source_dirs: Vec<&'static str>,
25
26    /// Directories to skip when scanning for source files.
27    pub skip_dirs: Vec<&'static str>,
28}
29
30impl AuditConfig {
31    /// Config matching agent-doc's current behavior: broad project detection,
32    /// includes CLAUDE.md, scans many source extensions.
33    pub fn agent_doc() -> Self {
34        Self {
35            root_markers: vec![
36                "Cargo.toml",
37                "package.json",
38                "pyproject.toml",
39                "setup.py",
40                "go.mod",
41                "Gemfile",
42                "pom.xml",
43                "build.gradle",
44                "CMakeLists.txt",
45                "Makefile",
46                "flake.nix",
47                "deno.json",
48                "composer.json",
49            ],
50            include_claude_md: true,
51            source_extensions: vec![
52                "rs", "ts", "tsx", "js", "jsx", "py", "go", "rb", "java", "kt", "c", "cpp", "h",
53                "hpp", "cs", "swift", "zig", "hs", "ml", "ex", "exs", "clj", "scala", "lua",
54                "php", "sh", "bash", "zsh",
55            ],
56            source_dirs: vec!["src", "lib", "app", "pkg", "cmd", "internal"],
57            skip_dirs: vec![
58                "node_modules",
59                "target",
60                "build",
61                "dist",
62                ".git",
63                "__pycache__",
64                ".venv",
65                "vendor",
66                ".next",
67                "out",
68            ],
69        }
70    }
71
72    /// Config matching corky's current behavior: Cargo.toml-only root detection,
73    /// excludes CLAUDE.md from audit, scans only .rs files.
74    pub fn corky() -> Self {
75        Self {
76            root_markers: vec!["Cargo.toml"],
77            include_claude_md: false,
78            source_extensions: vec!["rs"],
79            source_dirs: vec!["src"],
80            skip_dirs: vec!["target", ".git"],
81        }
82    }
83}
84
85/// An issue found during auditing.
86pub struct Issue {
87    pub file: String,
88    pub line: usize,
89    pub end_line: usize,
90    pub message: String,
91    pub warning: bool,
92}
93
94/// Check if a file path refers to an agent instruction file.
95pub fn is_agent_file(rel: &str, config: &AuditConfig) -> bool {
96    let name = Path::new(rel)
97        .file_name()
98        .and_then(|n| n.to_str())
99        .unwrap_or("");
100    if name == "AGENTS.md" || name == "SKILL.md" {
101        return true;
102    }
103    if config.include_claude_md && name == "CLAUDE.md" {
104        return true;
105    }
106    false
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn is_agent_file_with_claude() {
115        let config = AuditConfig::agent_doc();
116        assert!(is_agent_file("AGENTS.md", &config));
117        assert!(is_agent_file("SKILL.md", &config));
118        assert!(is_agent_file("CLAUDE.md", &config));
119        assert!(is_agent_file("src/AGENTS.md", &config));
120        assert!(is_agent_file(".claude/skills/email/SKILL.md", &config));
121        assert!(is_agent_file("nested/path/CLAUDE.md", &config));
122    }
123
124    #[test]
125    fn is_agent_file_without_claude() {
126        let config = AuditConfig::corky();
127        assert!(is_agent_file("AGENTS.md", &config));
128        assert!(is_agent_file("SKILL.md", &config));
129        assert!(!is_agent_file("CLAUDE.md", &config));
130    }
131
132    #[test]
133    fn is_agent_file_rejects() {
134        let config = AuditConfig::agent_doc();
135        assert!(!is_agent_file("README.md", &config));
136        assert!(!is_agent_file("agents.md", &config));
137        assert!(!is_agent_file("CHANGELOG.md", &config));
138        assert!(!is_agent_file("src/main.rs", &config));
139    }
140}