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::super::attestation::BLAKE3_SCHEME_PREFIX;
280 use super::*;
281 use tatara_core::domain::convergence_graph::*;
282 use tatara_core::domain::convergence_state::*;
283
284 fn make_point(name: &str) -> (PointId, ConvergencePoint) {
285 let id = PointId::compute(name.as_bytes(), &[], b"desired");
286
287 let mut pre = BoundaryCheck::new("ready", "point is ready");
289 pre.pass();
290 let mut post = BoundaryCheck::new("correct", "output is correct");
291 post.pass();
292
293 let point = ConvergencePoint {
294 name: name.into(),
295 description: format!("{name} convergence point"),
296 monotone: true,
297 mechanism: ConvergenceMechanism::Local,
298 state: ConvergenceState::new(name),
299 boundary: ConvergenceBoundary {
300 preconditions: vec![pre],
301 postconditions: vec![post],
302 input_attestation: None,
303 output_attestation: None,
304 phase: BoundaryPhase::Pending,
305 },
306 point_type: ConvergencePointType::Transform,
307 horizon: ConvergenceHorizon::Bounded,
308 substrate: SubstrateType::Compute,
309 computation_mode: ComputationMode::Mechanical,
310 };
311 (id, point)
312 }
313
314 #[tokio::test]
315 async fn test_single_point_execution() {
316 let mut graph = ConvergenceGraph::new();
317 let (id, point) = make_point("a");
318 graph.add_point(id, point);
319
320 let order = vec![id];
321 let result = DagExecutor::execute_graph(&graph, &order).await.unwrap();
322
323 assert_eq!(result.converged_count, 1);
324 assert_eq!(result.failed_count, 0);
325 assert!(result.final_attestation.is_some());
326 }
327
328 #[tokio::test]
329 async fn test_linear_chain_execution() {
330 let mut graph = ConvergenceGraph::new();
331 let (a, pa) = make_point("a");
332 let (b, pb) = make_point("b");
333 let (c, pc) = make_point("c");
334 graph.add_point(a, pa);
335 graph.add_point(b, pb);
336 graph.add_point(c, pc);
337 graph.add_edge(TypedEdge {
338 from: a,
339 to: b,
340 edge_type: EdgeType::Attestation,
341 });
342 graph.add_edge(TypedEdge {
343 from: b,
344 to: c,
345 edge_type: EdgeType::Attestation,
346 });
347
348 let order = graph.topological_order().unwrap();
349 let result = DagExecutor::execute_graph(&graph, &order).await.unwrap();
350
351 assert_eq!(result.converged_count, 3);
352 assert_eq!(result.failed_count, 0);
353 for (_, outcome) in &result.outcomes {
355 assert!(outcome.attestation.is_some());
356 assert!(matches!(outcome.phase, BoundaryPhase::Attested));
357 }
358 }
359
360 #[tokio::test]
361 async fn test_failed_precondition_blocks() {
362 let mut graph = ConvergenceGraph::new();
363 let id = PointId::compute(b"fail", &[], b"desired");
364
365 let pre = BoundaryCheck::new("not_ready", "not ready yet");
367 let mut post = BoundaryCheck::new("ok", "ok");
368 post.pass();
369
370 let point = ConvergencePoint {
371 name: "fail_point".into(),
372 description: "will fail".into(),
373 monotone: true,
374 mechanism: ConvergenceMechanism::Local,
375 state: ConvergenceState::new("fail_point"),
376 boundary: ConvergenceBoundary {
377 preconditions: vec![pre], postconditions: vec![post],
379 input_attestation: None,
380 output_attestation: None,
381 phase: BoundaryPhase::Pending,
382 },
383 point_type: ConvergencePointType::Transform,
384 horizon: ConvergenceHorizon::Bounded,
385 substrate: SubstrateType::Compute,
386 computation_mode: ComputationMode::Mechanical,
387 };
388 graph.add_point(id, point);
389
390 let result = DagExecutor::execute_graph(&graph, &[id]).await.unwrap();
391 assert_eq!(result.failed_count, 1);
392 assert_eq!(result.converged_count, 0);
393 }
394
395 #[tokio::test]
396 async fn test_degraded_postcondition() {
397 let mut graph = ConvergenceGraph::new();
398 let id = PointId::compute(b"degrade", &[], b"desired");
399
400 let mut pre = BoundaryCheck::new("ready", "ready");
401 pre.pass();
402 let post = BoundaryCheck::new("not_verified", "verification pending");
404
405 let point = ConvergencePoint {
406 name: "degrade_point".into(),
407 description: "will degrade".into(),
408 monotone: true,
409 mechanism: ConvergenceMechanism::Local,
410 state: ConvergenceState::new("degrade_point"),
411 boundary: ConvergenceBoundary {
412 preconditions: vec![pre],
413 postconditions: vec![post], input_attestation: None,
415 output_attestation: None,
416 phase: BoundaryPhase::Pending,
417 },
418 point_type: ConvergencePointType::Transform,
419 horizon: ConvergenceHorizon::Bounded,
420 substrate: SubstrateType::Compute,
421 computation_mode: ComputationMode::Mechanical,
422 };
423 graph.add_point(id, point);
424
425 let result = DagExecutor::execute_graph(&graph, &[id]).await.unwrap();
426 assert_eq!(result.degraded_count, 1);
427 }
428
429 #[tokio::test]
430 async fn test_attestation_chain_integrity() {
431 let mut graph = ConvergenceGraph::new();
432 let (a, pa) = make_point("first");
433 let (b, pb) = make_point("second");
434 graph.add_point(a, pa);
435 graph.add_point(b, pb);
436 graph.add_edge(TypedEdge {
437 from: a,
438 to: b,
439 edge_type: EdgeType::Attestation,
440 });
441
442 let order = graph.topological_order().unwrap();
443 let result = DagExecutor::execute_graph(&graph, &order).await.unwrap();
444
445 let att_a = result.outcomes[&a].attestation.as_ref().unwrap();
447 let att_b = result.outcomes[&b].attestation.as_ref().unwrap();
448 assert_ne!(att_a, att_b);
450 assert!(att_a.starts_with(BLAKE3_SCHEME_PREFIX));
455 assert!(att_b.starts_with(BLAKE3_SCHEME_PREFIX));
456 }
457
458 #[tokio::test]
459 async fn test_diamond_dag_execution() {
460 let mut graph = ConvergenceGraph::new();
461 let (a, pa) = make_point("root");
462 let (b, pb) = make_point("left");
463 let (c, pc) = make_point("right");
464 let (d, pd) = make_point("join");
465 graph.add_point(a, pa);
466 graph.add_point(b, pb);
467 graph.add_point(c, pc);
468 graph.add_point(d, pd);
469 graph.add_edge(TypedEdge {
470 from: a,
471 to: b,
472 edge_type: EdgeType::Data,
473 });
474 graph.add_edge(TypedEdge {
475 from: a,
476 to: c,
477 edge_type: EdgeType::Data,
478 });
479 graph.add_edge(TypedEdge {
480 from: b,
481 to: d,
482 edge_type: EdgeType::Data,
483 });
484 graph.add_edge(TypedEdge {
485 from: c,
486 to: d,
487 edge_type: EdgeType::Data,
488 });
489
490 let order = graph.topological_order().unwrap();
491 let result = DagExecutor::execute_graph(&graph, &order).await.unwrap();
492
493 assert_eq!(result.converged_count, 4);
494 assert_eq!(result.failed_count, 0);
495 }
496
497 #[tokio::test]
498 async fn test_empty_graph() {
499 let graph = ConvergenceGraph::new();
500 let result = DagExecutor::execute_graph(&graph, &[]).await.unwrap();
501 assert_eq!(result.converged_count, 0);
502 assert_eq!(result.failed_count, 0);
503 }
504}