diff options
Diffstat (limited to 'makima/src/db')
| -rw-r--r-- | makima/src/db/models.rs | 351 | ||||
| -rw-r--r-- | makima/src/db/repository.rs | 511 |
2 files changed, 849 insertions, 13 deletions
diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs index 30e1603..392d019 100644 --- a/makima/src/db/models.rs +++ b/makima/src/db/models.rs @@ -1449,6 +1449,13 @@ pub struct Contract { /// Chain ID if this contract is part of a chain (DAG of contracts) #[serde(skip_serializing_if = "Option::is_none")] pub chain_id: Option<Uuid>, + /// Reference to chain spawned by this directive contract + #[serde(skip_serializing_if = "Option::is_none")] + pub spawned_chain_id: Option<Uuid>, + /// Whether this contract is a chain directive orchestrator + #[serde(default)] + #[sqlx(default)] + pub is_chain_directive: bool, pub version: i32, pub created_at: DateTime<Utc>, pub updated_at: DateTime<Utc>, @@ -2652,12 +2659,28 @@ pub struct Chain { pub loop_current_iteration: Option<i32>, /// Progress check prompt/criteria for evaluating loop completion pub loop_progress_check: Option<String>, + /// Reference to the directive contract that created/orchestrates this chain + pub directive_contract_id: Option<Uuid>, + /// The directive document text (formal specification) + pub directive_document: Option<String>, + /// Whether LLM evaluation is enabled after contract completion + #[serde(default = "default_evaluation_enabled")] + #[sqlx(default)] + pub evaluation_enabled: bool, + /// Default pass threshold for evaluations (0.0-1.0) + pub default_pass_threshold: Option<f64>, + /// Default max retry attempts for evaluations + pub default_max_retries: Option<i32>, /// Version for optimistic locking pub version: i32, pub created_at: DateTime<Utc>, pub updated_at: DateTime<Utc>, } +fn default_evaluation_enabled() -> bool { + true +} + /// Chain repository record from the database #[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] @@ -2709,9 +2732,37 @@ pub struct ChainContract { pub editor_x: Option<f64>, /// Y position for GUI editor pub editor_y: Option<f64>, + /// Evaluation status: pending, evaluating, passed, failed, rework, escalated + #[serde(default = "default_evaluation_status")] + #[sqlx(default)] + pub evaluation_status: String, + /// Number of evaluation retry attempts + #[serde(default)] + #[sqlx(default)] + pub evaluation_retry_count: i32, + /// Maximum evaluation retry attempts (default: 3) + #[serde(default = "default_max_evaluation_retries")] + #[sqlx(default)] + pub max_evaluation_retries: i32, + /// Reference to the last evaluation result + pub last_evaluation_id: Option<Uuid>, + /// Rework feedback/instructions from failed evaluation + pub rework_feedback: Option<String>, + /// When rework was started + pub rework_started_at: Option<DateTime<Utc>>, + /// When contract originally completed (before rework) + pub original_completion_at: Option<DateTime<Utc>>, pub created_at: DateTime<Utc>, } +fn default_evaluation_status() -> String { + "pending".to_string() +} + +fn default_max_evaluation_retries() -> i32 { + 3 +} + /// Chain event for audit trail #[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] @@ -2765,6 +2816,15 @@ pub struct ChainContractDetail { pub order_index: i32, pub editor_x: Option<f64>, pub editor_y: Option<f64>, + /// Evaluation status: pending, passed, failed, rework + #[sqlx(default)] + pub evaluation_status: Option<String>, + /// Number of evaluation retries + #[sqlx(default)] + pub evaluation_retry_count: i32, + /// Maximum evaluation retry attempts + #[sqlx(default)] + pub max_evaluation_retries: i32, } /// DAG graph structure for visualization @@ -3058,6 +3118,19 @@ pub struct ChainContractDefinition { pub deliverables: Option<serde_json::Value>, /// Validation configuration for checkpoint contracts (JSON) pub validation: Option<serde_json::Value>, + /// Requirement IDs this contract addresses (for traceability) + #[sqlx(default)] + #[serde(default)] + pub requirement_ids: Vec<String>, + /// Acceptance criteria for this contract (JSON array) + #[serde(default)] + pub acceptance_criteria: Option<serde_json::Value>, + /// Whether LLM evaluation is enabled for this contract + #[serde(default = "default_evaluation_enabled")] + #[sqlx(default)] + pub evaluation_enabled: bool, + /// Pass threshold for evaluation (0.0-1.0) + pub pass_threshold: Option<f64>, /// Position in GUI editor pub editor_x: Option<f64>, pub editor_y: Option<f64>, @@ -3154,6 +3227,284 @@ pub struct ChainDefinitionGraphResponse { } // ============================================================================= +// Chain Directives (formal specification documents for directive-driven chains) +// ============================================================================= + +/// Chain directive - formal specification document that drives chain creation and evaluation +#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct ChainDirective { + pub id: Uuid, + pub chain_id: Uuid, + pub version: i32, + /// Requirements as JSON: [{ id, title, description, priority, category, parentId? }] + #[sqlx(json)] + pub requirements: serde_json::Value, + /// Acceptance criteria as JSON: [{ id, requirementIds[], description, testable, verificationMethod }] + #[sqlx(json)] + pub acceptance_criteria: serde_json::Value, + /// Constraints as JSON: [{ id, type, description, impact }] + #[sqlx(json)] + pub constraints: serde_json::Value, + /// External dependencies as JSON: [{ id, name, type, status, requiredBy[] }] + #[sqlx(json)] + pub external_dependencies: serde_json::Value, + /// Source type: 'manual', 'llm_generated', 'imported' + pub source_type: String, + pub created_at: DateTime<Utc>, + pub updated_at: DateTime<Utc>, +} + +/// Requirement in a directive +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct DirectiveRequirement { + pub id: String, + pub title: String, + pub description: String, + /// Priority: 'must', 'should', 'could', 'wont' + pub priority: String, + /// Category: 'feature', 'infrastructure', 'testing', etc. + pub category: Option<String>, + /// Parent requirement ID for hierarchical requirements + pub parent_id: Option<String>, +} + +/// Acceptance criterion in a directive +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct DirectiveAcceptanceCriterion { + pub id: String, + /// Requirement IDs this criterion validates + pub requirement_ids: Vec<String>, + pub description: String, + pub testable: bool, + /// Verification method: 'automated', 'manual', 'review', 'llm' + pub verification_method: String, +} + +/// Constraint in a directive +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct DirectiveConstraint { + pub id: String, + /// Type: 'technical', 'business', 'time', 'resource' + #[serde(rename = "type")] + pub constraint_type: String, + pub description: String, + /// Impact: 'high', 'medium', 'low' + pub impact: String, +} + +/// External dependency in a directive +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct DirectiveExternalDependency { + pub id: String, + pub name: String, + /// Type: 'api', 'service', 'library', 'data' + #[serde(rename = "type")] + pub dependency_type: String, + /// Status: 'available', 'pending', 'blocked' + pub status: String, + /// Requirement IDs that need this dependency + pub required_by: Vec<String>, +} + +/// Request to create or update a chain directive +#[derive(Debug, Clone, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct CreateChainDirectiveRequest { + pub requirements: Option<Vec<DirectiveRequirement>>, + pub acceptance_criteria: Option<Vec<DirectiveAcceptanceCriterion>>, + pub constraints: Option<Vec<DirectiveConstraint>>, + pub external_dependencies: Option<Vec<DirectiveExternalDependency>>, + pub source_type: Option<String>, +} + +/// Request to initialize a directive-driven chain +#[derive(Debug, Clone, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct InitChainRequest { + /// High-level goal/description for the directive contract + pub goal: String, + /// Repository URL for chain contracts + pub repository_url: Option<String>, + /// Local path for chain contracts + pub local_path: Option<String>, + /// Whether to enable phase guard (user approval between phases) + #[serde(default)] + pub phase_guard: bool, +} + +/// Response from initializing a directive-driven chain +#[derive(Debug, Clone, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct InitChainResponse { + pub chain_id: Uuid, + pub directive_contract_id: Uuid, + pub supervisor_task_id: Option<Uuid>, +} + +// ============================================================================= +// Contract Evaluations (LLM evaluation results for completed contracts) +// ============================================================================= + +/// Evaluation status for chain contracts +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum EvaluationStatus { + /// Not yet evaluated + Pending, + /// Currently being evaluated + Evaluating, + /// Evaluation passed + Passed, + /// Evaluation failed + Failed, + /// Contract is being reworked after failed evaluation + Rework, + /// Max retries exceeded, escalated to user + Escalated, + /// User approved despite partial failure + ApprovedWithIssues, +} + +impl std::fmt::Display for EvaluationStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Pending => write!(f, "pending"), + Self::Evaluating => write!(f, "evaluating"), + Self::Passed => write!(f, "passed"), + Self::Failed => write!(f, "failed"), + Self::Rework => write!(f, "rework"), + Self::Escalated => write!(f, "escalated"), + Self::ApprovedWithIssues => write!(f, "approved_with_issues"), + } + } +} + +impl std::str::FromStr for EvaluationStatus { + type Err = String; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s.to_lowercase().as_str() { + "pending" => Ok(Self::Pending), + "evaluating" => Ok(Self::Evaluating), + "passed" => Ok(Self::Passed), + "failed" => Ok(Self::Failed), + "rework" => Ok(Self::Rework), + "escalated" => Ok(Self::Escalated), + "approved_with_issues" => Ok(Self::ApprovedWithIssues), + _ => Err(format!("Unknown evaluation status: {}", s)), + } + } +} + +/// Contract evaluation - LLM evaluation result after contract completion +#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct ContractEvaluation { + pub id: Uuid, + pub contract_id: Uuid, + pub chain_id: Option<Uuid>, + pub chain_contract_id: Option<Uuid>, + /// Evaluation attempt number (1-based) + pub evaluation_number: i32, + /// Model used for evaluation + pub evaluator_model: Option<String>, + /// Whether the evaluation passed + pub passed: bool, + /// Overall score (0.0-1.0) + pub overall_score: Option<f64>, + /// Per-criterion results as JSON + #[sqlx(json)] + pub criteria_results: serde_json::Value, + /// Summary feedback from the evaluator + pub summary_feedback: String, + /// Instructions for rework if evaluation failed + pub rework_instructions: Option<String>, + /// Snapshot of directive at evaluation time + pub directive_snapshot: Option<serde_json::Value>, + /// Snapshot of deliverables at evaluation time + pub deliverables_snapshot: Option<serde_json::Value>, + pub started_at: DateTime<Utc>, + pub completed_at: Option<DateTime<Utc>>, + pub created_at: DateTime<Utc>, +} + +/// Per-criterion evaluation result +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct EvaluationCriterionResult { + pub criterion_id: String, + pub criterion_text: String, + pub passed: bool, + /// Score (0.0-1.0) + pub score: f64, + pub feedback: String, + /// Evidence supporting the evaluation + pub evidence: Vec<String>, +} + +/// Request to create a contract evaluation +#[derive(Debug, Clone, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct CreateContractEvaluationRequest { + pub contract_id: Uuid, + pub chain_id: Option<Uuid>, + pub chain_contract_id: Option<Uuid>, + pub evaluator_model: Option<String>, + pub passed: bool, + pub overall_score: Option<f64>, + pub criteria_results: Vec<EvaluationCriterionResult>, + pub summary_feedback: String, + pub rework_instructions: Option<String>, +} + +/// Summary of contract evaluation for list views +#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct ContractEvaluationSummary { + pub id: Uuid, + pub contract_id: Uuid, + pub evaluation_number: i32, + pub passed: bool, + pub overall_score: Option<f64>, + pub summary_feedback: String, + pub created_at: DateTime<Utc>, +} + +/// Response listing evaluations for a chain or contract +#[derive(Debug, Clone, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct ContractEvaluationsResponse { + pub evaluations: Vec<ContractEvaluationSummary>, + pub total: i64, +} + +/// Traceability matrix entry - maps requirements to contracts +#[derive(Debug, Clone, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct TraceabilityEntry { + pub requirement_id: String, + pub requirement_title: String, + pub contract_definition_ids: Vec<Uuid>, + pub contract_definition_names: Vec<String>, + pub acceptance_criteria_ids: Vec<String>, +} + +/// Response for directive traceability +#[derive(Debug, Clone, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct DirectiveTraceabilityResponse { + pub chain_id: Uuid, + pub entries: Vec<TraceabilityEntry>, + /// Requirements not mapped to any contract + pub uncovered_requirements: Vec<String>, +} + +// ============================================================================= // Unit Tests // ============================================================================= 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]) + } +} |
