Skip to main content

eval_magic/pipeline/grade/
diff_scope.rs

1//! Deterministic grading for final-environment diff-scope assertions.
2
3use crate::core::{AssertionDiffScope, AssertionResult, Grader};
4use crate::pipeline::DiffScopeMetrics;
5
6/// Compare the measured final-environment diff with every configured limit.
7pub 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}