From 1b692b8cde4a888c8a35af69231f181b57bf5619 Mon Sep 17 00:00:00 2001 From: soryu Date: Fri, 6 Feb 2026 20:06:30 +0000 Subject: Fix: Cleanup old chain code --- makima/src/daemon/chain/dag.rs | 450 -------------------------------------- makima/src/daemon/chain/mod.rs | 13 -- makima/src/daemon/chain/parser.rs | 414 ----------------------------------- makima/src/daemon/chain/runner.rs | 388 -------------------------------- 4 files changed, 1265 deletions(-) delete mode 100644 makima/src/daemon/chain/dag.rs delete mode 100644 makima/src/daemon/chain/mod.rs delete mode 100644 makima/src/daemon/chain/parser.rs delete mode 100644 makima/src/daemon/chain/runner.rs (limited to 'makima/src/daemon/chain') diff --git a/makima/src/daemon/chain/dag.rs b/makima/src/daemon/chain/dag.rs deleted file mode 100644 index 7ba5904..0000000 --- a/makima/src/daemon/chain/dag.rs +++ /dev/null @@ -1,450 +0,0 @@ -//! DAG validation and traversal for chain contracts. -//! -//! Provides cycle detection and topological sorting for contract dependencies. - -use std::collections::{HashMap, HashSet, VecDeque}; -use thiserror::Error; - -use super::parser::ChainDefinition; - -/// Error type for DAG operations. -#[derive(Error, Debug)] -pub enum DagError { - #[error("Cycle detected in dependency graph: {0}")] - CycleDetected(String), - - #[error("Unknown contract in dependency: {0}")] - UnknownContract(String), -} - -/// Validates that the chain definition forms a valid DAG (no cycles). -/// -/// Uses depth-first search with color marking to detect cycles. -/// Returns Ok(()) if valid, or an error describing the cycle. -pub fn validate_dag(chain: &ChainDefinition) -> Result<(), DagError> { - // Build adjacency list from contract dependencies - let mut adjacency: HashMap<&str, Vec<&str>> = HashMap::new(); - let contract_names: HashSet<&str> = chain.contracts.iter().map(|c| c.name.as_str()).collect(); - - for contract in &chain.contracts { - let deps: Vec<&str> = contract - .depends_on - .as_ref() - .map(|d| d.iter().map(|s| s.as_str()).collect()) - .unwrap_or_default(); - - // Validate all dependencies exist - for dep in &deps { - if !contract_names.contains(dep) { - return Err(DagError::UnknownContract(format!( - "Contract '{}' depends on unknown contract '{}'", - contract.name, dep - ))); - } - } - - adjacency.insert(contract.name.as_str(), deps); - } - - // Color-based DFS for cycle detection - // White (0): not visited, Gray (1): in progress, Black (2): completed - let mut color: HashMap<&str, u8> = HashMap::new(); - for name in &contract_names { - color.insert(name, 0); - } - - // Track path for cycle reporting - fn dfs<'a>( - node: &'a str, - adjacency: &HashMap<&'a str, Vec<&'a str>>, - color: &mut HashMap<&'a str, u8>, - path: &mut Vec<&'a str>, - ) -> Result<(), DagError> { - color.insert(node, 1); // Mark as in-progress - path.push(node); - - if let Some(deps) = adjacency.get(node) { - for dep in deps { - match color.get(dep) { - Some(1) => { - // Found cycle - dep is in current path - let cycle_start = path.iter().position(|&n| n == *dep).unwrap(); - let cycle: Vec<_> = path[cycle_start..].to_vec(); - return Err(DagError::CycleDetected(format!( - "{} -> {}", - cycle.join(" -> "), - dep - ))); - } - Some(0) => { - // Not visited - recurse - dfs(dep, adjacency, color, path)?; - } - _ => { - // Already completed - skip - } - } - } - } - - color.insert(node, 2); // Mark as completed - path.pop(); - Ok(()) - } - - // Run DFS from each unvisited node - for name in &contract_names { - if color.get(name) == Some(&0) { - let mut path = Vec::new(); - dfs(name, &adjacency, &mut color, &mut path)?; - } - } - - Ok(()) -} - -/// Returns contracts in topological order (dependencies before dependents). -/// -/// Uses Kahn's algorithm for topological sorting. -pub fn topological_sort(chain: &ChainDefinition) -> Result, DagError> { - // Validate first - validate_dag(chain)?; - - // Build in-degree map and adjacency list - let mut in_degree: HashMap<&str, usize> = HashMap::new(); - let mut dependents: HashMap<&str, Vec<&str>> = HashMap::new(); - - for contract in &chain.contracts { - in_degree.entry(contract.name.as_str()).or_insert(0); - dependents.entry(contract.name.as_str()).or_default(); - - if let Some(deps) = &contract.depends_on { - for dep in deps { - *in_degree.entry(contract.name.as_str()).or_insert(0) += 1; - dependents - .entry(dep.as_str()) - .or_default() - .push(contract.name.as_str()); - } - } - } - - // Kahn's algorithm - let mut queue: VecDeque<&str> = VecDeque::new(); - let mut result: Vec<&str> = Vec::new(); - - // Start with nodes that have no dependencies - for (name, °ree) in &in_degree { - if degree == 0 { - queue.push_back(name); - } - } - - while let Some(node) = queue.pop_front() { - result.push(node); - - if let Some(deps) = dependents.get(node) { - for dep in deps { - if let Some(degree) = in_degree.get_mut(dep) { - *degree -= 1; - if *degree == 0 { - queue.push_back(dep); - } - } - } - } - } - - Ok(result) -} - -/// Returns contracts that are ready to run (have no unmet dependencies). -/// -/// Takes a set of completed contract names and returns contracts that -/// can now be started. -pub fn get_ready_contracts<'a>( - chain: &'a ChainDefinition, - completed: &HashSet<&str>, -) -> Vec<&'a str> { - chain - .contracts - .iter() - .filter(|c| { - // Already completed? Skip - if completed.contains(c.name.as_str()) { - return false; - } - - // Check if all dependencies are met - match &c.depends_on { - None => true, // No dependencies - Some(deps) => deps.iter().all(|d| completed.contains(d.as_str())), - } - }) - .map(|c| c.name.as_str()) - .collect() -} - -/// Get the depth of each contract in the DAG (for layout purposes). -/// -/// Root nodes (no dependencies) have depth 0. -/// Each dependent has depth = max(dependency depths) + 1. -pub fn get_contract_depths(chain: &ChainDefinition) -> HashMap<&str, usize> { - let mut depths: HashMap<&str, usize> = HashMap::new(); - - // Multiple passes to handle dependencies - let max_iterations = chain.contracts.len(); - for _ in 0..max_iterations { - let mut changed = false; - - for contract in &chain.contracts { - let new_depth = match &contract.depends_on { - None => 0, - Some(deps) => { - if deps.iter().all(|d| depths.contains_key(d.as_str())) { - deps.iter() - .filter_map(|d| depths.get(d.as_str())) - .max() - .copied() - .unwrap_or(0) - + 1 - } else { - continue; // Dependencies not yet computed - } - } - }; - - if depths.get(contract.name.as_str()) != Some(&new_depth) { - depths.insert(contract.name.as_str(), new_depth); - changed = true; - } - } - - if !changed { - break; - } - } - - depths -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::daemon::chain::parser::parse_chain_yaml; - - #[test] - fn test_valid_dag() { - let yaml = r#" -name: Valid DAG -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" - - name: D - depends_on: [B, C] - tasks: - - name: Task - plan: "Do D" -"#; - let chain = parse_chain_yaml(yaml).unwrap(); - assert!(validate_dag(&chain).is_ok()); - } - - #[test] - fn test_simple_cycle() { - let yaml = r#" -name: Simple Cycle -contracts: - - name: A - depends_on: [B] - 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 result = validate_dag(&chain); - assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("Cycle detected")); - } - - #[test] - fn test_longer_cycle() { - let yaml = r#" -name: Longer Cycle -contracts: - - name: A - depends_on: [C] - tasks: - - name: Task - plan: "Do A" - - name: B - depends_on: [A] - tasks: - - name: Task - plan: "Do B" - - name: C - depends_on: [B] - tasks: - - name: Task - plan: "Do C" -"#; - let chain = parse_chain_yaml(yaml).unwrap(); - let result = validate_dag(&chain); - assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("Cycle detected")); - } - - #[test] - fn test_topological_sort() { - let yaml = r#" -name: Topo 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" - - name: D - depends_on: [B, C] - tasks: - - name: Task - plan: "Do D" -"#; - let chain = parse_chain_yaml(yaml).unwrap(); - let sorted = topological_sort(&chain).unwrap(); - - // A must come before B, C; B and C must come before D - let pos_a = sorted.iter().position(|&n| n == "A").unwrap(); - let pos_b = sorted.iter().position(|&n| n == "B").unwrap(); - let pos_c = sorted.iter().position(|&n| n == "C").unwrap(); - let pos_d = sorted.iter().position(|&n| n == "D").unwrap(); - - assert!(pos_a < pos_b); - assert!(pos_a < pos_c); - assert!(pos_b < pos_d); - assert!(pos_c < pos_d); - } - - #[test] - fn test_get_ready_contracts() { - let yaml = r#" -name: Ready Test -contracts: - - name: A - tasks: - - name: Task - plan: "Do A" - - name: B - depends_on: [A] - tasks: - - name: Task - plan: "Do B" - - name: C - tasks: - - name: Task - plan: "Do C" -"#; - let chain = parse_chain_yaml(yaml).unwrap(); - - // Initially A and C are ready (no dependencies) - let completed = HashSet::new(); - let mut ready = get_ready_contracts(&chain, &completed); - ready.sort(); - assert_eq!(ready, vec!["A", "C"]); - - // After A completes, B becomes ready - let mut completed = HashSet::new(); - completed.insert("A"); - let ready = get_ready_contracts(&chain, &completed); - assert!(ready.contains(&"B")); - assert!(ready.contains(&"C")); // C still ready if not started - } - - #[test] - fn test_get_contract_depths() { - let yaml = r#" -name: Depth 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: [B] - tasks: - - name: Task - plan: "Do C" -"#; - let chain = parse_chain_yaml(yaml).unwrap(); - let depths = get_contract_depths(&chain); - - assert_eq!(depths.get("A"), Some(&0)); - assert_eq!(depths.get("B"), Some(&1)); - assert_eq!(depths.get("C"), Some(&2)); - } - - #[test] - fn test_diamond_dependency_depths() { - let yaml = r#" -name: Diamond 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" - - name: D - depends_on: [B, C] - tasks: - - name: Task - plan: "Do D" -"#; - let chain = parse_chain_yaml(yaml).unwrap(); - let depths = get_contract_depths(&chain); - - assert_eq!(depths.get("A"), Some(&0)); - assert_eq!(depths.get("B"), Some(&1)); - assert_eq!(depths.get("C"), Some(&1)); - assert_eq!(depths.get("D"), Some(&2)); - } -} diff --git a/makima/src/daemon/chain/mod.rs b/makima/src/daemon/chain/mod.rs deleted file mode 100644 index 5588a27..0000000 --- a/makima/src/daemon/chain/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! Chain module - DAG-based multi-contract orchestration. -//! -//! Chains are directed acyclic graphs (DAGs) of contracts that work together -//! to achieve a larger goal. Each contract can depend on others, and contracts -//! run in parallel when no dependencies exist. - -pub mod dag; -pub mod parser; -pub mod runner; - -pub use dag::{validate_dag, DagError}; -pub use parser::{parse_chain_file, ChainDefinition, ParseError}; -pub use runner::{ChainRunner, RunnerError}; diff --git a/makima/src/daemon/chain/parser.rs b/makima/src/daemon/chain/parser.rs deleted file mode 100644 index b32d0f2..0000000 --- a/makima/src/daemon/chain/parser.rs +++ /dev/null @@ -1,414 +0,0 @@ -//! Chain YAML parser. -//! -//! Parses chain definition files in YAML format into structured data -//! that can be used to create chains and contracts. - -use serde::{Deserialize, Serialize}; -use std::path::Path; -use thiserror::Error; - -/// Error type for chain parsing operations. -#[derive(Error, Debug)] -pub enum ParseError { - #[error("Failed to read chain file: {0}")] - IoError(#[from] std::io::Error), - - #[error("Failed to parse YAML: {0}")] - YamlError(#[from] serde_yaml::Error), - - #[error("Validation error: {0}")] - ValidationError(String), -} - -/// Repository definition in a chain. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RepositoryDefinition { - /// Name of the repository - pub name: String, - /// Repository URL (for remote repos) - pub repository_url: Option, - /// Local path (for local repos) - pub local_path: Option, - /// Source type: remote, local, or managed - #[serde(default = "default_source_type")] - pub source_type: String, - /// Whether this is the primary repository - #[serde(default)] - pub is_primary: bool, -} - -fn default_source_type() -> String { - "remote".to_string() -} - -/// Chain definition parsed from YAML. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ChainDefinition { - /// Name of the chain - pub name: String, - /// Optional description - pub description: Option, - /// Repositories for this chain - #[serde(default)] - pub repositories: Vec, - /// Contracts in this chain - pub contracts: Vec, - /// Loop configuration - #[serde(rename = "loop")] - pub loop_config: Option, -} - -/// Contract definition within a chain. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ContractDefinition { - /// Name of the contract - pub name: String, - /// Optional description - pub description: Option, - /// Contract type (defaults to "simple") - #[serde(rename = "type", default = "default_contract_type")] - pub contract_type: String, - /// Phases for this contract - pub phases: Option>, - /// Names of contracts this depends on (DAG edges) - pub depends_on: Option>, - /// Tasks to create in this contract - pub tasks: Option>, - /// Deliverables for this contract - pub deliverables: Option>, -} - -fn default_contract_type() -> String { - "simple".to_string() -} - -/// Task definition within a contract. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TaskDefinition { - /// Name of the task - pub name: String, - /// Plan/instructions for the task - pub plan: String, -} - -/// Deliverable definition within a contract. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DeliverableDefinition { - /// Unique identifier for the deliverable - pub id: String, - /// Name of the deliverable - pub name: String, - /// Priority level (defaults to "required") - #[serde(default = "default_priority")] - pub priority: String, -} - -fn default_priority() -> String { - "required".to_string() -} - -/// Loop configuration for chain iteration. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct LoopConfig { - /// Whether loop is enabled - #[serde(default)] - pub enabled: bool, - /// Maximum number of iterations - #[serde(default = "default_max_iterations")] - pub max_iterations: i32, - /// Progress check prompt/criteria - pub progress_check: Option, -} - -fn default_max_iterations() -> i32 { - 10 -} - -impl ChainDefinition { - /// Validate the chain definition. - pub fn validate(&self) -> Result<(), ParseError> { - // Check for empty name - if self.name.trim().is_empty() { - return Err(ParseError::ValidationError( - "Chain name cannot be empty".to_string(), - )); - } - - // Check for at least one contract - if self.contracts.is_empty() { - return Err(ParseError::ValidationError( - "Chain must have at least one contract".to_string(), - )); - } - - // Collect all contract names for dependency validation - let contract_names: std::collections::HashSet<_> = - self.contracts.iter().map(|c| c.name.as_str()).collect(); - - // Check for duplicate contract names - if contract_names.len() != self.contracts.len() { - return Err(ParseError::ValidationError( - "Duplicate contract names found".to_string(), - )); - } - - // Validate each contract - for contract in &self.contracts { - contract.validate(&contract_names)?; - } - - Ok(()) - } -} - -impl ContractDefinition { - /// Validate the contract definition. - pub fn validate( - &self, - valid_contract_names: &std::collections::HashSet<&str>, - ) -> Result<(), ParseError> { - // Check for empty name - if self.name.trim().is_empty() { - return Err(ParseError::ValidationError( - "Contract name cannot be empty".to_string(), - )); - } - - // Validate dependencies exist - if let Some(deps) = &self.depends_on { - for dep in deps { - if !valid_contract_names.contains(dep.as_str()) { - return Err(ParseError::ValidationError(format!( - "Contract '{}' depends on unknown contract '{}'", - self.name, dep - ))); - } - // Self-dependency check - if dep == &self.name { - return Err(ParseError::ValidationError(format!( - "Contract '{}' cannot depend on itself", - self.name - ))); - } - } - } - - // Validate tasks - if let Some(tasks) = &self.tasks { - for task in tasks { - if task.name.trim().is_empty() { - return Err(ParseError::ValidationError(format!( - "Task name cannot be empty in contract '{}'", - self.name - ))); - } - if task.plan.trim().is_empty() { - return Err(ParseError::ValidationError(format!( - "Task '{}' in contract '{}' has empty plan", - task.name, self.name - ))); - } - } - } - - Ok(()) - } -} - -/// Parse a chain definition from a YAML file. -pub fn parse_chain_file>(path: P) -> Result { - let content = std::fs::read_to_string(path)?; - parse_chain_yaml(&content) -} - -/// Parse a chain definition from a YAML string. -pub fn parse_chain_yaml(yaml: &str) -> Result { - let definition: ChainDefinition = serde_yaml::from_str(yaml)?; - definition.validate()?; - Ok(definition) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_simple_chain() { - let yaml = r#" -name: Test Chain -description: A test chain -contracts: - - name: Research - type: simple - tasks: - - name: Analyze - plan: "Analyze the codebase" - - name: Implement - type: simple - depends_on: [Research] - tasks: - - name: Build - plan: "Build the feature" -"#; - let chain = parse_chain_yaml(yaml).unwrap(); - assert_eq!(chain.name, "Test Chain"); - assert_eq!(chain.contracts.len(), 2); - assert_eq!(chain.contracts[0].name, "Research"); - assert_eq!(chain.contracts[1].name, "Implement"); - assert_eq!( - chain.contracts[1].depends_on, - Some(vec!["Research".to_string()]) - ); - } - - #[test] - fn test_parse_chain_with_loop() { - let yaml = r#" -name: Iterative Chain -contracts: - - name: Phase1 - tasks: - - name: Task1 - plan: "Do something" -loop: - enabled: true - max_iterations: 5 - progress_check: "Check if goals are met" -"#; - let chain = parse_chain_yaml(yaml).unwrap(); - assert!(chain.loop_config.is_some()); - let loop_config = chain.loop_config.unwrap(); - assert!(loop_config.enabled); - assert_eq!(loop_config.max_iterations, 5); - } - - #[test] - fn test_parse_chain_with_deliverables() { - let yaml = r#" -name: Feature Chain -contracts: - - name: Research - tasks: - - name: Survey - plan: "Survey existing code" - deliverables: - - id: analysis - name: Codebase Analysis - priority: required -"#; - let chain = parse_chain_yaml(yaml).unwrap(); - let deliverables = chain.contracts[0].deliverables.as_ref().unwrap(); - assert_eq!(deliverables.len(), 1); - assert_eq!(deliverables[0].id, "analysis"); - } - - #[test] - fn test_validation_empty_name() { - let yaml = r#" -name: "" -contracts: - - name: Phase1 - tasks: - - name: Task1 - plan: "Do something" -"#; - let result = parse_chain_yaml(yaml); - assert!(result.is_err()); - assert!(result - .unwrap_err() - .to_string() - .contains("name cannot be empty")); - } - - #[test] - fn test_validation_no_contracts() { - let yaml = r#" -name: Empty Chain -contracts: [] -"#; - let result = parse_chain_yaml(yaml); - assert!(result.is_err()); - assert!(result - .unwrap_err() - .to_string() - .contains("at least one contract")); - } - - #[test] - fn test_validation_unknown_dependency() { - let yaml = r#" -name: Bad Chain -contracts: - - name: Phase1 - depends_on: [NonExistent] - tasks: - - name: Task1 - plan: "Do something" -"#; - let result = parse_chain_yaml(yaml); - assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("unknown contract")); - } - - #[test] - fn test_validation_self_dependency() { - let yaml = r#" -name: Self Ref Chain -contracts: - - name: Phase1 - depends_on: [Phase1] - tasks: - - name: Task1 - plan: "Do something" -"#; - let result = parse_chain_yaml(yaml); - assert!(result.is_err()); - assert!(result - .unwrap_err() - .to_string() - .contains("cannot depend on itself")); - } - - #[test] - fn test_validation_duplicate_names() { - let yaml = r#" -name: Dup Chain -contracts: - - name: Phase1 - tasks: - - name: Task1 - plan: "Do something" - - name: Phase1 - tasks: - - name: Task2 - plan: "Do another thing" -"#; - let result = parse_chain_yaml(yaml); - assert!(result.is_err()); - assert!(result - .unwrap_err() - .to_string() - .contains("Duplicate contract names")); - } - - #[test] - fn test_repo_alias() { - let yaml = r#" -name: Repo Chain -repositories: - - name: main - repository_url: https://github.com/user/project -contracts: - - name: Phase1 - tasks: - - name: Task1 - plan: "Work on repo" -"#; - let chain = parse_chain_yaml(yaml).unwrap(); - assert_eq!(chain.repositories.len(), 1); - assert_eq!( - chain.repositories[0].repository_url, - Some("https://github.com/user/project".to_string()) - ); - } -} diff --git a/makima/src/daemon/chain/runner.rs b/makima/src/daemon/chain/runner.rs deleted file mode 100644 index 1814581..0000000 --- a/makima/src/daemon/chain/runner.rs +++ /dev/null @@ -1,388 +0,0 @@ -//! 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::{ - AddChainRepositoryRequest, 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 - #[allow(dead_code)] - api_url: String, - /// API key for authentication - #[allow(dead_code)] - 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), - }; - - // Convert repository definitions to API format - let repositories: Vec = chain - .repositories - .iter() - .map(|r| AddChainRepositoryRequest { - name: r.name.clone(), - repository_url: r.repository_url.clone(), - local_path: r.local_path.clone(), - source_type: r.source_type.clone(), - is_primary: r.is_primary, - }) - .collect(); - - CreateChainRequest { - name: chain.name.clone(), - description: chain.description.clone(), - repository_url: None, // Legacy field, repositories take precedence - repositories: if repositories.is_empty() { - None - } else { - Some(repositories) - }, - 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 -repositories: - - name: main - repository_url: 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())); - // Repositories are now in a separate array - let repos = request.repositories.unwrap(); - assert_eq!(repos.len(), 1); - assert_eq!( - repos[0].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); - } -} -- cgit v1.2.3