1use std::collections::{BTreeSet, HashMap, HashSet, VecDeque};
5
6use redb::ReadableDatabase;
7use sinter_core::{Confidence, CorpusScope, Edge, Evidence, Node, NodeId, Relation};
8
9use crate::error::StoreError;
10use crate::store::{FILE_SCOPE, IN_EDGES, NODE_SCOPE, NODES, OUT_EDGES, Store};
11
12fn scope_of(
14 node_scopes: &impl redb::ReadableTable<&'static str, &'static str>,
15 file_scopes: &impl redb::ReadableTable<&'static str, &'static str>,
16 id: &str,
17) -> Result<CorpusScope, StoreError> {
18 let file = id.split_once('#').map_or(id, |(file, _)| file);
19 let file_scope = file_scopes
20 .get(file)?
21 .and_then(|guard| CorpusScope::from_str_opt(guard.value()));
22 Ok(crate::scope::resolve(
23 |key| {
24 node_scopes
25 .get(key)
26 .ok()
27 .flatten()
28 .and_then(|guard| CorpusScope::from_str_opt(guard.value()))
29 },
30 file_scope,
31 id,
32 file,
33 ))
34}
35
36#[derive(Debug, Default, Clone)]
39pub struct EdgeFilter {
40 pub evidence: Option<BTreeSet<Evidence>>,
42 pub min_confidence: Option<Confidence>,
44 pub relations: Option<BTreeSet<Relation>>,
46 pub scopes: Option<BTreeSet<CorpusScope>>,
49}
50
51impl EdgeFilter {
52 pub fn admits(&self, edge: &Edge) -> bool {
53 if edge.relation == Relation::Contains {
54 return false;
55 }
56 if let Some(allowed) = &self.relations
57 && !allowed.contains(&edge.relation)
58 {
59 return false;
60 }
61 if let Some(allowed) = &self.evidence
62 && !allowed.contains(&edge.evidence)
63 {
64 return false;
65 }
66 if self.min_confidence == Some(Confidence::Certain)
67 && edge.confidence != Confidence::Certain
68 {
69 return false;
70 }
71 true
72 }
73
74 pub fn admits_scope(&self, scope: CorpusScope) -> bool {
75 self.scopes
76 .as_ref()
77 .is_none_or(|allowed| allowed.contains(&scope))
78 }
79}
80
81pub struct Reached {
84 pub node: Node,
85 pub depth: usize,
86 pub via: Edge,
87}
88
89pub fn direct_summary(reached: &[Reached]) -> (usize, usize) {
93 let direct: Vec<&Reached> = reached
95 .iter()
96 .filter(|r| r.depth == 1 && r.via.relation != Relation::Imports)
97 .collect();
98 let files: std::collections::HashSet<&str> =
99 direct.iter().map(|r| r.node.file.as_str()).collect();
100 (direct.len(), files.len())
101}
102
103impl Store {
104 pub fn dependents(
107 &self,
108 id: &NodeId,
109 filter: &EdgeFilter,
110 max_depth: usize,
111 ) -> Result<Vec<Reached>, StoreError> {
112 let txn = self.db.begin_read()?;
113 let nodes = txn.open_table(NODES)?;
114 let scopes = txn.open_table(FILE_SCOPE)?;
115 let node_scopes = txn.open_table(NODE_SCOPE)?;
116 let incoming = txn.open_multimap_table(IN_EDGES)?;
117 let mut seen: HashSet<NodeId> = HashSet::from([id.clone()]);
118 let mut queue: VecDeque<(NodeId, usize)> = VecDeque::from([(id.clone(), 0)]);
119 let mut out = Vec::new();
120 while let Some((current, depth)) = queue.pop_front() {
121 if depth >= max_depth {
122 continue;
123 }
124 for guard in incoming.get(current.as_str())? {
125 let edge: Edge = postcard::from_bytes(guard?.value())?;
126 let scope = scope_of(&node_scopes, &scopes, edge.src.as_str())?;
127 if !filter.admits(&edge)
128 || !filter.admits_scope(scope)
129 || !seen.insert(edge.src.clone())
130 {
131 continue;
132 }
133 if let Some(guard) = nodes.get(edge.src.as_str())? {
134 let node = postcard::from_bytes(guard.value())?;
135 queue.push_back((edge.src.clone(), depth + 1));
136 out.push(Reached {
137 node,
138 depth: depth + 1,
139 via: edge,
140 });
141 }
142 }
143 }
144 Ok(out)
145 }
146
147 pub fn dependencies(
152 &self,
153 id: &NodeId,
154 filter: &EdgeFilter,
155 max_depth: usize,
156 ) -> Result<Vec<Reached>, StoreError> {
157 let txn = self.db.begin_read()?;
158 let nodes = txn.open_table(NODES)?;
159 let scopes = txn.open_table(FILE_SCOPE)?;
160 let node_scopes = txn.open_table(NODE_SCOPE)?;
161 let outgoing = txn.open_multimap_table(OUT_EDGES)?;
162 let mut seen: HashSet<NodeId> = HashSet::from([id.clone()]);
163 let mut queue: VecDeque<(NodeId, usize)> = VecDeque::from([(id.clone(), 0)]);
164 if nodes
165 .get(id.as_str())?
166 .map(|guard| postcard::from_bytes::<Node>(guard.value()))
167 .transpose()?
168 .is_some_and(|n| n.kind == sinter_core::SymbolKind::File)
169 {
170 for guard in outgoing.get(id.as_str())? {
171 let edge: Edge = postcard::from_bytes(guard?.value())?;
172 if edge.relation == Relation::Contains && seen.insert(edge.dst.clone()) {
173 queue.push_back((edge.dst.clone(), 0));
174 }
175 }
176 }
177 let mut out = Vec::new();
178 while let Some((current, depth)) = queue.pop_front() {
179 if depth >= max_depth {
180 continue;
181 }
182 for guard in outgoing.get(current.as_str())? {
183 let edge: Edge = postcard::from_bytes(guard?.value())?;
184 let scope = scope_of(&node_scopes, &scopes, edge.dst.as_str())?;
185 if !filter.admits(&edge)
186 || !filter.admits_scope(scope)
187 || !seen.insert(edge.dst.clone())
188 {
189 continue;
190 }
191 if let Some(guard) = nodes.get(edge.dst.as_str())? {
192 let node = postcard::from_bytes(guard.value())?;
193 queue.push_back((edge.dst.clone(), depth + 1));
194 out.push(Reached {
195 node,
196 depth: depth + 1,
197 via: edge,
198 });
199 }
200 }
201 }
202 Ok(out)
203 }
204
205 pub fn shortest_path(
207 &self,
208 from: &NodeId,
209 to: &NodeId,
210 filter: &EdgeFilter,
211 ) -> Result<Option<Vec<Edge>>, StoreError> {
212 let txn = self.db.begin_read()?;
213 let nodes = txn.open_table(NODES)?;
214 let scopes = txn.open_table(FILE_SCOPE)?;
215 let node_scopes = txn.open_table(NODE_SCOPE)?;
216 let outgoing = txn.open_multimap_table(OUT_EDGES)?;
217 let mut prev: HashMap<NodeId, Edge> = HashMap::new();
218 let mut seen: HashSet<NodeId> = HashSet::from([from.clone()]);
219 let mut queue: VecDeque<NodeId> = VecDeque::from([from.clone()]);
220 if nodes
224 .get(from.as_str())?
225 .map(|guard| postcard::from_bytes::<Node>(guard.value()))
226 .transpose()?
227 .is_some_and(|n| n.kind == sinter_core::SymbolKind::File)
228 {
229 for guard in outgoing.get(from.as_str())? {
230 let edge: Edge = postcard::from_bytes(guard?.value())?;
231 if edge.relation == Relation::Contains && seen.insert(edge.dst.clone()) {
232 prev.insert(edge.dst.clone(), edge.clone());
233 queue.push_back(edge.dst.clone());
234 }
235 }
236 }
237 while let Some(current) = queue.pop_front() {
238 if ¤t == to {
239 let mut path = Vec::new();
240 let mut at = to.clone();
241 while &at != from {
242 let edge = prev[&at].clone();
243 at = edge.src.clone();
244 path.push(edge);
245 }
246 path.reverse();
247 return Ok(Some(path));
248 }
249 for guard in outgoing.get(current.as_str())? {
250 let edge: Edge = postcard::from_bytes(guard?.value())?;
251 let scope = scope_of(&node_scopes, &scopes, edge.dst.as_str())?;
252 if !filter.admits(&edge)
253 || !filter.admits_scope(scope)
254 || !seen.insert(edge.dst.clone())
255 {
256 continue;
257 }
258 prev.insert(edge.dst.clone(), edge.clone());
259 queue.push_back(edge.dst.clone());
260 }
261 }
262 Ok(None)
263 }
264}