1use serde::{Deserialize, Serialize};
23
24use super::anchor::validate_anchor_path;
25use super::{require_schema, validate_text};
26use crate::ValidationError;
27
28pub const CODE_GRAPH_SCHEMA: &str = "leanctx.hive.code-graph/v1";
29pub const CODE_GRAPH_PUT_SCHEMA: &str = "leanctx.hive.code-graph-put/v1";
30pub const CODE_GRAPH_ACK_SCHEMA: &str = "leanctx.hive.code-graph-ack/v1";
31pub const MAX_CODE_GRAPH_NODES: usize = super::graph::MAX_GRAPH_NODES;
32pub const MAX_CODE_GRAPH_EDGES: usize = super::graph::MAX_GRAPH_EDGES;
33pub const MAX_CODE_NODE_ID_CHARS: usize = 700;
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(rename_all = "snake_case")]
39pub enum CodeGraphMode {
40 Anchors,
41 Full,
42 #[serde(other)]
43 Unknown,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
47#[serde(rename_all = "snake_case")]
48pub enum CodeNodeKind {
49 File,
50 Symbol,
51 #[serde(other)]
52 Unknown,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
56#[serde(rename_all = "snake_case")]
57pub enum CodeEdgeKind {
58 Contains,
60 Imports,
62 Calls,
64 #[serde(other)]
65 Unknown,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
70#[serde(deny_unknown_fields)]
71pub struct CodeNodeV1 {
72 pub id: String,
73 pub kind: CodeNodeKind,
74 #[serde(default, skip_serializing_if = "Option::is_none")]
76 pub lang: Option<String>,
77 #[serde(default, skip_serializing_if = "Option::is_none")]
79 pub symbol_kind: Option<String>,
80}
81
82impl CodeNodeV1 {
83 fn validate(&self) -> Result<(), ValidationError> {
84 if self.id.chars().count() > MAX_CODE_NODE_ID_CHARS {
85 return Err(ValidationError::new(format!(
86 "a code node id has at most {MAX_CODE_NODE_ID_CHARS} characters"
87 )));
88 }
89 let rest = self
90 .id
91 .strip_prefix("code:")
92 .ok_or_else(|| ValidationError::new("code node ids start with code:"))?;
93 let (path, symbol) = match rest.split_once('#') {
94 Some((path, symbol)) => (path, Some(symbol)),
95 None => (rest, None),
96 };
97 validate_anchor_path(path)?;
98 match (self.kind, symbol) {
99 (CodeNodeKind::File, None) => {}
100 (CodeNodeKind::Symbol, Some(symbol)) => {
101 validate_text(
102 symbol,
103 super::anchor::MAX_ANCHOR_SYMBOL_CHARS,
104 "symbol",
105 false,
106 )?;
107 }
108 _ => {
109 return Err(ValidationError::new(
110 "a file node has no #symbol and a symbol node has one",
111 ));
112 }
113 }
114 for (hint, field) in [(&self.lang, "lang"), (&self.symbol_kind, "symbol_kind")] {
115 if let Some(hint) = hint
116 && (hint.is_empty()
117 || hint.len() > 32
118 || !hint
119 .bytes()
120 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_'))
121 {
122 return Err(ValidationError::new(format!(
123 "{field} is 1-32 characters of [a-z0-9_]"
124 )));
125 }
126 }
127 Ok(())
128 }
129}
130
131#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
132#[serde(deny_unknown_fields)]
133pub struct CodeEdgeV1 {
134 pub from: String,
135 pub to: String,
136 #[serde(rename = "type")]
137 pub kind: CodeEdgeKind,
138}
139
140#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
142#[serde(deny_unknown_fields)]
143pub struct CodeGraphV1 {
144 pub schema: String,
145 #[serde(default, skip_serializing_if = "Option::is_none")]
147 pub revision_hint: Option<String>,
148 pub nodes: Vec<CodeNodeV1>,
150 pub edges: Vec<CodeEdgeV1>,
152}
153
154impl CodeGraphV1 {
155 pub fn validate(&self) -> Result<(), ValidationError> {
156 require_schema(&self.schema, CODE_GRAPH_SCHEMA)?;
157 if let Some(hint) = &self.revision_hint
158 && !(matches!(hint.len(), 40 | 64)
159 && hint
160 .bytes()
161 .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)))
162 {
163 return Err(ValidationError::new("revision_hint is a git commit id"));
164 }
165 if self.nodes.len() > MAX_CODE_GRAPH_NODES || self.edges.len() > MAX_CODE_GRAPH_EDGES {
166 return Err(ValidationError::new(format!(
167 "a code graph has at most {MAX_CODE_GRAPH_NODES} nodes and {MAX_CODE_GRAPH_EDGES} edges"
168 )));
169 }
170 for (i, node) in self.nodes.iter().enumerate() {
171 node.validate()?;
172 if i > 0 && self.nodes[i - 1].id >= node.id {
173 return Err(ValidationError::new(
174 "nodes must be sorted by id without repeats",
175 ));
176 }
177 }
178 let known = |id: &str| {
179 self.nodes
180 .binary_search_by(|node| node.id.as_str().cmp(id))
181 .is_ok()
182 };
183 for (i, edge) in self.edges.iter().enumerate() {
184 if edge.kind == CodeEdgeKind::Unknown {
185 return Err(ValidationError::new("unknown edge type"));
186 }
187 if !known(&edge.from) || !known(&edge.to) {
188 return Err(ValidationError::new("an edge names a missing node"));
189 }
190 if i > 0 {
191 let prev = &self.edges[i - 1];
192 if (&prev.from, &prev.to, prev.kind) >= (&edge.from, &edge.to, edge.kind) {
193 return Err(ValidationError::new("edges must be sorted without repeats"));
194 }
195 }
196 }
197 Ok(())
198 }
199}
200
201#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
203#[serde(deny_unknown_fields)]
204pub struct CodeGraphPutV1 {
205 pub schema: String,
206 pub graph: CodeGraphV1,
207}
208
209impl CodeGraphPutV1 {
210 pub fn validate(&self) -> Result<(), ValidationError> {
211 require_schema(&self.schema, CODE_GRAPH_PUT_SCHEMA)?;
212 self.graph.validate()
213 }
214}
215
216#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
217pub struct CodeGraphAckV1 {
218 pub schema: String,
219 pub nodes: u64,
221 pub edges: u64,
222 #[serde(default, skip_serializing_if = "Option::is_none")]
223 pub revision_hint: Option<String>,
224}