summaryrefslogtreecommitdiff
path: root/makima/src/db/repository.rs
diff options
context:
space:
mode:
Diffstat (limited to 'makima/src/db/repository.rs')
-rw-r--r--makima/src/db/repository.rs511
1 files changed, 498 insertions, 13 deletions
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<ChainDirective, sqlx::Error> {
+ 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<Option<ChainDirective>, 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<ChainDirective, sqlx::Error> {
+ 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<bool, sqlx::Error> {
+ 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<DirectiveTraceabilityResponse, sqlx::Error> {
+ // 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<super::models::DirectiveRequirement> = directive
+ .as_ref()
+ .and_then(|d| serde_json::from_value(d.requirements.clone()).ok())
+ .unwrap_or_default();
+
+ // Build traceability entries
+ let mut entries: Vec<TraceabilityEntry> = Vec::new();
+ let mut covered_requirements: std::collections::HashSet<String> =
+ std::collections::HashSet::new();
+
+ for req in &requirements {
+ let mut contract_def_ids: Vec<Uuid> = Vec::new();
+ let mut contract_def_names: Vec<String> = 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<super::models::DirectiveAcceptanceCriterion> = directive
+ .as_ref()
+ .and_then(|d| serde_json::from_value(d.acceptance_criteria.clone()).ok())
+ .unwrap_or_default();
+
+ let ac_ids: Vec<String> = 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<String> = 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<ContractEvaluation, sqlx::Error> {
+ 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<Option<ContractEvaluation>, 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<Vec<ContractEvaluationSummary>, 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<Vec<ContractEvaluationSummary>, 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<Option<ContractEvaluation>, 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<i32, sqlx::Error> {
+ 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<Uuid>,
+ rework_feedback: Option<&str>,
+) -> Result<ChainContract, sqlx::Error> {
+ 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<Option<ChainContract>, 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<InitChainResponse, sqlx::Error> {
+ // 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])
+ }
+}