1use serde::{Deserialize, Deserializer, Serialize, Serializer};
12
13use crate::Provenance;
14
15#[derive(Debug, Clone, PartialEq, Eq)]
20pub enum NodeKind {
21 Fn,
23 Struct,
25 Enum,
27 Trait,
29 Module,
31 File,
33 Adr,
35 AdrSection,
37 Blueprint,
39 Doc,
41 Marker,
43 Other(String),
45}
46
47impl NodeKind {
48 #[must_use]
50 pub fn as_str(&self) -> &str {
51 match self {
52 Self::Fn => "fn",
53 Self::Struct => "struct",
54 Self::Enum => "enum",
55 Self::Trait => "trait",
56 Self::Module => "module",
57 Self::File => "file",
58 Self::Adr => "adr",
59 Self::AdrSection => "adr_section",
60 Self::Blueprint => "blueprint",
61 Self::Doc => "doc",
62 Self::Marker => "marker",
63 Self::Other(s) => s,
64 }
65 }
66
67 #[must_use]
70 pub fn from_token(s: &str) -> Self {
71 match s {
72 "fn" => Self::Fn,
73 "struct" => Self::Struct,
74 "enum" => Self::Enum,
75 "trait" => Self::Trait,
76 "module" => Self::Module,
77 "file" => Self::File,
78 "adr" => Self::Adr,
79 "adr_section" => Self::AdrSection,
80 "blueprint" => Self::Blueprint,
81 "doc" => Self::Doc,
82 "marker" => Self::Marker,
83 other => Self::Other(other.to_owned()),
84 }
85 }
86}
87
88#[derive(Debug, Clone, PartialEq, Eq)]
93pub enum EdgeKind {
94 Calls,
96 Imports,
98 Defines,
100 Contains,
102 References,
104 Supersedes,
106 AuthoredBy,
108 InferredFrom,
110 Related,
112 Other(String),
114}
115
116impl EdgeKind {
117 #[must_use]
119 pub fn as_str(&self) -> &str {
120 match self {
121 Self::Calls => "calls",
122 Self::Imports => "imports",
123 Self::Defines => "defines",
124 Self::Contains => "contains",
125 Self::References => "references",
126 Self::Supersedes => "supersedes",
127 Self::AuthoredBy => "authored_by",
128 Self::InferredFrom => "inferred_from",
129 Self::Related => "related",
130 Self::Other(s) => s,
131 }
132 }
133
134 #[must_use]
137 pub fn from_token(s: &str) -> Self {
138 match s {
139 "calls" => Self::Calls,
140 "imports" => Self::Imports,
141 "defines" => Self::Defines,
142 "contains" => Self::Contains,
143 "references" => Self::References,
144 "supersedes" => Self::Supersedes,
145 "authored_by" => Self::AuthoredBy,
146 "inferred_from" => Self::InferredFrom,
147 "related" => Self::Related,
148 other => Self::Other(other.to_owned()),
149 }
150 }
151}
152
153impl Serialize for NodeKind {
156 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
157 serializer.serialize_str(self.as_str())
158 }
159}
160
161impl<'de> Deserialize<'de> for NodeKind {
162 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
163 let s = String::deserialize(deserializer)?;
164 Ok(Self::from_token(&s))
165 }
166}
167
168impl Serialize for EdgeKind {
169 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
170 serializer.serialize_str(self.as_str())
171 }
172}
173
174impl<'de> Deserialize<'de> for EdgeKind {
175 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
176 let s = String::deserialize(deserializer)?;
177 Ok(Self::from_token(&s))
178 }
179}
180
181#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
183pub struct Span {
184 pub start: u32,
186 pub end: u32,
188}
189
190impl Span {
191 #[must_use]
193 pub fn new(start: u32, end: u32) -> Self {
194 Self { start, end }
195 }
196}
197
198#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
203pub struct Node {
204 pub key: String,
206 pub kind: NodeKind,
208 pub name: String,
210 pub path: Option<String>,
212 pub lang: Option<String>,
214 pub blob_hash: Option<String>,
216 pub span: Option<Span>,
218 #[serde(default)]
220 pub meta: serde_json::Value,
221}
222
223impl Node {
224 #[must_use]
227 pub fn new(key: impl Into<String>, kind: NodeKind, name: impl Into<String>) -> Self {
228 Self {
229 key: key.into(),
230 kind,
231 name: name.into(),
232 path: None,
233 lang: None,
234 blob_hash: None,
235 span: None,
236 meta: serde_json::Value::Null,
237 }
238 }
239}
240
241#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
246pub struct Edge {
247 pub src: String,
249 pub dst: String,
251 pub kind: EdgeKind,
253 pub provenance: Provenance,
255 pub confidence: Option<f64>,
257 pub src_ref: Option<String>,
259}
260
261impl Edge {
262 #[must_use]
264 pub fn derived(src: impl Into<String>, dst: impl Into<String>, kind: EdgeKind) -> Self {
265 Self {
266 src: src.into(),
267 dst: dst.into(),
268 kind,
269 provenance: Provenance::Derived,
270 confidence: None,
271 src_ref: None,
272 }
273 }
274
275 #[must_use]
277 pub fn authored(src: impl Into<String>, dst: impl Into<String>, kind: EdgeKind) -> Self {
278 Self {
279 src: src.into(),
280 dst: dst.into(),
281 kind,
282 provenance: Provenance::Authored,
283 confidence: None,
284 src_ref: None,
285 }
286 }
287
288 #[must_use]
290 pub fn inferred(
291 src: impl Into<String>,
292 dst: impl Into<String>,
293 kind: EdgeKind,
294 confidence: f64,
295 ) -> Self {
296 Self {
297 src: src.into(),
298 dst: dst.into(),
299 kind,
300 provenance: Provenance::Inferred,
301 confidence: Some(confidence),
302 src_ref: None,
303 }
304 }
305
306 #[must_use]
310 pub fn is_valid(&self) -> bool {
311 let inferred = matches!(self.provenance, Provenance::Inferred);
312 match self.confidence {
313 Some(c) => inferred && (0.0..=1.0).contains(&c),
314 None => !inferred,
315 }
316 }
317}
318
319#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
322pub struct FactSet {
323 pub nodes: Vec<Node>,
325 pub edges: Vec<Edge>,
328}
329
330impl FactSet {
331 #[must_use]
333 pub fn new() -> Self {
334 Self::default()
335 }
336
337 #[must_use]
339 pub fn with_node(mut self, node: Node) -> Self {
340 self.nodes.push(node);
341 self
342 }
343
344 #[must_use]
346 pub fn with_edge(mut self, edge: Edge) -> Self {
347 self.edges.push(edge);
348 self
349 }
350
351 #[must_use]
353 pub fn is_empty(&self) -> bool {
354 self.nodes.is_empty() && self.edges.is_empty()
355 }
356}
357
358#[derive(Debug, Clone, Copy, PartialEq, Eq)]
360pub enum Direction {
361 Outgoing,
363 Incoming,
365 Both,
367}
368
369#[cfg(test)]
370mod tests {
371 use super::{Edge, EdgeKind, FactSet, Node, NodeKind};
372 use crate::Provenance;
373
374 #[test]
375 fn node_kind_tokens_round_trip() {
376 let kinds = [
377 NodeKind::Fn,
378 NodeKind::Struct,
379 NodeKind::Enum,
380 NodeKind::Trait,
381 NodeKind::Module,
382 NodeKind::File,
383 NodeKind::Adr,
384 NodeKind::AdrSection,
385 NodeKind::Blueprint,
386 NodeKind::Doc,
387 NodeKind::Marker,
388 NodeKind::Other("weird".to_owned()),
389 ];
390 for k in kinds {
391 assert_eq!(NodeKind::from_token(k.as_str()), k);
392 }
393 }
394
395 #[test]
396 fn edge_kind_tokens_round_trip() {
397 let kinds = [
398 EdgeKind::Calls,
399 EdgeKind::Imports,
400 EdgeKind::Defines,
401 EdgeKind::Contains,
402 EdgeKind::References,
403 EdgeKind::Supersedes,
404 EdgeKind::AuthoredBy,
405 EdgeKind::InferredFrom,
406 EdgeKind::Other("weird".to_owned()),
407 ];
408 for k in kinds {
409 assert_eq!(EdgeKind::from_token(k.as_str()), k);
410 }
411 }
412
413 #[test]
414 fn kinds_serialize_as_bare_tokens() {
415 assert_eq!(
416 serde_json::to_string(&NodeKind::AdrSection).unwrap(),
417 "\"adr_section\""
418 );
419 assert_eq!(
420 serde_json::to_string(&EdgeKind::AuthoredBy).unwrap(),
421 "\"authored_by\""
422 );
423 let k: NodeKind = serde_json::from_str("\"struct\"").unwrap();
424 assert_eq!(k, NodeKind::Struct);
425 }
426
427 #[test]
428 fn edge_validity_tracks_provenance() {
429 assert!(Edge::derived("a", "b", EdgeKind::Calls).is_valid());
430 assert!(Edge::authored("a", "b", EdgeKind::AuthoredBy).is_valid());
431 assert!(Edge::inferred("a", "b", EdgeKind::References, 0.5).is_valid());
432 assert!(Edge::inferred("a", "b", EdgeKind::References, 0.0).is_valid());
434 assert!(Edge::inferred("a", "b", EdgeKind::References, 1.0).is_valid());
435
436 let inferred = Edge::inferred("a", "b", EdgeKind::References, 0.5);
437 let bad = Edge {
439 provenance: Provenance::Derived,
440 confidence: Some(0.9),
441 ..Edge::derived("a", "b", EdgeKind::Calls)
442 };
443 assert!(!bad.is_valid());
444 assert!(
446 !Edge {
447 confidence: None,
448 ..inferred.clone()
449 }
450 .is_valid()
451 );
452 for c in [-0.1, 1.1, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
454 assert!(
455 !Edge {
456 confidence: Some(c),
457 ..inferred.clone()
458 }
459 .is_valid(),
460 "confidence {c} should be rejected"
461 );
462 }
463 }
464
465 #[test]
466 fn factset_builders() {
467 let fs = FactSet::new()
468 .with_node(Node::new("a", NodeKind::Fn, "a"))
469 .with_edge(Edge::derived("a", "a", EdgeKind::Calls));
470 assert_eq!(fs.nodes.len(), 1);
471 assert_eq!(fs.edges.len(), 1);
472 assert!(!fs.is_empty());
473 assert!(FactSet::new().is_empty());
474 }
475}