diff options
Diffstat (limited to 'makima/src/server/handlers')
| -rw-r--r-- | makima/src/server/handlers/chains.rs | 62 | ||||
| -rw-r--r-- | makima/src/server/handlers/contract_chat.rs | 1214 | ||||
| -rw-r--r-- | makima/src/server/handlers/contracts.rs | 46 |
3 files changed, 1319 insertions, 3 deletions
diff --git a/makima/src/server/handlers/chains.rs b/makima/src/server/handlers/chains.rs index 9b32495..b8716ca 100644 --- a/makima/src/server/handlers/chains.rs +++ b/makima/src/server/handlers/chains.rs @@ -16,7 +16,8 @@ use crate::db::models::{ AddChainRepositoryRequest, AddContractDefinitionRequest, ChainContractDefinition, ChainContractDetail, ChainDefinitionGraphResponse, ChainEditorData, ChainEvent, ChainGraphResponse, ChainRepository, ChainSummary, ChainWithContracts, CreateChainRequest, - StartChainRequest, StartChainResponse, UpdateChainRequest, UpdateContractDefinitionRequest, + InitChainRequest, InitChainResponse, StartChainRequest, StartChainResponse, UpdateChainRequest, + UpdateContractDefinitionRequest, }; use crate::db::repository::{self, RepositoryError}; use crate::server::auth::Authenticated; @@ -172,6 +173,65 @@ pub async fn create_chain( } } +/// Initialize a directive-driven chain. +/// +/// Creates a directive contract that will research, plan, create, and orchestrate +/// a chain of contracts to accomplish the given goal. The directive contract goes +/// through Research -> Specify -> Plan -> Execute -> Review phases. +/// +/// POST /api/v1/chains/init +#[utoipa::path( + post, + path = "/api/v1/chains/init", + request_body = InitChainRequest, + responses( + (status = 201, description = "Directive chain initialized", body = InitChainResponse), + (status = 400, description = "Invalid request", body = ApiError), + (status = 401, description = "Unauthorized", body = ApiError), + (status = 503, description = "Database not configured", body = ApiError), + (status = 500, description = "Internal server error", body = ApiError) + ), + security( + ("bearer_auth" = []), + ("api_key" = []) + ), + tag = "Chains" +)] +pub async fn init_chain( + State(state): State<SharedState>, + Authenticated(auth): Authenticated, + Json(req): Json<InitChainRequest>, +) -> impl IntoResponse { + let Some(ref pool) = state.db_pool else { + return ( + StatusCode::SERVICE_UNAVAILABLE, + Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), + ) + .into_response(); + }; + + // Validate the request + if req.goal.trim().is_empty() { + return ( + StatusCode::BAD_REQUEST, + Json(ApiError::new("VALIDATION_ERROR", "Goal cannot be empty")), + ) + .into_response(); + } + + match repository::init_chain_for_owner(pool, auth.owner_id, req).await { + Ok(response) => (StatusCode::CREATED, Json(response)).into_response(), + Err(e) => { + tracing::error!("Failed to initialize directive chain: {}", e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DB_ERROR", e.to_string())), + ) + .into_response() + } + } +} + /// Get a chain by ID. /// /// GET /api/v1/chains/{id} diff --git a/makima/src/server/handlers/contract_chat.rs b/makima/src/server/handlers/contract_chat.rs index 2d54894..06b3a7c 100644 --- a/makima/src/server/handlers/contract_chat.rs +++ b/makima/src/server/handlers/contract_chat.rs @@ -15,7 +15,11 @@ use utoipa::ToSchema; use uuid::Uuid; use crate::db::{ - models::{ContractChatHistoryResponse, ContractWithRelations, CreateTaskRequest, UpdateFileRequest}, + models::{ + ContractChatHistoryResponse, ContractWithRelations, CreateTaskRequest, UpdateFileRequest, + AddContractDefinitionRequest, UpdateContractDefinitionRequest, CreateChainRequest, + CreateChainDirectiveRequest, CreateContractEvaluationRequest, + }, repository, }; use crate::llm::{ @@ -2762,6 +2766,1214 @@ 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(¤t_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, + }; + } + + 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 + })), + } + } } } diff --git a/makima/src/server/handlers/contracts.rs b/makima/src/server/handlers/contracts.rs index 54bae71..2b2fc26 100644 --- a/makima/src/server/handlers/contracts.rs +++ b/makima/src/server/handlers/contracts.rs @@ -575,11 +575,55 @@ pub async fn update_contract( }), ).await; - // If contract is part of a chain, progress the chain + // 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" + ); + } + } + // 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() { |
