Skip to main content

code_system_graph_core/
incremental.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use code_system_graph_model::{
4    ArtifactChange, ArtifactChangeKind, ArtifactFingerprint, CheckoutId, NativePath, RepoId
5};
6
7type ArtifactKey = (RepoId, CheckoutId, NativePath, String);
8
9/// Deterministic incremental plan for extractor-relevant artifacts.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct IncrementalPlan {
12    /// Changes sorted by repository, checkout, path, and extractor.
13    pub changes: Vec<ArtifactChange>,
14}
15
16impl IncrementalPlan {
17    /// Returns whether any artifact requires add, modify, or delete processing.
18    #[must_use]
19    pub fn has_changes(&self) -> bool {
20        self.changes
21            .iter()
22            .any(|change| change.kind != ArtifactChangeKind::Unchanged)
23    }
24
25    /// Returns the number of artifacts requiring extractor or deletion work.
26    #[must_use]
27    pub fn changed_count(&self) -> usize {
28        self.changes
29            .iter()
30            .filter(|change| change.kind != ArtifactChangeKind::Unchanged)
31            .count()
32    }
33}
34
35/// Compares current artifact fingerprints with the previous published snapshot.
36#[must_use]
37pub fn plan_incremental_scan(
38    previous: &[ArtifactFingerprint],
39    current: &[ArtifactFingerprint],
40) -> IncrementalPlan {
41    let previous = fingerprint_map(previous);
42    let current = fingerprint_map(current);
43    let keys = previous
44        .keys()
45        .chain(current.keys())
46        .cloned()
47        .collect::<BTreeSet<_>>();
48    let changes = keys
49        .into_iter()
50        .filter_map(|key| {
51            let kind = match (previous.get(&key), current.get(&key)) {
52                (None, Some(_)) => ArtifactChangeKind::Added,
53                (Some(_), None) => ArtifactChangeKind::Deleted,
54                (Some(before), Some(after)) if before.content_hash != after.content_hash => {
55                    ArtifactChangeKind::Modified
56                }
57                (Some(_), Some(_)) => ArtifactChangeKind::Unchanged,
58                (None, None) => return None,
59            };
60            Some(ArtifactChange {
61                repo_id: key.0,
62                checkout_id: key.1,
63                path: key.2,
64                extractor: key.3,
65                kind,
66            })
67        })
68        .collect();
69    IncrementalPlan { changes }
70}
71
72fn fingerprint_map(
73    fingerprints: &[ArtifactFingerprint],
74) -> BTreeMap<ArtifactKey, &ArtifactFingerprint> {
75    fingerprints
76        .iter()
77        .map(|fingerprint| {
78            (
79                (
80                    fingerprint.repo_id.clone(),
81                    fingerprint.checkout_id.clone(),
82                    fingerprint.path.clone(),
83                    fingerprint.extractor.clone(),
84                ),
85                fingerprint,
86            )
87        })
88        .collect()
89}
90
91#[cfg(test)]
92mod tests {
93    use code_system_graph_model::{
94        ArtifactChangeKind, ArtifactFingerprint, CheckoutId, NativePath, NativePathEncoding, RepoId
95    };
96
97    use super::plan_incremental_scan;
98
99    fn fingerprint(path: &str, hash: &str) -> ArtifactFingerprint {
100        ArtifactFingerprint {
101            repo_id: RepoId::new("repo:api"),
102            checkout_id: CheckoutId::new("checkout:api"),
103            path: NativePath {
104                encoding: NativePathEncoding::Utf8,
105                bytes: path.as_bytes().to_vec(),
106                display: path.to_owned(),
107            },
108            extractor: "openapi".to_owned(),
109            content_hash: hash.to_owned(),
110            size_bytes: 1,
111        }
112    }
113
114    #[test]
115    fn plan_should_classify_add_modify_delete_and_unchanged() {
116        let previous = vec![
117            fingerprint("deleted.yaml", "a"),
118            fingerprint("modified.yaml", "a"),
119            fingerprint("same.yaml", "a"),
120        ];
121        let current = vec![
122            fingerprint("added.yaml", "a"),
123            fingerprint("modified.yaml", "b"),
124            fingerprint("same.yaml", "a"),
125        ];
126
127        let plan = plan_incremental_scan(&previous, &current);
128        let kinds = plan
129            .changes
130            .iter()
131            .map(|change| change.kind)
132            .collect::<Vec<_>>();
133
134        assert_eq!(
135            kinds,
136            vec![
137                ArtifactChangeKind::Added,
138                ArtifactChangeKind::Deleted,
139                ArtifactChangeKind::Modified,
140                ArtifactChangeKind::Unchanged,
141            ]
142        );
143    }
144}