eidos-kernel 0.1.0

Eidos kernel — the pure-logic brain engine (schema, retrieval, ranking, eval). No IO.
Documentation
//! Indexed read view over a [`Graph`].
//!
//! The serialized graph stays as simple vectors for deterministic artifacts. Hot read paths build
//! this borrowed view when they need repeated node/edge lookups.

use std::collections::HashMap;

use crate::schema::{Edge, Graph, Kind, Node};

#[derive(Debug)]
pub struct GraphIndex<'a> {
    nodes_by_id: HashMap<&'a str, &'a Node>,
    incoming_by_id: HashMap<&'a str, Vec<&'a Edge>>,
    outgoing_by_id: HashMap<&'a str, Vec<&'a Edge>>,
}

impl<'a> GraphIndex<'a> {
    pub fn build(graph: &'a Graph) -> Self {
        let mut nodes_by_id = HashMap::with_capacity(graph.nodes.len());
        for node in &graph.nodes {
            nodes_by_id.insert(node.id.as_str(), node);
        }

        let mut incoming_by_id: HashMap<&'a str, Vec<&'a Edge>> = HashMap::new();
        let mut outgoing_by_id: HashMap<&'a str, Vec<&'a Edge>> = HashMap::new();
        for edge in &graph.edges {
            outgoing_by_id
                .entry(edge.from.as_str())
                .or_default()
                .push(edge);
            incoming_by_id
                .entry(edge.to.as_str())
                .or_default()
                .push(edge);
        }

        for edges in incoming_by_id.values_mut() {
            edges.sort_by(|a, b| (&a.from, &a.to, &a.relation).cmp(&(&b.from, &b.to, &b.relation)));
        }
        for edges in outgoing_by_id.values_mut() {
            edges.sort_by(|a, b| (&a.from, &a.to, &a.relation).cmp(&(&b.from, &b.to, &b.relation)));
        }

        Self {
            nodes_by_id,
            incoming_by_id,
            outgoing_by_id,
        }
    }

    pub fn node(&self, id: &str) -> Option<&'a Node> {
        self.nodes_by_id.get(id).copied()
    }

    pub fn is_kind(&self, id: &str, kind: Kind) -> bool {
        self.node(id).is_some_and(|node| node.kind == kind)
    }

    pub fn incoming(&self, id: &str) -> impl Iterator<Item = &'a Edge> + '_ {
        self.incoming_by_id
            .get(id)
            .into_iter()
            .flat_map(|edges| edges.iter().copied())
    }

    pub fn outgoing(&self, id: &str) -> impl Iterator<Item = &'a Edge> + '_ {
        self.outgoing_by_id
            .get(id)
            .into_iter()
            .flat_map(|edges| edges.iter().copied())
    }

    pub fn has_outgoing(&self, from: &str, relation: &str, to: &str) -> bool {
        self.outgoing(from)
            .any(|edge| edge.relation == relation && edge.to == to)
    }

    pub fn source_of(&self, node_id: &str) -> Option<String> {
        let node = self.node(node_id)?;
        if let Some(span) = &node.span {
            if span.start_line == span.end_line {
                return Some(format!("{}:{}", span.path, span.start_line));
            }
            return Some(format!(
                "{}:{}-{}",
                span.path, span.start_line, span.end_line
            ));
        }
        node.source_files.first().cloned()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::schema::{Edge, Kind, Node, Span, relation};

    fn node(id: &str, kind: Kind) -> Node {
        Node {
            id: id.into(),
            kind,
            subkind: None,
            title: id.into(),
            summary: String::new(),
            aliases: Vec::new(),
            tags: Vec::new(),
            query_examples: Vec::new(),
            source_files: vec![format!("fixtures/{id}.rs")],
            span: None,
            partition: None,
        }
    }

    #[test]
    fn indexes_nodes_and_edges() {
        let graph = Graph {
            nodes: vec![node("a", Kind::Function), node("b", Kind::Type)],
            edges: vec![Edge {
                from: "a".into(),
                to: "b".into(),
                relation: relation::REFERENCES.into(),
                evidence: "test".into(),
                ..Default::default()
            }],
            ..Default::default()
        };

        let index = GraphIndex::build(&graph);

        assert!(index.is_kind("a", Kind::Function));
        assert_eq!(index.node("b").map(|n| n.title.as_str()), Some("b"));
        assert!(index.has_outgoing("a", relation::REFERENCES, "b"));
        assert_eq!(index.incoming("b").count(), 1);
        assert_eq!(index.outgoing("a").count(), 1);
    }

    #[test]
    fn source_receipt_prefers_exact_span() {
        let mut n = node("a", Kind::Function);
        n.span = Some(Span {
            path: "fixtures/a.rs".into(),
            start_line: 4,
            end_line: 7,
        });
        let graph = Graph {
            nodes: vec![n],
            edges: Vec::new(),
            ..Default::default()
        };

        let index = GraphIndex::build(&graph);

        assert_eq!(index.source_of("a").as_deref(), Some("fixtures/a.rs:4-7"));
    }
}