Skip to main content

tatara_engine/domain/
substrate_manager.rs

1//! Substrate manager — manages per-substrate convergence DAGs.
2//!
3//! Each operational substrate (financial, compute, network, etc.) has its
4//! own convergence DAG. The substrate manager composes them into a single
5//! ConvergenceGraph and computes per-substrate convergence distances.
6
7use std::collections::BTreeMap;
8
9use tatara_core::domain::convergence_graph::*;
10use tatara_core::domain::convergence_state::*;
11use tatara_core::domain::multi_distance::*;
12use tatara_core::domain::point_id::PointId;
13
14/// Manages multiple substrate-scoped convergence DAGs and composes them
15/// into a unified convergence graph.
16pub struct SubstrateManager {
17    substrates: BTreeMap<SubstrateType, SubstrateDAG>,
18}
19
20impl SubstrateManager {
21    pub fn new() -> Self {
22        Self {
23            substrates: BTreeMap::new(),
24        }
25    }
26
27    /// Add a substrate DAG.
28    pub fn add_substrate(&mut self, dag: SubstrateDAG) {
29        self.substrates.insert(dag.substrate, dag);
30    }
31
32    /// Get a substrate DAG by type.
33    pub fn get_substrate(&self, substrate: &SubstrateType) -> Option<&SubstrateDAG> {
34        self.substrates.get(substrate)
35    }
36
37    /// Number of managed substrates.
38    pub fn substrate_count(&self) -> usize {
39        self.substrates.len()
40    }
41
42    /// Compose all substrate DAGs into a single convergence graph.
43    /// Internal edges stay within substrates. Cross-substrate edges
44    /// connect boundary points between substrates.
45    pub fn compose_graph(&self) -> ConvergenceGraph {
46        let mut graph = ConvergenceGraph::new();
47
48        for dag in self.substrates.values() {
49            // Add all points from this substrate
50            for (id, point) in &dag.points {
51                graph.add_point(*id, point.clone());
52            }
53            // Add internal edges
54            for edge in &dag.internal_edges {
55                graph.add_edge(edge.clone());
56            }
57            // Add cross-substrate edges
58            for edge in &dag.cross_edges {
59                graph.add_edge(edge.clone());
60            }
61        }
62
63        graph
64    }
65
66    /// Compute per-substrate convergence distance.
67    pub fn convergence_per_substrate(&self) -> MultiDimensionalDistance {
68        let mut distance = MultiDimensionalDistance::new();
69
70        for (substrate_type, dag) in &self.substrates {
71            if dag.points.is_empty() {
72                distance.set(*substrate_type, 0.0);
73                continue;
74            }
75
76            let max_distance: f64 = dag
77                .points
78                .values()
79                .map(|p| p.state.distance.numeric())
80                .fold(0.0_f64, f64::max);
81            distance.set(*substrate_type, max_distance);
82        }
83
84        distance
85    }
86
87    /// List cross-substrate dependencies.
88    pub fn cross_substrate_edges(&self) -> Vec<(SubstrateType, SubstrateType, TypedEdge)> {
89        let mut result = Vec::new();
90
91        for (substrate_type, dag) in &self.substrates {
92            for edge in &dag.cross_edges {
93                // Determine which substrate the target belongs to
94                for (other_type, other_dag) in &self.substrates {
95                    if other_type != substrate_type && other_dag.points.contains_key(&edge.to) {
96                        result.push((*substrate_type, *other_type, edge.clone()));
97                    }
98                }
99            }
100        }
101
102        result
103    }
104}
105
106impl Default for SubstrateManager {
107    fn default() -> Self {
108        Self::new()
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    fn make_substrate_dag(substrate: SubstrateType, point_names: &[&str]) -> SubstrateDAG {
117        let mut points = BTreeMap::new();
118        let mut ids = Vec::new();
119
120        for name in point_names {
121            let id = PointId::compute(name.as_bytes(), &[], b"desired");
122            let point = ConvergencePoint {
123                name: (*name).into(),
124                description: format!("{name} point"),
125                monotone: true,
126                mechanism: ConvergenceMechanism::Local,
127                state: ConvergenceState::new(*name),
128                boundary: ConvergenceBoundary::default(),
129                point_type: ConvergencePointType::Transform,
130                horizon: ConvergenceHorizon::Bounded,
131                substrate,
132                computation_mode: ComputationMode::Mechanical,
133            };
134            points.insert(id, point);
135            ids.push(id);
136        }
137
138        let mut internal_edges = Vec::new();
139        for i in 1..ids.len() {
140            internal_edges.push(TypedEdge {
141                from: ids[i - 1],
142                to: ids[i],
143                edge_type: EdgeType::Attestation,
144            });
145        }
146
147        SubstrateDAG {
148            substrate,
149            points,
150            internal_edges,
151            cross_edges: Vec::new(),
152            bandwidth: ConvergenceBandwidth::Seconds(30),
153        }
154    }
155
156    #[test]
157    fn test_single_substrate() {
158        let mut mgr = SubstrateManager::new();
159        mgr.add_substrate(make_substrate_dag(
160            SubstrateType::Compute,
161            &["cpu_alloc", "mem_alloc", "driver_start"],
162        ));
163
164        assert_eq!(mgr.substrate_count(), 1);
165        let graph = mgr.compose_graph();
166        assert_eq!(graph.point_count(), 3);
167        assert_eq!(graph.edge_count(), 2);
168    }
169
170    #[test]
171    fn test_multi_substrate_composition() {
172        let mut mgr = SubstrateManager::new();
173        mgr.add_substrate(make_substrate_dag(SubstrateType::Compute, &["cpu", "mem"]));
174        mgr.add_substrate(make_substrate_dag(
175            SubstrateType::Network,
176            &["dns", "route"],
177        ));
178        mgr.add_substrate(make_substrate_dag(SubstrateType::Security, &["secret"]));
179
180        assert_eq!(mgr.substrate_count(), 3);
181        let graph = mgr.compose_graph();
182        assert_eq!(graph.point_count(), 5);
183    }
184
185    #[test]
186    fn test_convergence_per_substrate() {
187        let mut mgr = SubstrateManager::new();
188        mgr.add_substrate(make_substrate_dag(SubstrateType::Compute, &["a"]));
189        mgr.add_substrate(make_substrate_dag(SubstrateType::Network, &["b"]));
190
191        let dist = mgr.convergence_per_substrate();
192        // Default ConvergenceState has Unknown distance (1.0)
193        assert_eq!(dist.get(&SubstrateType::Compute), 1.0);
194        assert_eq!(dist.get(&SubstrateType::Network), 1.0);
195        assert!(!dist.is_converged());
196    }
197
198    #[test]
199    fn test_empty_manager() {
200        let mgr = SubstrateManager::new();
201        assert_eq!(mgr.substrate_count(), 0);
202        let graph = mgr.compose_graph();
203        assert_eq!(graph.point_count(), 0);
204    }
205}