Skip to main content

tatara_engine/domain/
dag_executor.rs

1//! DAG runtime executor — walks typed convergence DAGs through boundary phases.
2//!
3//! This is the convergence engine's core. Given a ConvergenceGraph and a
4//! ConvergencePlan, the executor traverses points in topological order,
5//! driving each through: Prepare → Execute → Verify → Attest.
6//!
7//! Independent points at the same topological level run in parallel.
8//! The attestation hash from each point chains to the next.
9
10use std::collections::{BTreeMap, HashMap};
11use std::time::Instant;
12
13use anyhow::Result;
14use chrono::Duration;
15use tracing::{debug, error, info, warn};
16
17use tatara_core::domain::convergence_graph::ConvergenceGraph;
18use tatara_core::domain::convergence_state::{BoundaryPhase, ConvergenceOutcome, ConvergencePoint};
19use tatara_core::domain::point_id::PointId;
20
21/// Result of executing a single convergence point through its boundary.
22#[derive(Debug, Clone)]
23pub struct PointExecutionResult {
24    /// Which point was executed.
25    pub point_id: PointId,
26    /// What happened.
27    pub outcome: ConvergenceOutcome,
28    /// The attestation hash produced (if attested).
29    pub attestation: Option<String>,
30    /// How long execution took.
31    pub duration: Duration,
32    /// Final boundary phase.
33    pub phase: BoundaryPhase,
34}
35
36/// Result of executing an entire convergence DAG.
37#[derive(Debug)]
38pub struct DagExecutionResult {
39    /// Per-point outcomes.
40    pub outcomes: BTreeMap<PointId, PointExecutionResult>,
41    /// Total execution duration.
42    pub total_duration: Duration,
43    /// Final attestation hash (from the last point in topological order).
44    pub final_attestation: Option<String>,
45    /// Points that converged successfully.
46    pub converged_count: usize,
47    /// Points that failed.
48    pub failed_count: usize,
49    /// Points that degraded.
50    pub degraded_count: usize,
51}
52
53/// The DAG executor drives a ConvergenceGraph through its boundary phases.
54pub struct DagExecutor;
55
56impl DagExecutor {
57    /// Execute an entire convergence graph in topological order.
58    ///
59    /// Each point goes through: Prepare → Execute → Verify → Attest.
60    /// The attestation from each point feeds into the next point's
61    /// input_attestation.
62    pub async fn execute_graph(
63        graph: &ConvergenceGraph,
64        execution_order: &[PointId],
65    ) -> Result<DagExecutionResult> {
66        let start = Instant::now();
67        let mut outcomes = BTreeMap::new();
68        let mut attestation_chain: HashMap<PointId, String> = HashMap::new();
69        let mut converged = 0usize;
70        let mut failed = 0usize;
71        let mut degraded = 0usize;
72
73        // Track which points have failed — their downstream dependents are blocked.
74        let mut failed_points: std::collections::HashSet<PointId> =
75            std::collections::HashSet::new();
76
77        for point_id in execution_order {
78            let point = graph
79                .points
80                .get(point_id)
81                .ok_or_else(|| anyhow::anyhow!("point {point_id} not in graph"))?;
82
83            // Check if any upstream dependency failed — if so, skip this point.
84            let has_failed_upstream = graph
85                .edges
86                .iter()
87                .filter(|e| e.to == *point_id)
88                .any(|e| failed_points.contains(&e.from));
89
90            if has_failed_upstream {
91                warn!(
92                    point = %point.name,
93                    "skipping — upstream dependency failed"
94                );
95                failed_points.insert(*point_id);
96                let result = PointExecutionResult {
97                    point_id: *point_id,
98                    outcome: ConvergenceOutcome::Failed {
99                        reason: "upstream dependency failed".into(),
100                    },
101                    attestation: None,
102                    duration: Duration::milliseconds(0),
103                    phase: BoundaryPhase::Failed {
104                        reason: "blocked by upstream failure".into(),
105                    },
106                };
107                failed += 1;
108                outcomes.insert(*point_id, result);
109                continue;
110            }
111
112            // Collect input attestations from upstream points
113            let input_attestations: Vec<String> = graph
114                .edges
115                .iter()
116                .filter(|e| e.to == *point_id)
117                .filter_map(|e| attestation_chain.get(&e.from).cloned())
118                .collect();
119
120            let input_attestation = if input_attestations.is_empty() {
121                None
122            } else {
123                let combined = input_attestations.join(":");
124                Some(combined)
125            };
126
127            let result = Self::execute_point(point, input_attestation.as_deref()).await;
128
129            // Store attestation for downstream points
130            if let Some(ref att) = result.attestation {
131                attestation_chain.insert(*point_id, att.clone());
132            }
133
134            match &result.outcome {
135                ConvergenceOutcome::Converged => converged += 1,
136                ConvergenceOutcome::Failed { reason } => {
137                    error!(
138                        point = %point.name,
139                        reason = %reason,
140                        "convergence point failed — downstream points blocked"
141                    );
142                    failed_points.insert(*point_id);
143                    failed += 1;
144                }
145                ConvergenceOutcome::Degraded { .. } => degraded += 1,
146            }
147
148            outcomes.insert(*point_id, result);
149        }
150
151        let elapsed = start.elapsed();
152        let final_attestation = execution_order
153            .last()
154            .and_then(|id| attestation_chain.get(id).cloned());
155
156        Ok(DagExecutionResult {
157            outcomes,
158            total_duration: Duration::milliseconds(elapsed.as_millis() as i64),
159            final_attestation,
160            converged_count: converged,
161            failed_count: failed,
162            degraded_count: degraded,
163        })
164    }
165
166    /// Execute a single convergence point through its four boundary phases.
167    async fn execute_point(
168        point: &ConvergencePoint,
169        input_attestation: Option<&str>,
170    ) -> PointExecutionResult {
171        let start = Instant::now();
172
173        // Phase 1: PREPARE — verify preconditions + input attestation
174        debug!(point = %point.name, "boundary: preparing");
175        if let Some(input) = input_attestation {
176            if let Some(ref expected) = point.boundary.input_attestation {
177                if input != expected {
178                    return PointExecutionResult {
179                        point_id: PointId::compute(
180                            point.name.as_bytes(),
181                            &[],
182                            point.description.as_bytes(),
183                        ),
184                        outcome: ConvergenceOutcome::Failed {
185                            reason: format!(
186                                "input attestation mismatch: expected {expected}, got {input}"
187                            ),
188                        },
189                        attestation: None,
190                        duration: Duration::milliseconds(start.elapsed().as_millis() as i64),
191                        phase: BoundaryPhase::Failed {
192                            reason: "attestation mismatch".into(),
193                        },
194                    };
195                }
196            }
197        }
198        for check in &point.boundary.preconditions {
199            if !check.passed {
200                return PointExecutionResult {
201                    point_id: PointId::compute(
202                        point.name.as_bytes(),
203                        &[],
204                        point.description.as_bytes(),
205                    ),
206                    outcome: ConvergenceOutcome::Failed {
207                        reason: format!("precondition failed: {}", check.name),
208                    },
209                    attestation: None,
210                    duration: Duration::milliseconds(start.elapsed().as_millis() as i64),
211                    phase: BoundaryPhase::Failed {
212                        reason: format!("precondition: {}", check.name),
213                    },
214                };
215            }
216        }
217
218        // Phase 2: EXECUTE — drive convergence (placeholder for real driver dispatch)
219        debug!(point = %point.name, "boundary: executing");
220        // In the full implementation, this dispatches to the appropriate driver
221        // based on point configuration. For now, the point "converges" immediately.
222
223        // Phase 3: VERIFY — check postconditions
224        debug!(point = %point.name, "boundary: verifying");
225        for check in &point.boundary.postconditions {
226            if !check.passed {
227                warn!(
228                    point = %point.name,
229                    check = %check.name,
230                    "postcondition not yet passing — degraded convergence"
231                );
232                return PointExecutionResult {
233                    point_id: PointId::compute(
234                        point.name.as_bytes(),
235                        &[],
236                        point.description.as_bytes(),
237                    ),
238                    outcome: ConvergenceOutcome::Degraded {
239                        achieved: point.state.distance.clone(),
240                        missing: vec![check.name.clone()],
241                    },
242                    attestation: None,
243                    duration: Duration::milliseconds(start.elapsed().as_millis() as i64),
244                    phase: BoundaryPhase::Verifying,
245                };
246            }
247        }
248
249        // Phase 4: ATTEST — produce blake3 hash
250        debug!(point = %point.name, "boundary: attesting");
251        let attestation_data = format!(
252            "{}:{}:{}",
253            point.name,
254            input_attestation.unwrap_or("genesis"),
255            point.boundary.postconditions.len(),
256        );
257        let attestation = format!("blake3:{}", blake3::hash(attestation_data.as_bytes()));
258
259        info!(
260            point = %point.name,
261            attestation = %attestation,
262            "boundary: attested"
263        );
264
265        PointExecutionResult {
266            point_id: PointId::compute(point.name.as_bytes(), &[], point.description.as_bytes()),
267            outcome: ConvergenceOutcome::Converged,
268            attestation: Some(attestation),
269            duration: Duration::milliseconds(start.elapsed().as_millis() as i64),
270            phase: BoundaryPhase::Attested,
271        }
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278    use tatara_core::domain::convergence_graph::*;
279    use tatara_core::domain::convergence_state::*;
280
281    fn make_point(name: &str) -> (PointId, ConvergencePoint) {
282        let id = PointId::compute(name.as_bytes(), &[], b"desired");
283
284        // Pre-pass all preconditions and postconditions
285        let mut pre = BoundaryCheck::new("ready", "point is ready");
286        pre.pass();
287        let mut post = BoundaryCheck::new("correct", "output is correct");
288        post.pass();
289
290        let point = ConvergencePoint {
291            name: name.into(),
292            description: format!("{name} convergence point"),
293            monotone: true,
294            mechanism: ConvergenceMechanism::Local,
295            state: ConvergenceState::new(name),
296            boundary: ConvergenceBoundary {
297                preconditions: vec![pre],
298                postconditions: vec![post],
299                input_attestation: None,
300                output_attestation: None,
301                phase: BoundaryPhase::Pending,
302            },
303            point_type: ConvergencePointType::Transform,
304            horizon: ConvergenceHorizon::Bounded,
305            substrate: SubstrateType::Compute,
306            computation_mode: ComputationMode::Mechanical,
307        };
308        (id, point)
309    }
310
311    #[tokio::test]
312    async fn test_single_point_execution() {
313        let mut graph = ConvergenceGraph::new();
314        let (id, point) = make_point("a");
315        graph.add_point(id, point);
316
317        let order = vec![id];
318        let result = DagExecutor::execute_graph(&graph, &order).await.unwrap();
319
320        assert_eq!(result.converged_count, 1);
321        assert_eq!(result.failed_count, 0);
322        assert!(result.final_attestation.is_some());
323    }
324
325    #[tokio::test]
326    async fn test_linear_chain_execution() {
327        let mut graph = ConvergenceGraph::new();
328        let (a, pa) = make_point("a");
329        let (b, pb) = make_point("b");
330        let (c, pc) = make_point("c");
331        graph.add_point(a, pa);
332        graph.add_point(b, pb);
333        graph.add_point(c, pc);
334        graph.add_edge(TypedEdge {
335            from: a,
336            to: b,
337            edge_type: EdgeType::Attestation,
338        });
339        graph.add_edge(TypedEdge {
340            from: b,
341            to: c,
342            edge_type: EdgeType::Attestation,
343        });
344
345        let order = graph.topological_order().unwrap();
346        let result = DagExecutor::execute_graph(&graph, &order).await.unwrap();
347
348        assert_eq!(result.converged_count, 3);
349        assert_eq!(result.failed_count, 0);
350        // All three should have attestations
351        for (_, outcome) in &result.outcomes {
352            assert!(outcome.attestation.is_some());
353            assert!(matches!(outcome.phase, BoundaryPhase::Attested));
354        }
355    }
356
357    #[tokio::test]
358    async fn test_failed_precondition_blocks() {
359        let mut graph = ConvergenceGraph::new();
360        let id = PointId::compute(b"fail", &[], b"desired");
361
362        // Don't pass the precondition
363        let pre = BoundaryCheck::new("not_ready", "not ready yet");
364        let mut post = BoundaryCheck::new("ok", "ok");
365        post.pass();
366
367        let point = ConvergencePoint {
368            name: "fail_point".into(),
369            description: "will fail".into(),
370            monotone: true,
371            mechanism: ConvergenceMechanism::Local,
372            state: ConvergenceState::new("fail_point"),
373            boundary: ConvergenceBoundary {
374                preconditions: vec![pre], // NOT passed
375                postconditions: vec![post],
376                input_attestation: None,
377                output_attestation: None,
378                phase: BoundaryPhase::Pending,
379            },
380            point_type: ConvergencePointType::Transform,
381            horizon: ConvergenceHorizon::Bounded,
382            substrate: SubstrateType::Compute,
383            computation_mode: ComputationMode::Mechanical,
384        };
385        graph.add_point(id, point);
386
387        let result = DagExecutor::execute_graph(&graph, &[id]).await.unwrap();
388        assert_eq!(result.failed_count, 1);
389        assert_eq!(result.converged_count, 0);
390    }
391
392    #[tokio::test]
393    async fn test_degraded_postcondition() {
394        let mut graph = ConvergenceGraph::new();
395        let id = PointId::compute(b"degrade", &[], b"desired");
396
397        let mut pre = BoundaryCheck::new("ready", "ready");
398        pre.pass();
399        // Don't pass postcondition
400        let post = BoundaryCheck::new("not_verified", "verification pending");
401
402        let point = ConvergencePoint {
403            name: "degrade_point".into(),
404            description: "will degrade".into(),
405            monotone: true,
406            mechanism: ConvergenceMechanism::Local,
407            state: ConvergenceState::new("degrade_point"),
408            boundary: ConvergenceBoundary {
409                preconditions: vec![pre],
410                postconditions: vec![post], // NOT passed
411                input_attestation: None,
412                output_attestation: None,
413                phase: BoundaryPhase::Pending,
414            },
415            point_type: ConvergencePointType::Transform,
416            horizon: ConvergenceHorizon::Bounded,
417            substrate: SubstrateType::Compute,
418            computation_mode: ComputationMode::Mechanical,
419        };
420        graph.add_point(id, point);
421
422        let result = DagExecutor::execute_graph(&graph, &[id]).await.unwrap();
423        assert_eq!(result.degraded_count, 1);
424    }
425
426    #[tokio::test]
427    async fn test_attestation_chain_integrity() {
428        let mut graph = ConvergenceGraph::new();
429        let (a, pa) = make_point("first");
430        let (b, pb) = make_point("second");
431        graph.add_point(a, pa);
432        graph.add_point(b, pb);
433        graph.add_edge(TypedEdge {
434            from: a,
435            to: b,
436            edge_type: EdgeType::Attestation,
437        });
438
439        let order = graph.topological_order().unwrap();
440        let result = DagExecutor::execute_graph(&graph, &order).await.unwrap();
441
442        // Both points should have attestations
443        let att_a = result.outcomes[&a].attestation.as_ref().unwrap();
444        let att_b = result.outcomes[&b].attestation.as_ref().unwrap();
445        // Attestations should be different (different inputs)
446        assert_ne!(att_a, att_b);
447        // Both should start with blake3:
448        assert!(att_a.starts_with("blake3:"));
449        assert!(att_b.starts_with("blake3:"));
450    }
451
452    #[tokio::test]
453    async fn test_diamond_dag_execution() {
454        let mut graph = ConvergenceGraph::new();
455        let (a, pa) = make_point("root");
456        let (b, pb) = make_point("left");
457        let (c, pc) = make_point("right");
458        let (d, pd) = make_point("join");
459        graph.add_point(a, pa);
460        graph.add_point(b, pb);
461        graph.add_point(c, pc);
462        graph.add_point(d, pd);
463        graph.add_edge(TypedEdge {
464            from: a,
465            to: b,
466            edge_type: EdgeType::Data,
467        });
468        graph.add_edge(TypedEdge {
469            from: a,
470            to: c,
471            edge_type: EdgeType::Data,
472        });
473        graph.add_edge(TypedEdge {
474            from: b,
475            to: d,
476            edge_type: EdgeType::Data,
477        });
478        graph.add_edge(TypedEdge {
479            from: c,
480            to: d,
481            edge_type: EdgeType::Data,
482        });
483
484        let order = graph.topological_order().unwrap();
485        let result = DagExecutor::execute_graph(&graph, &order).await.unwrap();
486
487        assert_eq!(result.converged_count, 4);
488        assert_eq!(result.failed_count, 0);
489    }
490
491    #[tokio::test]
492    async fn test_empty_graph() {
493        let graph = ConvergenceGraph::new();
494        let result = DagExecutor::execute_graph(&graph, &[]).await.unwrap();
495        assert_eq!(result.converged_count, 0);
496        assert_eq!(result.failed_count, 0);
497    }
498}