summaryrefslogtreecommitdiff
path: root/makima/src/daemon/chain/runner.rs
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-02-03 22:01:29 +0000
committersoryu <soryu@soryu.co>2026-02-03 22:01:37 +0000
commitcf0a25af1d2834bfe6c5ea892ce5769936e5a673 (patch)
tree476ba326ac1752281a441b5c17d2b3be4b23a2a9 /makima/src/daemon/chain/runner.rs
parent8361916ce67f3d2ba191ebf27cb50e79cb42e39c (diff)
downloadsoryu-cf0a25af1d2834bfe6c5ea892ce5769936e5a673.tar.gz
soryu-cf0a25af1d2834bfe6c5ea892ce5769936e5a673.zip
Add makima chain mechanism
Diffstat (limited to 'makima/src/daemon/chain/runner.rs')
-rw-r--r--makima/src/daemon/chain/runner.rs364
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);
+ }
+}