use anyhow::Result;
use crate::graph::Graph;
use super::topology::{compute_layers, critical_path};
use super::types::{ExecutionPlan, ExecutionLayer, TaskInfo};
pub fn create_plan(graph: &Graph) -> Result<ExecutionPlan> {
let layers = compute_layers(graph)?;
let cp = critical_path(graph);
let mut execution_layers = Vec::new();
let mut total_tasks = 0;
let mut estimated_total_turns: u32 = 0;
for (index, task_ids) in layers.iter().enumerate() {
let mut tasks = Vec::new();
for id in task_ids {
if let Some(node) = graph.get_node(id) {
let task_info = extract_task_info(node, graph);
estimated_total_turns = estimated_total_turns.saturating_add(task_info.estimated_turns);
tasks.push(task_info);
total_tasks += 1;
}
}
let checkpoint = detect_default_checkpoint(graph);
execution_layers.push(ExecutionLayer {
index,
tasks,
checkpoint,
});
}
Ok(ExecutionPlan {
layers: execution_layers,
critical_path: cp,
total_tasks,
estimated_total_turns,
})
}
fn extract_task_info(node: &crate::graph::Node, graph: &Graph) -> TaskInfo {
let description = node.description.clone().unwrap_or_default();
let verify = node.metadata.get("verify")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let estimated_turns = node.metadata.get("estimated_turns")
.and_then(|v| v.as_u64())
.unwrap_or(15) as u32;
let design_ref = node.metadata.get("design_ref")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let satisfies = node.metadata.get("satisfies")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
let goals = node.metadata.get("goals")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
let depends_on: Vec<String> = graph.edges.iter()
.filter(|e| e.from == node.id && e.relation == "depends_on")
.map(|e| e.to.clone())
.collect();
TaskInfo {
id: node.id.clone(),
title: node.title.clone(),
description,
goals,
verify,
estimated_turns,
depends_on,
design_ref,
satisfies,
}
}
fn detect_default_checkpoint(graph: &Graph) -> Option<String> {
if let Some(ref project) = graph.project {
if let Some(ref desc) = project.description {
let desc_lower = desc.to_lowercase();
if desc_lower.contains("rust") || desc_lower.contains("cargo") {
return Some("cargo check && cargo test".to_string());
}
if desc_lower.contains("node") || desc_lower.contains("typescript") || desc_lower.contains("javascript") {
return Some("npm test".to_string());
}
if desc_lower.contains("python") {
return Some("pytest".to_string());
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::{Node, Edge, NodeStatus};
fn make_task(id: &str, title: &str) -> Node {
let mut n = Node::new(id, title);
n.node_type = Some("task".to_string());
n
}
fn make_task_with_meta(id: &str, title: &str, turns: u32, verify: &str) -> Node {
let mut n = make_task(id, title);
n.metadata.insert("estimated_turns".to_string(), serde_json::json!(turns));
n.metadata.insert("verify".to_string(), serde_json::json!(verify));
n
}
#[test]
fn test_create_plan_basic() {
let mut graph = Graph::new();
graph.add_node(make_task_with_meta("a", "Task A", 10, "cargo test"));
graph.add_node(make_task_with_meta("b", "Task B", 20, "cargo test"));
graph.add_edge(Edge::depends_on("b", "a"));
let plan = create_plan(&graph).unwrap();
assert_eq!(plan.total_tasks, 2);
assert_eq!(plan.estimated_total_turns, 30);
assert_eq!(plan.layers.len(), 2);
assert_eq!(plan.layers[0].tasks[0].id, "a");
assert_eq!(plan.layers[1].tasks[0].id, "b");
}
#[test]
fn test_create_plan_skips_done() {
let mut graph = Graph::new();
let mut done = make_task("a", "Done Task");
done.status = NodeStatus::Done;
graph.add_node(done);
graph.add_node(make_task("b", "Pending Task"));
graph.add_edge(Edge::depends_on("b", "a"));
let plan = create_plan(&graph).unwrap();
assert_eq!(plan.total_tasks, 1);
assert_eq!(plan.layers[0].tasks[0].id, "b");
}
#[test]
fn test_create_plan_idempotent() {
let mut graph = Graph::new();
graph.add_node(make_task("a", "A"));
graph.add_node(make_task("b", "B"));
graph.add_node(make_task("c", "C"));
graph.add_edge(Edge::depends_on("b", "a"));
graph.add_edge(Edge::depends_on("c", "a"));
let plan1 = serde_json::to_string(&create_plan(&graph).unwrap()).unwrap();
let plan2 = serde_json::to_string(&create_plan(&graph).unwrap()).unwrap();
assert_eq!(plan1, plan2, "create_plan must be idempotent (GUARD-1)");
}
#[test]
fn test_create_plan_extracts_metadata() {
let mut graph = Graph::new();
let mut task = make_task("auth", "Implement auth");
task.metadata.insert("verify".to_string(), serde_json::json!("cargo test --test auth"));
task.metadata.insert("estimated_turns".to_string(), serde_json::json!(20));
task.metadata.insert("design_ref".to_string(), serde_json::json!("3.2"));
task.metadata.insert("satisfies".to_string(), serde_json::json!(["GOAL-1.1", "GOAL-1.2"]));
graph.add_node(task);
let plan = create_plan(&graph).unwrap();
let info = &plan.layers[0].tasks[0];
assert_eq!(info.verify, Some("cargo test --test auth".to_string()));
assert_eq!(info.estimated_turns, 20);
assert_eq!(info.design_ref, Some("3.2".to_string()));
assert_eq!(info.satisfies, vec!["GOAL-1.1", "GOAL-1.2"]);
}
}