Skip to main content

xchecker_fixup_model/
lib.rs

1//! Fixup model types for spec generation workflows
2//!
3//! This crate provides core types and models for detecting and applying changes
4//! to specification artifacts through fixup plans.
5
6use std::collections::HashMap;
7
8/// Fixup execution mode
9///
10/// Determines whether fixup changes are previewed or applied to files.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum FixupMode {
13    /// Preview mode - show what would change without applying
14    Preview,
15    /// Apply mode - actually apply changes to files
16    Apply,
17}
18
19impl FixupMode {
20    /// Returns the string representation of the mode
21    #[must_use]
22    pub fn as_str(&self) -> &'static str {
23        match self {
24            Self::Preview => "preview",
25            Self::Apply => "apply",
26        }
27    }
28}
29
30/// A single hunk in a unified diff
31///
32/// Represents a contiguous block of changes with context lines.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct DiffHunk {
35    /// Starting line number in the original file
36    pub start: usize,
37    /// Number of lines to remove from the original file
38    pub remove_count: usize,
39    /// Number of lines to add to the new file
40    pub add_count: usize,
41    /// Lines to remove (without the '-' prefix)
42    pub remove_lines: Vec<String>,
43    /// Lines to add (without the '+' prefix)
44    pub add_lines: Vec<String>,
45    /// Original file line range: (start, count)
46    pub old_range: (usize, usize),
47    /// New file line range: (start, count)
48    pub new_range: (usize, usize),
49    /// Full hunk content including header and context
50    pub content: String,
51}
52
53/// A unified diff for a single file
54///
55/// Represents a complete diff with all hunks for a target file.
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct UnifiedDiff {
58    /// Path from the diff header (may include a/ or b/ prefix)
59    pub path: String,
60    /// Target file path (normalized, without a/ or b/ prefix)
61    pub target_file: String,
62    /// Full diff content as a string
63    pub diff_content: String,
64    /// All hunks in this diff
65    pub hunks: Vec<DiffHunk>,
66}
67
68/// Summary of changes for a single file
69///
70/// Provides statistics and validation results for a file's changes.
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct ChangeSummary {
73    /// Number of hunks in the diff
74    pub hunk_count: usize,
75    /// Number of lines added
76    pub lines_added: usize,
77    /// Number of lines removed
78    pub lines_removed: usize,
79    /// Whether validation passed for this file
80    pub validation_passed: bool,
81    /// Validation messages (errors, warnings, etc.)
82    pub validation_messages: Vec<String>,
83}
84
85/// Result of applying a single file
86///
87/// Contains information about a successfully applied file.
88#[derive(Debug, Clone, PartialEq, Eq)]
89pub struct AppliedFile {
90    /// Path to the file that was applied
91    pub path: String,
92    /// First 8 characters of BLAKE3 hash of new content
93    pub blake3_first8: String,
94    /// Whether the file was successfully applied
95    pub applied: bool,
96    /// Any warnings generated during application
97    pub warnings: Vec<String>,
98}
99
100/// Preview of fixup changes
101///
102/// Shows what would change without actually applying changes.
103#[derive(Debug, Clone, PartialEq, Eq)]
104pub struct FixupPreview {
105    /// List of target files in the diffs
106    pub target_files: Vec<String>,
107    /// Change summary per file
108    pub change_summary: HashMap<String, ChangeSummary>,
109    /// Any warnings generated during preview
110    pub warnings: Vec<String>,
111    /// Whether all diffs validated successfully
112    pub all_valid: bool,
113}
114
115/// Result of applying fixup changes
116///
117/// Contains information about applied and failed files.
118#[derive(Debug, Clone, PartialEq, Eq)]
119pub struct FixupResult {
120    /// Files that were successfully applied
121    pub applied_files: Vec<AppliedFile>,
122    /// Files that failed to apply
123    pub failed_files: Vec<String>,
124    /// Any warnings generated during application
125    pub warnings: Vec<String>,
126    /// Whether 3-way merge was used (legacy git apply mode)
127    pub three_way_used: bool,
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn test_fixup_mode() {
136        let preview = FixupMode::Preview;
137        let apply = FixupMode::Apply;
138
139        assert_eq!(preview, FixupMode::Preview);
140        assert_eq!(apply, FixupMode::Apply);
141        assert_ne!(preview, apply);
142    }
143
144    #[test]
145    fn test_diff_hunk() {
146        let hunk = DiffHunk {
147            start: 10,
148            remove_count: 2,
149            add_count: 3,
150            remove_lines: vec!["old line 1".to_string(), "old line 2".to_string()],
151            add_lines: vec![
152                "new line 1".to_string(),
153                "new line 2".to_string(),
154                "new line 3".to_string(),
155            ],
156            old_range: (10, 2),
157            new_range: (10, 3),
158            content:
159                "@@ -10,2 +10,3 @@\n-old line 1\n-old line 2\n+new line 1\n+new line 2\n+new line 3"
160                    .to_string(),
161        };
162
163        assert_eq!(hunk.start, 10);
164        assert_eq!(hunk.remove_count, 2);
165        assert_eq!(hunk.add_count, 3);
166        assert_eq!(hunk.remove_lines.len(), 2);
167        assert_eq!(hunk.add_lines.len(), 3);
168        assert_eq!(hunk.old_range, (10, 2));
169        assert_eq!(hunk.new_range, (10, 3));
170    }
171
172    #[test]
173    fn test_unified_diff() {
174        let hunk = DiffHunk {
175            start: 1,
176            remove_count: 1,
177            add_count: 1,
178            remove_lines: vec!["old".to_string()],
179            add_lines: vec!["new".to_string()],
180            old_range: (1, 1),
181            new_range: (1, 1),
182            content: "@@ -1,1 +1,1 @@\n-old\n+new".to_string(),
183        };
184
185        let diff = UnifiedDiff {
186            path: "a/src/main.rs".to_string(),
187            target_file: "src/main.rs".to_string(),
188            diff_content: "--- a/src/main.rs\n+++ b/src/main.rs\n@@ -1,1 +1,1 @@\n-old\n+new"
189                .to_string(),
190            hunks: vec![hunk],
191        };
192
193        assert_eq!(diff.path, "a/src/main.rs");
194        assert_eq!(diff.target_file, "src/main.rs");
195        assert_eq!(diff.hunks.len(), 1);
196    }
197
198    #[test]
199    fn test_change_summary() {
200        let summary = ChangeSummary {
201            hunk_count: 2,
202            lines_added: 5,
203            lines_removed: 3,
204            validation_passed: true,
205            validation_messages: vec![],
206        };
207
208        assert_eq!(summary.hunk_count, 2);
209        assert_eq!(summary.lines_added, 5);
210        assert_eq!(summary.lines_removed, 3);
211        assert!(summary.validation_passed);
212    }
213
214    #[test]
215    fn test_applied_file() {
216        let applied = AppliedFile {
217            path: "src/main.rs".to_string(),
218            blake3_first8: "a1b2c3d4".to_string(),
219            applied: true,
220            warnings: vec![],
221        };
222
223        assert_eq!(applied.path, "src/main.rs");
224        assert_eq!(applied.blake3_first8, "a1b2c3d4");
225        assert!(applied.applied);
226    }
227
228    #[test]
229    fn test_fixup_preview() {
230        let mut summary = HashMap::new();
231        summary.insert(
232            "src/main.rs".to_string(),
233            ChangeSummary {
234                hunk_count: 1,
235                lines_added: 2,
236                lines_removed: 1,
237                validation_passed: true,
238                validation_messages: vec![],
239            },
240        );
241
242        let preview = FixupPreview {
243            target_files: vec!["src/main.rs".to_string()],
244            change_summary: summary,
245            warnings: vec![],
246            all_valid: true,
247        };
248
249        assert_eq!(preview.target_files.len(), 1);
250        assert!(preview.all_valid);
251        assert!(preview.change_summary.contains_key("src/main.rs"));
252    }
253
254    #[test]
255    fn test_fixup_result() {
256        let result = FixupResult {
257            applied_files: vec![AppliedFile {
258                path: "src/main.rs".to_string(),
259                blake3_first8: "a1b2c3d4".to_string(),
260                applied: true,
261                warnings: vec![],
262            }],
263            failed_files: vec![],
264            warnings: vec![],
265            three_way_used: false,
266        };
267
268        assert_eq!(result.applied_files.len(), 1);
269        assert_eq!(result.failed_files.len(), 0);
270        assert!(!result.three_way_used);
271    }
272}