Skip to main content

sinter_store/
traverse.rs

1//! Graph traversals over the persisted adjacency tables: reverse blast
2//! radius and shortest path. Point reads only — never loads the corpus.
3
4use std::collections::{BTreeSet, HashMap, HashSet, VecDeque};
5
6use redb::ReadableDatabase;
7use sinter_core::{Confidence, Edge, Evidence, Node, NodeId, Relation};
8
9use crate::error::StoreError;
10use crate::store::{IN_EDGES, NODES, OUT_EDGES, Store};
11
12/// Which edges a traversal may walk. Containment is structure, not
13/// dependency, so it never participates.
14#[derive(Debug, Default, Clone)]
15pub struct EdgeFilter {
16    /// Allowed evidence kinds; None = all.
17    pub evidence: Option<BTreeSet<Evidence>>,
18    /// Minimum confidence; None = any.
19    pub min_confidence: Option<Confidence>,
20    /// Allowed relations; None = all (Contains stays excluded either way).
21    pub relations: Option<BTreeSet<Relation>>,
22}
23
24impl EdgeFilter {
25    pub fn admits(&self, edge: &Edge) -> bool {
26        if edge.relation == Relation::Contains {
27            return false;
28        }
29        if let Some(allowed) = &self.relations
30            && !allowed.contains(&edge.relation)
31        {
32            return false;
33        }
34        if let Some(allowed) = &self.evidence
35            && !allowed.contains(&edge.evidence)
36        {
37            return false;
38        }
39        if self.min_confidence == Some(Confidence::Certain)
40            && edge.confidence != Confidence::Certain
41        {
42            return false;
43        }
44        true
45    }
46}
47
48/// One step of a traversal result: the node reached, how deep, and the edge
49/// that reached it.
50pub struct Reached {
51    pub node: Node,
52    pub depth: usize,
53    pub via: Edge,
54}
55
56/// Depth-1 dependents and the distinct files they live in — the "who
57/// actually calls this" number, distinct from the transitive total that
58/// otherwise reads as a caller count.
59pub fn direct_summary(reached: &[Reached]) -> (usize, usize) {
60    let direct: Vec<&Reached> = reached.iter().filter(|r| r.depth == 1).collect();
61    let files: std::collections::HashSet<&str> =
62        direct.iter().map(|r| r.node.file.as_str()).collect();
63    (direct.len(), files.len())
64}
65
66impl Store {
67    /// Reverse blast radius: everything transitively depending on `id`
68    /// (incoming non-Contains edges), breadth-first, deduplicated.
69    pub fn dependents(
70        &self,
71        id: &NodeId,
72        filter: &EdgeFilter,
73        max_depth: usize,
74    ) -> Result<Vec<Reached>, StoreError> {
75        let txn = self.db.begin_read()?;
76        let nodes = txn.open_table(NODES)?;
77        let incoming = txn.open_multimap_table(IN_EDGES)?;
78        let mut seen: HashSet<NodeId> = HashSet::from([id.clone()]);
79        let mut queue: VecDeque<(NodeId, usize)> = VecDeque::from([(id.clone(), 0)]);
80        let mut out = Vec::new();
81        while let Some((current, depth)) = queue.pop_front() {
82            if depth >= max_depth {
83                continue;
84            }
85            for guard in incoming.get(current.as_str())? {
86                let edge: Edge = postcard::from_bytes(guard?.value())?;
87                if !filter.admits(&edge) || !seen.insert(edge.src.clone()) {
88                    continue;
89                }
90                if let Some(guard) = nodes.get(edge.src.as_str())? {
91                    let node = postcard::from_bytes(guard.value())?;
92                    queue.push_back((edge.src.clone(), depth + 1));
93                    out.push(Reached {
94                        node,
95                        depth: depth + 1,
96                        via: edge,
97                    });
98                }
99            }
100        }
101        Ok(out)
102    }
103
104    /// Forward transitive closure: everything `id` depends on (outgoing
105    /// non-Contains edges), breadth-first, deduplicated. A file start seeds
106    /// through its Contains edges (a file's dependencies live in the
107    /// symbols it contains), silently — containment is not a dependency.
108    pub fn dependencies(
109        &self,
110        id: &NodeId,
111        filter: &EdgeFilter,
112        max_depth: usize,
113    ) -> Result<Vec<Reached>, StoreError> {
114        let txn = self.db.begin_read()?;
115        let nodes = txn.open_table(NODES)?;
116        let outgoing = txn.open_multimap_table(OUT_EDGES)?;
117        let mut seen: HashSet<NodeId> = HashSet::from([id.clone()]);
118        let mut queue: VecDeque<(NodeId, usize)> = VecDeque::from([(id.clone(), 0)]);
119        if nodes
120            .get(id.as_str())?
121            .map(|guard| postcard::from_bytes::<Node>(guard.value()))
122            .transpose()?
123            .is_some_and(|n| n.kind == sinter_core::SymbolKind::File)
124        {
125            for guard in outgoing.get(id.as_str())? {
126                let edge: Edge = postcard::from_bytes(guard?.value())?;
127                if edge.relation == Relation::Contains && seen.insert(edge.dst.clone()) {
128                    queue.push_back((edge.dst.clone(), 0));
129                }
130            }
131        }
132        let mut out = Vec::new();
133        while let Some((current, depth)) = queue.pop_front() {
134            if depth >= max_depth {
135                continue;
136            }
137            for guard in outgoing.get(current.as_str())? {
138                let edge: Edge = postcard::from_bytes(guard?.value())?;
139                if !filter.admits(&edge) || !seen.insert(edge.dst.clone()) {
140                    continue;
141                }
142                if let Some(guard) = nodes.get(edge.dst.as_str())? {
143                    let node = postcard::from_bytes(guard.value())?;
144                    queue.push_back((edge.dst.clone(), depth + 1));
145                    out.push(Reached {
146                        node,
147                        depth: depth + 1,
148                        via: edge,
149                    });
150                }
151            }
152        }
153        Ok(out)
154    }
155
156    /// Shortest edge path `from -> to` over outgoing edges, or None.
157    pub fn shortest_path(
158        &self,
159        from: &NodeId,
160        to: &NodeId,
161        filter: &EdgeFilter,
162    ) -> Result<Option<Vec<Edge>>, StoreError> {
163        let txn = self.db.begin_read()?;
164        let nodes = txn.open_table(NODES)?;
165        let outgoing = txn.open_multimap_table(OUT_EDGES)?;
166        let mut prev: HashMap<NodeId, Edge> = HashMap::new();
167        let mut seen: HashSet<NodeId> = HashSet::from([from.clone()]);
168        let mut queue: VecDeque<NodeId> = VecDeque::from([from.clone()]);
169        // A file's dependencies live in the symbols it contains; a file
170        // start seeds through its contains edges (shown as path steps).
171        // Containment stays non-traversable everywhere past the start.
172        if nodes
173            .get(from.as_str())?
174            .map(|guard| postcard::from_bytes::<Node>(guard.value()))
175            .transpose()?
176            .is_some_and(|n| n.kind == sinter_core::SymbolKind::File)
177        {
178            for guard in outgoing.get(from.as_str())? {
179                let edge: Edge = postcard::from_bytes(guard?.value())?;
180                if edge.relation == Relation::Contains && seen.insert(edge.dst.clone()) {
181                    prev.insert(edge.dst.clone(), edge.clone());
182                    queue.push_back(edge.dst.clone());
183                }
184            }
185        }
186        while let Some(current) = queue.pop_front() {
187            if &current == to {
188                let mut path = Vec::new();
189                let mut at = to.clone();
190                while &at != from {
191                    let edge = prev[&at].clone();
192                    at = edge.src.clone();
193                    path.push(edge);
194                }
195                path.reverse();
196                return Ok(Some(path));
197            }
198            for guard in outgoing.get(current.as_str())? {
199                let edge: Edge = postcard::from_bytes(guard?.value())?;
200                if !filter.admits(&edge) || !seen.insert(edge.dst.clone()) {
201                    continue;
202                }
203                prev.insert(edge.dst.clone(), edge.clone());
204                queue.push_back(edge.dst.clone());
205            }
206        }
207        Ok(None)
208    }
209}