aimdb_core/graph.rs
1//! Dependency graph for AimDB record topology
2//!
3//! This module provides the `DependencyGraph` and related types that represent
4//! the entire database topology — sources, links, transforms, taps, and their
5//! relationships.
6//!
7//! The graph is constructed once during `build()` and is immutable thereafter.
8//! It enables:
9//! - Build-time validation (cycle detection, missing inputs)
10//! - Spawn ordering (topological sort)
11//! - Runtime introspection (AimX protocol, MCP tools, CLI)
12
13use alloc::{
14 string::{String, ToString},
15 vec::Vec,
16};
17
18// ============================================================================
19// Record Origin
20// ============================================================================
21
22/// How a record gets its values — part of the dependency graph.
23#[derive(Clone, Debug)]
24#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
25#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
26pub enum RecordOrigin {
27 /// Autonomous producer via `.source()`
28 Source,
29 /// Inbound connector via `.link_from()`
30 Link { protocol: String, address: String },
31 /// Single-input reactive derivation via `.transform()`
32 Transform { input: String },
33 /// Multi-input reactive join via `.transform_join()`
34 TransformJoin { inputs: Vec<String> },
35 /// No registered producer (writable via `record.set` / `db.produce()`)
36 Passive,
37}
38
39// ============================================================================
40// Graph Node
41// ============================================================================
42
43/// Metadata for one node in the dependency graph.
44#[derive(Clone, Debug)]
45#[cfg_attr(feature = "serde", derive(serde::Serialize))]
46pub struct GraphNode {
47 /// Record key (e.g. "temp.vienna")
48 pub key: String,
49 /// How this record gets its values
50 pub origin: RecordOrigin,
51 /// Buffer type ("spmc_ring", "single_latest", "mailbox", "none")
52 pub buffer_type: String,
53 /// Buffer capacity (None for unbounded or non-ring buffers)
54 pub buffer_capacity: Option<usize>,
55 /// Number of taps attached
56 pub tap_count: usize,
57 /// Whether an outbound link is configured
58 pub has_outbound_link: bool,
59}
60
61// ============================================================================
62// Graph Edge
63// ============================================================================
64
65/// One directed edge in the dependency graph.
66#[derive(Clone, Debug)]
67#[cfg_attr(feature = "serde", derive(serde::Serialize))]
68pub struct GraphEdge {
69 /// Source record key (None for external origins like source/link)
70 pub from: Option<String>,
71 /// Target record key (None for side-effects like taps/link_out)
72 pub to: Option<String>,
73 /// Classification of this edge
74 pub edge_type: EdgeType,
75}
76
77/// Classification of a dependency graph edge.
78#[derive(Clone, Debug)]
79#[cfg_attr(feature = "serde", derive(serde::Serialize))]
80#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
81pub enum EdgeType {
82 Source,
83 Link { protocol: String },
84 Transform,
85 TransformJoin,
86 Tap { index: usize },
87 LinkOut { protocol: String },
88}
89
90// ============================================================================
91// Dependency Graph
92// ============================================================================
93
94/// The dependency graph, constructed once during `build()` and immutable thereafter.
95#[derive(Clone, Debug)]
96pub struct DependencyGraph {
97 /// All nodes indexed by record key.
98 pub nodes: Vec<GraphNode>,
99 /// All edges (both internal and external).
100 pub edges: Vec<GraphEdge>,
101 /// Topological order of record keys (transforms come after their inputs).
102 pub topo_order: Vec<String>,
103}
104
105impl DependencyGraph {
106 /// Construct and validate the dependency graph from registered records.
107 ///
108 /// This builds a complete `DependencyGraph` with nodes, edges, and topological order.
109 /// The graph is immutable after construction and can be used for introspection.
110 ///
111 /// # Arguments
112 /// * `nodes` - One [`GraphNode`] per registered record (origin, buffer, etc.)
113 ///
114 /// # Returns
115 /// * `Ok(DependencyGraph)` - The complete, validated graph
116 /// * `Err(DbError::TransformInputNotFound)` - If a transform references a missing record
117 /// * `Err(DbError::CyclicDependency)` - If the transform edges form a cycle
118 pub fn build_and_validate(nodes: Vec<GraphNode>) -> crate::DbResult<Self> {
119 use alloc::collections::VecDeque;
120
121 // Everything in this block borrows `nodes` (&str keys); scoping it
122 // lets `nodes` move into the returned graph afterwards.
123 let (topo_order, edges) = {
124 // Build set of all keys for validation
125 let all_keys: hashbrown::HashSet<&str> =
126 nodes.iter().map(|info| info.key.as_str()).collect();
127
128 // Extract transform inputs for validation and edge construction
129 let mut transform_inputs: Vec<(&str, Vec<&str>)> = Vec::new();
130 for info in &nodes {
131 match &info.origin {
132 RecordOrigin::Transform { input } => {
133 transform_inputs.push((info.key.as_str(), alloc::vec![input.as_str()]));
134 }
135 RecordOrigin::TransformJoin { inputs } => {
136 let input_refs: Vec<&str> = inputs.iter().map(|s| s.as_str()).collect();
137 transform_inputs.push((info.key.as_str(), input_refs));
138 }
139 _ => {}
140 }
141 }
142
143 // Validate: check all transform input keys exist
144 for (output_key, input_keys) in &transform_inputs {
145 for input_key in input_keys {
146 if !all_keys.contains(input_key) {
147 return Err(crate::DbError::TransformInputNotFound {
148 output_key: output_key.to_string(),
149 input_key: input_key.to_string(),
150 });
151 }
152 }
153 }
154
155 // Build adjacency list and in-degree map for Kahn's algorithm
156 let mut in_degree: hashbrown::HashMap<&str, usize> = hashbrown::HashMap::new();
157 let mut adjacency: hashbrown::HashMap<&str, Vec<&str>> = hashbrown::HashMap::new();
158
159 // Initialize all keys with in-degree 0
160 for key in &all_keys {
161 in_degree.entry(*key).or_insert(0);
162 adjacency.entry(*key).or_default();
163 }
164
165 // Add transform edges: input → output
166 for (output_key, input_keys) in &transform_inputs {
167 for input_key in input_keys {
168 adjacency.entry(*input_key).or_default().push(*output_key);
169 *in_degree.entry(*output_key).or_insert(0) += 1;
170 }
171 }
172
173 // Kahn's algorithm — topological sort
174 let mut queue: VecDeque<&str> = in_degree
175 .iter()
176 .filter(|(_, °)| deg == 0)
177 .map(|(&node, _)| node)
178 .collect();
179 let mut topo_order = Vec::new();
180
181 while let Some(node) = queue.pop_front() {
182 topo_order.push(node.to_string());
183 if let Some(neighbors) = adjacency.get(node) {
184 for &neighbor in neighbors {
185 if let Some(deg) = in_degree.get_mut(neighbor) {
186 *deg -= 1;
187 if *deg == 0 {
188 queue.push_back(neighbor);
189 }
190 }
191 }
192 }
193 }
194
195 if topo_order.len() != all_keys.len() {
196 // Find the cycle participants for a helpful error message
197 let cycle_records: Vec<String> = in_degree
198 .iter()
199 .filter(|(_, °)| deg > 0)
200 .map(|(&k, _)| k.to_string())
201 .collect();
202
203 return Err(crate::DbError::CyclicDependency {
204 records: cycle_records,
205 });
206 }
207
208 // Build edges from origins
209 let mut edges: Vec<GraphEdge> = Vec::new();
210 for info in &nodes {
211 match &info.origin {
212 RecordOrigin::Source => {
213 edges.push(GraphEdge {
214 from: None,
215 to: Some(info.key.clone()),
216 edge_type: EdgeType::Source,
217 });
218 }
219 RecordOrigin::Link { protocol, .. } => {
220 edges.push(GraphEdge {
221 from: None,
222 to: Some(info.key.clone()),
223 edge_type: EdgeType::Link {
224 protocol: protocol.clone(),
225 },
226 });
227 }
228 RecordOrigin::Transform { input } => {
229 edges.push(GraphEdge {
230 from: Some(input.clone()),
231 to: Some(info.key.clone()),
232 edge_type: EdgeType::Transform,
233 });
234 }
235 RecordOrigin::TransformJoin { inputs } => {
236 for input in inputs {
237 edges.push(GraphEdge {
238 from: Some(input.clone()),
239 to: Some(info.key.clone()),
240 edge_type: EdgeType::TransformJoin,
241 });
242 }
243 }
244 RecordOrigin::Passive => {
245 // No inbound edge for passive records
246 }
247 }
248
249 // Add outbound link edges if present
250 if info.has_outbound_link {
251 edges.push(GraphEdge {
252 from: Some(info.key.clone()),
253 to: None,
254 edge_type: EdgeType::LinkOut {
255 protocol: "unknown".to_string(), // Protocol info not available here
256 },
257 });
258 }
259
260 // Add tap edges
261 for tap_idx in 0..info.tap_count {
262 edges.push(GraphEdge {
263 from: Some(info.key.clone()),
264 to: None,
265 edge_type: EdgeType::Tap { index: tap_idx },
266 });
267 }
268 }
269
270 (topo_order, edges)
271 };
272
273 Ok(DependencyGraph {
274 nodes,
275 edges,
276 topo_order,
277 })
278 }
279
280 /// Get a node by key
281 pub fn node(&self, key: &str) -> Option<&GraphNode> {
282 self.nodes.iter().find(|n| n.key == key)
283 }
284
285 /// Get all nodes
286 pub fn nodes(&self) -> &[GraphNode] {
287 &self.nodes
288 }
289
290 /// Get all edges
291 pub fn edges(&self) -> &[GraphEdge] {
292 &self.edges
293 }
294
295 /// Get the topological order
296 pub fn topo_order(&self) -> &[String] {
297 &self.topo_order
298 }
299}