summaryrefslogtreecommitdiff
path: root/makima/src/server
diff options
context:
space:
mode:
Diffstat (limited to 'makima/src/server')
-rw-r--r--makima/src/server/handlers/contract_chat.rs1223
-rw-r--r--makima/src/server/handlers/contracts.rs140
-rw-r--r--makima/src/server/handlers/directives.rs1488
-rw-r--r--makima/src/server/handlers/mod.rs3
-rw-r--r--makima/src/server/mod.rs72
5 files changed, 1622 insertions, 1304 deletions
diff --git a/makima/src/server/handlers/contract_chat.rs b/makima/src/server/handlers/contract_chat.rs
index 06b3a7c..8153093 100644
--- a/makima/src/server/handlers/contract_chat.rs
+++ b/makima/src/server/handlers/contract_chat.rs
@@ -17,8 +17,6 @@ use uuid::Uuid;
use crate::db::{
models::{
ContractChatHistoryResponse, ContractWithRelations, CreateTaskRequest, UpdateFileRequest,
- AddContractDefinitionRequest, UpdateContractDefinitionRequest, CreateChainRequest,
- CreateChainDirectiveRequest, CreateContractEvaluationRequest,
},
repository,
};
@@ -2767,1211 +2765,26 @@ async fn handle_contract_request(
}
}
- // Chain directive tools - for directive contracts to create and manage chains
- ContractToolRequest::CreateChainFromDirective { name, description } => {
- // First, get the current contract to verify it's a directive contract
- let contract = match repository::get_contract_for_owner(pool, contract_id, owner_id).await {
- Ok(Some(c)) => c,
- Ok(None) => return ContractRequestResult {
- success: false,
- message: "Contract not found".to_string(),
- data: None,
- },
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Database error: {}", e),
- data: None,
- },
- };
-
- // Check if contract already has a spawned chain
- if contract.spawned_chain_id.is_some() {
- return ContractRequestResult {
- success: false,
- message: "This contract already has a chain associated with it".to_string(),
- data: Some(json!({ "existing_chain_id": contract.spawned_chain_id })),
- };
- }
-
- // Create the chain
- let chain_req = CreateChainRequest {
- name: name.clone(),
- description: description.clone(),
- repositories: None,
- loop_enabled: None,
- loop_max_iterations: None,
- loop_progress_check: None,
- contracts: None,
- };
-
- let chain = match repository::create_chain_for_owner(pool, owner_id, chain_req).await {
- Ok(c) => c,
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Failed to create chain: {}", e),
- data: None,
- },
- };
-
- // Link the chain to this directive contract
- if let Err(e) = sqlx::query(
- r#"
- UPDATE chains SET directive_contract_id = $2, evaluation_enabled = true WHERE id = $1;
- UPDATE contracts SET spawned_chain_id = $1, is_chain_directive = true WHERE id = $2;
- "#,
- )
- .bind(chain.id)
- .bind(contract_id)
- .execute(pool)
- .await {
- return ContractRequestResult {
- success: false,
- message: format!("Failed to link chain to contract: {}", e),
- data: None,
- };
- }
-
- // Create empty directive for the chain
- let directive_req = CreateChainDirectiveRequest {
- requirements: Some(vec![]),
- acceptance_criteria: Some(vec![]),
- constraints: Some(vec![]),
- external_dependencies: Some(vec![]),
- source_type: Some("llm_generated".to_string()),
- };
-
- if let Err(e) = repository::create_chain_directive(pool, chain.id, directive_req).await {
- return ContractRequestResult {
- success: false,
- message: format!("Failed to create directive: {}", e),
- data: None,
- };
- }
-
- ContractRequestResult {
- success: true,
- message: format!("Created chain '{}' linked to this directive contract", name),
- data: Some(json!({
- "chain_id": chain.id,
- "chain_name": name,
- "description": description
- })),
- }
- }
-
- ContractToolRequest::AddChainContract { name, description, contract_type, depends_on, requirement_ids } => {
- // Get the contract's spawned chain
- let contract = match repository::get_contract_for_owner(pool, contract_id, owner_id).await {
- Ok(Some(c)) => c,
- Ok(None) => return ContractRequestResult {
- success: false,
- message: "Contract not found".to_string(),
- data: None,
- },
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Database error: {}", e),
- data: None,
- },
- };
-
- let chain_id = match contract.spawned_chain_id {
- Some(id) => id,
- None => return ContractRequestResult {
- success: false,
- message: "This contract has no associated chain. Use create_chain_from_directive first.".to_string(),
- data: None,
- },
- };
-
- // Check for duplicate names
- let existing_defs = match repository::list_chain_contract_definitions(pool, chain_id).await {
- Ok(defs) => defs,
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Failed to list definitions: {}", e),
- data: None,
- },
- };
-
- if existing_defs.iter().any(|d| d.name == name) {
- return ContractRequestResult {
- success: false,
- message: format!("A contract definition with name '{}' already exists", name),
- data: None,
- };
- }
-
- // Create the contract definition
- let def_req = AddContractDefinitionRequest {
- name: name.clone(),
- description,
- contract_type: contract_type.unwrap_or_else(|| "implementation".to_string()),
- initial_phase: Some("research".to_string()),
- depends_on,
- tasks: None,
- deliverables: None,
- validation: None,
- editor_x: None,
- editor_y: None,
- };
-
- let definition = match repository::create_chain_contract_definition(pool, chain_id, def_req).await {
- Ok(d) => d,
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Failed to create contract definition: {}", e),
- data: None,
- },
- };
-
- // Update requirement_ids if provided
- if let Some(req_ids) = requirement_ids {
- if !req_ids.is_empty() {
- if let Err(e) = sqlx::query(
- "UPDATE chain_contract_definitions SET requirement_ids = $2 WHERE id = $1"
- )
- .bind(definition.id)
- .bind(&req_ids)
- .execute(pool)
- .await {
- tracing::warn!("Failed to set requirement_ids: {}", e);
- }
- }
- }
-
- ContractRequestResult {
- success: true,
- message: format!("Added contract '{}' to chain", name),
- data: Some(json!({
- "definition_id": definition.id,
- "name": name,
- "order_index": definition.order_index
- })),
- }
- }
-
- ContractToolRequest::SetChainDependencies { contract_name, depends_on } => {
- // Get the contract's spawned chain
- let contract = match repository::get_contract_for_owner(pool, contract_id, owner_id).await {
- Ok(Some(c)) => c,
- Ok(None) => return ContractRequestResult {
- success: false,
- message: "Contract not found".to_string(),
- data: None,
- },
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Database error: {}", e),
- data: None,
- },
- };
-
- let chain_id = match contract.spawned_chain_id {
- Some(id) => id,
- None => return ContractRequestResult {
- success: false,
- message: "This contract has no associated chain".to_string(),
- data: None,
- },
- };
-
- // Find the definition by name
- let definitions = match repository::list_chain_contract_definitions(pool, chain_id).await {
- Ok(defs) => defs,
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Failed to list definitions: {}", e),
- data: None,
- },
- };
-
- let definition = match definitions.iter().find(|d| d.name == contract_name) {
- Some(d) => d,
- None => return ContractRequestResult {
- success: false,
- message: format!("No contract definition named '{}' found", contract_name),
- data: None,
- },
- };
-
- // Validate that all dependencies exist
- for dep_name in &depends_on {
- if !definitions.iter().any(|d| &d.name == dep_name) {
- return ContractRequestResult {
- success: false,
- message: format!("Dependency '{}' does not exist", dep_name),
- data: None,
- };
- }
- }
-
- // Check for circular dependencies (simple check)
- if depends_on.contains(&contract_name) {
- return ContractRequestResult {
- success: false,
- message: "A contract cannot depend on itself".to_string(),
- data: None,
- };
- }
-
- // Update dependencies
- let update_req = UpdateContractDefinitionRequest {
- name: None,
- description: None,
- contract_type: None,
- initial_phase: None,
- depends_on: Some(depends_on.clone()),
- tasks: None,
- deliverables: None,
- validation: None,
- editor_x: None,
- editor_y: None,
- };
-
- match repository::update_chain_contract_definition(pool, definition.id, update_req).await {
- Ok(_) => ContractRequestResult {
- success: true,
- message: format!("Updated dependencies for '{}'", contract_name),
- data: Some(json!({
- "contract_name": contract_name,
- "depends_on": depends_on
- })),
- },
- Err(e) => ContractRequestResult {
- success: false,
- message: format!("Failed to update dependencies: {}", e),
- data: None,
- },
- }
- }
-
- ContractToolRequest::ModifyChainContract { name, new_name, description, add_requirement_ids, remove_requirement_ids } => {
- // Get the contract's spawned chain
- let contract = match repository::get_contract_for_owner(pool, contract_id, owner_id).await {
- Ok(Some(c)) => c,
- Ok(None) => return ContractRequestResult {
- success: false,
- message: "Contract not found".to_string(),
- data: None,
- },
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Database error: {}", e),
- data: None,
- },
- };
-
- let chain_id = match contract.spawned_chain_id {
- Some(id) => id,
- None => return ContractRequestResult {
- success: false,
- message: "This contract has no associated chain".to_string(),
- data: None,
- },
- };
-
- // Find the definition by name
- let definitions = match repository::list_chain_contract_definitions(pool, chain_id).await {
- Ok(defs) => defs,
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Failed to list definitions: {}", e),
- data: None,
- },
- };
-
- let definition = match definitions.iter().find(|d| d.name == name) {
- Some(d) => d.clone(),
- None => return ContractRequestResult {
- success: false,
- message: format!("No contract definition named '{}' found", name),
- data: None,
- },
- };
-
- // Check if new name would conflict
- if let Some(ref nn) = new_name {
- if nn != &name && definitions.iter().any(|d| &d.name == nn) {
- return ContractRequestResult {
- success: false,
- message: format!("A contract definition named '{}' already exists", nn),
- data: None,
- };
- }
- }
-
- // Update the definition
- let update_req = UpdateContractDefinitionRequest {
- name: new_name.clone(),
- description,
- contract_type: None,
- initial_phase: None,
- depends_on: None,
- tasks: None,
- deliverables: None,
- validation: None,
- editor_x: None,
- editor_y: None,
- };
-
- if let Err(e) = repository::update_chain_contract_definition(pool, definition.id, update_req).await {
- return ContractRequestResult {
- success: false,
- message: format!("Failed to update definition: {}", e),
- data: None,
- };
- }
-
- // Handle requirement_ids modifications
- let mut current_req_ids: Vec<String> = definition.requirement_ids.clone();
- if let Some(add_ids) = add_requirement_ids {
- for id in add_ids {
- if !current_req_ids.contains(&id) {
- current_req_ids.push(id);
- }
- }
- }
- if let Some(remove_ids) = remove_requirement_ids {
- current_req_ids.retain(|id| !remove_ids.contains(id));
- }
-
- if current_req_ids != definition.requirement_ids {
- if let Err(e) = sqlx::query(
- "UPDATE chain_contract_definitions SET requirement_ids = $2 WHERE id = $1"
- )
- .bind(definition.id)
- .bind(&current_req_ids)
- .execute(pool)
- .await {
- tracing::warn!("Failed to update requirement_ids: {}", e);
- }
- }
-
- ContractRequestResult {
- success: true,
- message: format!("Modified contract definition '{}'", new_name.as_ref().unwrap_or(&name)),
- data: Some(json!({
- "definition_id": definition.id,
- "name": new_name.as_ref().unwrap_or(&name),
- "requirement_ids": current_req_ids
- })),
- }
- }
-
- ContractToolRequest::RemoveChainContract { name } => {
- // Get the contract's spawned chain
- let contract = match repository::get_contract_for_owner(pool, contract_id, owner_id).await {
- Ok(Some(c)) => c,
- Ok(None) => return ContractRequestResult {
- success: false,
- message: "Contract not found".to_string(),
- data: None,
- },
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Database error: {}", e),
- data: None,
- },
- };
-
- let chain_id = match contract.spawned_chain_id {
- Some(id) => id,
- None => return ContractRequestResult {
- success: false,
- message: "This contract has no associated chain".to_string(),
- data: None,
- },
- };
-
- // Find the definition by name
- let definitions = match repository::list_chain_contract_definitions(pool, chain_id).await {
- Ok(defs) => defs,
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Failed to list definitions: {}", e),
- data: None,
- },
- };
-
- let definition = match definitions.iter().find(|d| d.name == name) {
- Some(d) => d,
- None => return ContractRequestResult {
- success: false,
- message: format!("No contract definition named '{}' found", name),
- data: None,
- },
- };
-
- // Check if other definitions depend on this one
- let dependents: Vec<&str> = definitions.iter()
- .filter(|d| d.depends_on_names.contains(&name))
- .map(|d| d.name.as_str())
- .collect();
-
- if !dependents.is_empty() {
- return ContractRequestResult {
- success: false,
- message: format!("Cannot remove '{}': other contracts depend on it: {}", name, dependents.join(", ")),
- data: None,
- };
- }
-
- // Delete the definition
- match repository::delete_chain_contract_definition(pool, definition.id).await {
- Ok(true) => ContractRequestResult {
- success: true,
- message: format!("Removed contract definition '{}'", name),
- data: Some(json!({ "removed": name })),
- },
- Ok(false) => ContractRequestResult {
- success: false,
- message: format!("Failed to remove '{}': not found", name),
- data: None,
- },
- Err(e) => ContractRequestResult {
- success: false,
- message: format!("Failed to remove definition: {}", e),
- data: None,
- },
- }
- }
-
- ContractToolRequest::PreviewChainDag => {
- // Get the contract's spawned chain
- let contract = match repository::get_contract_for_owner(pool, contract_id, owner_id).await {
- Ok(Some(c)) => c,
- Ok(None) => return ContractRequestResult {
- success: false,
- message: "Contract not found".to_string(),
- data: None,
- },
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Database error: {}", e),
- data: None,
- },
- };
-
- let chain_id = match contract.spawned_chain_id {
- Some(id) => id,
- None => return ContractRequestResult {
- success: false,
- message: "This contract has no associated chain".to_string(),
- data: None,
- },
- };
-
- // Get chain details and definitions
- let chain = match repository::get_chain_for_owner(pool, chain_id, owner_id).await {
- Ok(Some(c)) => c,
- Ok(None) => return ContractRequestResult {
- success: false,
- message: "Chain not found".to_string(),
- data: None,
- },
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Database error: {}", e),
- data: None,
- },
- };
-
- let definitions = match repository::list_chain_contract_definitions(pool, chain_id).await {
- Ok(defs) => defs,
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Failed to list definitions: {}", e),
- data: None,
- },
- };
-
- // Build DAG representation
- let nodes: Vec<serde_json::Value> = definitions.iter().map(|d| {
- json!({
- "name": d.name,
- "description": d.description,
- "contract_type": d.contract_type,
- "depends_on": d.depends_on_names,
- "requirement_ids": d.requirement_ids
- })
- }).collect();
-
- // Build ASCII DAG representation
- let mut ascii_dag = String::new();
- ascii_dag.push_str(&format!("Chain: {} ({})\n", chain.name, chain.status));
- ascii_dag.push_str(&format!("Contracts: {}\n\n", definitions.len()));
-
- // Find root nodes (no dependencies)
- let roots: Vec<&str> = definitions.iter()
- .filter(|d| d.depends_on_names.is_empty())
- .map(|d| d.name.as_str())
- .collect();
-
- ascii_dag.push_str("Root contracts (no dependencies):\n");
- for root in &roots {
- ascii_dag.push_str(&format!(" [{}]\n", root));
- }
-
- ascii_dag.push_str("\nDependency relationships:\n");
- for def in &definitions {
- if !def.depends_on_names.is_empty() {
- ascii_dag.push_str(&format!(" {} <- {}\n", def.name, def.depends_on_names.join(", ")));
- }
- }
-
- ContractRequestResult {
- success: true,
- message: format!("Chain DAG preview with {} contracts", definitions.len()),
- data: Some(json!({
- "chain_id": chain_id,
- "chain_name": chain.name,
- "chain_status": chain.status,
- "contract_count": definitions.len(),
- "nodes": nodes,
- "ascii_dag": ascii_dag
- })),
- }
- }
-
- ContractToolRequest::ValidateChainDirective => {
- // Get the contract's spawned chain
- let contract = match repository::get_contract_for_owner(pool, contract_id, owner_id).await {
- Ok(Some(c)) => c,
- Ok(None) => return ContractRequestResult {
- success: false,
- message: "Contract not found".to_string(),
- data: None,
- },
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Database error: {}", e),
- data: None,
- },
- };
-
- let chain_id = match contract.spawned_chain_id {
- Some(id) => id,
- None => return ContractRequestResult {
- success: false,
- message: "This contract has no associated chain".to_string(),
- data: None,
- },
- };
-
- let definitions = match repository::list_chain_contract_definitions(pool, chain_id).await {
- Ok(defs) => defs,
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Failed to list definitions: {}", e),
- data: None,
- },
- };
-
- let mut errors: Vec<String> = Vec::new();
- let mut warnings: Vec<String> = Vec::new();
-
- // Check for empty chain
- if definitions.is_empty() {
- errors.push("Chain has no contract definitions".to_string());
- }
-
- // Check for circular dependencies
- let def_names: std::collections::HashSet<String> = definitions.iter().map(|d| d.name.clone()).collect();
- for def in &definitions {
- for dep in &def.depends_on_names {
- if !def_names.contains(dep) {
- errors.push(format!("'{}' depends on non-existent contract '{}'", def.name, dep));
- }
- }
- }
-
- // Simple cycle detection using DFS
- fn has_cycle(
- name: &str,
- definitions: &[crate::db::models::ChainContractDefinition],
- visited: &mut std::collections::HashSet<String>,
- rec_stack: &mut std::collections::HashSet<String>,
- ) -> Option<String> {
- visited.insert(name.to_string());
- rec_stack.insert(name.to_string());
-
- if let Some(def) = definitions.iter().find(|d| d.name == name) {
- for dep in &def.depends_on_names {
- if !visited.contains(dep) {
- if let Some(cycle) = has_cycle(dep, definitions, visited, rec_stack) {
- return Some(cycle);
- }
- } else if rec_stack.contains(dep) {
- return Some(format!("{} -> {}", name, dep));
- }
- }
- }
-
- rec_stack.remove(name);
- None
- }
-
- let mut visited = std::collections::HashSet::new();
- for def in &definitions {
- if !visited.contains(&def.name) {
- let mut rec_stack = std::collections::HashSet::new();
- if let Some(cycle) = has_cycle(&def.name, &definitions, &mut visited, &mut rec_stack) {
- errors.push(format!("Circular dependency detected: {}", cycle));
- break;
- }
- }
- }
-
- // Check for orphan contracts (no one depends on them and they're not root)
- let roots: std::collections::HashSet<&str> = definitions.iter()
- .filter(|d| d.depends_on_names.is_empty())
- .map(|d| d.name.as_str())
- .collect();
-
- let depended_on: std::collections::HashSet<&str> = definitions.iter()
- .flat_map(|d| d.depends_on_names.iter().map(|s| s.as_str()))
- .collect();
-
- for def in &definitions {
- if !roots.contains(def.name.as_str()) && !depended_on.contains(def.name.as_str()) {
- warnings.push(format!("'{}' has dependencies but nothing depends on it (orphan leaf)", def.name));
- }
- }
-
- // Get directive to check requirement coverage
- if let Ok(Some(directive)) = repository::get_chain_directive(pool, chain_id).await {
- let requirements: Vec<crate::db::models::DirectiveRequirement> =
- serde_json::from_value(directive.requirements.clone()).unwrap_or_default();
-
- let covered: std::collections::HashSet<&str> = definitions.iter()
- .flat_map(|d| d.requirement_ids.iter().map(|s| s.as_str()))
- .collect();
-
- for req in &requirements {
- if !covered.contains(req.id.as_str()) {
- warnings.push(format!("Requirement '{}' ({}) is not covered by any contract", req.id, req.title));
- }
- }
- }
-
- let is_valid = errors.is_empty();
-
- ContractRequestResult {
- success: is_valid,
- message: if is_valid {
- format!("Chain is valid with {} contracts", definitions.len())
- } else {
- format!("Chain validation failed with {} errors", errors.len())
- },
- data: Some(json!({
- "valid": is_valid,
- "contract_count": definitions.len(),
- "errors": errors,
- "warnings": warnings
- })),
- }
- }
-
- ContractToolRequest::FinalizeChainDirective { auto_start } => {
- // Get the contract's spawned chain
- let contract = match repository::get_contract_for_owner(pool, contract_id, owner_id).await {
- Ok(Some(c)) => c,
- Ok(None) => return ContractRequestResult {
- success: false,
- message: "Contract not found".to_string(),
- data: None,
- },
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Database error: {}", e),
- data: None,
- },
- };
-
- let chain_id = match contract.spawned_chain_id {
- Some(id) => id,
- None => return ContractRequestResult {
- success: false,
- message: "This contract has no associated chain".to_string(),
- data: None,
- },
- };
-
- // Get chain
- let chain = match repository::get_chain_for_owner(pool, chain_id, owner_id).await {
- Ok(Some(c)) => c,
- Ok(None) => return ContractRequestResult {
- success: false,
- message: "Chain not found".to_string(),
- data: None,
- },
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Database error: {}", e),
- data: None,
- },
- };
-
- if chain.status != "pending" {
- return ContractRequestResult {
- success: false,
- message: format!("Chain is already {} - cannot finalize", chain.status),
- data: None,
- };
- }
-
- // Update chain status
- let new_status = if auto_start { "active" } else { "pending" };
- if let Err(e) = sqlx::query("UPDATE chains SET status = $2 WHERE id = $1")
- .bind(chain_id)
- .bind(new_status)
- .execute(pool)
- .await {
- return ContractRequestResult {
- success: false,
- message: format!("Failed to update chain status: {}", e),
- data: None,
- };
- }
-
- // If auto_start, trigger chain progression to create root contracts
- if auto_start {
- match repository::progress_chain(pool, chain_id, owner_id).await {
- Ok(result) => {
- ContractRequestResult {
- success: true,
- message: format!("Chain finalized and started. Created {} root contracts.", result.contracts_created.len()),
- data: Some(json!({
- "chain_id": chain_id,
- "status": "active",
- "contracts_created": result.contracts_created,
- "chain_completed": result.chain_completed
- })),
- }
- }
- Err(e) => ContractRequestResult {
- success: false,
- message: format!("Chain finalized but failed to start: {}", e),
- data: Some(json!({ "chain_id": chain_id, "status": "active" })),
- },
- }
- } else {
- ContractRequestResult {
- success: true,
- message: "Chain finalized but not started. Call finalize_chain_directive with auto_start=true to start.".to_string(),
- data: Some(json!({
- "chain_id": chain_id,
- "status": "pending"
- })),
- }
- }
- }
-
- ContractToolRequest::GetChainStatus => {
- // Get the contract's spawned chain
- let contract = match repository::get_contract_for_owner(pool, contract_id, owner_id).await {
- Ok(Some(c)) => c,
- Ok(None) => return ContractRequestResult {
- success: false,
- message: "Contract not found".to_string(),
- data: None,
- },
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Database error: {}", e),
- data: None,
- },
- };
-
- let chain_id = match contract.spawned_chain_id {
- Some(id) => id,
- None => return ContractRequestResult {
- success: false,
- message: "This contract has no associated chain".to_string(),
- data: None,
- },
- };
-
- // Get chain details
- let chain = match repository::get_chain_for_owner(pool, chain_id, owner_id).await {
- Ok(Some(c)) => c,
- Ok(None) => return ContractRequestResult {
- success: false,
- message: "Chain not found".to_string(),
- data: None,
- },
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Database error: {}", e),
- data: None,
- },
- };
-
- // Get definitions
- let definitions = match repository::list_chain_contract_definitions(pool, chain_id).await {
- Ok(defs) => defs,
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Failed to list definitions: {}", e),
- data: None,
- },
- };
-
- // Get instantiated contracts
- let chain_contracts = match repository::list_chain_contracts(pool, chain_id).await {
- Ok(cc) => cc,
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Failed to list chain contracts: {}", e),
- data: None,
- },
- };
-
- // Build status map
- let contract_statuses: Vec<serde_json::Value> = chain_contracts.iter().map(|cc| {
- json!({
- "name": cc.contract_name,
- "contract_id": cc.contract_id,
- "status": cc.contract_status,
- "phase": cc.contract_phase,
- "evaluation_status": cc.evaluation_status,
- "evaluation_retry_count": cc.evaluation_retry_count
- })
- }).collect();
-
- let completed = chain_contracts.iter().filter(|cc| cc.contract_status == "completed").count();
- let active = chain_contracts.iter().filter(|cc| cc.contract_status == "active").count();
- let pending = definitions.len() - chain_contracts.len();
-
- ContractRequestResult {
- success: true,
- message: format!("Chain '{}': {} completed, {} active, {} pending",
- chain.name, completed, active, pending),
- data: Some(json!({
- "chain_id": chain_id,
- "chain_name": chain.name,
- "chain_status": chain.status,
- "total_definitions": definitions.len(),
- "instantiated": chain_contracts.len(),
- "completed": completed,
- "active": active,
- "pending": pending,
- "contracts": contract_statuses
- })),
- }
- }
-
- ContractToolRequest::GetUncoveredRequirements => {
- // Get the contract's spawned chain
- let contract = match repository::get_contract_for_owner(pool, contract_id, owner_id).await {
- Ok(Some(c)) => c,
- Ok(None) => return ContractRequestResult {
- success: false,
- message: "Contract not found".to_string(),
- data: None,
- },
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Database error: {}", e),
- data: None,
- },
- };
-
- let chain_id = match contract.spawned_chain_id {
- Some(id) => id,
- None => return ContractRequestResult {
- success: false,
- message: "This contract has no associated chain".to_string(),
- data: None,
- },
- };
-
- // Get directive
- let directive = match repository::get_chain_directive(pool, chain_id).await {
- Ok(Some(d)) => d,
- Ok(None) => return ContractRequestResult {
- success: true,
- message: "No directive found for this chain".to_string(),
- data: Some(json!({ "uncovered": [], "total_requirements": 0 })),
- },
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Database error: {}", e),
- data: None,
- },
- };
-
- // Get definitions
- let definitions = match repository::list_chain_contract_definitions(pool, chain_id).await {
- Ok(defs) => defs,
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Failed to list definitions: {}", e),
- data: None,
- },
- };
-
- // Parse requirements
- let requirements: Vec<crate::db::models::DirectiveRequirement> =
- serde_json::from_value(directive.requirements.clone()).unwrap_or_default();
-
- // Find covered requirement IDs
- let covered: std::collections::HashSet<String> = definitions.iter()
- .flat_map(|d| d.requirement_ids.iter().cloned())
- .collect();
-
- // Find uncovered requirements
- let uncovered: Vec<serde_json::Value> = requirements.iter()
- .filter(|r| !covered.contains(&r.id))
- .map(|r| json!({
- "id": r.id,
- "title": r.title,
- "priority": r.priority
- }))
- .collect();
-
- ContractRequestResult {
- success: true,
- message: format!("{} of {} requirements are uncovered", uncovered.len(), requirements.len()),
- data: Some(json!({
- "uncovered": uncovered,
- "uncovered_count": uncovered.len(),
- "total_requirements": requirements.len(),
- "coverage_percent": if requirements.is_empty() { 100.0 } else {
- ((requirements.len() - uncovered.len()) as f64 / requirements.len() as f64 * 100.0).round()
- }
- })),
- }
- }
-
- ContractToolRequest::EvaluateContractCompletion { contract_id: target_contract_id, passed, feedback, rework_instructions } => {
- // Get the directive contract's spawned chain
- let contract = match repository::get_contract_for_owner(pool, contract_id, owner_id).await {
- Ok(Some(c)) => c,
- Ok(None) => return ContractRequestResult {
- success: false,
- message: "Contract not found".to_string(),
- data: None,
- },
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Database error: {}", e),
- data: None,
- },
- };
-
- let chain_id = match contract.spawned_chain_id {
- Some(id) => id,
- None => return ContractRequestResult {
- success: false,
- message: "This contract has no associated chain".to_string(),
- data: None,
- },
- };
-
- // Verify the target contract is part of this chain
- let chain_contract = match repository::get_chain_contract_by_contract_id(pool, target_contract_id).await {
- Ok(Some(cc)) => cc,
- Ok(None) => return ContractRequestResult {
- success: false,
- message: format!("Contract {} is not part of a chain", target_contract_id),
- data: None,
- },
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Database error: {}", e),
- data: None,
- },
- };
-
- if chain_contract.chain_id != chain_id {
- return ContractRequestResult {
- success: false,
- message: "Contract is not part of this directive's chain".to_string(),
- data: None,
- };
- }
-
- // Create evaluation record
- let eval_req = CreateContractEvaluationRequest {
- contract_id: target_contract_id,
- chain_id: Some(chain_id),
- chain_contract_id: Some(chain_contract.id),
- evaluator_model: Some("directive_contract".to_string()),
- passed,
- overall_score: if passed { Some(1.0) } else { Some(0.0) },
- criteria_results: vec![],
- summary_feedback: feedback.clone(),
- rework_instructions: rework_instructions.clone(),
- };
-
- let evaluation = match repository::create_contract_evaluation(pool, eval_req).await {
- Ok(e) => e,
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Failed to create evaluation: {}", e),
- data: None,
- },
- };
-
- // Update chain contract evaluation status
- let new_status = if passed { "passed" } else { "failed" };
- if let Err(e) = repository::update_chain_contract_evaluation_status(
- pool,
- chain_contract.id,
- new_status,
- Some(evaluation.id),
- None, // No rework feedback for passed/failed status
- ).await {
- tracing::warn!("Failed to update chain contract evaluation status: {}", e);
- }
-
- if passed {
- // Progress the chain to create downstream contracts
- match repository::progress_chain(pool, chain_id, owner_id).await {
- Ok(result) => ContractRequestResult {
- success: true,
- message: format!("Evaluation passed. Created {} downstream contracts.", result.contracts_created.len()),
- data: Some(json!({
- "evaluation_id": evaluation.id,
- "passed": true,
- "contracts_created": result.contracts_created,
- "chain_completed": result.chain_completed
- })),
- },
- Err(e) => ContractRequestResult {
- success: true,
- message: format!("Evaluation passed but failed to progress chain: {}", e),
- data: Some(json!({
- "evaluation_id": evaluation.id,
- "passed": true
- })),
- },
- }
- } else {
- // Mark contract for rework
- if let Err(e) = sqlx::query(
- r#"
- UPDATE chain_contracts SET evaluation_status = 'rework', rework_feedback = $2 WHERE id = $1;
- UPDATE contracts SET status = 'active' WHERE id = (SELECT contract_id FROM chain_contracts WHERE id = $1);
- "#
- )
- .bind(chain_contract.id)
- .bind(&rework_instructions)
- .execute(pool)
- .await {
- tracing::warn!("Failed to mark contract for rework: {}", e);
- }
-
- ContractRequestResult {
- success: true,
- message: format!("Evaluation failed. Contract marked for rework."),
- data: Some(json!({
- "evaluation_id": evaluation.id,
- "passed": false,
- "rework_instructions": rework_instructions,
- "retry_count": chain_contract.evaluation_retry_count + 1
- })),
- }
- }
- }
-
- ContractToolRequest::RequestRework { contract_id: target_contract_id, feedback } => {
- // Get the directive contract's spawned chain
- let contract = match repository::get_contract_for_owner(pool, contract_id, owner_id).await {
- Ok(Some(c)) => c,
- Ok(None) => return ContractRequestResult {
- success: false,
- message: "Contract not found".to_string(),
- data: None,
- },
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Database error: {}", e),
- data: None,
- },
- };
-
- let chain_id = match contract.spawned_chain_id {
- Some(id) => id,
- None => return ContractRequestResult {
- success: false,
- message: "This contract has no associated chain".to_string(),
- data: None,
- },
- };
-
- // Verify the target contract is part of this chain
- let chain_contract = match repository::get_chain_contract_by_contract_id(pool, target_contract_id).await {
- Ok(Some(cc)) => cc,
- Ok(None) => return ContractRequestResult {
- success: false,
- message: format!("Contract {} is not part of a chain", target_contract_id),
- data: None,
- },
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Database error: {}", e),
- data: None,
- },
- };
-
- if chain_contract.chain_id != chain_id {
- return ContractRequestResult {
- success: false,
- message: "Contract is not part of this directive's chain".to_string(),
- data: None,
- };
- }
-
- // Check retry count
- let max_retries = chain_contract.max_evaluation_retries;
- if chain_contract.evaluation_retry_count >= max_retries {
- return ContractRequestResult {
- success: false,
- message: format!("Contract has exceeded max retries ({}/{}). Escalate to user.",
- chain_contract.evaluation_retry_count, max_retries),
- data: Some(json!({
- "retry_count": chain_contract.evaluation_retry_count,
- "max_retries": max_retries,
- "escalation_required": true
- })),
- };
- }
-
- // Mark contract for rework and increment retry count
- if let Err(e) = sqlx::query(
- r#"
- UPDATE chain_contracts
- SET evaluation_status = 'rework',
- rework_feedback = $2,
- evaluation_retry_count = evaluation_retry_count + 1
- WHERE id = $1;
- UPDATE contracts SET status = 'active' WHERE id = (SELECT contract_id FROM chain_contracts WHERE id = $1);
- "#
- )
- .bind(chain_contract.id)
- .bind(&feedback)
- .execute(pool)
- .await {
- return ContractRequestResult {
- success: false,
- message: format!("Failed to request rework: {}", e),
- data: None,
- };
- }
+ // Chain directive tools - TEMPORARILY DISABLED
+ // These tools will be reimplemented using the new directive system.
+ // See the orchestration module for the new implementation.
+ ContractToolRequest::CreateChainFromDirective { .. } |
+ ContractToolRequest::AddChainContract { .. } |
+ ContractToolRequest::SetChainDependencies { .. } |
+ ContractToolRequest::ModifyChainContract { .. } |
+ ContractToolRequest::RemoveChainContract { .. } |
+ ContractToolRequest::PreviewChainDag |
+ ContractToolRequest::ValidateChainDirective |
+ ContractToolRequest::FinalizeChainDirective { .. } |
+ ContractToolRequest::GetChainStatus |
+ ContractToolRequest::GetUncoveredRequirements |
+ ContractToolRequest::EvaluateContractCompletion { .. } |
+ ContractToolRequest::RequestRework { .. } => {
ContractRequestResult {
- success: true,
- message: format!("Rework requested for contract. Retry {}/{}",
- chain_contract.evaluation_retry_count + 1, max_retries),
- data: Some(json!({
- "contract_id": target_contract_id,
- "retry_count": chain_contract.evaluation_retry_count + 1,
- "max_retries": max_retries,
- "feedback": feedback
- })),
+ success: false,
+ message: "Chain directive tools are temporarily disabled. The directive system is being reimplemented.".to_string(),
+ data: None,
}
}
}
diff --git a/makima/src/server/handlers/contracts.rs b/makima/src/server/handlers/contracts.rs
index 2b2fc26..8a6ce0f 100644
--- a/makima/src/server/handlers/contracts.rs
+++ b/makima/src/server/handlers/contracts.rs
@@ -575,78 +575,90 @@ pub async fn update_contract(
}),
).await;
- // If contract is part of a chain, check evaluation requirements
- if let Some(chain_id) = contract.chain_id {
- let pool_clone = pool.clone();
- let owner_id = auth.owner_id;
- let contract_id = contract.id;
- tokio::spawn(async move {
- // Check if chain has evaluation enabled
- let chain = match repository::get_chain_for_owner(&pool_clone, chain_id, owner_id).await {
- Ok(Some(c)) => c,
- Ok(None) => {
- tracing::warn!(chain_id = %chain_id, "Chain not found for progression");
- return;
- }
- Err(e) => {
- tracing::error!(chain_id = %chain_id, error = %e, "Failed to get chain");
- return;
- }
- };
-
- // If evaluation is enabled, mark contract for evaluation
- if chain.evaluation_enabled {
- // Mark the chain_contract as pending evaluation
- if let Ok(Some(chain_contract)) = repository::get_chain_contract_by_contract_id(&pool_clone, contract_id).await {
- if let Err(e) = repository::update_chain_contract_evaluation_status(
- &pool_clone,
- chain_contract.id,
- "pending_evaluation",
- None,
- None,
- ).await {
- tracing::error!(
- chain_id = %chain_id,
- contract_id = %contract_id,
- error = %e,
- "Failed to mark contract for evaluation"
- );
- } else {
- tracing::info!(
- chain_id = %chain_id,
- contract_id = %contract_id,
- "Contract marked for evaluation - waiting for directive contract to evaluate"
+ // If contract is part of a directive chain step, update the step status
+ // and emit an event for the directive engine to process
+ let pool_for_step = pool.clone();
+ let contract_id_for_step = contract.id;
+ tokio::spawn(async move {
+ // Look up the step by contract_id
+ match repository::get_step_by_contract_id(&pool_for_step, contract_id_for_step).await {
+ Ok(Some(step)) => {
+ // Get the chain to find the directive_id
+ let directive_id = match repository::get_directive_chain(&pool_for_step, step.chain_id).await {
+ Ok(Some(chain)) => chain.directive_id,
+ Ok(None) => {
+ tracing::warn!(
+ chain_id = %step.chain_id,
+ "Chain not found for step"
);
+ return;
}
- }
- // Don't progress chain - directive contract will evaluate and progress
- return;
- }
-
- // If evaluation is disabled, progress chain directly
- match repository::progress_chain(&pool_clone, chain_id, owner_id).await {
- Ok(result) => {
- if !result.contracts_created.is_empty() {
- tracing::info!(
- chain_id = %chain_id,
- contracts_created = ?result.contracts_created,
- "Chain progressed - created new contracts"
+ Err(e) => {
+ tracing::warn!(
+ chain_id = %step.chain_id,
+ error = %e,
+ "Failed to get chain for step"
);
+ return;
}
- if result.chain_completed {
- tracing::info!(chain_id = %chain_id, "Chain completed");
- }
- }
- Err(e) => {
- tracing::error!(
- chain_id = %chain_id,
+ };
+
+ // Update step status to 'evaluating'
+ if let Err(e) = repository::update_step_status(&pool_for_step, step.id, "evaluating").await {
+ tracing::warn!(
+ step_id = %step.id,
+ contract_id = %contract_id_for_step,
error = %e,
- "Failed to progress chain after contract completion"
+ "Failed to update step status to evaluating"
);
+ } else {
+ tracing::info!(
+ step_id = %step.id,
+ contract_id = %contract_id_for_step,
+ chain_id = %step.chain_id,
+ directive_id = %directive_id,
+ "Contract completed - step transitioned to evaluating"
+ );
+
+ // Emit directive event for contract completion
+ if let Err(e) = repository::emit_directive_event(
+ &pool_for_step,
+ directive_id,
+ Some(step.chain_id),
+ Some(step.id),
+ "contract_completed",
+ "info",
+ Some(serde_json::json!({
+ "contract_id": contract_id_for_step,
+ "step_id": step.id,
+ "step_name": step.name
+ })),
+ "system",
+ None,
+ ).await {
+ tracing::warn!(
+ step_id = %step.id,
+ error = %e,
+ "Failed to emit contract_completed directive event"
+ );
+ }
}
}
- });
- }
+ Ok(None) => {
+ tracing::debug!(
+ contract_id = %contract_id_for_step,
+ "Contract not linked to any directive chain step"
+ );
+ }
+ Err(e) => {
+ tracing::warn!(
+ contract_id = %contract_id_for_step,
+ error = %e,
+ "Failed to look up step for completed contract"
+ );
+ }
+ }
+ });
}
// Get summary with counts
diff --git a/makima/src/server/handlers/directives.rs b/makima/src/server/handlers/directives.rs
new file mode 100644
index 0000000..6f6c3f1
--- /dev/null
+++ b/makima/src/server/handlers/directives.rs
@@ -0,0 +1,1488 @@
+//! API handlers for directives.
+//!
+//! Provides REST endpoints for managing directives, chains, steps,
+//! evaluations, events, verifiers, and approvals.
+
+use axum::{
+ extract::{Path, Query, State},
+ http::StatusCode,
+ response::{
+ sse::{Event, Sse},
+ IntoResponse,
+ },
+ Json,
+};
+use futures::stream;
+use serde::{Deserialize, Serialize};
+use std::convert::Infallible;
+use std::time::Duration;
+use uuid::Uuid;
+
+use crate::db::models::{
+ AddStepRequest, CreateDirectiveRequest, CreateVerifierRequest, UpdateDirectiveRequest,
+ UpdateStepRequest, UpdateVerifierRequest,
+};
+use crate::db::repository;
+use crate::server::auth::Authenticated;
+use crate::server::messages::ApiError;
+use crate::server::state::SharedState;
+
+/// Query parameters for listing directives
+#[derive(Debug, Deserialize)]
+pub struct ListDirectivesQuery {
+ pub status: Option<String>,
+}
+
+/// Query parameters for listing events
+#[derive(Debug, Deserialize)]
+pub struct ListEventsQuery {
+ pub limit: Option<i64>,
+}
+
+/// Query parameters for listing evaluations
+#[derive(Debug, Deserialize)]
+pub struct ListEvaluationsQuery {
+ pub limit: Option<i64>,
+ #[serde(rename = "stepId")]
+ pub step_id: Option<Uuid>,
+}
+
+/// Response for directive creation
+#[derive(Debug, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CreateDirectiveResponse {
+ pub id: Uuid,
+ pub title: String,
+ pub status: String,
+}
+
+/// Response for approval actions
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ApprovalActionRequest {
+ pub response: Option<String>,
+}
+
+// =============================================================================
+// Directive CRUD
+// =============================================================================
+
+/// Create a new directive
+/// POST /api/v1/directives
+pub async fn create_directive(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Json(req): Json<CreateDirectiveRequest>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ match repository::create_directive_for_owner(pool, auth.owner_id, req).await {
+ Ok(directive) => Json(CreateDirectiveResponse {
+ id: directive.id,
+ title: directive.title,
+ status: directive.status,
+ })
+ .into_response(),
+ Err(e) => {
+ tracing::error!("Failed to create directive: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// List directives for the authenticated owner
+/// GET /api/v1/directives
+pub async fn list_directives(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Query(params): Query<ListDirectivesQuery>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ match repository::list_directives_for_owner(pool, auth.owner_id, params.status.as_deref()).await
+ {
+ Ok(directives) => Json(directives).into_response(),
+ Err(e) => {
+ tracing::error!("Failed to list directives: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Get a directive with progress details
+/// GET /api/v1/directives/:id
+pub async fn get_directive(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path(id): Path<Uuid>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ match repository::get_directive_with_progress(pool, id, auth.owner_id).await {
+ Ok(Some(directive)) => Json(directive).into_response(),
+ Ok(None) => (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Directive not found")),
+ )
+ .into_response(),
+ Err(e) => {
+ tracing::error!("Failed to get directive: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Update a directive
+/// PUT /api/v1/directives/:id
+pub async fn update_directive(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path(id): Path<Uuid>,
+ Json(req): Json<UpdateDirectiveRequest>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ match repository::update_directive_for_owner(pool, id, auth.owner_id, req).await {
+ Ok(directive) => Json(directive).into_response(),
+ Err(repository::RepositoryError::VersionConflict { expected, actual }) => (
+ StatusCode::CONFLICT,
+ Json(ApiError::new(
+ "VERSION_CONFLICT",
+ &format!(
+ "Version conflict: expected {}, got {}",
+ expected, actual
+ ),
+ )),
+ )
+ .into_response(),
+ Err(e) => {
+ tracing::error!("Failed to update directive: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Archive a directive
+/// DELETE /api/v1/directives/:id
+pub async fn archive_directive(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path(id): Path<Uuid>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ match repository::archive_directive_for_owner(pool, id, auth.owner_id).await {
+ Ok(true) => StatusCode::NO_CONTENT.into_response(),
+ Ok(false) => (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Directive not found")),
+ )
+ .into_response(),
+ Err(e) => {
+ tracing::error!("Failed to archive directive: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+// =============================================================================
+// Directive Lifecycle
+// =============================================================================
+
+/// Start a directive (generate chain and begin execution)
+/// POST /api/v1/directives/:id/start
+pub async fn start_directive(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path(id): Path<Uuid>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ // Verify ownership
+ match repository::get_directive_for_owner(pool, id, auth.owner_id).await {
+ Ok(Some(_)) => {}
+ Ok(None) => {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Directive not found")),
+ )
+ .into_response()
+ }
+ Err(e) => {
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+
+ // Start directive via orchestration engine
+ let engine = crate::orchestration::DirectiveEngine::new(pool.clone());
+ match engine.start_directive(id).await {
+ Ok(()) => {
+ // Return the updated directive with progress
+ match repository::get_directive_with_progress(pool, id, auth.owner_id).await {
+ Ok(Some(directive)) => Json(directive).into_response(),
+ Ok(None) => (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Directive not found")),
+ )
+ .into_response(),
+ Err(e) => (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response(),
+ }
+ }
+ Err(e) => {
+ tracing::error!("Failed to start directive: {}", e);
+ (
+ StatusCode::BAD_REQUEST,
+ Json(ApiError::new("START_FAILED", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Pause a directive
+/// POST /api/v1/directives/:id/pause
+pub async fn pause_directive(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path(id): Path<Uuid>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ // Verify ownership
+ match repository::get_directive_for_owner(pool, id, auth.owner_id).await {
+ Ok(Some(_)) => {}
+ Ok(None) => {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Directive not found")),
+ )
+ .into_response()
+ }
+ Err(e) => {
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+
+ let engine = crate::orchestration::DirectiveEngine::new(pool.clone());
+ match engine.pause_directive(id).await {
+ Ok(()) => match repository::get_directive(pool, id).await {
+ Ok(Some(directive)) => Json(directive).into_response(),
+ Ok(None) => (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Directive not found")),
+ )
+ .into_response(),
+ Err(e) => (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response(),
+ },
+ Err(e) => {
+ tracing::error!("Failed to pause directive: {}", e);
+ (
+ StatusCode::BAD_REQUEST,
+ Json(ApiError::new("PAUSE_FAILED", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Resume a paused directive
+/// POST /api/v1/directives/:id/resume
+pub async fn resume_directive(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path(id): Path<Uuid>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ // Verify ownership
+ match repository::get_directive_for_owner(pool, id, auth.owner_id).await {
+ Ok(Some(_)) => {}
+ Ok(None) => {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Directive not found")),
+ )
+ .into_response()
+ }
+ Err(e) => {
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+
+ let engine = crate::orchestration::DirectiveEngine::new(pool.clone());
+ match engine.resume_directive(id).await {
+ Ok(()) => match repository::get_directive_with_progress(pool, id, auth.owner_id).await {
+ Ok(Some(directive)) => Json(directive).into_response(),
+ Ok(None) => (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Directive not found")),
+ )
+ .into_response(),
+ Err(e) => (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response(),
+ },
+ Err(e) => {
+ tracing::error!("Failed to resume directive: {}", e);
+ (
+ StatusCode::BAD_REQUEST,
+ Json(ApiError::new("RESUME_FAILED", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Stop a directive (cannot be resumed)
+/// POST /api/v1/directives/:id/stop
+pub async fn stop_directive(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path(id): Path<Uuid>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ // Verify ownership
+ match repository::get_directive_for_owner(pool, id, auth.owner_id).await {
+ Ok(Some(_)) => {}
+ Ok(None) => {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Directive not found")),
+ )
+ .into_response()
+ }
+ Err(e) => {
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+
+ let engine = crate::orchestration::DirectiveEngine::new(pool.clone());
+ match engine.stop_directive(id).await {
+ Ok(()) => match repository::get_directive(pool, id).await {
+ Ok(Some(directive)) => Json(directive).into_response(),
+ Ok(None) => (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Directive not found")),
+ )
+ .into_response(),
+ Err(e) => (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response(),
+ },
+ Err(e) => {
+ tracing::error!("Failed to stop directive: {}", e);
+ (
+ StatusCode::BAD_REQUEST,
+ Json(ApiError::new("STOP_FAILED", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+// =============================================================================
+// Chain Management
+// =============================================================================
+
+/// Get current chain for a directive
+/// GET /api/v1/directives/:id/chain
+pub async fn get_chain(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path(id): Path<Uuid>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ // Verify ownership
+ match repository::get_directive_for_owner(pool, id, auth.owner_id).await {
+ Ok(Some(_)) => {}
+ Ok(None) => {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Directive not found")),
+ )
+ .into_response()
+ }
+ Err(e) => {
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+
+ match repository::get_current_chain(pool, id).await {
+ Ok(Some(chain)) => {
+ match repository::list_chain_steps(pool, chain.id).await {
+ Ok(steps) => Json(serde_json::json!({
+ "chain": chain,
+ "steps": steps,
+ }))
+ .into_response(),
+ Err(e) => (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response(),
+ }
+ }
+ Ok(None) => (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "No active chain")),
+ )
+ .into_response(),
+ Err(e) => {
+ tracing::error!("Failed to get chain: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Get chain graph for DAG visualization
+/// GET /api/v1/directives/:id/chain/graph
+pub async fn get_chain_graph(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path(id): Path<Uuid>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ // Verify ownership
+ match repository::get_directive_for_owner(pool, id, auth.owner_id).await {
+ Ok(Some(_)) => {}
+ Ok(None) => {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Directive not found")),
+ )
+ .into_response()
+ }
+ Err(e) => {
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+
+ // Get current chain
+ let chain = match repository::get_current_chain(pool, id).await {
+ Ok(Some(chain)) => chain,
+ Ok(None) => {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "No active chain")),
+ )
+ .into_response()
+ }
+ Err(e) => {
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response()
+ }
+ };
+
+ match repository::get_chain_graph(pool, chain.id).await {
+ Ok(graph) => Json(graph).into_response(),
+ Err(e) => {
+ tracing::error!("Failed to get chain graph: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Regenerate chain (force replan)
+/// POST /api/v1/directives/:id/chain/replan
+pub async fn replan_chain(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path(id): Path<Uuid>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ // Verify ownership
+ match repository::get_directive_for_owner(pool, id, auth.owner_id).await {
+ Ok(Some(_)) => {}
+ Ok(None) => {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Directive not found")),
+ )
+ .into_response()
+ }
+ Err(e) => {
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+
+ let engine = crate::orchestration::DirectiveEngine::new(pool.clone());
+ match engine.regenerate_chain(id, "Manual replan requested").await {
+ Ok(new_chain_id) => Json(serde_json::json!({
+ "chainId": new_chain_id,
+ "message": "Chain regenerated successfully",
+ }))
+ .into_response(),
+ Err(e) => {
+ tracing::error!("Failed to replan chain: {}", e);
+ (
+ StatusCode::BAD_REQUEST,
+ Json(ApiError::new("REPLAN_FAILED", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+// =============================================================================
+// Step Management
+// =============================================================================
+
+/// Add a step to the current chain
+/// POST /api/v1/directives/:id/chain/steps
+pub async fn add_step(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path(id): Path<Uuid>,
+ Json(req): Json<AddStepRequest>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ // Verify ownership
+ match repository::get_directive_for_owner(pool, id, auth.owner_id).await {
+ Ok(Some(_)) => {}
+ Ok(None) => {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Directive not found")),
+ )
+ .into_response()
+ }
+ Err(e) => {
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+
+ // Get current chain
+ let chain = match repository::get_current_chain(pool, id).await {
+ Ok(Some(chain)) => chain,
+ Ok(None) => {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "No active chain")),
+ )
+ .into_response()
+ }
+ Err(e) => {
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response()
+ }
+ };
+
+ match repository::create_chain_step(pool, chain.id, req).await {
+ Ok(step) => (StatusCode::CREATED, Json(step)).into_response(),
+ Err(e) => {
+ tracing::error!("Failed to add step: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Get step details
+/// GET /api/v1/directives/:id/chain/steps/:step_id
+pub async fn get_step(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path((id, step_id)): Path<(Uuid, Uuid)>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ // Verify ownership
+ match repository::get_directive_for_owner(pool, id, auth.owner_id).await {
+ Ok(Some(_)) => {}
+ Ok(None) => {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Directive not found")),
+ )
+ .into_response()
+ }
+ Err(e) => {
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+
+ match repository::get_chain_step(pool, step_id).await {
+ Ok(Some(step)) => Json(step).into_response(),
+ Ok(None) => (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Step not found")),
+ )
+ .into_response(),
+ Err(e) => {
+ tracing::error!("Failed to get step: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Update a step
+/// PUT /api/v1/directives/:id/chain/steps/:step_id
+pub async fn update_step(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path((id, step_id)): Path<(Uuid, Uuid)>,
+ Json(req): Json<UpdateStepRequest>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ // Verify ownership
+ match repository::get_directive_for_owner(pool, id, auth.owner_id).await {
+ Ok(Some(_)) => {}
+ Ok(None) => {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Directive not found")),
+ )
+ .into_response()
+ }
+ Err(e) => {
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+
+ match repository::update_chain_step(pool, step_id, req).await {
+ Ok(step) => Json(step).into_response(),
+ Err(e) => {
+ tracing::error!("Failed to update step: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Delete a step
+/// DELETE /api/v1/directives/:id/chain/steps/:step_id
+pub async fn delete_step(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path((id, step_id)): Path<(Uuid, Uuid)>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ // Verify ownership
+ match repository::get_directive_for_owner(pool, id, auth.owner_id).await {
+ Ok(Some(_)) => {}
+ Ok(None) => {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Directive not found")),
+ )
+ .into_response()
+ }
+ Err(e) => {
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+
+ match repository::delete_chain_step(pool, step_id).await {
+ Ok(true) => StatusCode::NO_CONTENT.into_response(),
+ Ok(false) => (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Step not found")),
+ )
+ .into_response(),
+ Err(e) => {
+ tracing::error!("Failed to delete step: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Skip a step
+/// POST /api/v1/directives/:id/chain/steps/:step_id/skip
+pub async fn skip_step(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path((id, step_id)): Path<(Uuid, Uuid)>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ // Verify ownership
+ match repository::get_directive_for_owner(pool, id, auth.owner_id).await {
+ Ok(Some(_)) => {}
+ Ok(None) => {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Directive not found")),
+ )
+ .into_response()
+ }
+ Err(e) => {
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+
+ match repository::update_step_status(pool, step_id, "skipped").await {
+ Ok(step) => Json(step).into_response(),
+ Err(e) => {
+ tracing::error!("Failed to skip step: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+// =============================================================================
+// Evaluations
+// =============================================================================
+
+/// List evaluations for a directive
+/// GET /api/v1/directives/:id/evaluations
+pub async fn list_evaluations(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path(id): Path<Uuid>,
+ Query(params): Query<ListEvaluationsQuery>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ // Verify ownership
+ match repository::get_directive_for_owner(pool, id, auth.owner_id).await {
+ Ok(Some(_)) => {}
+ Ok(None) => {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Directive not found")),
+ )
+ .into_response()
+ }
+ Err(e) => {
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+
+ let result = if let Some(step_id) = params.step_id {
+ repository::list_step_evaluations(pool, step_id).await
+ } else {
+ repository::list_directive_evaluations(pool, id, params.limit).await
+ };
+
+ match result {
+ Ok(evaluations) => Json(evaluations).into_response(),
+ Err(e) => {
+ tracing::error!("Failed to list evaluations: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+// =============================================================================
+// Events
+// =============================================================================
+
+/// List events for a directive
+/// GET /api/v1/directives/:id/events
+pub async fn list_events(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path(id): Path<Uuid>,
+ Query(params): Query<ListEventsQuery>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ // Verify ownership
+ match repository::get_directive_for_owner(pool, id, auth.owner_id).await {
+ Ok(Some(_)) => {}
+ Ok(None) => {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Directive not found")),
+ )
+ .into_response()
+ }
+ Err(e) => {
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+
+ match repository::list_directive_events(pool, id, params.limit).await {
+ Ok(events) => Json(events).into_response(),
+ Err(e) => {
+ tracing::error!("Failed to list events: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// SSE stream of events for a directive
+/// GET /api/v1/directives/:id/events/stream
+pub async fn stream_events(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path(id): Path<Uuid>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ // Verify ownership
+ match repository::get_directive_for_owner(pool, id, auth.owner_id).await {
+ Ok(Some(_)) => {}
+ Ok(None) => {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Directive not found")),
+ )
+ .into_response()
+ }
+ Err(e) => {
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+
+ // Create SSE stream that polls for new events
+ let pool_clone = pool.clone();
+ let stream = stream::unfold(
+ (pool_clone, id, None::<chrono::DateTime<chrono::Utc>>),
+ move |(pool, directive_id, last_seen)| async move {
+ // Wait a bit before next poll
+ tokio::time::sleep(Duration::from_secs(1)).await;
+
+ // Get recent events
+ let events = repository::list_directive_events(&pool, directive_id, Some(10))
+ .await
+ .unwrap_or_default();
+
+ // Filter to only new events
+ let new_events: Vec<_> = events
+ .into_iter()
+ .filter(|e| last_seen.map(|ls| e.created_at > ls).unwrap_or(true))
+ .collect();
+
+ let new_last_seen = new_events.first().map(|e| e.created_at).or(last_seen);
+
+ // Convert to SSE events
+ let sse_events: Vec<Result<Event, Infallible>> = new_events
+ .into_iter()
+ .map(|e| {
+ Ok(Event::default()
+ .event("directive_event")
+ .data(serde_json::to_string(&e).unwrap_or_default()))
+ })
+ .collect();
+
+ Some((stream::iter(sse_events), (pool, directive_id, new_last_seen)))
+ },
+ );
+
+ use futures::StreamExt;
+ Sse::new(stream.flatten())
+ .keep_alive(
+ axum::response::sse::KeepAlive::new()
+ .interval(Duration::from_secs(15))
+ .text("keepalive"),
+ )
+ .into_response()
+}
+
+// =============================================================================
+// Verifiers
+// =============================================================================
+
+/// List verifiers for a directive
+/// GET /api/v1/directives/:id/verifiers
+pub async fn list_verifiers(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path(id): Path<Uuid>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ // Verify ownership
+ match repository::get_directive_for_owner(pool, id, auth.owner_id).await {
+ Ok(Some(_)) => {}
+ Ok(None) => {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Directive not found")),
+ )
+ .into_response()
+ }
+ Err(e) => {
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+
+ match repository::list_directive_verifiers(pool, id).await {
+ Ok(verifiers) => Json(verifiers).into_response(),
+ Err(e) => {
+ tracing::error!("Failed to list verifiers: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Add a verifier to a directive
+/// POST /api/v1/directives/:id/verifiers
+pub async fn add_verifier(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path(id): Path<Uuid>,
+ Json(req): Json<CreateVerifierRequest>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ // Verify ownership
+ match repository::get_directive_for_owner(pool, id, auth.owner_id).await {
+ Ok(Some(_)) => {}
+ Ok(None) => {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Directive not found")),
+ )
+ .into_response()
+ }
+ Err(e) => {
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+
+ match repository::create_directive_verifier(
+ pool,
+ id,
+ &req.name,
+ &req.verifier_type,
+ req.command.as_deref(),
+ req.working_directory.as_deref(),
+ false, // auto_detect
+ vec![], // detect_files
+ req.weight.unwrap_or(1.0),
+ req.required.unwrap_or(false),
+ )
+ .await
+ {
+ Ok(verifier) => (StatusCode::CREATED, Json(verifier)).into_response(),
+ Err(e) => {
+ tracing::error!("Failed to add verifier: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Update a verifier
+/// PUT /api/v1/directives/:id/verifiers/:verifier_id
+pub async fn update_verifier(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path((id, verifier_id)): Path<(Uuid, Uuid)>,
+ Json(req): Json<UpdateVerifierRequest>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ // Verify ownership
+ match repository::get_directive_for_owner(pool, id, auth.owner_id).await {
+ Ok(Some(_)) => {}
+ Ok(None) => {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Directive not found")),
+ )
+ .into_response()
+ }
+ Err(e) => {
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+
+ match repository::update_directive_verifier(
+ pool,
+ verifier_id,
+ req.enabled,
+ req.command.as_deref(),
+ req.weight,
+ req.required,
+ )
+ .await
+ {
+ Ok(verifier) => Json(verifier).into_response(),
+ Err(e) => {
+ tracing::error!("Failed to update verifier: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+// =============================================================================
+// Approvals
+// =============================================================================
+
+/// List pending approvals for a directive
+/// GET /api/v1/directives/:id/approvals
+pub async fn list_approvals(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path(id): Path<Uuid>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ // Verify ownership
+ match repository::get_directive_for_owner(pool, id, auth.owner_id).await {
+ Ok(Some(_)) => {}
+ Ok(None) => {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Directive not found")),
+ )
+ .into_response()
+ }
+ Err(e) => {
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+
+ match repository::list_pending_approvals(pool, id).await {
+ Ok(approvals) => Json(approvals).into_response(),
+ Err(e) => {
+ tracing::error!("Failed to list approvals: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Approve a pending approval request
+/// POST /api/v1/directives/:id/approvals/:approval_id/approve
+pub async fn approve_request(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path((id, approval_id)): Path<(Uuid, Uuid)>,
+ Json(req): Json<ApprovalActionRequest>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ // Verify ownership
+ match repository::get_directive_for_owner(pool, id, auth.owner_id).await {
+ Ok(Some(_)) => {}
+ Ok(None) => {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Directive not found")),
+ )
+ .into_response()
+ }
+ Err(e) => {
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+
+ let engine = crate::orchestration::DirectiveEngine::new(pool.clone());
+ match engine
+ .on_approval_resolved(approval_id, true, auth.owner_id)
+ .await
+ {
+ Ok(()) => {
+ match repository::resolve_approval(
+ pool,
+ approval_id,
+ "approved",
+ req.response.as_deref(),
+ auth.owner_id,
+ )
+ .await
+ {
+ Ok(approval) => Json(approval).into_response(),
+ Err(e) => (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response(),
+ }
+ }
+ Err(e) => {
+ tracing::error!("Failed to process approval: {}", e);
+ (
+ StatusCode::BAD_REQUEST,
+ Json(ApiError::new("APPROVAL_FAILED", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Deny a pending approval request
+/// POST /api/v1/directives/:id/approvals/:approval_id/deny
+pub async fn deny_request(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path((id, approval_id)): Path<(Uuid, Uuid)>,
+ Json(req): Json<ApprovalActionRequest>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ // Verify ownership
+ match repository::get_directive_for_owner(pool, id, auth.owner_id).await {
+ Ok(Some(_)) => {}
+ Ok(None) => {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Directive not found")),
+ )
+ .into_response()
+ }
+ Err(e) => {
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+
+ let engine = crate::orchestration::DirectiveEngine::new(pool.clone());
+ match engine
+ .on_approval_resolved(approval_id, false, auth.owner_id)
+ .await
+ {
+ Ok(()) => {
+ match repository::resolve_approval(
+ pool,
+ approval_id,
+ "denied",
+ req.response.as_deref(),
+ auth.owner_id,
+ )
+ .await
+ {
+ Ok(approval) => Json(approval).into_response(),
+ Err(e) => (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", &e.to_string())),
+ )
+ .into_response(),
+ }
+ }
+ Err(e) => {
+ tracing::error!("Failed to process denial: {}", e);
+ (
+ StatusCode::BAD_REQUEST,
+ Json(ApiError::new("DENIAL_FAILED", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
diff --git a/makima/src/server/handlers/mod.rs b/makima/src/server/handlers/mod.rs
index 5e172bc..d3fabf7 100644
--- a/makima/src/server/handlers/mod.rs
+++ b/makima/src/server/handlers/mod.rs
@@ -1,7 +1,8 @@
//! HTTP and WebSocket request handlers.
pub mod api_keys;
-pub mod chains;
+// pub mod chains; // Removed - replaced by directives
+pub mod directives;
pub mod chat;
pub mod contract_chat;
pub mod contract_daemon;
diff --git a/makima/src/server/mod.rs b/makima/src/server/mod.rs
index e5b55e7..927e9a5 100644
--- a/makima/src/server/mod.rs
+++ b/makima/src/server/mod.rs
@@ -18,7 +18,7 @@ use tower_http::trace::TraceLayer;
use utoipa::OpenApi;
use utoipa_swagger_ui::SwaggerUi;
-use crate::server::handlers::{api_keys, chains, chat, contract_chat, contract_daemon, contract_discuss, contracts, file_ws, files, history, listen, mesh, mesh_chat, mesh_daemon, mesh_merge, mesh_supervisor, mesh_ws, repository_history, speak, templates, transcript_analysis, users, versions};
+use crate::server::handlers::{api_keys, chat, contract_chat, contract_daemon, contract_discuss, contracts, directives, file_ws, files, history, listen, mesh, mesh_chat, mesh_daemon, mesh_merge, mesh_supervisor, mesh_ws, repository_history, speak, templates, transcript_analysis, users, versions};
use crate::server::openapi::ApiDoc;
use crate::server::state::SharedState;
@@ -214,51 +214,55 @@ pub fn make_router(state: SharedState) -> Router {
)
// Timeline endpoint (unified history for user)
.route("/timeline", get(history::get_timeline))
- // Chain endpoints (multi-contract orchestration)
+ // Directive endpoints (replacement for chains)
.route(
- "/chains",
- get(chains::list_chains).post(chains::create_chain),
+ "/directives",
+ get(directives::list_directives).post(directives::create_directive),
)
- .route("/chains/init", post(chains::init_chain))
.route(
- "/chains/{id}",
- get(chains::get_chain)
- .put(chains::update_chain)
- .delete(chains::delete_chain),
+ "/directives/{id}",
+ get(directives::get_directive)
+ .put(directives::update_directive)
+ .delete(directives::archive_directive),
)
- .route("/chains/{id}/contracts", get(chains::get_chain_contracts))
- .route("/chains/{id}/graph", get(chains::get_chain_graph))
- .route("/chains/{id}/events", get(chains::get_chain_events))
- .route("/chains/{id}/editor", get(chains::get_chain_editor))
- // Chain contract definitions
+ .route("/directives/{id}/start", post(directives::start_directive))
+ .route("/directives/{id}/pause", post(directives::pause_directive))
+ .route("/directives/{id}/resume", post(directives::resume_directive))
+ .route("/directives/{id}/stop", post(directives::stop_directive))
+ // Directive chain management
+ .route("/directives/{id}/chain", get(directives::get_chain))
+ .route("/directives/{id}/chain/graph", get(directives::get_chain_graph))
+ .route("/directives/{id}/chain/replan", post(directives::replan_chain))
+ // Directive step management
.route(
- "/chains/{id}/definitions",
- get(chains::list_chain_definitions).post(chains::create_chain_definition),
+ "/directives/{id}/chain/steps",
+ post(directives::add_step),
)
.route(
- "/chains/{chain_id}/definitions/{definition_id}",
- put(chains::update_chain_definition).delete(chains::delete_chain_definition),
+ "/directives/{id}/chain/steps/{step_id}",
+ get(directives::get_step)
+ .put(directives::update_step)
+ .delete(directives::delete_step),
)
+ .route("/directives/{id}/chain/steps/{step_id}/skip", post(directives::skip_step))
+ // Directive evaluations
+ .route("/directives/{id}/evaluations", get(directives::list_evaluations))
+ // Directive events
+ .route("/directives/{id}/events", get(directives::list_events))
+ .route("/directives/{id}/events/stream", get(directives::stream_events))
+ // Directive verifiers
.route(
- "/chains/{id}/definitions/graph",
- get(chains::get_chain_definition_graph),
+ "/directives/{id}/verifiers",
+ get(directives::list_verifiers).post(directives::add_verifier),
)
- // Chain control
- .route("/chains/{id}/start", post(chains::start_chain))
- .route("/chains/{id}/stop", post(chains::stop_chain))
- // Chain repositories
.route(
- "/chains/{id}/repositories",
- get(chains::list_chain_repositories).post(chains::add_chain_repository),
- )
- .route(
- "/chains/{chain_id}/repositories/{repository_id}",
- axum::routing::delete(chains::delete_chain_repository),
- )
- .route(
- "/chains/{chain_id}/repositories/{repository_id}/primary",
- put(chains::set_chain_repository_primary),
+ "/directives/{id}/verifiers/{verifier_id}",
+ axum::routing::put(directives::update_verifier),
)
+ // Directive approvals
+ .route("/directives/{id}/approvals", get(directives::list_approvals))
+ .route("/directives/{id}/approvals/{approval_id}/approve", post(directives::approve_request))
+ .route("/directives/{id}/approvals/{approval_id}/deny", post(directives::deny_request))
// Contract type templates (built-in only)
.route("/contract-types", get(templates::list_contract_types))
// Settings endpoints