Skip to main content

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)| 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)| 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}