Skip to main content

lean_ctx_protocol/hive/
graph.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! The vault graph view (C0 1.9, section 3).
4//!
5//! `GET /v1/vaults/{vault_id}/graph?revision=` → [`GraphV1`] (right
6//! `knowledge.read`): one released revision's knowledge as typed nodes and
7//! edges, for the web view and for engines that link it to their code graph.
8//! Hive derives it from the document, the anchors, the tasks and the
9//! contribution credits; nobody writes it.
10//!
11//! With a published code graph (section 8, `code_graph` `full`, clear
12//! vaults) the code nodes and their `contains|imports|calls` edges come
13//! from it as well.
14//!
15//! Node ids are typed so they never collide: `fact:<fact id>`,
16//! `page:<page id>`, `code:<path>` or `code:<path>#<symbol>`,
17//! `task:<number>`, `proposal:<number>`, `actor:<label>`.
18
19use serde::{Deserialize, Serialize};
20
21use super::{require_schema, validate_text};
22use crate::ValidationError;
23
24pub const GRAPH_SCHEMA: &str = "leanctx.hive.graph/v1";
25pub const MAX_GRAPH_NODES: usize = 20_000;
26pub const MAX_GRAPH_EDGES: usize = 60_000;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
29#[serde(rename_all = "snake_case")]
30pub enum GraphNodeKind {
31    /// A path, or a symbol in it (from anchors).
32    Code,
33    /// A fact or a page.
34    Knowledge,
35    /// A task or a proposal.
36    Work,
37    /// A person or an agent instance (by its public label).
38    Actor,
39    #[serde(other)]
40    Unknown,
41}
42
43/// Edge types: the document's relation types between facts
44/// (`knowledge::RELATION_TYPES`), plus the derived ones.
45pub const GRAPH_EDGE_TYPES: [&str; 8] = [
46    // fact → code: an anchor of the fact.
47    "applies_to",
48    // 1.9 section 8, from a published code graph: file → symbol, file →
49    // file, symbol → symbol.
50    "contains",
51    "imports",
52    "calls",
53    // page → fact: the page shows the fact.
54    "shows",
55    // task/proposal → fact: the work added or changed the fact.
56    "changed",
57    // actor → task/proposal: who did the work.
58    "worked_on",
59    // fact → fact, task → fact: an open doubt or question about it.
60    "questions",
61];
62
63#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
64pub struct GraphNodeV1 {
65    pub id: String,
66    pub kind: GraphNodeKind,
67    pub label: String,
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
71pub struct GraphEdgeV1 {
72    pub from: String,
73    pub to: String,
74    #[serde(rename = "type")]
75    pub kind: String,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
79pub struct GraphV1 {
80    pub schema: String,
81    pub vault_id: String,
82    pub revision: u64,
83    /// Sorted by `id`.
84    pub nodes: Vec<GraphNodeV1>,
85    /// Sorted by `(from, to, type)`; both ends are nodes.
86    pub edges: Vec<GraphEdgeV1>,
87}
88
89impl GraphV1 {
90    pub fn validate(&self) -> Result<(), ValidationError> {
91        require_schema(&self.schema, GRAPH_SCHEMA)?;
92        super::validate_vault_id(&self.vault_id)?;
93        if self.nodes.len() > MAX_GRAPH_NODES || self.edges.len() > MAX_GRAPH_EDGES {
94            return Err(ValidationError::new(format!(
95                "a graph has at most {MAX_GRAPH_NODES} nodes and {MAX_GRAPH_EDGES} edges"
96            )));
97        }
98        for (i, node) in self.nodes.iter().enumerate() {
99            validate_text(&node.id, 700, "node id", false)?;
100            validate_text(&node.label, 300, "node label", false)?;
101            if i > 0 && self.nodes[i - 1].id >= node.id {
102                return Err(ValidationError::new(
103                    "nodes must be sorted by id without repeats",
104                ));
105            }
106        }
107        let known = |id: &str| {
108            self.nodes
109                .binary_search_by(|node| node.id.as_str().cmp(id))
110                .is_ok()
111        };
112        for (i, edge) in self.edges.iter().enumerate() {
113            if !known(&edge.from) || !known(&edge.to) {
114                return Err(ValidationError::new("an edge names a missing node"));
115            }
116            if !GRAPH_EDGE_TYPES.contains(&edge.kind.as_str())
117                && !super::knowledge::RELATION_TYPES.contains(&edge.kind.as_str())
118            {
119                return Err(ValidationError::new("unknown edge type"));
120            }
121            if i > 0 {
122                let prev = &self.edges[i - 1];
123                if (&prev.from, &prev.to, &prev.kind) >= (&edge.from, &edge.to, &edge.kind) {
124                    return Err(ValidationError::new("edges must be sorted without repeats"));
125                }
126            }
127        }
128        Ok(())
129    }
130}