Skip to main content

rto_graph/
model.rs

1//! In-memory graph domain types: nodes, edges, and the [`FactSet`] that groups
2//! the facts extracted from a single source blob.
3//!
4//! Node and edge *kinds* are open sets: known variants have stable string
5//! tokens, and any other token round-trips through [`NodeKind::Other`] /
6//! [`EdgeKind::Other`] so new extractors can introduce kinds without a schema
7//! change. Nodes are addressed by a deterministic natural [`Node::key`]; edges
8//! reference their endpoints by that key, and the store resolves keys to row
9//! ids on insert.
10
11use serde::{Deserialize, Deserializer, Serialize, Serializer};
12
13use crate::Provenance;
14
15/// The kind of a graph node.
16///
17/// Known kinds have stable tokens (`fn`, `struct`, …); unrecognised tokens are
18/// preserved verbatim in [`NodeKind::Other`].
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub enum NodeKind {
21    /// A function or method.
22    Fn,
23    /// A struct type.
24    Struct,
25    /// An enum type.
26    Enum,
27    /// A trait / interface.
28    Trait,
29    /// A module or namespace.
30    Module,
31    /// A source file.
32    File,
33    /// An Architecture Decision Record.
34    Adr,
35    /// A section within an ADR.
36    AdrSection,
37    /// A blueprint document.
38    Blueprint,
39    /// A free-form documentation artifact.
40    Doc,
41    /// Any kind not covered above, kept verbatim.
42    Other(String),
43}
44
45impl NodeKind {
46    /// The stable string token for this kind, as stored in the database.
47    #[must_use]
48    pub fn as_str(&self) -> &str {
49        match self {
50            Self::Fn => "fn",
51            Self::Struct => "struct",
52            Self::Enum => "enum",
53            Self::Trait => "trait",
54            Self::Module => "module",
55            Self::File => "file",
56            Self::Adr => "adr",
57            Self::AdrSection => "adr_section",
58            Self::Blueprint => "blueprint",
59            Self::Doc => "doc",
60            Self::Other(s) => s,
61        }
62    }
63
64    /// Parse a kind from its string token. Unknown tokens become
65    /// [`NodeKind::Other`], so this is infallible.
66    #[must_use]
67    pub fn from_token(s: &str) -> Self {
68        match s {
69            "fn" => Self::Fn,
70            "struct" => Self::Struct,
71            "enum" => Self::Enum,
72            "trait" => Self::Trait,
73            "module" => Self::Module,
74            "file" => Self::File,
75            "adr" => Self::Adr,
76            "adr_section" => Self::AdrSection,
77            "blueprint" => Self::Blueprint,
78            "doc" => Self::Doc,
79            other => Self::Other(other.to_owned()),
80        }
81    }
82}
83
84/// The kind of a graph edge (the relationship it records).
85///
86/// Known kinds have stable tokens; unrecognised tokens are preserved in
87/// [`EdgeKind::Other`].
88#[derive(Debug, Clone, PartialEq, Eq)]
89pub enum EdgeKind {
90    /// Source calls target.
91    Calls,
92    /// Source imports target.
93    Imports,
94    /// Source defines target.
95    Defines,
96    /// Source contains target (structural nesting).
97    Contains,
98    /// Source references target (unspecified use).
99    References,
100    /// Source supersedes target (e.g. a later ADR).
101    Supersedes,
102    /// Target is authored by / documented in source.
103    AuthoredBy,
104    /// Target is inferred from source.
105    InferredFrom,
106    /// Any kind not covered above, kept verbatim.
107    Other(String),
108}
109
110impl EdgeKind {
111    /// The stable string token for this kind, as stored in the database.
112    #[must_use]
113    pub fn as_str(&self) -> &str {
114        match self {
115            Self::Calls => "calls",
116            Self::Imports => "imports",
117            Self::Defines => "defines",
118            Self::Contains => "contains",
119            Self::References => "references",
120            Self::Supersedes => "supersedes",
121            Self::AuthoredBy => "authored_by",
122            Self::InferredFrom => "inferred_from",
123            Self::Other(s) => s,
124        }
125    }
126
127    /// Parse a kind from its string token. Unknown tokens become
128    /// [`EdgeKind::Other`], so this is infallible.
129    #[must_use]
130    pub fn from_token(s: &str) -> Self {
131        match s {
132            "calls" => Self::Calls,
133            "imports" => Self::Imports,
134            "defines" => Self::Defines,
135            "contains" => Self::Contains,
136            "references" => Self::References,
137            "supersedes" => Self::Supersedes,
138            "authored_by" => Self::AuthoredBy,
139            "inferred_from" => Self::InferredFrom,
140            other => Self::Other(other.to_owned()),
141        }
142    }
143}
144
145// Kinds (de)serialize as their bare string token so on-disk fact sets and the
146// database agree on representation.
147impl Serialize for NodeKind {
148    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
149        serializer.serialize_str(self.as_str())
150    }
151}
152
153impl<'de> Deserialize<'de> for NodeKind {
154    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
155        let s = String::deserialize(deserializer)?;
156        Ok(Self::from_token(&s))
157    }
158}
159
160impl Serialize for EdgeKind {
161    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
162        serializer.serialize_str(self.as_str())
163    }
164}
165
166impl<'de> Deserialize<'de> for EdgeKind {
167    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
168        let s = String::deserialize(deserializer)?;
169        Ok(Self::from_token(&s))
170    }
171}
172
173/// A byte-offset range within a source blob (`start..end`).
174#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
175pub struct Span {
176    /// Inclusive start byte offset.
177    pub start: u32,
178    /// Exclusive end byte offset.
179    pub end: u32,
180}
181
182impl Span {
183    /// Construct a span from a start and end byte offset.
184    #[must_use]
185    pub fn new(start: u32, end: u32) -> Self {
186        Self { start, end }
187    }
188}
189
190/// A node in the knowledge graph.
191///
192/// [`Node::key`] is the deterministic natural identity used for upserts (e.g.
193/// `sym:rust:src/lib.rs#Store`); the database row id is an internal detail.
194#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
195pub struct Node {
196    /// Deterministic, unique natural key.
197    pub key: String,
198    /// The kind of thing this node represents.
199    pub kind: NodeKind,
200    /// Human-facing name (need not be unique).
201    pub name: String,
202    /// Repository-relative source path, if any.
203    pub path: Option<String>,
204    /// Language token (e.g. `rust`), if applicable.
205    pub lang: Option<String>,
206    /// Git blob hash this node was extracted from, if applicable.
207    pub blob_hash: Option<String>,
208    /// Byte span within the source blob, if applicable.
209    pub span: Option<Span>,
210    /// Arbitrary structured metadata.
211    #[serde(default)]
212    pub meta: serde_json::Value,
213}
214
215impl Node {
216    /// Construct a node with the given key, kind, and name; all optional fields
217    /// unset and `meta` null.
218    #[must_use]
219    pub fn new(key: impl Into<String>, kind: NodeKind, name: impl Into<String>) -> Self {
220        Self {
221            key: key.into(),
222            kind,
223            name: name.into(),
224            path: None,
225            lang: None,
226            blob_hash: None,
227            span: None,
228            meta: serde_json::Value::Null,
229        }
230    }
231}
232
233/// An edge in the knowledge graph, connecting two nodes by their keys.
234///
235/// The [`Provenance`] invariant is enforced on insert: `confidence` is present
236/// if and only if the provenance is [`Provenance::Inferred`].
237#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
238pub struct Edge {
239    /// Natural key of the source node.
240    pub src: String,
241    /// Natural key of the destination node.
242    pub dst: String,
243    /// The relationship this edge records.
244    pub kind: EdgeKind,
245    /// How this edge was produced.
246    pub provenance: Provenance,
247    /// Confidence score in `0.0..=1.0`; `Some` iff `provenance` is inferred.
248    pub confidence: Option<f64>,
249    /// Where the fact came from (e.g. `blob#span`, or an ADR id).
250    pub src_ref: Option<String>,
251}
252
253impl Edge {
254    /// Construct a `derived` edge (no confidence).
255    #[must_use]
256    pub fn derived(src: impl Into<String>, dst: impl Into<String>, kind: EdgeKind) -> Self {
257        Self {
258            src: src.into(),
259            dst: dst.into(),
260            kind,
261            provenance: Provenance::Derived,
262            confidence: None,
263            src_ref: None,
264        }
265    }
266
267    /// Construct an `authored` edge (no confidence).
268    #[must_use]
269    pub fn authored(src: impl Into<String>, dst: impl Into<String>, kind: EdgeKind) -> Self {
270        Self {
271            src: src.into(),
272            dst: dst.into(),
273            kind,
274            provenance: Provenance::Authored,
275            confidence: None,
276            src_ref: None,
277        }
278    }
279
280    /// Construct an `inferred` edge carrying a confidence score.
281    #[must_use]
282    pub fn inferred(
283        src: impl Into<String>,
284        dst: impl Into<String>,
285        kind: EdgeKind,
286        confidence: f64,
287    ) -> Self {
288        Self {
289            src: src.into(),
290            dst: dst.into(),
291            kind,
292            provenance: Provenance::Inferred,
293            confidence: Some(confidence),
294            src_ref: None,
295        }
296    }
297
298    /// Whether this edge is valid for storage: a confidence score is present
299    /// exactly when the edge is inferred, and any present score is a finite
300    /// value in `0.0..=1.0` (rejecting NaN, infinities, and out-of-range).
301    #[must_use]
302    pub fn is_valid(&self) -> bool {
303        let inferred = matches!(self.provenance, Provenance::Inferred);
304        match self.confidence {
305            Some(c) => inferred && (0.0..=1.0).contains(&c),
306            None => !inferred,
307        }
308    }
309}
310
311/// The set of nodes and edges extracted from a single source blob (or otherwise
312/// assembled together). Applying a fact set to a [`crate::Store`] is atomic.
313#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
314pub struct FactSet {
315    /// Nodes to upsert.
316    pub nodes: Vec<Node>,
317    /// Edges to insert (endpoints must resolve to nodes in this set or already
318    /// present in the store).
319    pub edges: Vec<Edge>,
320}
321
322impl FactSet {
323    /// An empty fact set.
324    #[must_use]
325    pub fn new() -> Self {
326        Self::default()
327    }
328
329    /// Add a node, returning `self` for chaining.
330    #[must_use]
331    pub fn with_node(mut self, node: Node) -> Self {
332        self.nodes.push(node);
333        self
334    }
335
336    /// Add an edge, returning `self` for chaining.
337    #[must_use]
338    pub fn with_edge(mut self, edge: Edge) -> Self {
339        self.edges.push(edge);
340        self
341    }
342
343    /// Whether the fact set has no nodes and no edges.
344    #[must_use]
345    pub fn is_empty(&self) -> bool {
346        self.nodes.is_empty() && self.edges.is_empty()
347    }
348}
349
350/// Direction of traversal when querying a node's neighbours.
351#[derive(Debug, Clone, Copy, PartialEq, Eq)]
352pub enum Direction {
353    /// Follow edges where the node is the source.
354    Outgoing,
355    /// Follow edges where the node is the destination.
356    Incoming,
357    /// Follow edges in either direction.
358    Both,
359}
360
361#[cfg(test)]
362mod tests {
363    use super::{Edge, EdgeKind, FactSet, Node, NodeKind};
364    use crate::Provenance;
365
366    #[test]
367    fn node_kind_tokens_round_trip() {
368        let kinds = [
369            NodeKind::Fn,
370            NodeKind::Struct,
371            NodeKind::Enum,
372            NodeKind::Trait,
373            NodeKind::Module,
374            NodeKind::File,
375            NodeKind::Adr,
376            NodeKind::AdrSection,
377            NodeKind::Blueprint,
378            NodeKind::Doc,
379            NodeKind::Other("weird".to_owned()),
380        ];
381        for k in kinds {
382            assert_eq!(NodeKind::from_token(k.as_str()), k);
383        }
384    }
385
386    #[test]
387    fn edge_kind_tokens_round_trip() {
388        let kinds = [
389            EdgeKind::Calls,
390            EdgeKind::Imports,
391            EdgeKind::Defines,
392            EdgeKind::Contains,
393            EdgeKind::References,
394            EdgeKind::Supersedes,
395            EdgeKind::AuthoredBy,
396            EdgeKind::InferredFrom,
397            EdgeKind::Other("weird".to_owned()),
398        ];
399        for k in kinds {
400            assert_eq!(EdgeKind::from_token(k.as_str()), k);
401        }
402    }
403
404    #[test]
405    fn kinds_serialize_as_bare_tokens() {
406        assert_eq!(
407            serde_json::to_string(&NodeKind::AdrSection).unwrap(),
408            "\"adr_section\""
409        );
410        assert_eq!(
411            serde_json::to_string(&EdgeKind::AuthoredBy).unwrap(),
412            "\"authored_by\""
413        );
414        let k: NodeKind = serde_json::from_str("\"struct\"").unwrap();
415        assert_eq!(k, NodeKind::Struct);
416    }
417
418    #[test]
419    fn edge_validity_tracks_provenance() {
420        assert!(Edge::derived("a", "b", EdgeKind::Calls).is_valid());
421        assert!(Edge::authored("a", "b", EdgeKind::AuthoredBy).is_valid());
422        assert!(Edge::inferred("a", "b", EdgeKind::References, 0.5).is_valid());
423        // Boundary values are valid.
424        assert!(Edge::inferred("a", "b", EdgeKind::References, 0.0).is_valid());
425        assert!(Edge::inferred("a", "b", EdgeKind::References, 1.0).is_valid());
426
427        let inferred = Edge::inferred("a", "b", EdgeKind::References, 0.5);
428        // Non-inferred edge carrying confidence is invalid.
429        let bad = Edge {
430            provenance: Provenance::Derived,
431            confidence: Some(0.9),
432            ..Edge::derived("a", "b", EdgeKind::Calls)
433        };
434        assert!(!bad.is_valid());
435        // Inferred edge without confidence is invalid.
436        assert!(
437            !Edge {
438                confidence: None,
439                ..inferred.clone()
440            }
441            .is_valid()
442        );
443        // Out-of-range and non-finite confidences are invalid.
444        for c in [-0.1, 1.1, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
445            assert!(
446                !Edge {
447                    confidence: Some(c),
448                    ..inferred.clone()
449                }
450                .is_valid(),
451                "confidence {c} should be rejected"
452            );
453        }
454    }
455
456    #[test]
457    fn factset_builders() {
458        let fs = FactSet::new()
459            .with_node(Node::new("a", NodeKind::Fn, "a"))
460            .with_edge(Edge::derived("a", "a", EdgeKind::Calls));
461        assert_eq!(fs.nodes.len(), 1);
462        assert_eq!(fs.edges.len(), 1);
463        assert!(!fs.is_empty());
464        assert!(FactSet::new().is_empty());
465    }
466}