1use 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#[derive(Debug, Clone)]
23pub struct PointExecutionResult {
24 pub point_id: PointId,
26 pub outcome: ConvergenceOutcome,
28 pub attestation: Option<String>,
30 pub duration: Duration,
32 pub phase: BoundaryPhase,
34}
35
36#[derive(Debug)]
38pub struct DagExecutionResult {
39 pub outcomes: BTreeMap<PointId, PointExecutionResult>,
41 pub total_duration: Duration,
43 pub final_attestation: Option<String>,
45 pub converged_count: usize,
47 pub failed_count: usize,
49 pub degraded_count: usize,
51}
52
53pub struct DagExecutor;
55
56impl DagExecutor {
57 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 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 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 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 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 async fn execute_point(
168 point: &ConvergencePoint,
169 input_attestation: Option<&str>,
170 ) -> PointExecutionResult {
171 let start = Instant::now();
172
173 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 debug!(point = %point.name, "boundary: executing");
220 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 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 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 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 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], 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 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], 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 let att_a = result.outcomes[&a].attestation.as_ref().unwrap();
444 let att_b = result.outcomes[&b].attestation.as_ref().unwrap();
445 assert_ne!(att_a, att_b);
447 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}