1use serde::{Deserialize, Deserializer, Serialize, Serializer};
14
15use crate::Provenance;
16
17#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum NodeKind {
23 Fn,
25 Struct,
27 Enum,
29 Trait,
31 Module,
33 File,
35 Adr,
37 AdrSection,
39 Blueprint,
41 Doc,
43 Marker,
45 Other(String),
47}
48
49impl NodeKind {
50 #[must_use]
52 pub fn as_str(&self) -> &str {
53 match self {
54 Self::Fn => "fn",
55 Self::Struct => "struct",
56 Self::Enum => "enum",
57 Self::Trait => "trait",
58 Self::Module => "module",
59 Self::File => "file",
60 Self::Adr => "adr",
61 Self::AdrSection => "adr_section",
62 Self::Blueprint => "blueprint",
63 Self::Doc => "doc",
64 Self::Marker => "marker",
65 Self::Other(s) => s,
66 }
67 }
68
69 #[must_use]
72 pub fn from_token(s: &str) -> Self {
73 match s {
74 "fn" => Self::Fn,
75 "struct" => Self::Struct,
76 "enum" => Self::Enum,
77 "trait" => Self::Trait,
78 "module" => Self::Module,
79 "file" => Self::File,
80 "adr" => Self::Adr,
81 "adr_section" => Self::AdrSection,
82 "blueprint" => Self::Blueprint,
83 "doc" => Self::Doc,
84 "marker" => Self::Marker,
85 other => Self::Other(other.to_owned()),
86 }
87 }
88}
89
90#[derive(Debug, Clone, PartialEq, Eq)]
95pub enum EdgeKind {
96 Calls,
98 Imports,
100 Defines,
102 Contains,
104 References,
106 Supersedes,
108 AuthoredBy,
110 InferredFrom,
112 Related,
114 Other(String),
116}
117
118impl EdgeKind {
119 #[must_use]
121 pub fn as_str(&self) -> &str {
122 match self {
123 Self::Calls => "calls",
124 Self::Imports => "imports",
125 Self::Defines => "defines",
126 Self::Contains => "contains",
127 Self::References => "references",
128 Self::Supersedes => "supersedes",
129 Self::AuthoredBy => "authored_by",
130 Self::InferredFrom => "inferred_from",
131 Self::Related => "related",
132 Self::Other(s) => s,
133 }
134 }
135
136 #[must_use]
139 pub fn from_token(s: &str) -> Self {
140 match s {
141 "calls" => Self::Calls,
142 "imports" => Self::Imports,
143 "defines" => Self::Defines,
144 "contains" => Self::Contains,
145 "references" => Self::References,
146 "supersedes" => Self::Supersedes,
147 "authored_by" => Self::AuthoredBy,
148 "inferred_from" => Self::InferredFrom,
149 "related" => Self::Related,
150 other => Self::Other(other.to_owned()),
151 }
152 }
153}
154
155impl Serialize for NodeKind {
158 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
159 serializer.serialize_str(self.as_str())
160 }
161}
162
163impl<'de> Deserialize<'de> for NodeKind {
164 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
165 let s = String::deserialize(deserializer)?;
166 Ok(Self::from_token(&s))
167 }
168}
169
170impl Serialize for EdgeKind {
171 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
172 serializer.serialize_str(self.as_str())
173 }
174}
175
176impl<'de> Deserialize<'de> for EdgeKind {
177 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
178 let s = String::deserialize(deserializer)?;
179 Ok(Self::from_token(&s))
180 }
181}
182
183#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
185pub struct Span {
186 pub start: u32,
188 pub end: u32,
190}
191
192impl Span {
193 #[must_use]
195 pub fn new(start: u32, end: u32) -> Self {
196 Self { start, end }
197 }
198}
199
200#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
205pub struct Node {
206 pub key: String,
208 pub kind: NodeKind,
210 pub name: String,
212 pub path: Option<String>,
214 pub lang: Option<String>,
216 pub blob_hash: Option<String>,
218 pub span: Option<Span>,
220 #[serde(default)]
232 pub provenance: Provenance,
233 #[serde(default)]
235 pub meta: serde_json::Value,
236}
237
238impl Node {
239 #[must_use]
243 pub fn new(key: impl Into<String>, kind: NodeKind, name: impl Into<String>) -> Self {
244 Self {
245 key: key.into(),
246 kind,
247 name: name.into(),
248 path: None,
249 lang: None,
250 blob_hash: None,
251 span: None,
252 provenance: Provenance::Derived,
253 meta: serde_json::Value::Null,
254 }
255 }
256
257 #[must_use]
260 pub fn with_provenance(mut self, provenance: Provenance) -> Self {
261 self.provenance = provenance;
262 self
263 }
264}
265
266#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
271pub struct Edge {
272 pub src: String,
274 pub dst: String,
276 pub kind: EdgeKind,
278 pub provenance: Provenance,
280 pub confidence: Option<f64>,
282 pub src_ref: Option<String>,
284}
285
286impl Edge {
287 #[must_use]
289 pub fn derived(src: impl Into<String>, dst: impl Into<String>, kind: EdgeKind) -> Self {
290 Self {
291 src: src.into(),
292 dst: dst.into(),
293 kind,
294 provenance: Provenance::Derived,
295 confidence: None,
296 src_ref: None,
297 }
298 }
299
300 #[must_use]
302 pub fn authored(src: impl Into<String>, dst: impl Into<String>, kind: EdgeKind) -> Self {
303 Self {
304 src: src.into(),
305 dst: dst.into(),
306 kind,
307 provenance: Provenance::Authored,
308 confidence: None,
309 src_ref: None,
310 }
311 }
312
313 #[must_use]
315 pub fn inferred(
316 src: impl Into<String>,
317 dst: impl Into<String>,
318 kind: EdgeKind,
319 confidence: f64,
320 ) -> Self {
321 Self {
322 src: src.into(),
323 dst: dst.into(),
324 kind,
325 provenance: Provenance::Inferred,
326 confidence: Some(confidence),
327 src_ref: None,
328 }
329 }
330
331 #[must_use]
335 pub fn is_valid(&self) -> bool {
336 let inferred = matches!(self.provenance, Provenance::Inferred);
337 match self.confidence {
338 Some(c) => inferred && (0.0..=1.0).contains(&c),
339 None => !inferred,
340 }
341 }
342}
343
344#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
347pub struct FactSet {
348 pub nodes: Vec<Node>,
350 pub edges: Vec<Edge>,
353}
354
355impl FactSet {
356 #[must_use]
358 pub fn new() -> Self {
359 Self::default()
360 }
361
362 #[must_use]
364 pub fn with_node(mut self, node: Node) -> Self {
365 self.nodes.push(node);
366 self
367 }
368
369 #[must_use]
371 pub fn with_edge(mut self, edge: Edge) -> Self {
372 self.edges.push(edge);
373 self
374 }
375
376 #[must_use]
378 pub fn is_empty(&self) -> bool {
379 self.nodes.is_empty() && self.edges.is_empty()
380 }
381}
382
383#[derive(Debug, Clone, Copy, PartialEq, Eq)]
385pub enum Direction {
386 Outgoing,
388 Incoming,
390 Both,
392}
393
394#[cfg(test)]
395mod tests {
396 use super::{Edge, EdgeKind, FactSet, Node, NodeKind};
397 use crate::Provenance;
398
399 #[test]
400 fn node_kind_tokens_round_trip() {
401 let kinds = [
402 NodeKind::Fn,
403 NodeKind::Struct,
404 NodeKind::Enum,
405 NodeKind::Trait,
406 NodeKind::Module,
407 NodeKind::File,
408 NodeKind::Adr,
409 NodeKind::AdrSection,
410 NodeKind::Blueprint,
411 NodeKind::Doc,
412 NodeKind::Marker,
413 NodeKind::Other("weird".to_owned()),
414 ];
415 for k in kinds {
416 assert_eq!(NodeKind::from_token(k.as_str()), k);
417 }
418 }
419
420 #[test]
421 fn edge_kind_tokens_round_trip() {
422 let kinds = [
423 EdgeKind::Calls,
424 EdgeKind::Imports,
425 EdgeKind::Defines,
426 EdgeKind::Contains,
427 EdgeKind::References,
428 EdgeKind::Supersedes,
429 EdgeKind::AuthoredBy,
430 EdgeKind::InferredFrom,
431 EdgeKind::Other("weird".to_owned()),
432 ];
433 for k in kinds {
434 assert_eq!(EdgeKind::from_token(k.as_str()), k);
435 }
436 }
437
438 #[test]
439 fn kinds_serialize_as_bare_tokens() {
440 assert_eq!(
441 serde_json::to_string(&NodeKind::AdrSection).unwrap(),
442 "\"adr_section\""
443 );
444 assert_eq!(
445 serde_json::to_string(&EdgeKind::AuthoredBy).unwrap(),
446 "\"authored_by\""
447 );
448 let k: NodeKind = serde_json::from_str("\"struct\"").unwrap();
449 assert_eq!(k, NodeKind::Struct);
450 }
451
452 #[test]
453 fn edge_validity_tracks_provenance() {
454 assert!(Edge::derived("a", "b", EdgeKind::Calls).is_valid());
455 assert!(Edge::authored("a", "b", EdgeKind::AuthoredBy).is_valid());
456 assert!(Edge::inferred("a", "b", EdgeKind::References, 0.5).is_valid());
457 assert!(Edge::inferred("a", "b", EdgeKind::References, 0.0).is_valid());
459 assert!(Edge::inferred("a", "b", EdgeKind::References, 1.0).is_valid());
460
461 let inferred = Edge::inferred("a", "b", EdgeKind::References, 0.5);
462 let bad = Edge {
464 provenance: Provenance::Derived,
465 confidence: Some(0.9),
466 ..Edge::derived("a", "b", EdgeKind::Calls)
467 };
468 assert!(!bad.is_valid());
469 assert!(
471 !Edge {
472 confidence: None,
473 ..inferred.clone()
474 }
475 .is_valid()
476 );
477 for c in [-0.1, 1.1, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
479 assert!(
480 !Edge {
481 confidence: Some(c),
482 ..inferred.clone()
483 }
484 .is_valid(),
485 "confidence {c} should be rejected"
486 );
487 }
488 }
489
490 #[test]
491 fn factset_builders() {
492 let fs = FactSet::new()
493 .with_node(Node::new("a", NodeKind::Fn, "a"))
494 .with_edge(Edge::derived("a", "a", EdgeKind::Calls));
495 assert_eq!(fs.nodes.len(), 1);
496 assert_eq!(fs.edges.len(), 1);
497 assert!(!fs.is_empty());
498 assert!(FactSet::new().is_empty());
499 }
500
501 #[test]
502 fn node_provenance_defaults_and_builder() {
503 let legacy = r#"{"key":"k","kind":"fn","name":"n","path":null,"lang":null,"blob_hash":null,"span":null,"meta":null}"#;
508 let node: Node = serde_json::from_str(legacy).expect("legacy node deserializes");
509 assert_eq!(node.provenance, Provenance::Derived);
510 assert_eq!(
512 Node::new("k", NodeKind::Fn, "n").provenance,
513 Provenance::Derived
514 );
515 assert_eq!(
516 Node::new("k", NodeKind::Adr, "n")
517 .with_provenance(Provenance::Authored)
518 .provenance,
519 Provenance::Authored
520 );
521 }
522}