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 description: "d".into(),
181 checks,
182 }
183 }
184
185 #[test]
186 fn extracts_paths_and_ignores_flags_and_bare_words() {
187 let paths = referenced_paths("python -m pytest -q tests/test_x.py --maxfail=1");
188 assert_eq!(paths, vec!["tests/test_x.py"]);
189 }
190
191 #[test]
192 fn strips_pytest_node_ids_quotes_and_line_suffixes() {
193 assert_eq!(
194 referenced_paths("pytest 'tests/test_x.py::test_a'"),
195 vec!["tests/test_x.py"]
196 );
197 assert_eq!(
198 referenced_paths("pytest \"tests/test_x.py\""),
199 vec!["tests/test_x.py"]
200 );
201 assert_eq!(referenced_paths("vet src/main.rs:42"), vec!["src/main.rs"]);
202 }
203
204 #[test]
205 fn a_bare_source_file_counts_even_without_a_separator() {
206 assert_eq!(referenced_paths("pytest test_x.py"), vec!["test_x.py"]);
207 assert!(referenced_paths("cargo test").is_empty());
210 }
211
212 #[test]
213 fn splits_on_shell_operators_so_chained_commands_are_covered() {
214 let paths = referenced_paths("cargo build && pytest tests/test_x.py | tee out.log");
215 assert!(paths.contains(&"tests/test_x.py".to_string()));
216 }
217
218 #[test]
221 fn a_diff_touching_a_contract_executed_file_is_flagged() {
222 let c = contract(vec![check("run_failing_test", "pytest tests/test_x.py")]);
223 let changed = vec!["tests/test_x.py".to_string(), "src/lib.rs".to_string()];
224
225 let overlap = contract_overlap(&c, &changed);
226 assert_eq!(
227 overlap,
228 vec![CheckOverlap {
229 check: "run_failing_test".into(),
230 paths: vec!["tests/test_x.py".into()],
231 }]
232 );
233 assert!(disclosure(&overlap).unwrap().contains("tests/test_x.py"));
234 }
235
236 #[test]
239 fn a_diff_touching_only_source_is_not_flagged() {
240 let c = contract(vec![check("run_failing_test", "pytest tests/test_x.py")]);
241 let changed = vec!["src/lib.rs".to_string(), "src/main.rs".to_string()];
242 assert!(contract_overlap(&c, &changed).is_empty());
243 assert!(disclosure(&[]).is_none());
244 }
245
246 #[test]
249 fn a_directory_reference_covers_files_under_it() {
250 let c = contract(vec![check("suite", "pytest tests/")]);
251 let changed = vec!["tests/unit/test_x.py".to_string()];
252 assert_eq!(
253 contract_overlap(&c, &changed)[0].paths,
254 vec!["tests/unit/test_x.py"]
255 );
256 }
257
258 #[test]
261 fn a_prefix_that_is_not_a_path_segment_does_not_match() {
262 let c = contract(vec![check("suite", "pytest tests/")]);
263 let changed = vec!["tests_helpers/util.py".to_string()];
264 assert!(contract_overlap(&c, &changed).is_empty());
265 }
266
267 #[test]
268 fn several_checks_each_report_their_own_paths() {
269 let c = contract(vec![
270 check("unit", "pytest tests/test_x.py"),
271 check("lint", "ruff check src/"),
272 check("unrelated", "cargo build"),
273 ]);
274 let changed = vec![
275 "tests/test_x.py".to_string(),
276 "src/app.py".to_string(),
277 "README.md".to_string(),
278 ];
279
280 let overlap = contract_overlap(&c, &changed);
281 assert_eq!(
282 overlap.len(),
283 2,
284 "the `cargo build` check references no path"
285 );
286 assert_eq!(overlap[0].check, "unit");
287 assert_eq!(overlap[0].paths, vec!["tests/test_x.py"]);
288 assert_eq!(overlap[1].check, "lint");
289 assert_eq!(overlap[1].paths, vec!["src/app.py"]);
290 }
291
292 #[test]
293 fn disclosure_reads_as_a_fact_not_an_accusation() {
294 let overlap = vec![CheckOverlap {
295 check: "unit".into(),
296 paths: vec!["tests/test_x.py".into()],
297 }];
298 let text = disclosure(&overlap).unwrap();
299 assert!(text.contains("often legitimate"));
300 assert!(text.contains("not an independent verdict"));
301 }
302}