car_server_core/coder/
overlap.rs1use serde::{Deserialize, Serialize};
35
36use super::contract::{ContractCheck, OutcomeContract};
37
38#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
40pub struct CheckOverlap {
41 pub check: String,
43 pub paths: Vec<String>,
45}
46
47const PATHY_EXTENSIONS: [&str; 12] = [
50 ".py", ".rs", ".ts", ".tsx", ".js", ".jsx", ".go", ".java", ".rb", ".swift", ".kt", ".c",
51];
52
53fn normalize_token(raw: &str) -> Option<String> {
57 let mut t = raw.trim();
58 t = t.trim_matches(|c| c == '"' || c == '\'');
61 if let Some((head, _)) = t.split_once("::") {
63 t = head;
64 }
65 if let Some((head, tail)) = t.rsplit_once(':') {
68 if !tail.is_empty() && tail.chars().all(|c| c.is_ascii_digit()) {
69 t = head;
70 }
71 }
72 t = t.trim_start_matches("./");
73 let lower = t.to_ascii_lowercase();
79 let pathy = t.contains('/') || PATHY_EXTENSIONS.iter().any(|e| lower.ends_with(e));
80 t = t.trim_end_matches('/');
81 if t.is_empty() || t.starts_with('-') {
82 return None;
83 }
84 pathy.then(|| t.to_string())
85}
86
87pub fn referenced_paths(command: &str) -> Vec<String> {
93 let mut out: Vec<String> = command
94 .split(|c: char| c.is_whitespace() || matches!(c, '|' | ';' | '(' | ')' | '&'))
95 .filter_map(normalize_token)
96 .collect();
97 out.sort();
98 out.dedup();
99 out
100}
101
102fn covers(referenced: &str, changed: &str) -> bool {
108 changed == referenced || changed.starts_with(&format!("{referenced}/"))
109}
110
111pub fn contract_overlap(contract: &OutcomeContract, changed_paths: &[String]) -> Vec<CheckOverlap> {
116 contract
117 .checks
118 .iter()
119 .filter_map(|check| {
120 let overlap = overlapping_paths(check, changed_paths);
121 (!overlap.is_empty()).then(|| CheckOverlap {
122 check: check.name.clone(),
123 paths: overlap,
124 })
125 })
126 .collect()
127}
128
129fn overlapping_paths(check: &ContractCheck, changed_paths: &[String]) -> Vec<String> {
130 let referenced = referenced_paths(&check.command);
131 let mut hits: Vec<String> = changed_paths
132 .iter()
133 .filter(|changed| referenced.iter().any(|r| covers(r, changed)))
134 .cloned()
135 .collect();
136 hits.sort();
137 hits.dedup();
138 hits
139}
140
141pub fn disclosure(overlap: &[CheckOverlap]) -> Option<String> {
147 if overlap.is_empty() {
148 return None;
149 }
150 let parts: Vec<String> = overlap
151 .iter()
152 .map(|o| format!("{} (run by check `{}`)", o.paths.join(", "), o.check))
153 .collect();
154 Some(format!(
155 "This session modified files that its own contract executes: {}. \
156 That is often legitimate — editing tests is frequently the task — but it \
157 means those checks are not an independent verdict on this change.",
158 parts.join("; ")
159 ))
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 fn check(name: &str, command: &str) -> ContractCheck {
167 ContractCheck {
168 name: name.into(),
169 command: command.into(),
170 expect_exit_zero: true,
171 output_contains: None,
172 timeout_secs: 120,
173 baseline: false,
174 differential: None,
175 }
176 }
177
178 fn contract(checks: Vec<ContractCheck>) -> OutcomeContract {
179 OutcomeContract {
180 allow_credentials: false,
181 description: "d".into(),
182 checks,
183 }
184 }
185
186 #[test]
187 fn extracts_paths_and_ignores_flags_and_bare_words() {
188 let paths = referenced_paths("python -m pytest -q tests/test_x.py --maxfail=1");
189 assert_eq!(paths, vec!["tests/test_x.py"]);
190 }
191
192 #[test]
193 fn strips_pytest_node_ids_quotes_and_line_suffixes() {
194 assert_eq!(
195 referenced_paths("pytest 'tests/test_x.py::test_a'"),
196 vec!["tests/test_x.py"]
197 );
198 assert_eq!(
199 referenced_paths("pytest \"tests/test_x.py\""),
200 vec!["tests/test_x.py"]
201 );
202 assert_eq!(referenced_paths("vet src/main.rs:42"), vec!["src/main.rs"]);
203 }
204
205 #[test]
206 fn a_bare_source_file_counts_even_without_a_separator() {
207 assert_eq!(referenced_paths("pytest test_x.py"), vec!["test_x.py"]);
208 assert!(referenced_paths("cargo test").is_empty());
211 }
212
213 #[test]
214 fn splits_on_shell_operators_so_chained_commands_are_covered() {
215 let paths = referenced_paths("cargo build && pytest tests/test_x.py | tee out.log");
216 assert!(paths.contains(&"tests/test_x.py".to_string()));
217 }
218
219 #[test]
222 fn a_diff_touching_a_contract_executed_file_is_flagged() {
223 let c = contract(vec![check("run_failing_test", "pytest tests/test_x.py")]);
224 let changed = vec!["tests/test_x.py".to_string(), "src/lib.rs".to_string()];
225
226 let overlap = contract_overlap(&c, &changed);
227 assert_eq!(
228 overlap,
229 vec![CheckOverlap {
230 check: "run_failing_test".into(),
231 paths: vec!["tests/test_x.py".into()],
232 }]
233 );
234 assert!(disclosure(&overlap).unwrap().contains("tests/test_x.py"));
235 }
236
237 #[test]
240 fn a_diff_touching_only_source_is_not_flagged() {
241 let c = contract(vec![check("run_failing_test", "pytest tests/test_x.py")]);
242 let changed = vec!["src/lib.rs".to_string(), "src/main.rs".to_string()];
243 assert!(contract_overlap(&c, &changed).is_empty());
244 assert!(disclosure(&[]).is_none());
245 }
246
247 #[test]
250 fn a_directory_reference_covers_files_under_it() {
251 let c = contract(vec![check("suite", "pytest tests/")]);
252 let changed = vec!["tests/unit/test_x.py".to_string()];
253 assert_eq!(
254 contract_overlap(&c, &changed)[0].paths,
255 vec!["tests/unit/test_x.py"]
256 );
257 }
258
259 #[test]
262 fn a_prefix_that_is_not_a_path_segment_does_not_match() {
263 let c = contract(vec![check("suite", "pytest tests/")]);
264 let changed = vec!["tests_helpers/util.py".to_string()];
265 assert!(contract_overlap(&c, &changed).is_empty());
266 }
267
268 #[test]
269 fn several_checks_each_report_their_own_paths() {
270 let c = contract(vec![
271 check("unit", "pytest tests/test_x.py"),
272 check("lint", "ruff check src/"),
273 check("unrelated", "cargo build"),
274 ]);
275 let changed = vec![
276 "tests/test_x.py".to_string(),
277 "src/app.py".to_string(),
278 "README.md".to_string(),
279 ];
280
281 let overlap = contract_overlap(&c, &changed);
282 assert_eq!(
283 overlap.len(),
284 2,
285 "the `cargo build` check references no path"
286 );
287 assert_eq!(overlap[0].check, "unit");
288 assert_eq!(overlap[0].paths, vec!["tests/test_x.py"]);
289 assert_eq!(overlap[1].check, "lint");
290 assert_eq!(overlap[1].paths, vec!["src/app.py"]);
291 }
292
293 #[test]
294 fn disclosure_reads_as_a_fact_not_an_accusation() {
295 let overlap = vec![CheckOverlap {
296 check: "unit".into(),
297 paths: vec!["tests/test_x.py".into()],
298 }];
299 let text = disclosure(&overlap).unwrap();
300 assert!(text.contains("often legitimate"));
301 assert!(text.contains("not an independent verdict"));
302 }
303}