summaryrefslogtreecommitdiff
path: root/makima/src/daemon/chain
diff options
context:
space:
mode:
Diffstat (limited to 'makima/src/daemon/chain')
-rw-r--r--makima/src/daemon/chain/dag.rs450
-rw-r--r--makima/src/daemon/chain/mod.rs13
-rw-r--r--makima/src/daemon/chain/parser.rs414
-rw-r--r--makima/src/daemon/chain/runner.rs388
4 files changed, 0 insertions, 1265 deletions
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<Vec<&str>, 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, &degree) 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<String>,
- /// Local path (for local repos)
- pub local_path: Option<String>,
- /// 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<String>,
- /// Repositories for this chain
- #[serde(default)]
- pub repositories: Vec<RepositoryDefinition>,
- /// Contracts in this chain
- pub contracts: Vec<ContractDefinition>,
- /// Loop configuration
- #[serde(rename = "loop")]
- pub loop_config: Option<LoopConfig>,
-}
-
-/// 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<String>,
- /// Contract type (defaults to "simple")
- #[serde(rename = "type", default = "default_contract_type")]
- pub contract_type: String,
- /// Phases for this contract
- pub phases: Option<Vec<String>>,
- /// Names of contracts this depends on (DAG edges)
- pub depends_on: Option<Vec<String>>,
- /// Tasks to create in this contract
- pub tasks: Option<Vec<TaskDefinition>>,
- /// Deliverables for this contract
- pub deliverables: Option<Vec<DeliverableDefinition>>,
-}
-
-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<String>,
-}
-
-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<P: AsRef<Path>>(path: P) -> Result<ChainDefinition, ParseError> {
- 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<ChainDefinition, ParseError> {
- 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<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),
- };
-
- // Convert repository definitions to API format
- let repositories: Vec<AddChainRepositoryRequest> = 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<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
-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);
- }
-}