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