tsift_resolution/
blocklist.rs1use std::path::Path;
2
3pub fn relative_path_is_generated_artifact(relative: &str) -> bool {
4 let relative = relative.trim_start_matches("./").replace('\\', "/");
5 relative == ".tsift"
6 || relative.starts_with(".tsift/")
7 || relative.ends_with("/.tsift")
8 || relative.contains("/.tsift/")
9 || relative == ".agent-doc"
10 || relative.starts_with(".agent-doc/")
11 || relative.ends_with("/.agent-doc")
12 || relative.contains("/.agent-doc/")
13 || relative == "target"
14 || relative.starts_with("target/")
15 || relative.contains("/target/")
16}
17
18pub fn path_is_generated_artifact(root: &Path, source_root: &Path, path: &Path) -> bool {
19 let mut relatives = Vec::new();
20 if let Ok(relative) = path.strip_prefix(source_root) {
21 relatives.push(relative.to_string_lossy().replace('\\', "/"));
22 }
23 if let Ok(relative) = path.strip_prefix(root) {
24 relatives.push(relative.to_string_lossy().replace('\\', "/"));
25 }
26 if !path.is_absolute() {
27 relatives.push(path.to_string_lossy().replace('\\', "/"));
28 }
29 relatives
30 .iter()
31 .any(|relative| relative_path_is_generated_artifact(relative))
32}
33
34pub fn index_snapshot_part_is_generated(root: &Path, source_root: &Path, part: &str) -> bool {
35 let Some(rest) = part.strip_prefix("file:") else {
36 return false;
37 };
38 let path = rest.split(':').next().unwrap_or(rest);
39 path_is_generated_artifact(root, source_root, Path::new(path))
40}
41
42pub fn is_planner_config_path(path: &str) -> bool {
43 let normalized = path.replace('\\', "/").to_ascii_lowercase();
44 let file_name = normalized.rsplit('/').next().unwrap_or(normalized.as_str());
45 normalized.starts_with(".github/")
46 || normalized.starts_with(".codex/")
47 || normalized.starts_with(".agent-doc/")
48 || normalized.contains("/.github/")
49 || matches!(
50 file_name,
51 "agents.md"
52 | "claude.md"
53 | "cargo.toml"
54 | "cargo.lock"
55 | "package.json"
56 | "package-lock.json"
57 | "pnpm-lock.yaml"
58 | "yarn.lock"
59 | "makefile"
60 | "justfile"
61 | "dockerfile"
62 | "docker-compose.yml"
63 | "docker-compose.yaml"
64 | "config.toml"
65 | "tsconfig.json"
66 )
67 || file_name.ends_with(".config.js")
68 || file_name.ends_with(".config.ts")
69 || file_name.ends_with(".yml")
70 || file_name.ends_with(".yaml")
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76 use std::path::PathBuf;
77
78 #[test]
79 fn relative_path_is_tsift_dir() {
80 assert!(relative_path_is_generated_artifact(".tsift"));
81 assert!(relative_path_is_generated_artifact(".tsift/graph.db"));
82 assert!(relative_path_is_generated_artifact("foo/.tsift"));
83 assert!(relative_path_is_generated_artifact("foo/.tsift/bar"));
84 }
85
86 #[test]
87 fn relative_path_is_agent_doc_dir() {
88 assert!(relative_path_is_generated_artifact(".agent-doc"));
89 assert!(relative_path_is_generated_artifact(".agent-doc/snapshots"));
90 }
91
92 #[test]
93 fn relative_path_is_target_dir() {
94 assert!(relative_path_is_generated_artifact("target"));
95 assert!(relative_path_is_generated_artifact("target/debug"));
96 assert!(relative_path_is_generated_artifact("foo/target/bar"));
97 }
98
99 #[test]
100 fn relative_path_not_generated() {
101 assert!(!relative_path_is_generated_artifact("src/main.rs"));
102 assert!(!relative_path_is_generated_artifact("lib/index.ts"));
103 }
104
105 #[test]
106 fn relative_path_normalizes_backslashes() {
107 assert!(relative_path_is_generated_artifact("target\\debug"));
108 }
109
110 #[test]
111 fn relative_path_strips_dot_slash() {
112 assert!(relative_path_is_generated_artifact("./.tsift"));
113 }
114
115 #[test]
116 fn path_is_generated_artifact_with_source_root() {
117 let root = PathBuf::from("/project");
118 let source_root = PathBuf::from("/project/src");
119 let path = PathBuf::from("/project/src/.tsift/config");
120 assert!(path_is_generated_artifact(&root, &source_root, &path));
121 }
122
123 #[test]
124 fn path_is_generated_artifact_not_generated() {
125 let root = PathBuf::from("/project");
126 let source_root = PathBuf::from("/project/src");
127 let path = PathBuf::from("/project/src/main.rs");
128 assert!(!path_is_generated_artifact(&root, &source_root, &path));
129 }
130
131 #[test]
132 fn index_snapshot_part_generated() {
133 let root = PathBuf::from("/project");
134 let source_root = PathBuf::from("/project");
135 assert!(index_snapshot_part_is_generated(
136 &root,
137 &source_root,
138 "file:target/debug/out.rs:line"
139 ));
140 }
141
142 #[test]
143 fn index_snapshot_part_not_generated() {
144 let root = PathBuf::from("/project");
145 let source_root = PathBuf::from("/project");
146 assert!(!index_snapshot_part_is_generated(
147 &root,
148 &source_root,
149 "file:src/main.rs:line"
150 ));
151 }
152
153 #[test]
154 fn index_snapshot_part_no_file_prefix() {
155 let root = PathBuf::from("/project");
156 let source_root = PathBuf::from("/project");
157 assert!(!index_snapshot_part_is_generated(
158 &root,
159 &source_root,
160 "scope:main"
161 ));
162 }
163
164 #[test]
165 fn is_planner_config_github() {
166 assert!(is_planner_config_path(".github/workflows/ci.yml"));
167 }
168
169 #[test]
170 fn is_planner_config_agents_md() {
171 assert!(is_planner_config_path("AGENTS.md"));
172 }
173
174 #[test]
175 fn is_planner_config_cargo() {
176 assert!(is_planner_config_path("Cargo.toml"));
177 }
178
179 #[test]
180 fn is_planner_config_yml_extension() {
181 assert!(is_planner_config_path("config/production.yml"));
182 }
183
184 #[test]
185 fn is_planner_config_config_ts() {
186 assert!(is_planner_config_path("vite.config.ts"));
187 }
188
189 #[test]
190 fn is_planner_config_not_config() {
191 assert!(!is_planner_config_path("src/main.rs"));
192 assert!(!is_planner_config_path("lib/index.ts"));
193 }
194
195 #[test]
196 fn is_planner_config_case_insensitive() {
197 assert!(is_planner_config_path(".GitHub/test.yml"));
198 }
199
200 #[test]
201 fn is_planner_config_codex() {
202 assert!(is_planner_config_path(".codex/hooks.json"));
203 }
204}