From 6a0c912a3fbd8e9b3e87ef40e960803d819d966d Mon Sep 17 00:00:00 2001 From: soryu Date: Thu, 5 Feb 2026 01:42:59 +0000 Subject: Add makima directives --- makima/src/db/repository.rs | 511 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 498 insertions(+), 13 deletions(-) (limited to 'makima/src/db/repository.rs') diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs index 2b595b5..9cb653f 100644 --- a/makima/src/db/repository.rs +++ b/makima/src/db/repository.rs @@ -8,18 +8,21 @@ use uuid::Uuid; use super::models::{ AddChainRepositoryRequest, AddContractDefinitionRequest, AddContractToChainRequest, Chain, ChainContract, ChainContractDefinition, ChainContractDetail, ChainDefinitionGraphNode, - ChainDefinitionGraphResponse, ChainEditorContract, ChainEditorData, ChainEditorDeliverable, - ChainEditorEdge, ChainEditorNode, ChainEditorTask, ChainEvent, ChainGraphEdge, ChainGraphNode, - ChainGraphResponse, ChainRepository, ChainSummary, ChainWithContracts, CheckpointPatch, - CheckpointPatchInfo, Contract, ContractChatConversation, ContractChatMessageRecord, - ContractEvent, ContractRepository, ContractSummary, ContractTypeTemplateRecord, - ConversationMessage, ConversationSnapshot, CreateChainRequest, CreateContractRequest, - CreateFileRequest, CreateTaskRequest, CreateTemplateRequest, Daemon, DaemonTaskAssignment, - DaemonWithCapacity, DeliverableDefinition, File, FileSummary, FileVersion, HistoryEvent, - HistoryQueryFilters, MeshChatConversation, MeshChatMessageRecord, PhaseChangeResult, - PhaseConfig, PhaseDefinition, SupervisorHeartbeatRecord, SupervisorState, Task, TaskCheckpoint, - TaskEvent, TaskSummary, UpdateChainRequest, UpdateContractDefinitionRequest, - UpdateContractRequest, UpdateFileRequest, UpdateTaskRequest, UpdateTemplateRequest, + ChainDefinitionGraphResponse, ChainDirective, ChainEditorContract, ChainEditorData, + ChainEditorDeliverable, ChainEditorEdge, ChainEditorNode, ChainEditorTask, ChainEvent, + ChainGraphEdge, ChainGraphNode, ChainGraphResponse, ChainRepository, ChainSummary, + ChainWithContracts, CheckpointPatch, CheckpointPatchInfo, Contract, ContractChatConversation, + ContractChatMessageRecord, ContractEvaluation, ContractEvaluationSummary, ContractEvent, + ContractRepository, ContractSummary, ContractTypeTemplateRecord, ConversationMessage, + ConversationSnapshot, CreateChainDirectiveRequest, CreateChainRequest, + CreateContractEvaluationRequest, CreateContractRequest, CreateFileRequest, CreateTaskRequest, + CreateTemplateRequest, Daemon, DaemonTaskAssignment, DaemonWithCapacity, DeliverableDefinition, + DirectiveTraceabilityResponse, EvaluationCriterionResult, File, FileSummary, FileVersion, + HistoryEvent, HistoryQueryFilters, InitChainRequest, InitChainResponse, MeshChatConversation, + MeshChatMessageRecord, PhaseChangeResult, PhaseConfig, PhaseDefinition, + SupervisorHeartbeatRecord, SupervisorState, Task, TaskCheckpoint, TaskEvent, TaskSummary, + TraceabilityEntry, UpdateChainRequest, UpdateContractDefinitionRequest, UpdateContractRequest, + UpdateFileRequest, UpdateTaskRequest, UpdateTemplateRequest, }; /// Repository error types. @@ -5156,7 +5159,10 @@ pub async fn list_chain_contracts( cc.depends_on, cc.order_index, cc.editor_x, - cc.editor_y + cc.editor_y, + cc.evaluation_status, + cc.evaluation_retry_count, + cc.max_evaluation_retries FROM chain_contracts cc JOIN contracts c ON c.id = cc.contract_id WHERE cc.chain_id = $1 @@ -6262,3 +6268,482 @@ async fn create_contract_from_definition( Ok(contract.id) } + +// ============================================================================= +// Chain Directives +// ============================================================================= + +/// Create a directive for a chain. +pub async fn create_chain_directive( + pool: &PgPool, + chain_id: Uuid, + req: CreateChainDirectiveRequest, +) -> Result { + let requirements = serde_json::to_value(&req.requirements.unwrap_or_default()) + .unwrap_or(serde_json::json!([])); + let acceptance_criteria = serde_json::to_value(&req.acceptance_criteria.unwrap_or_default()) + .unwrap_or(serde_json::json!([])); + let constraints = + serde_json::to_value(&req.constraints.unwrap_or_default()).unwrap_or(serde_json::json!([])); + let external_dependencies = + serde_json::to_value(&req.external_dependencies.unwrap_or_default()) + .unwrap_or(serde_json::json!([])); + let source_type = req.source_type.unwrap_or_else(|| "llm_generated".to_string()); + + sqlx::query_as::<_, ChainDirective>( + r#" + INSERT INTO chain_directives (chain_id, requirements, acceptance_criteria, constraints, external_dependencies, source_type) + VALUES ($1, $2, $3, $4, $5, $6) + RETURNING * + "#, + ) + .bind(chain_id) + .bind(&requirements) + .bind(&acceptance_criteria) + .bind(&constraints) + .bind(&external_dependencies) + .bind(&source_type) + .fetch_one(pool) + .await +} + +/// Get the directive for a chain. +pub async fn get_chain_directive( + pool: &PgPool, + chain_id: Uuid, +) -> Result, sqlx::Error> { + sqlx::query_as::<_, ChainDirective>( + r#" + SELECT * + FROM chain_directives + WHERE chain_id = $1 + "#, + ) + .bind(chain_id) + .fetch_optional(pool) + .await +} + +/// Update a chain directive. +pub async fn update_chain_directive( + pool: &PgPool, + chain_id: Uuid, + req: CreateChainDirectiveRequest, +) -> Result { + let requirements = req + .requirements + .map(|r| serde_json::to_value(&r).unwrap_or(serde_json::json!([]))); + let acceptance_criteria = req + .acceptance_criteria + .map(|ac| serde_json::to_value(&ac).unwrap_or(serde_json::json!([]))); + let constraints = req + .constraints + .map(|c| serde_json::to_value(&c).unwrap_or(serde_json::json!([]))); + let external_dependencies = req + .external_dependencies + .map(|ed| serde_json::to_value(&ed).unwrap_or(serde_json::json!([]))); + + sqlx::query_as::<_, ChainDirective>( + r#" + UPDATE chain_directives SET + requirements = COALESCE($2, requirements), + acceptance_criteria = COALESCE($3, acceptance_criteria), + constraints = COALESCE($4, constraints), + external_dependencies = COALESCE($5, external_dependencies), + source_type = COALESCE($6, source_type), + version = version + 1, + updated_at = NOW() + WHERE chain_id = $1 + RETURNING * + "#, + ) + .bind(chain_id) + .bind(&requirements) + .bind(&acceptance_criteria) + .bind(&constraints) + .bind(&external_dependencies) + .bind(&req.source_type) + .fetch_one(pool) + .await +} + +/// Delete a chain directive. +pub async fn delete_chain_directive(pool: &PgPool, chain_id: Uuid) -> Result { + let result = sqlx::query("DELETE FROM chain_directives WHERE chain_id = $1") + .bind(chain_id) + .execute(pool) + .await?; + Ok(result.rows_affected() > 0) +} + +/// Get directive traceability (requirement -> contract mapping). +pub async fn get_directive_traceability( + pool: &PgPool, + chain_id: Uuid, +) -> Result { + // Get the directive + let directive = get_chain_directive(pool, chain_id).await?; + + // Get all contract definitions with their requirement mappings + let definitions = list_chain_contract_definitions(pool, chain_id).await?; + + // Parse requirements from directive + let requirements: Vec = directive + .as_ref() + .and_then(|d| serde_json::from_value(d.requirements.clone()).ok()) + .unwrap_or_default(); + + // Build traceability entries + let mut entries: Vec = Vec::new(); + let mut covered_requirements: std::collections::HashSet = + std::collections::HashSet::new(); + + for req in &requirements { + let mut contract_def_ids: Vec = Vec::new(); + let mut contract_def_names: Vec = Vec::new(); + + for def in &definitions { + if def.requirement_ids.contains(&req.id) { + contract_def_ids.push(def.id); + contract_def_names.push(def.name.clone()); + covered_requirements.insert(req.id.clone()); + } + } + + // Get acceptance criteria for this requirement + let acceptance_criteria: Vec = directive + .as_ref() + .and_then(|d| serde_json::from_value(d.acceptance_criteria.clone()).ok()) + .unwrap_or_default(); + + let ac_ids: Vec = acceptance_criteria + .iter() + .filter(|ac| ac.requirement_ids.contains(&req.id)) + .map(|ac| ac.id.clone()) + .collect(); + + entries.push(TraceabilityEntry { + requirement_id: req.id.clone(), + requirement_title: req.title.clone(), + contract_definition_ids: contract_def_ids, + contract_definition_names: contract_def_names, + acceptance_criteria_ids: ac_ids, + }); + } + + // Find uncovered requirements + let uncovered: Vec = requirements + .iter() + .filter(|r| !covered_requirements.contains(&r.id)) + .map(|r| r.id.clone()) + .collect(); + + Ok(DirectiveTraceabilityResponse { + chain_id, + entries, + uncovered_requirements: uncovered, + }) +} + +// ============================================================================= +// Contract Evaluations +// ============================================================================= + +/// Create a contract evaluation record. +pub async fn create_contract_evaluation( + pool: &PgPool, + req: CreateContractEvaluationRequest, +) -> Result { + let criteria_results = serde_json::to_value(&req.criteria_results).unwrap_or(serde_json::json!([])); + + sqlx::query_as::<_, ContractEvaluation>( + r#" + INSERT INTO contract_evaluations ( + contract_id, chain_id, chain_contract_id, + evaluator_model, passed, overall_score, + criteria_results, summary_feedback, rework_instructions, + completed_at + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW()) + RETURNING * + "#, + ) + .bind(req.contract_id) + .bind(req.chain_id) + .bind(req.chain_contract_id) + .bind(&req.evaluator_model) + .bind(req.passed) + .bind(req.overall_score) + .bind(&criteria_results) + .bind(&req.summary_feedback) + .bind(&req.rework_instructions) + .fetch_one(pool) + .await +} + +/// Get a contract evaluation by ID. +pub async fn get_contract_evaluation( + pool: &PgPool, + id: Uuid, +) -> Result, sqlx::Error> { + sqlx::query_as::<_, ContractEvaluation>( + r#" + SELECT * + FROM contract_evaluations + WHERE id = $1 + "#, + ) + .bind(id) + .fetch_optional(pool) + .await +} + +/// List evaluations for a contract. +pub async fn list_contract_evaluations( + pool: &PgPool, + contract_id: Uuid, +) -> Result, sqlx::Error> { + sqlx::query_as::<_, ContractEvaluationSummary>( + r#" + SELECT id, contract_id, evaluation_number, passed, overall_score, summary_feedback, created_at + FROM contract_evaluations + WHERE contract_id = $1 + ORDER BY evaluation_number DESC + "#, + ) + .bind(contract_id) + .fetch_all(pool) + .await +} + +/// List evaluations for a chain. +pub async fn list_chain_evaluations( + pool: &PgPool, + chain_id: Uuid, +) -> Result, sqlx::Error> { + sqlx::query_as::<_, ContractEvaluationSummary>( + r#" + SELECT id, contract_id, evaluation_number, passed, overall_score, summary_feedback, created_at + FROM contract_evaluations + WHERE chain_id = $1 + ORDER BY created_at DESC + "#, + ) + .bind(chain_id) + .fetch_all(pool) + .await +} + +/// Get the latest evaluation for a chain contract. +pub async fn get_latest_chain_contract_evaluation( + pool: &PgPool, + chain_contract_id: Uuid, +) -> Result, sqlx::Error> { + sqlx::query_as::<_, ContractEvaluation>( + r#" + SELECT * + FROM contract_evaluations + WHERE chain_contract_id = $1 + ORDER BY evaluation_number DESC + LIMIT 1 + "#, + ) + .bind(chain_contract_id) + .fetch_optional(pool) + .await +} + +/// Get the next evaluation number for a chain contract. +pub async fn get_next_evaluation_number( + pool: &PgPool, + chain_contract_id: Uuid, +) -> Result { + let result: Option<(i32,)> = sqlx::query_as( + r#" + SELECT COALESCE(MAX(evaluation_number), 0) + 1 as next_number + FROM contract_evaluations + WHERE chain_contract_id = $1 + "#, + ) + .bind(chain_contract_id) + .fetch_optional(pool) + .await?; + + Ok(result.map(|(n,)| n).unwrap_or(1)) +} + +/// Update chain contract evaluation status. +pub async fn update_chain_contract_evaluation_status( + pool: &PgPool, + chain_contract_id: Uuid, + status: &str, + evaluation_id: Option, + rework_feedback: Option<&str>, +) -> Result { + sqlx::query_as::<_, ChainContract>( + r#" + UPDATE chain_contracts SET + evaluation_status = $2, + last_evaluation_id = COALESCE($3, last_evaluation_id), + rework_feedback = COALESCE($4, rework_feedback), + evaluation_retry_count = CASE + WHEN $2 = 'rework' THEN evaluation_retry_count + 1 + ELSE evaluation_retry_count + END, + rework_started_at = CASE + WHEN $2 = 'rework' THEN NOW() + ELSE rework_started_at + END + WHERE id = $1 + RETURNING * + "#, + ) + .bind(chain_contract_id) + .bind(status) + .bind(evaluation_id) + .bind(rework_feedback) + .fetch_one(pool) + .await +} + +/// Mark a chain contract's original completion time (before rework). +pub async fn mark_chain_contract_original_completion( + pool: &PgPool, + chain_contract_id: Uuid, +) -> Result<(), sqlx::Error> { + sqlx::query( + r#" + UPDATE chain_contracts SET + original_completion_at = COALESCE(original_completion_at, NOW()) + WHERE id = $1 + "#, + ) + .bind(chain_contract_id) + .execute(pool) + .await?; + Ok(()) +} + +/// Get chain contract by contract ID. +pub async fn get_chain_contract_by_contract_id( + pool: &PgPool, + contract_id: Uuid, +) -> Result, sqlx::Error> { + sqlx::query_as::<_, ChainContract>( + r#" + SELECT * + FROM chain_contracts + WHERE contract_id = $1 + "#, + ) + .bind(contract_id) + .fetch_optional(pool) + .await +} + +// ============================================================================= +// Init Chain (Directive-Driven Chain Creation) +// ============================================================================= + +/// Initialize a directive-driven chain. +/// Creates a directive contract and an empty chain linked to it. +pub async fn init_chain_for_owner( + pool: &PgPool, + owner_id: Uuid, + req: InitChainRequest, +) -> Result { + // Create the directive contract + // Note: "directive" contract type uses the "specification" phases by default + let contract_req = CreateContractRequest { + name: format!("Directive: {}", truncate_string(&req.goal, 50)), + description: Some(req.goal.clone()), + contract_type: Some("specification".to_string()), // Directive uses spec workflow + template_id: None, + initial_phase: Some("research".to_string()), + phase_guard: Some(req.phase_guard), + autonomous_loop: Some(false), + local_only: Some(false), + auto_merge_local: Some(false), + }; + + let contract = create_contract_for_owner(pool, owner_id, contract_req).await?; + + // Mark it as a chain directive + sqlx::query("UPDATE contracts SET is_chain_directive = true WHERE id = $1") + .bind(contract.id) + .execute(pool) + .await?; + + // Build repositories list from request + let repositories = match (req.repository_url.as_ref(), req.local_path.as_ref()) { + (Some(url), _) => Some(vec![AddChainRepositoryRequest { + name: "Primary".to_string(), + repository_url: Some(url.clone()), + local_path: None, + source_type: "remote".to_string(), + is_primary: true, + }]), + (None, Some(path)) => Some(vec![AddChainRepositoryRequest { + name: "Primary".to_string(), + repository_url: None, + local_path: Some(path.clone()), + source_type: "local".to_string(), + is_primary: true, + }]), + (None, None) => None, + }; + + // Create the chain with directive contract reference + let chain_req = CreateChainRequest { + name: truncate_string(&req.goal, 100), + description: Some(req.goal), + repositories, + loop_enabled: Some(false), + loop_max_iterations: None, + loop_progress_check: None, + contracts: None, + }; + + let chain = create_chain_for_owner(pool, owner_id, chain_req).await?; + + // Link directive contract to chain + sqlx::query( + r#" + UPDATE chains SET directive_contract_id = $2 WHERE id = $1; + UPDATE contracts SET spawned_chain_id = $1 WHERE id = $2; + "#, + ) + .bind(chain.id) + .bind(contract.id) + .execute(pool) + .await?; + + // Create empty directive document + create_chain_directive( + pool, + chain.id, + CreateChainDirectiveRequest { + requirements: Some(vec![]), + acceptance_criteria: Some(vec![]), + constraints: Some(vec![]), + external_dependencies: Some(vec![]), + source_type: Some("llm_generated".to_string()), + }, + ) + .await?; + + Ok(InitChainResponse { + chain_id: chain.id, + directive_contract_id: contract.id, + supervisor_task_id: contract.supervisor_task_id, + }) +} + +/// Helper to truncate string to max length +fn truncate_string(s: &str, max_len: usize) -> String { + if s.len() <= max_len { + s.to_string() + } else { + format!("{}...", &s[..max_len - 3]) + } +} -- cgit v1.2.3