cadi_core/graph/
node.rs

1//! Graph Node representation
2//!
3//! A GraphNode represents a chunk in the dependency graph with all its
4//! relationships and metadata for efficient querying.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9use super::EdgeType;
10
11/// A node in the dependency graph
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct GraphNode {
14    /// The chunk ID (e.g., "chunk:sha256:abc123...")
15    pub chunk_id: String,
16
17    /// Content hash of the chunk
18    pub content_hash: String,
19
20    /// Primary alias for this chunk
21    pub primary_alias: Option<String>,
22
23    /// All aliases pointing to this chunk
24    pub aliases: Vec<String>,
25
26    /// Symbols this chunk defines/exports
27    pub symbols_defined: Vec<String>,
28
29    /// Symbols this chunk references/imports
30    pub symbols_referenced: Vec<String>,
31
32    /// Language of the chunk
33    pub language: String,
34
35    /// Granularity (function, type, module, etc.)
36    pub granularity: String,
37
38    /// Byte size of content
39    pub byte_size: usize,
40
41    /// Estimated token count (for LLM budgeting)
42    pub token_estimate: usize,
43
44    /// Source file location
45    pub source_file: Option<String>,
46
47    /// Line range in source file
48    pub source_lines: Option<(usize, usize)>,
49
50    /// Outgoing edges (this chunk depends on)
51    pub outgoing_edges: Vec<(EdgeType, String)>,
52
53    /// Incoming edges (chunks that depend on this)
54    pub incoming_edges: Vec<(EdgeType, String)>,
55
56    /// Additional metadata
57    #[serde(default)]
58    pub metadata: HashMap<String, String>,
59
60    /// When this node was created
61    pub created_at: String,
62
63    /// When this node was last updated
64    pub updated_at: String,
65}
66
67impl GraphNode {
68    /// Create a new graph node
69    pub fn new(chunk_id: impl Into<String>, content_hash: impl Into<String>) -> Self {
70        let now = chrono::Utc::now().to_rfc3339();
71        Self {
72            chunk_id: chunk_id.into(),
73            content_hash: content_hash.into(),
74            primary_alias: None,
75            aliases: Vec::new(),
76            symbols_defined: Vec::new(),
77            symbols_referenced: Vec::new(),
78            language: "unknown".to_string(),
79            granularity: "unknown".to_string(),
80            byte_size: 0,
81            token_estimate: 0,
82            source_file: None,
83            source_lines: None,
84            outgoing_edges: Vec::new(),
85            incoming_edges: Vec::new(),
86            metadata: HashMap::new(),
87            created_at: now.clone(),
88            updated_at: now,
89        }
90    }
91
92    /// Set the primary alias
93    pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
94        let alias = alias.into();
95        self.primary_alias = Some(alias.clone());
96        if !self.aliases.contains(&alias) {
97            self.aliases.push(alias);
98        }
99        self
100    }
101
102    /// Set the language
103    pub fn with_language(mut self, language: impl Into<String>) -> Self {
104        self.language = language.into();
105        self
106    }
107
108    /// Set the granularity
109    pub fn with_granularity(mut self, granularity: impl Into<String>) -> Self {
110        self.granularity = granularity.into();
111        self
112    }
113
114    /// Set the byte size
115    pub fn with_size(mut self, byte_size: usize) -> Self {
116        self.byte_size = byte_size;
117        // Rough token estimate: ~4 chars per token
118        self.token_estimate = byte_size / 4;
119        self
120    }
121
122    /// Set source location
123    pub fn with_source(mut self, file: impl Into<String>, start: usize, end: usize) -> Self {
124        self.source_file = Some(file.into());
125        self.source_lines = Some((start, end));
126        self
127    }
128
129    /// Add symbols this chunk defines
130    pub fn with_defines(mut self, symbols: Vec<String>) -> Self {
131        self.symbols_defined = symbols;
132        self
133    }
134
135    /// Add symbols this chunk references
136    pub fn with_references(mut self, symbols: Vec<String>) -> Self {
137        self.symbols_referenced = symbols;
138        self
139    }
140
141    /// Add an outgoing edge (dependency)
142    pub fn add_dependency(&mut self, edge_type: EdgeType, target_id: String) {
143        self.outgoing_edges.push((edge_type, target_id));
144        self.updated_at = chrono::Utc::now().to_rfc3339();
145    }
146
147    /// Add an incoming edge (dependent)
148    pub fn add_dependent(&mut self, edge_type: EdgeType, source_id: String) {
149        self.incoming_edges.push((edge_type, source_id));
150        self.updated_at = chrono::Utc::now().to_rfc3339();
151    }
152
153    /// Get all chunks this depends on
154    pub fn dependencies(&self) -> impl Iterator<Item = &String> {
155        self.outgoing_edges.iter().map(|(_, id)| id)
156    }
157
158    /// Get all chunks that depend on this
159    pub fn dependents(&self) -> impl Iterator<Item = &String> {
160        self.incoming_edges.iter().map(|(_, id)| id)
161    }
162
163    /// Get dependencies filtered by edge type
164    pub fn dependencies_of_type(&self, edge_type: EdgeType) -> Vec<&String> {
165        self.outgoing_edges
166            .iter()
167            .filter(|(et, _)| *et == edge_type)
168            .map(|(_, id)| id)
169            .collect()
170    }
171
172    /// Serialize for storage
173    pub fn to_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
174        serde_json::to_vec(self)
175    }
176
177    /// Deserialize from storage
178    pub fn from_bytes(bytes: &[u8]) -> Result<Self, serde_json::Error> {
179        serde_json::from_slice(bytes)
180    }
181}
182
183impl Default for GraphNode {
184    fn default() -> Self {
185        Self::new("", "")
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    #[test]
194    fn test_node_creation() {
195        let node = GraphNode::new("chunk:sha256:abc123", "abc123")
196            .with_alias("myproject/utils/tax")
197            .with_language("rust")
198            .with_granularity("function")
199            .with_size(1000)
200            .with_defines(vec!["calculate_tax".to_string()])
201            .with_references(vec!["TaxRate".to_string()]);
202
203        assert_eq!(node.chunk_id, "chunk:sha256:abc123");
204        assert_eq!(node.primary_alias, Some("myproject/utils/tax".to_string()));
205        assert_eq!(node.language, "rust");
206        assert_eq!(node.token_estimate, 250); // 1000 / 4
207        assert!(node.symbols_defined.contains(&"calculate_tax".to_string()));
208    }
209
210    #[test]
211    fn test_edge_operations() {
212        let mut node = GraphNode::new("chunk:sha256:abc123", "abc123");
213        
214        node.add_dependency(EdgeType::Imports, "chunk:sha256:def456".to_string());
215        node.add_dependency(EdgeType::TypeRef, "chunk:sha256:ghi789".to_string());
216        
217        assert_eq!(node.dependencies().count(), 2);
218        assert_eq!(node.dependencies_of_type(EdgeType::Imports).len(), 1);
219    }
220
221    #[test]
222    fn test_serialization() {
223        let node = GraphNode::new("chunk:sha256:abc123", "abc123")
224            .with_alias("test/node");
225        
226        let bytes = node.to_bytes().unwrap();
227        let restored = GraphNode::from_bytes(&bytes).unwrap();
228        
229        assert_eq!(node.chunk_id, restored.chunk_id);
230        assert_eq!(node.primary_alias, restored.primary_alias);
231    }
232}