1use std::collections::{BTreeMap, VecDeque};
12
13use serde::Serialize;
14
15use crate::store::{Store, StoreError};
16use crate::{Edge, NodeKind};
17
18pub const SCHEMA: &str = "roteiro.query/v1";
21
22#[derive(Debug, Clone, PartialEq, Serialize)]
25pub struct NodeSummary {
26 pub key: String,
28 pub kind: String,
30 pub name: String,
32 pub path: Option<String>,
34 pub lang: Option<String>,
36}
37
38impl NodeSummary {
39 fn from_node(node: &crate::Node) -> Self {
40 Self {
41 key: node.key.clone(),
42 kind: node.kind.as_str().to_owned(),
43 name: node.name.clone(),
44 path: node.path.clone(),
45 lang: node.lang.clone(),
46 }
47 }
48}
49
50#[derive(Debug, Clone, PartialEq, Serialize)]
53pub struct EdgeRef {
54 pub kind: String,
56 pub provenance: &'static str,
58 pub confidence: Option<f64>,
60 pub node: String,
62}
63
64#[derive(Debug, Clone, PartialEq, Serialize)]
66pub struct Explanation {
67 pub schema: &'static str,
69 pub node: NodeSummary,
71 pub meta: serde_json::Value,
73 pub outgoing: Vec<EdgeRef>,
75 pub incoming: Vec<EdgeRef>,
77}
78
79#[derive(Debug, Clone, PartialEq, Serialize)]
81pub struct Listing {
82 pub schema: &'static str,
84 pub kind: String,
86 pub nodes: Vec<NodeSummary>,
88}
89
90#[derive(Debug, Clone, PartialEq, Serialize)]
92pub struct PathHop {
93 pub kind: String,
95 pub provenance: &'static str,
97 pub confidence: Option<f64>,
99 pub direction: &'static str,
102 pub node: String,
104}
105
106#[derive(Debug, Clone, PartialEq, Serialize)]
110pub struct Path {
111 pub schema: &'static str,
113 pub from: String,
115 pub to: String,
117 pub found: bool,
119 pub length: usize,
121 pub hops: Vec<PathHop>,
123}
124
125fn out_ref(edge: &Edge) -> EdgeRef {
126 EdgeRef {
127 kind: edge.kind.as_str().to_owned(),
128 provenance: edge.provenance.as_str(),
129 confidence: edge.confidence,
130 node: edge.dst.clone(),
131 }
132}
133
134fn in_ref(edge: &Edge) -> EdgeRef {
135 EdgeRef {
136 kind: edge.kind.as_str().to_owned(),
137 provenance: edge.provenance.as_str(),
138 confidence: edge.confidence,
139 node: edge.src.clone(),
140 }
141}
142
143fn sort_refs(refs: &mut [EdgeRef]) {
144 refs.sort_by(|a, b| (&a.kind, &a.node, a.provenance).cmp(&(&b.kind, &b.node, b.provenance)));
147}
148
149pub fn explain(store: &Store, key: &str) -> Result<Option<Explanation>, StoreError> {
155 let Some(node) = store.get_node(key)? else {
156 return Ok(None);
157 };
158 let mut outgoing: Vec<EdgeRef> = store.edges_from(key)?.iter().map(out_ref).collect();
159 let mut incoming: Vec<EdgeRef> = store.edges_to(key)?.iter().map(in_ref).collect();
160 sort_refs(&mut outgoing);
161 sort_refs(&mut incoming);
162 Ok(Some(Explanation {
163 schema: SCHEMA,
164 node: NodeSummary::from_node(&node),
165 meta: node.meta,
166 outgoing,
167 incoming,
168 }))
169}
170
171pub fn list_kind(store: &Store, kind: &NodeKind) -> Result<Listing, StoreError> {
176 let nodes = store
177 .nodes_by_kind(kind)?
178 .iter()
179 .map(NodeSummary::from_node)
180 .collect();
181 Ok(Listing {
182 schema: SCHEMA,
183 kind: kind.as_str().to_owned(),
184 nodes,
185 })
186}
187
188struct Step {
191 node: String,
192 hop: PathHop,
193}
194
195fn steps_from(store: &Store, key: &str) -> Result<Vec<Step>, StoreError> {
198 let mut steps = Vec::new();
199 for edge in store.edges_from(key)? {
200 steps.push(Step {
201 node: edge.dst.clone(),
202 hop: hop(&edge, "outgoing", edge.dst.clone()),
203 });
204 }
205 for edge in store.edges_to(key)? {
206 steps.push(Step {
207 node: edge.src.clone(),
208 hop: hop(&edge, "incoming", edge.src.clone()),
209 });
210 }
211 steps.sort_by(|a, b| {
212 (&a.node, &a.hop.kind, a.hop.provenance, a.hop.direction).cmp(&(
213 &b.node,
214 &b.hop.kind,
215 b.hop.provenance,
216 b.hop.direction,
217 ))
218 });
219 Ok(steps)
220}
221
222fn hop(edge: &Edge, direction: &'static str, node: String) -> PathHop {
223 PathHop {
224 kind: edge.kind.as_str().to_owned(),
225 provenance: edge.provenance.as_str(),
226 confidence: edge.confidence,
227 direction,
228 node,
229 }
230}
231
232pub fn path(store: &Store, from: &str, to: &str) -> Result<Path, StoreError> {
243 let not_found = |found: bool, hops: Vec<PathHop>| Path {
244 schema: SCHEMA,
245 from: from.to_owned(),
246 to: to.to_owned(),
247 found,
248 length: hops.len(),
249 hops,
250 };
251
252 if store.get_node(from)?.is_none() || store.get_node(to)?.is_none() {
254 return Ok(not_found(false, Vec::new()));
255 }
256 if from == to {
257 return Ok(not_found(true, Vec::new()));
258 }
259
260 let mut came_from: BTreeMap<String, (String, PathHop)> = BTreeMap::new();
263 let mut queue: VecDeque<String> = VecDeque::new();
264 queue.push_back(from.to_owned());
265 came_from.insert(from.to_owned(), (String::new(), placeholder_hop()));
266
267 while let Some(current) = queue.pop_front() {
268 if current == to {
269 break;
270 }
271 for step in steps_from(store, ¤t)? {
272 if came_from.contains_key(&step.node) {
273 continue;
274 }
275 came_from.insert(step.node.clone(), (current.clone(), step.hop));
276 queue.push_back(step.node);
277 }
278 }
279
280 let mut hops = Vec::new();
285 let mut cursor = to.to_owned();
286 while cursor != from {
287 let Some((prev, hop)) = came_from.get(&cursor) else {
288 return Ok(not_found(false, Vec::new()));
289 };
290 hops.push(hop.clone());
291 cursor = prev.clone();
292 }
293 hops.reverse();
294 Ok(not_found(true, hops))
295}
296
297fn placeholder_hop() -> PathHop {
299 PathHop {
300 kind: String::new(),
301 provenance: "derived",
302 confidence: None,
303 direction: "outgoing",
304 node: String::new(),
305 }
306}
307
308#[cfg(test)]
309mod tests {
310 use super::{SCHEMA, explain, list_kind, path};
311 use crate::{Edge, EdgeKind, FactSet, Node, NodeKind, Store};
312
313 fn seeded() -> Store {
314 let mut store = Store::open_in_memory().expect("store");
315 let facts = FactSet::new()
316 .with_node(Node::new("sym:rust:a.rs#main", NodeKind::Fn, "main"))
317 .with_node(Node::new("sym:rust:a.rs#helper", NodeKind::Fn, "helper"))
318 .with_node(Node::new("adr:0001", NodeKind::Adr, "Build Roteiro"))
319 .with_edge(Edge::derived(
320 "sym:rust:a.rs#main",
321 "sym:rust:a.rs#helper",
322 EdgeKind::Calls,
323 ))
324 .with_edge(Edge::authored(
325 "adr:0001",
326 "sym:rust:a.rs#main",
327 EdgeKind::References,
328 ));
329 store.apply_factset(&facts).expect("apply");
330 store
331 }
332
333 #[test]
334 fn explain_reports_labelled_neighbourhood() {
335 let store = seeded();
336 let ex = explain(&store, "sym:rust:a.rs#main")
337 .expect("query")
338 .expect("present");
339 assert_eq!(ex.schema, SCHEMA);
340 assert_eq!(ex.node.kind, "fn");
341
342 assert_eq!(ex.outgoing.len(), 1);
344 assert_eq!(ex.outgoing[0].kind, "calls");
345 assert_eq!(ex.outgoing[0].provenance, "derived");
346 assert_eq!(ex.outgoing[0].node, "sym:rust:a.rs#helper");
347
348 assert_eq!(ex.incoming.len(), 1);
350 assert_eq!(ex.incoming[0].provenance, "authored");
351 assert_eq!(ex.incoming[0].node, "adr:0001");
352 }
353
354 #[test]
355 fn explain_missing_node_is_none() {
356 let store = seeded();
357 assert!(explain(&store, "sym:rust:a.rs#ghost").expect("q").is_none());
358 }
359
360 #[test]
361 fn edges_differing_only_in_provenance_are_ordered() {
362 let mut store = Store::open_in_memory().expect("store");
365 let facts = FactSet::new()
366 .with_node(Node::new("a", NodeKind::Fn, "a"))
367 .with_node(Node::new("b", NodeKind::Fn, "b"))
368 .with_edge(Edge::derived("a", "b", EdgeKind::References))
369 .with_edge(Edge::authored("a", "b", EdgeKind::References));
370 store.apply_factset(&facts).expect("apply");
371
372 let ex = explain(&store, "a").expect("q").expect("present");
373 let provs: Vec<_> = ex.outgoing.iter().map(|e| e.provenance).collect();
374 assert_eq!(provs, ["authored", "derived"]);
375 }
376
377 #[test]
378 fn list_kind_is_ordered() {
379 let store = seeded();
380 let listing = list_kind(&store, &NodeKind::Fn).expect("list");
381 let keys: Vec<_> = listing.nodes.iter().map(|n| n.key.as_str()).collect();
382 assert_eq!(keys, ["sym:rust:a.rs#helper", "sym:rust:a.rs#main"]);
383 }
384
385 #[test]
386 fn json_schema_is_stable() {
387 let store = seeded();
388 let ex = explain(&store, "adr:0001").expect("q").expect("present");
389 let json = serde_json::to_value(&ex).expect("json");
390 assert_eq!(json["schema"], SCHEMA);
391 assert_eq!(json["node"]["key"], "adr:0001");
392 assert_eq!(json["node"]["kind"], "adr");
393 assert_eq!(json["outgoing"][0]["kind"], "references");
395 assert_eq!(json["outgoing"][0]["provenance"], "authored");
396 assert_eq!(json["outgoing"][0]["node"], "sym:rust:a.rs#main");
397 assert!(json["outgoing"][0]["confidence"].is_null());
398 }
399
400 #[test]
401 fn path_crosses_provenance_and_direction() {
402 let store = seeded();
405 let p = path(&store, "adr:0001", "sym:rust:a.rs#helper").expect("path");
406 assert!(p.found);
407 assert_eq!(p.length, 2);
408 assert_eq!(p.schema, SCHEMA);
409
410 assert_eq!(p.hops[0].kind, "references");
411 assert_eq!(p.hops[0].provenance, "authored");
412 assert_eq!(p.hops[0].direction, "outgoing");
413 assert_eq!(p.hops[0].node, "sym:rust:a.rs#main");
414
415 assert_eq!(p.hops[1].kind, "calls");
416 assert_eq!(p.hops[1].provenance, "derived");
417 assert_eq!(p.hops[1].node, "sym:rust:a.rs#helper");
418 }
419
420 #[test]
421 fn path_follows_edges_against_direction() {
422 let store = seeded();
425 let p = path(&store, "sym:rust:a.rs#helper", "adr:0001").expect("path");
426 assert!(p.found);
427 assert_eq!(p.length, 2);
428 assert!(p.hops.iter().all(|h| h.direction == "incoming"));
429 assert_eq!(p.hops.last().unwrap().node, "adr:0001");
430 }
431
432 #[test]
433 fn path_same_node_is_trivial() {
434 let store = seeded();
435 let p = path(&store, "adr:0001", "adr:0001").expect("path");
436 assert!(p.found);
437 assert_eq!(p.length, 0);
438 assert!(p.hops.is_empty());
439 }
440
441 #[test]
442 fn path_missing_endpoint_or_unreachable_is_not_found() {
443 let mut store = Store::open_in_memory().expect("store");
444 let facts = FactSet::new()
446 .with_node(Node::new("a", NodeKind::Fn, "a"))
447 .with_node(Node::new("b", NodeKind::Fn, "b"))
448 .with_node(Node::new("island", NodeKind::Fn, "island"))
449 .with_edge(Edge::derived("a", "b", EdgeKind::Calls));
450 store.apply_factset(&facts).expect("apply");
451
452 let missing = path(&store, "a", "ghost").expect("path");
454 assert!(!missing.found);
455 assert!(missing.hops.is_empty());
456
457 let unreachable = path(&store, "a", "island").expect("path");
459 assert!(!unreachable.found);
460 assert!(unreachable.hops.is_empty());
461 }
462
463 #[test]
464 fn path_is_shortest() {
465 let mut store = Store::open_in_memory().expect("store");
467 let facts = FactSet::new()
468 .with_node(Node::new("a", NodeKind::Fn, "a"))
469 .with_node(Node::new("b", NodeKind::Fn, "b"))
470 .with_node(Node::new("c", NodeKind::Fn, "c"))
471 .with_node(Node::new("d", NodeKind::Fn, "d"))
472 .with_edge(Edge::derived("a", "b", EdgeKind::Calls))
473 .with_edge(Edge::derived("b", "c", EdgeKind::Calls))
474 .with_edge(Edge::derived("c", "d", EdgeKind::Calls))
475 .with_edge(Edge::derived("a", "d", EdgeKind::Calls));
476 store.apply_factset(&facts).expect("apply");
477
478 let p = path(&store, "a", "d").expect("path");
479 assert!(p.found);
480 assert_eq!(p.length, 1, "the direct a->d edge is the shortest path");
481 assert_eq!(p.hops[0].node, "d");
482 }
483}