summaryrefslogtreecommitdiff
path: root/makima/src/server/handlers
diff options
context:
space:
mode:
Diffstat (limited to 'makima/src/server/handlers')
-rw-r--r--makima/src/server/handlers/chains.rs62
-rw-r--r--makima/src/server/handlers/contract_chat.rs1214
-rw-r--r--makima/src/server/handlers/contracts.rs46
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(&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,
+ };
+ }
+
+ 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() {