eval_magic/pipeline/grade/
diff_scope.rs1use crate::core::{AssertionDiffScope, AssertionResult, Grader};
4use crate::pipeline::DiffScopeMetrics;
5
6pub fn grade_diff_scope(
8 assertion: &AssertionDiffScope,
9 metrics: DiffScopeMetrics,
10) -> AssertionResult {
11 let files_passed = assertion
12 .max_files_touched
13 .is_none_or(|limit| metrics.files_touched <= limit);
14 let lines_changed = metrics.lines_changed();
15 let lines_passed = assertion
16 .max_lines_changed
17 .is_none_or(|limit| lines_changed <= limit);
18
19 let files_evidence = assertion.max_files_touched.map_or_else(
20 || format!("files touched: {} (no limit)", metrics.files_touched),
21 |limit| {
22 let comparison = if metrics.files_touched <= limit {
23 "<="
24 } else {
25 ">"
26 };
27 format!(
28 "files touched: {} {comparison} {limit}",
29 metrics.files_touched
30 )
31 },
32 );
33 let lines_evidence = assertion.max_lines_changed.map_or_else(
34 || {
35 format!(
36 "lines changed: {lines_changed} ({} added + {} removed; no limit)",
37 metrics.lines_added, metrics.lines_removed
38 )
39 },
40 |limit| {
41 let comparison = if lines_changed <= limit { "<=" } else { ">" };
42 format!(
43 "lines changed: {lines_changed} {comparison} {limit} ({} added + {} removed)",
44 metrics.lines_added, metrics.lines_removed
45 )
46 },
47 );
48
49 AssertionResult {
50 id: assertion.id.clone(),
51 passed: files_passed && lines_passed,
52 evidence: format!(
53 "{files_evidence}; {lines_evidence}; hunks: {}",
54 metrics.hunks
55 ),
56 confidence: Some(1.0),
57 grader: Some(Grader::DiffScope),
58 }
59}
60
61#[cfg(test)]
62mod tests {
63 use super::*;
64
65 fn assertion(files: Option<u64>, lines: Option<u64>) -> AssertionDiffScope {
66 AssertionDiffScope {
67 id: "focused-change".to_string(),
68 max_files_touched: files,
69 max_lines_changed: lines,
70 }
71 }
72
73 #[test]
74 fn passes_when_all_configured_thresholds_are_met() {
75 let result = grade_diff_scope(
76 &assertion(Some(2), Some(5)),
77 DiffScopeMetrics {
78 files_touched: 2,
79 lines_added: 3,
80 lines_removed: 2,
81 hunks: 2,
82 },
83 );
84
85 assert!(result.passed);
86 assert_eq!(result.grader, Some(Grader::DiffScope));
87 assert_eq!(result.confidence, Some(1.0));
88 assert!(result.evidence.contains("files touched: 2 <= 2"));
89 assert!(result.evidence.contains("lines changed: 5 <= 5"));
90 assert!(result.evidence.contains("3 added + 2 removed"));
91 }
92
93 #[test]
94 fn fails_when_any_configured_threshold_is_exceeded() {
95 let result = grade_diff_scope(
96 &assertion(Some(1), Some(4)),
97 DiffScopeMetrics {
98 files_touched: 2,
99 lines_added: 3,
100 lines_removed: 2,
101 hunks: 2,
102 },
103 );
104
105 assert!(!result.passed);
106 assert!(result.evidence.contains("files touched: 2 > 1"));
107 assert!(result.evidence.contains("lines changed: 5 > 4"));
108 assert!(result.evidence.contains("hunks: 2"));
109 }
110
111 #[test]
112 fn only_configured_thresholds_gate_the_result() {
113 let files_only = grade_diff_scope(
114 &assertion(Some(1), None),
115 DiffScopeMetrics {
116 files_touched: 1,
117 lines_added: 100,
118 lines_removed: 100,
119 hunks: 50,
120 },
121 );
122 assert!(files_only.passed);
123
124 let lines_only = grade_diff_scope(
125 &assertion(None, Some(1)),
126 DiffScopeMetrics {
127 files_touched: 100,
128 lines_added: 1,
129 lines_removed: 0,
130 hunks: 1,
131 },
132 );
133 assert!(lines_only.passed);
134 }
135}