Skip to main content

code_split_core/
diff.rs

1use crate::{Graph, Snapshot};
2use serde::Serialize;
3use std::collections::HashMap;
4
5#[derive(Debug, Clone, Serialize)]
6pub struct DiffCounts {
7    pub added: usize,
8    pub removed: usize,
9    pub affected: usize,
10    pub unchanged: usize,
11}
12
13#[derive(Debug, Clone, Serialize)]
14pub struct LevelDiff {
15    pub nodes: DiffCounts,
16    pub edges: DiffCounts,
17    /// Nodes participating in cycles (SCCs with ≥ 2 members).
18    pub cycle_nodes_before: usize,
19    pub cycle_nodes_after: usize,
20    /// Number of SCCs.
21    pub sccs_before: usize,
22    pub sccs_after: usize,
23}
24
25#[derive(Debug, Clone, Serialize)]
26pub struct SnapMeta {
27    pub target: String,
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub branch: Option<String>,
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub commit: Option<String>,
32}
33
34#[derive(Debug, Clone, Serialize)]
35pub struct CompareSummary {
36    pub schema_version: String,
37    pub before: SnapMeta,
38    pub after: SnapMeta,
39    pub identical: bool,
40    pub modules: LevelDiff,
41    pub files: LevelDiff,
42    pub functions: LevelDiff,
43}
44
45pub fn compare_snapshots(before: &Snapshot, after: &Snapshot) -> CompareSummary {
46    let modules = diff_graph(&before.graphs.modules, &after.graphs.modules);
47    let files = diff_graph(&before.graphs.files, &after.graphs.files);
48    let functions = diff_graph(&before.graphs.functions, &after.graphs.functions);
49
50    let identical = [&modules, &files, &functions].iter().all(|d| {
51        d.nodes.added == 0
52            && d.nodes.removed == 0
53            && d.nodes.affected == 0
54            && d.edges.added == 0
55            && d.edges.removed == 0
56            && d.edges.affected == 0
57    });
58
59    CompareSummary {
60        schema_version: "1".to_string(),
61        before: snap_meta(before),
62        after: snap_meta(after),
63        identical,
64        modules,
65        files,
66        functions,
67    }
68}
69
70fn snap_meta(snap: &Snapshot) -> SnapMeta {
71    let commit_short = snap
72        .git
73        .as_ref()
74        .map(|g| g.commit[..8.min(g.commit.len())].to_string());
75    SnapMeta {
76        target: snap
77            .target
78            .split('/')
79            .next_back()
80            .unwrap_or(&snap.target)
81            .to_string(),
82        branch: snap.git.as_ref().map(|g| g.branch.clone()),
83        commit: commit_short,
84    }
85}
86
87// Mirrors computeDiff() from assets/diff.js.
88fn diff_graph(before: &Graph, after: &Graph) -> LevelDiff {
89    // Non-external node id sets.
90    let bg: HashMap<String, ()> = before
91        .nodes
92        .iter()
93        .filter(|n| !n.external.unwrap_or(false))
94        .map(|n| (n.id.clone(), ()))
95        .collect();
96    let ag: HashMap<String, ()> = after
97        .nodes
98        .iter()
99        .filter(|n| !n.external.unwrap_or(false))
100        .map(|n| (n.id.clone(), ()))
101        .collect();
102
103    // Node status: 0 = unchanged, 1 = added, 2 = removed, 3 = affected.
104    let mut node_status: HashMap<String, u8> = HashMap::new();
105    for id in ag.keys() {
106        node_status.insert(id.clone(), if bg.contains_key(id) { 0 } else { 1 });
107    }
108    for id in bg.keys() {
109        if !ag.contains_key(id) {
110            node_status.insert(id.clone(), 2);
111        }
112    }
113
114    // Edge key: "from\0to\0Kind".
115    let ekey = |e: &crate::Edge| format!("{}\x00{}\x00{:?}", e.from, e.to, e.kind);
116
117    // Local edges: both endpoints present in node_status.
118    let local_edges = |edges: &[crate::Edge]| -> HashMap<String, (String, String)> {
119        edges
120            .iter()
121            .filter(|e| node_status.contains_key(&e.from) && node_status.contains_key(&e.to))
122            .map(|e| (ekey(e), (e.from.clone(), e.to.clone())))
123            .collect()
124    };
125
126    let bg_edges = local_edges(&before.edges);
127    let ag_edges = local_edges(&after.edges);
128
129    // Collect all edges with status.
130    let mut edge_list: Vec<(String, String, u8)> = Vec::new();
131    for (key, (from, to)) in &ag_edges {
132        edge_list.push((
133            from.clone(),
134            to.clone(),
135            if bg_edges.contains_key(key) { 0 } else { 1 },
136        ));
137    }
138    for (key, (from, to)) in &bg_edges {
139        if !ag_edges.contains_key(key) {
140            edge_list.push((from.clone(), to.clone(), 2));
141        }
142    }
143
144    // Propagate "affected" to unchanged nodes adjacent to changed edges.
145    for (from, to, status) in &edge_list {
146        if *status != 0 {
147            if node_status.get(from.as_str()) == Some(&0) {
148                node_status.insert(from.clone(), 3);
149            }
150            if node_status.get(to.as_str()) == Some(&0) {
151                node_status.insert(to.clone(), 3);
152            }
153        }
154    }
155
156    // Count nodes.
157    let mut nodes = DiffCounts {
158        added: 0,
159        removed: 0,
160        affected: 0,
161        unchanged: 0,
162    };
163    for &s in node_status.values() {
164        match s {
165            1 => nodes.added += 1,
166            2 => nodes.removed += 1,
167            3 => nodes.affected += 1,
168            _ => nodes.unchanged += 1,
169        }
170    }
171
172    // Count edges (unchanged edge connecting non-unchanged nodes → affected).
173    let mut edges = DiffCounts {
174        added: 0,
175        removed: 0,
176        affected: 0,
177        unchanged: 0,
178    };
179    for (from, to, status) in &edge_list {
180        let s = if *status == 0
181            && (node_status.get(from.as_str()) != Some(&0)
182                || node_status.get(to.as_str()) != Some(&0))
183        {
184            3u8
185        } else {
186            *status
187        };
188        match s {
189            1 => edges.added += 1,
190            2 => edges.removed += 1,
191            3 => edges.affected += 1,
192            _ => edges.unchanged += 1,
193        }
194    }
195
196    LevelDiff {
197        nodes,
198        edges,
199        cycle_nodes_before: before.cycles.iter().map(|c| c.nodes.len()).sum(),
200        cycle_nodes_after: after.cycles.iter().map(|c| c.nodes.len()).sum(),
201        sccs_before: before.cycles.len(),
202        sccs_after: after.cycles.len(),
203    }
204}