Skip to main content

rto_graph/
query.rs

1//! The agent- and human-facing query surface over the graph.
2//!
3//! Everything here is a read-only view built from the store's typed queries,
4//! serialised under a **stable, versioned** JSON schema ([`SCHEMA`]) so agents
5//! can depend on the shape. Two primitives are provided: [`explain`] (a node
6//! and its provenance-labelled neighbourhood) and [`list_kind`] (all nodes of a
7//! kind). Both return mixed-provenance results — the "one query surface" from
8//! ADR-0001 — with every edge carrying its `provenance`.
9
10use serde::Serialize;
11
12use crate::store::{Store, StoreError};
13use crate::{Edge, NodeKind};
14
15/// The versioned schema tag emitted on every query result. Bump the version on
16/// any breaking change to the shape.
17pub const SCHEMA: &str = "roteiro.query/v1";
18
19/// A compact node summary (used in listings and as the subject of an
20/// [`Explanation`]).
21#[derive(Debug, Clone, PartialEq, Serialize)]
22pub struct NodeSummary {
23    /// Natural key.
24    pub key: String,
25    /// Kind token (e.g. `fn`, `adr`).
26    pub kind: String,
27    /// Human-facing name.
28    pub name: String,
29    /// Repository-relative path, if any.
30    pub path: Option<String>,
31    /// Language token, if any.
32    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/// One end of an edge as seen from a subject node: the relationship, how it was
48/// produced, and the node on the other end.
49#[derive(Debug, Clone, PartialEq, Serialize)]
50pub struct EdgeRef {
51    /// Edge kind token (e.g. `calls`, `references`).
52    pub kind: String,
53    /// How the edge was produced (`derived` | `authored` | `inferred`).
54    pub provenance: &'static str,
55    /// Confidence score, present only for inferred edges.
56    pub confidence: Option<f64>,
57    /// The natural key of the node at the other end.
58    pub node: String,
59}
60
61/// A node together with its provenance-labelled neighbourhood.
62#[derive(Debug, Clone, PartialEq, Serialize)]
63pub struct Explanation {
64    /// Stable schema tag ([`SCHEMA`]).
65    pub schema: &'static str,
66    /// The subject node.
67    pub node: NodeSummary,
68    /// Structured metadata attached to the node.
69    pub meta: serde_json::Value,
70    /// Edges where the subject is the source.
71    pub outgoing: Vec<EdgeRef>,
72    /// Edges where the subject is the destination.
73    pub incoming: Vec<EdgeRef>,
74}
75
76/// A listing of all nodes of one kind.
77#[derive(Debug, Clone, PartialEq, Serialize)]
78pub struct Listing {
79    /// Stable schema tag ([`SCHEMA`]).
80    pub schema: &'static str,
81    /// The kind that was listed.
82    pub kind: String,
83    /// Matching nodes, ordered by key.
84    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    // Include provenance so edges differing only in provenance have a total,
107    // stable order; with the edge-uniqueness constraint this key is unique.
108    refs.sort_by(|a, b| (&a.kind, &a.node, a.provenance).cmp(&(&b.kind, &b.node, b.provenance)));
109}
110
111/// Explain a node: its record plus every incoming and outgoing edge, each
112/// labelled with provenance. Returns `None` if no node has that key.
113///
114/// # Errors
115/// Returns [`StoreError`] on query failure.
116pub 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
133/// List every node of the given `kind`, ordered by key.
134///
135/// # Errors
136/// Returns [`StoreError`] on query failure.
137pub 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        // Outgoing: derived call to helper.
185        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        // Incoming: authored reference from the ADR.
191        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        // Two edges A->B with the same kind but different provenance must sort
205        // into a stable, deterministic order (authored before derived).
206        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        // Outgoing authored reference is present with its provenance label.
236        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}