1use serde::Serialize;
11
12use crate::store::{Store, StoreError};
13use crate::{Edge, NodeKind};
14
15pub const SCHEMA: &str = "roteiro.query/v1";
18
19#[derive(Debug, Clone, PartialEq, Serialize)]
22pub struct NodeSummary {
23 pub key: String,
25 pub kind: String,
27 pub name: String,
29 pub path: Option<String>,
31 pub lang: Option<String>,
33}
34
35impl NodeSummary {
36 fn from_node(node: &crate::Node) -> Self {
37 Self {
38 key: node.key.clone(),
39 kind: node.kind.as_str().to_owned(),
40 name: node.name.clone(),
41 path: node.path.clone(),
42 lang: node.lang.clone(),
43 }
44 }
45}
46
47#[derive(Debug, Clone, PartialEq, Serialize)]
50pub struct EdgeRef {
51 pub kind: String,
53 pub provenance: &'static str,
55 pub confidence: Option<f64>,
57 pub node: String,
59}
60
61#[derive(Debug, Clone, PartialEq, Serialize)]
63pub struct Explanation {
64 pub schema: &'static str,
66 pub node: NodeSummary,
68 pub meta: serde_json::Value,
70 pub outgoing: Vec<EdgeRef>,
72 pub incoming: Vec<EdgeRef>,
74}
75
76#[derive(Debug, Clone, PartialEq, Serialize)]
78pub struct Listing {
79 pub schema: &'static str,
81 pub kind: String,
83 pub nodes: Vec<NodeSummary>,
85}
86
87fn out_ref(edge: &Edge) -> EdgeRef {
88 EdgeRef {
89 kind: edge.kind.as_str().to_owned(),
90 provenance: edge.provenance.as_str(),
91 confidence: edge.confidence,
92 node: edge.dst.clone(),
93 }
94}
95
96fn in_ref(edge: &Edge) -> EdgeRef {
97 EdgeRef {
98 kind: edge.kind.as_str().to_owned(),
99 provenance: edge.provenance.as_str(),
100 confidence: edge.confidence,
101 node: edge.src.clone(),
102 }
103}
104
105fn sort_refs(refs: &mut [EdgeRef]) {
106 refs.sort_by(|a, b| (&a.kind, &a.node, a.provenance).cmp(&(&b.kind, &b.node, b.provenance)));
109}
110
111pub fn explain(store: &Store, key: &str) -> Result<Option<Explanation>, StoreError> {
117 let Some(node) = store.get_node(key)? else {
118 return Ok(None);
119 };
120 let mut outgoing: Vec<EdgeRef> = store.edges_from(key)?.iter().map(out_ref).collect();
121 let mut incoming: Vec<EdgeRef> = store.edges_to(key)?.iter().map(in_ref).collect();
122 sort_refs(&mut outgoing);
123 sort_refs(&mut incoming);
124 Ok(Some(Explanation {
125 schema: SCHEMA,
126 node: NodeSummary::from_node(&node),
127 meta: node.meta,
128 outgoing,
129 incoming,
130 }))
131}
132
133pub fn list_kind(store: &Store, kind: &NodeKind) -> Result<Listing, StoreError> {
138 let nodes = store
139 .nodes_by_kind(kind)?
140 .iter()
141 .map(NodeSummary::from_node)
142 .collect();
143 Ok(Listing {
144 schema: SCHEMA,
145 kind: kind.as_str().to_owned(),
146 nodes,
147 })
148}
149
150#[cfg(test)]
151mod tests {
152 use super::{SCHEMA, explain, list_kind};
153 use crate::{Edge, EdgeKind, FactSet, Node, NodeKind, Store};
154
155 fn seeded() -> Store {
156 let mut store = Store::open_in_memory().expect("store");
157 let facts = FactSet::new()
158 .with_node(Node::new("sym:rust:a.rs#main", NodeKind::Fn, "main"))
159 .with_node(Node::new("sym:rust:a.rs#helper", NodeKind::Fn, "helper"))
160 .with_node(Node::new("adr:0001", NodeKind::Adr, "Build Roteiro"))
161 .with_edge(Edge::derived(
162 "sym:rust:a.rs#main",
163 "sym:rust:a.rs#helper",
164 EdgeKind::Calls,
165 ))
166 .with_edge(Edge::authored(
167 "adr:0001",
168 "sym:rust:a.rs#main",
169 EdgeKind::References,
170 ));
171 store.apply_factset(&facts).expect("apply");
172 store
173 }
174
175 #[test]
176 fn explain_reports_labelled_neighbourhood() {
177 let store = seeded();
178 let ex = explain(&store, "sym:rust:a.rs#main")
179 .expect("query")
180 .expect("present");
181 assert_eq!(ex.schema, SCHEMA);
182 assert_eq!(ex.node.kind, "fn");
183
184 assert_eq!(ex.outgoing.len(), 1);
186 assert_eq!(ex.outgoing[0].kind, "calls");
187 assert_eq!(ex.outgoing[0].provenance, "derived");
188 assert_eq!(ex.outgoing[0].node, "sym:rust:a.rs#helper");
189
190 assert_eq!(ex.incoming.len(), 1);
192 assert_eq!(ex.incoming[0].provenance, "authored");
193 assert_eq!(ex.incoming[0].node, "adr:0001");
194 }
195
196 #[test]
197 fn explain_missing_node_is_none() {
198 let store = seeded();
199 assert!(explain(&store, "sym:rust:a.rs#ghost").expect("q").is_none());
200 }
201
202 #[test]
203 fn edges_differing_only_in_provenance_are_ordered() {
204 let mut store = Store::open_in_memory().expect("store");
207 let facts = FactSet::new()
208 .with_node(Node::new("a", NodeKind::Fn, "a"))
209 .with_node(Node::new("b", NodeKind::Fn, "b"))
210 .with_edge(Edge::derived("a", "b", EdgeKind::References))
211 .with_edge(Edge::authored("a", "b", EdgeKind::References));
212 store.apply_factset(&facts).expect("apply");
213
214 let ex = explain(&store, "a").expect("q").expect("present");
215 let provs: Vec<_> = ex.outgoing.iter().map(|e| e.provenance).collect();
216 assert_eq!(provs, ["authored", "derived"]);
217 }
218
219 #[test]
220 fn list_kind_is_ordered() {
221 let store = seeded();
222 let listing = list_kind(&store, &NodeKind::Fn).expect("list");
223 let keys: Vec<_> = listing.nodes.iter().map(|n| n.key.as_str()).collect();
224 assert_eq!(keys, ["sym:rust:a.rs#helper", "sym:rust:a.rs#main"]);
225 }
226
227 #[test]
228 fn json_schema_is_stable() {
229 let store = seeded();
230 let ex = explain(&store, "adr:0001").expect("q").expect("present");
231 let json = serde_json::to_value(&ex).expect("json");
232 assert_eq!(json["schema"], SCHEMA);
233 assert_eq!(json["node"]["key"], "adr:0001");
234 assert_eq!(json["node"]["kind"], "adr");
235 assert_eq!(json["outgoing"][0]["kind"], "references");
237 assert_eq!(json["outgoing"][0]["provenance"], "authored");
238 assert_eq!(json["outgoing"][0]["node"], "sym:rust:a.rs#main");
239 assert!(json["outgoing"][0]["confidence"].is_null());
240 }
241}