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