diff options
| author | soryu <soryu@soryu.co> | 2026-02-03 23:19:40 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-02-03 23:19:40 +0000 |
| commit | bfa3af9ef16fd5e255bdb606a99a5ebb535ba7cc (patch) | |
| tree | 53da855b4ca61a5c0856fc15112daa7a3748c637 /makima/src/daemon/chain/runner.rs | |
| parent | 1ce281adb89683a5fccfd153706383b14b944f32 (diff) | |
| parent | dcbf8c834626870a43b633b099f409d69d4f9b87 (diff) | |
| download | soryu-makima/discuss-contract-feature.tar.gz soryu-makima/discuss-contract-feature.zip | |
fix: Resolve merge conflict in server/mod.rsmakima/discuss-contract-feature
Combine imports from both branches - include both chains and contract_discuss handlers.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'makima/src/daemon/chain/runner.rs')
| -rw-r--r-- | makima/src/daemon/chain/runner.rs | 364 |
1 files changed, 364 insertions, 0 deletions
diff --git a/makima/src/daemon/chain/runner.rs b/makima/src/daemon/chain/runner.rs new file mode 100644 index 0000000..9c6f6b4 --- /dev/null +++ b/makima/src/daemon/chain/runner.rs @@ -0,0 +1,364 @@ +//! 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); + } +} |
