Skip to main content

rto_graph/
extract.rs

1//! Extraction: turning the bytes of a source blob into a [`FactSet`].
2//!
3//! Extraction must be a deterministic pure function of `(path, blob_id, bytes)`
4//! so its output can be cached; because the facts are path-dependent (node keys
5//! are path-scoped), the cache is keyed by both path and blob id (see
6//! [`crate::sync`]). Language-aware extraction (tree-sitter) arrives in a later
7//! stage; [`FileNodeExtractor`] is the minimal placeholder that lets the sync
8//! pipeline run end-to-end today.
9
10use crate::{FactSet, Node, NodeKind, Span};
11
12/// Turns one source blob into the nodes and edges derived from it.
13pub trait Extractor {
14    /// Extract a [`FactSet`] from a blob's `path`, git `blob_id`, and `bytes`.
15    ///
16    /// Implementations must be deterministic: identical inputs must always
17    /// produce an identical fact set.
18    fn extract(&self, path: &str, blob_id: &str, bytes: &[u8]) -> FactSet;
19}
20
21/// Placeholder extractor: emits a single `file` node per blob, tagged with its
22/// blob hash and basic size metadata. Produces no edges. Superseded by the
23/// tree-sitter extractor in a later stage.
24#[derive(Debug, Clone, Copy, Default)]
25pub struct FileNodeExtractor;
26
27impl Extractor for FileNodeExtractor {
28    fn extract(&self, path: &str, blob_id: &str, bytes: &[u8]) -> FactSet {
29        let name = path.rsplit('/').next().unwrap_or(path).to_owned();
30        let lines = bytes
31            .iter()
32            .fold(0usize, |n, &b| n + usize::from(b == b'\n'));
33        let end = u32::try_from(bytes.len()).unwrap_or(u32::MAX);
34        let node = Node {
35            key: format!("file:{path}"),
36            kind: NodeKind::File,
37            name,
38            path: Some(path.to_owned()),
39            lang: None,
40            blob_hash: Some(blob_id.to_owned()),
41            span: Some(Span::new(0, end)),
42            meta: serde_json::json!({ "bytes": bytes.len(), "lines": lines }),
43        };
44        FactSet::new().with_node(node)
45    }
46}
47
48#[cfg(test)]
49mod tests {
50    use super::{Extractor, FileNodeExtractor};
51    use crate::NodeKind;
52
53    #[test]
54    fn file_node_extractor_is_deterministic_and_tagged() {
55        let ex = FileNodeExtractor;
56        let a = ex.extract("src/lib.rs", "abc123", b"one\ntwo\n");
57        let b = ex.extract("src/lib.rs", "abc123", b"one\ntwo\n");
58        assert_eq!(a, b, "extraction must be deterministic");
59
60        assert_eq!(a.nodes.len(), 1);
61        assert!(a.edges.is_empty());
62        let node = &a.nodes[0];
63        assert_eq!(node.key, "file:src/lib.rs");
64        assert_eq!(node.kind, NodeKind::File);
65        assert_eq!(node.name, "lib.rs");
66        assert_eq!(node.blob_hash.as_deref(), Some("abc123"));
67        assert_eq!(node.meta["lines"], 2);
68        assert_eq!(node.meta["bytes"], 8);
69    }
70}