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