//! Chain runner - creates and orchestrates contracts from chain definitions. //! //! Handles the lifecycle of a chain: //! 1. Parse chain definition //! 2. Validate DAG //! 3. Create chain record //! 4. Create contracts in dependency order //! 5. Monitor and trigger dependent contracts use std::collections::HashMap; use std::path::Path; use thiserror::Error; use super::dag::{topological_sort, validate_dag, DagError}; use super::parser::{parse_chain_file, ChainDefinition, ParseError}; use crate::db::models::{ CreateChainContractRequest, CreateChainDeliverableRequest, CreateChainRequest, CreateChainTaskRequest, }; /// Error type for chain runner operations. #[derive(Error, Debug)] pub enum RunnerError { #[error("Parse error: {0}")] Parse(#[from] ParseError), #[error("DAG error: {0}")] Dag(#[from] DagError), #[error("API error: {0}")] Api(String), #[error("Contract creation failed: {0}")] ContractCreation(String), } /// Chain runner for creating and managing chains. pub struct ChainRunner { /// Base API URL api_url: String, /// API key for authentication api_key: String, } impl ChainRunner { /// Create a new chain runner. pub fn new(api_url: String, api_key: String) -> Self { Self { api_url, api_key } } /// Load and validate a chain from a YAML file. pub fn load_chain>(&self, path: P) -> Result { let chain = parse_chain_file(path)?; validate_dag(&chain)?; Ok(chain) } /// Convert a chain definition to a CreateChainRequest for API submission. pub fn to_create_request(&self, chain: &ChainDefinition) -> CreateChainRequest { let contracts: Vec = chain .contracts .iter() .map(|c| CreateChainContractRequest { name: c.name.clone(), description: c.description.clone(), contract_type: Some(c.contract_type.clone()), initial_phase: None, phases: c.phases.clone(), depends_on: c.depends_on.clone(), tasks: c.tasks.as_ref().map(|tasks| { tasks .iter() .map(|t| CreateChainTaskRequest { name: t.name.clone(), plan: t.plan.clone(), }) .collect() }), deliverables: c.deliverables.as_ref().map(|dels| { dels.iter() .map(|d| CreateChainDeliverableRequest { id: d.id.clone(), name: d.name.clone(), priority: Some(d.priority.clone()), }) .collect() }), editor_x: None, editor_y: None, }) .collect(); let (loop_enabled, loop_max_iterations, loop_progress_check) = match &chain.loop_config { Some(lc) => ( Some(lc.enabled), Some(lc.max_iterations), lc.progress_check.clone(), ), None => (None, None, None), }; CreateChainRequest { name: chain.name.clone(), description: chain.description.clone(), repository_url: chain.repository_url.clone(), local_path: chain.local_path.clone(), loop_enabled, loop_max_iterations, loop_progress_check, contracts: Some(contracts), } } /// Get contracts in topological order (for display/debugging). pub fn get_execution_order<'a>( &self, chain: &'a ChainDefinition, ) -> Result, RunnerError> { Ok(topological_sort(chain)?) } /// Generate ASCII visualization of the chain DAG. pub fn visualize_dag(&self, chain: &ChainDefinition) -> String { use super::dag::get_contract_depths; let depths = get_contract_depths(chain); let mut lines: Vec = vec![]; lines.push(format!("Chain: {}", chain.name)); if let Some(desc) = &chain.description { lines.push(format!(" {}", desc)); } lines.push(String::new()); // Group contracts by depth let mut by_depth: HashMap> = HashMap::new(); for contract in &chain.contracts { let depth = depths.get(contract.name.as_str()).copied().unwrap_or(0); by_depth.entry(depth).or_default().push(&contract.name); } // Find max depth let max_depth = by_depth.keys().max().copied().unwrap_or(0); // Build visualization for depth in 0..=max_depth { if let Some(contracts) = by_depth.get(&depth) { let contract_strs: Vec = contracts .iter() .map(|name| format!("[{}]", name)) .collect(); let indent = " ".repeat(depth); lines.push(format!("{}{}", indent, contract_strs.join(" "))); // Draw arrows to next level if depth < max_depth { if let Some(next_contracts) = by_depth.get(&(depth + 1)) { // Find which contracts connect to the next level for next in next_contracts { let next_contract = chain .contracts .iter() .find(|c| c.name.as_str() == *next) .unwrap(); if let Some(deps) = &next_contract.depends_on { for dep in deps { if contracts.contains(&dep.as_str()) { let arrow_indent = " ".repeat(depth); lines.push(format!("{} │", arrow_indent)); lines.push(format!("{} ▼", arrow_indent)); } } } } } } } } lines.join("\n") } } /// Compute editor positions for contracts based on DAG layout. /// /// Returns a map of contract name to (x, y) positions suitable for /// the GUI editor. pub fn compute_editor_positions(chain: &ChainDefinition) -> HashMap { use super::dag::get_contract_depths; let depths = get_contract_depths(chain); let mut positions: HashMap = HashMap::new(); // Group by depth let mut by_depth: HashMap> = HashMap::new(); for contract in &chain.contracts { let depth = depths.get(contract.name.as_str()).copied().unwrap_or(0); by_depth.entry(depth).or_default().push(&contract.name); } // Compute positions: x based on depth, y based on index within depth let x_spacing = 250.0; let y_spacing = 150.0; for (depth, contracts) in &by_depth { let x = (*depth as f64) * x_spacing + 100.0; for (i, name) in contracts.iter().enumerate() { let y = (i as f64) * y_spacing + 100.0; positions.insert(name.to_string(), (x, y)); } } positions } #[cfg(test)] mod tests { use super::*; use crate::daemon::chain::parser::parse_chain_yaml; #[test] fn test_to_create_request() { let yaml = r#" name: Test Chain description: A test chain repo: https://github.com/test/repo contracts: - name: Research type: simple phases: [plan, execute] tasks: - name: Analyze plan: "Analyze the codebase" deliverables: - id: analysis name: Analysis Doc priority: required - name: Implement depends_on: [Research] tasks: - name: Build plan: "Build the feature" loop: enabled: true max_iterations: 5 progress_check: "Check completion" "#; let chain = parse_chain_yaml(yaml).unwrap(); let runner = ChainRunner::new("http://localhost".to_string(), "key".to_string()); let request = runner.to_create_request(&chain); assert_eq!(request.name, "Test Chain"); assert_eq!(request.description, Some("A test chain".to_string())); assert_eq!( request.repository_url, Some("https://github.com/test/repo".to_string()) ); assert_eq!(request.loop_enabled, Some(true)); assert_eq!(request.loop_max_iterations, Some(5)); let contracts = request.contracts.unwrap(); assert_eq!(contracts.len(), 2); assert_eq!(contracts[0].name, "Research"); assert_eq!(contracts[0].phases, Some(vec!["plan".to_string(), "execute".to_string()])); assert_eq!( contracts[1].depends_on, Some(vec!["Research".to_string()]) ); } #[test] fn test_get_execution_order() { let yaml = r#" name: Order Test contracts: - name: C depends_on: [B] tasks: - name: Task plan: "Do C" - name: A tasks: - name: Task plan: "Do A" - name: B depends_on: [A] tasks: - name: Task plan: "Do B" "#; let chain = parse_chain_yaml(yaml).unwrap(); let runner = ChainRunner::new("http://localhost".to_string(), "key".to_string()); let order = runner.get_execution_order(&chain).unwrap(); let pos_a = order.iter().position(|&n| n == "A").unwrap(); let pos_b = order.iter().position(|&n| n == "B").unwrap(); let pos_c = order.iter().position(|&n| n == "C").unwrap(); assert!(pos_a < pos_b); assert!(pos_b < pos_c); } #[test] fn test_visualize_dag() { let yaml = r#" name: Visual Test description: Test visualization contracts: - name: A tasks: - name: Task plan: "Do A" - name: B depends_on: [A] tasks: - name: Task plan: "Do B" "#; let chain = parse_chain_yaml(yaml).unwrap(); let runner = ChainRunner::new("http://localhost".to_string(), "key".to_string()); let viz = runner.visualize_dag(&chain); assert!(viz.contains("Chain: Visual Test")); assert!(viz.contains("[A]")); assert!(viz.contains("[B]")); } #[test] fn test_compute_editor_positions() { let yaml = r#" name: Position Test contracts: - name: A tasks: - name: Task plan: "Do A" - name: B depends_on: [A] tasks: - name: Task plan: "Do B" - name: C depends_on: [A] tasks: - name: Task plan: "Do C" "#; let chain = parse_chain_yaml(yaml).unwrap(); let positions = compute_editor_positions(&chain); // A should be at depth 0 (x = 100) let (a_x, _) = positions.get("A").unwrap(); assert_eq!(*a_x, 100.0); // B and C should be at depth 1 (x = 350) let (b_x, _) = positions.get("B").unwrap(); let (c_x, _) = positions.get("C").unwrap(); assert_eq!(*b_x, 350.0); assert_eq!(*c_x, 350.0); } }