//! 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<P: AsRef<Path>>(&self, path: P) -> Result<ChainDefinition, RunnerError> {
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<CreateChainContractRequest> = 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<Vec<&'a str>, 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<String> = 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<usize, Vec<&str>> = 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<String> = 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<String, (f64, f64)> {
use super::dag::get_contract_depths;
let depths = get_contract_depths(chain);
let mut positions: HashMap<String, (f64, f64)> = HashMap::new();
// Group by depth
let mut by_depth: HashMap<usize, Vec<&str>> = 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);
}
}