Skip to main content

kaizen/prompt/
diff.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2//! Diff two `PromptSnapshot`s to find added, removed, and changed files.
3
4use crate::prompt::types::{PromptDiff, PromptSnapshot};
5use std::collections::HashMap;
6
7/// Compute the diff between snapshot `a` (before) and `b` (after).
8pub fn diff(a: &PromptSnapshot, b: &PromptSnapshot) -> PromptDiff {
9    let files_a = a.files();
10    let map_a: HashMap<&str, &str> = files_a
11        .iter()
12        .map(|f| (f.path.as_str(), f.sha256.as_str()))
13        .collect();
14    let map_b: HashMap<String, String> =
15        b.files().into_iter().map(|f| (f.path, f.sha256)).collect();
16    let mut added = Vec::new();
17    let mut removed = Vec::new();
18    let mut changed = Vec::new();
19    for (path, hash_b) in &map_b {
20        match map_a.get(path.as_str()) {
21            None => added.push(path.clone()),
22            Some(hash_a) if *hash_a != hash_b.as_str() => changed.push(path.clone()),
23            _ => {}
24        }
25    }
26    for path in map_a.keys() {
27        if !map_b.contains_key(*path) {
28            removed.push((*path).to_string());
29        }
30    }
31    added.sort();
32    removed.sort();
33    changed.sort();
34    PromptDiff {
35        added,
36        removed,
37        changed,
38    }
39}
40
41#[cfg(test)]
42mod tests {
43    use super::*;
44    use crate::prompt::types::PromptFile;
45
46    fn snap(files: &[(&str, &str)]) -> PromptSnapshot {
47        let pfs: Vec<PromptFile> = files
48            .iter()
49            .map(|(p, h)| PromptFile {
50                path: p.to_string(),
51                sha256: h.to_string(),
52                bytes: 0,
53            })
54            .collect();
55        PromptSnapshot {
56            fingerprint: "x".into(),
57            captured_at_ms: 0,
58            files_json: serde_json::to_string(&pfs).unwrap(),
59            total_bytes: 0,
60        }
61    }
62
63    #[test]
64    fn identical_snapshots_empty_diff() {
65        let a = snap(&[("a.md", "h1")]);
66        let d = diff(&a, &a);
67        assert!(d.is_empty());
68    }
69
70    #[test]
71    fn detects_added_removed_changed() {
72        let a = snap(&[("a.md", "h1"), ("b.md", "h2")]);
73        let b = snap(&[("a.md", "h9"), ("c.md", "h3")]);
74        let d = diff(&a, &b);
75        assert_eq!(d.changed, vec!["a.md"]);
76        assert_eq!(d.added, vec!["c.md"]);
77        assert_eq!(d.removed, vec!["b.md"]);
78    }
79}