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