1use aiproof_config::Config;
2use ignore::WalkBuilder;
3use std::path::{Path, PathBuf};
4
5pub struct Discovered {
6 pub path: PathBuf,
7}
8
9pub fn discover(paths: &[PathBuf], config: &Config) -> anyhow::Result<Vec<Discovered>> {
10 let mut out = Vec::new();
11 for root in paths {
12 for result in WalkBuilder::new(root).build() {
13 let entry = result?;
14 if !entry.file_type().is_some_and(|t| t.is_file()) {
15 continue;
16 }
17 let path = entry.into_path();
18 if !should_scan(&path, config) {
19 continue;
20 }
21 out.push(Discovered { path });
22 }
23 }
24 Ok(out)
25}
26
27fn should_scan(path: &Path, config: &Config) -> bool {
28 if !config.include.is_empty() {
30 let s = path.to_string_lossy();
31 if !config.include.iter().any(|pat| glob_match(pat, &s)) {
32 return false;
33 }
34 }
35 let s = path.to_string_lossy();
37 if config.exclude.iter().any(|pat| glob_match(pat, &s)) {
38 return false;
39 }
40
41 if is_known_prompt_file(path) {
43 return true;
44 }
45
46 if matches!(
48 path.extension().and_then(|e| e.to_str()),
49 Some("py" | "ts" | "tsx")
50 ) {
51 return true;
52 }
53
54 false
57}
58
59fn is_known_prompt_file(path: &Path) -> bool {
60 let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
61 if name.ends_with(".prompt.md") || name.ends_with(".prompt") {
63 return true;
64 }
65 if matches!(
67 path.extension().and_then(|e| e.to_str()),
68 Some("j2" | "jinja" | "jinja2" | "mustache")
69 ) {
70 return true;
71 }
72 if matches!(
74 path.extension().and_then(|e| e.to_str()),
75 Some("md" | "yaml" | "yml")
76 ) {
77 return true;
78 }
79 let components: Vec<_> = path
81 .components()
82 .filter_map(|c| c.as_os_str().to_str())
83 .collect();
84 components
85 .iter()
86 .any(|c| matches!(*c, "prompts" | "templates" | "system_prompts"))
87}
88
89fn glob_match(pattern: &str, path: &str) -> bool {
90 glob::Pattern::new(pattern)
91 .map(|p| p.matches(path))
92 .unwrap_or(false)
93}