diff options
Diffstat (limited to 'makima/src/db')
| -rw-r--r-- | makima/src/db/models.rs | 234 | ||||
| -rw-r--r-- | makima/src/db/repository.rs | 283 |
2 files changed, 512 insertions, 5 deletions
diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs index 3b10cb5..ec4ee15 100644 --- a/makima/src/db/models.rs +++ b/makima/src/db/models.rs @@ -2690,3 +2690,237 @@ mod tests { assert_eq!(deserialized.progress, 50); } } + +// ============================================================================= +// Directive Types +// ============================================================================= + +/// Default autonomy level for directives +fn default_autonomy_level() -> String { + "guardrails".to_string() +} + +/// Default empty JSON array +fn default_json_array() -> serde_json::Value { + serde_json::json!([]) +} + +/// Default empty JSON object +fn default_json_object() -> serde_json::Value { + serde_json::json!({}) +} + +/// Default confidence threshold (green) +fn default_confidence_green() -> f64 { + 0.85 +} + +/// Default confidence threshold (yellow) +fn default_confidence_yellow() -> f64 { + 0.60 +} + +/// Default max rework cycles +fn default_max_rework_cycles() -> Option<i32> { + Some(3) +} + +/// Default max chain regenerations +fn default_max_chain_regenerations() -> Option<i32> { + Some(2) +} + +/// Full directive row from the database. +#[derive(Debug, Clone, FromRow, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct Directive { + pub id: Uuid, + pub owner_id: Uuid, + pub title: String, + pub goal: String, + #[sqlx(json)] + pub requirements: serde_json::Value, + #[sqlx(json)] + pub acceptance_criteria: serde_json::Value, + #[sqlx(json)] + pub constraints: serde_json::Value, + #[sqlx(json)] + pub external_dependencies: serde_json::Value, + pub status: String, + pub autonomy_level: String, + pub confidence_threshold_green: f64, + pub confidence_threshold_yellow: f64, + pub max_total_cost_usd: Option<f64>, + pub max_wall_time_minutes: Option<i32>, + pub max_rework_cycles: Option<i32>, + pub max_chain_regenerations: Option<i32>, + pub repository_url: Option<String>, + pub local_path: Option<String>, + pub base_branch: Option<String>, + pub orchestrator_contract_id: Option<Uuid>, + pub current_chain_id: Option<Uuid>, + pub chain_generation_count: i32, + pub total_cost_usd: f64, + pub started_at: Option<DateTime<Utc>>, + pub completed_at: Option<DateTime<Utc>>, + pub version: i32, + pub created_at: DateTime<Utc>, + pub updated_at: DateTime<Utc>, +} + +/// Summary of a directive for list views. +#[derive(Debug, Clone, FromRow, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct DirectiveSummary { + pub id: Uuid, + pub title: String, + pub goal: String, + pub status: String, + pub autonomy_level: String, + pub chain_count: i64, + pub step_count: i64, + pub total_cost_usd: f64, + pub version: i32, + pub created_at: DateTime<Utc>, + pub updated_at: DateTime<Utc>, +} + +/// Response for directive list endpoint. +#[derive(Debug, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct DirectiveListResponse { + pub directives: Vec<DirectiveSummary>, + pub total: i64, +} + +/// Request to create a new directive. +#[derive(Debug, Clone, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct CreateDirectiveRequest { + pub title: String, + pub goal: String, + #[serde(default = "default_json_array")] + pub requirements: serde_json::Value, + #[serde(default = "default_json_array")] + pub acceptance_criteria: serde_json::Value, + #[serde(default = "default_json_array")] + pub constraints: serde_json::Value, + #[serde(default = "default_json_array")] + pub external_dependencies: serde_json::Value, + #[serde(default = "default_autonomy_level")] + pub autonomy_level: String, + #[serde(default = "default_confidence_green")] + pub confidence_threshold_green: f64, + #[serde(default = "default_confidence_yellow")] + pub confidence_threshold_yellow: f64, + pub max_total_cost_usd: Option<f64>, + pub max_wall_time_minutes: Option<i32>, + #[serde(default = "default_max_rework_cycles")] + pub max_rework_cycles: Option<i32>, + #[serde(default = "default_max_chain_regenerations")] + pub max_chain_regenerations: Option<i32>, + pub repository_url: Option<String>, + pub local_path: Option<String>, + pub base_branch: Option<String>, +} + +/// Request to update an existing directive. +#[derive(Debug, Clone, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct UpdateDirectiveRequest { + pub title: Option<String>, + pub goal: Option<String>, + pub requirements: Option<serde_json::Value>, + pub acceptance_criteria: Option<serde_json::Value>, + pub constraints: Option<serde_json::Value>, + pub external_dependencies: Option<serde_json::Value>, + pub status: Option<String>, + pub autonomy_level: Option<String>, + pub confidence_threshold_green: Option<f64>, + pub confidence_threshold_yellow: Option<f64>, + pub max_total_cost_usd: Option<f64>, + pub max_wall_time_minutes: Option<i32>, + pub max_rework_cycles: Option<i32>, + pub max_chain_regenerations: Option<i32>, + pub repository_url: Option<String>, + pub local_path: Option<String>, + pub base_branch: Option<String>, + /// Version for optimistic locking + pub version: Option<i32>, +} + +/// Directive with its chains for detail view. +#[derive(Debug, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct DirectiveWithChains { + #[serde(flatten)] + pub directive: Directive, + pub chains: Vec<DirectiveChain>, +} + +/// Full row from directive_chains table. +#[derive(Debug, Clone, FromRow, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct DirectiveChain { + pub id: Uuid, + pub directive_id: Uuid, + pub generation: i32, + pub name: String, + pub description: Option<String>, + pub rationale: Option<String>, + pub planning_model: Option<String>, + pub status: String, + pub total_steps: i32, + pub completed_steps: i32, + pub failed_steps: i32, + pub current_confidence: Option<f64>, + pub started_at: Option<DateTime<Utc>>, + pub completed_at: Option<DateTime<Utc>>, + pub version: i32, + pub created_at: DateTime<Utc>, + pub updated_at: DateTime<Utc>, +} + +/// Full row from chain_steps table. +#[derive(Debug, Clone, FromRow, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct ChainStep { + pub id: Uuid, + pub chain_id: Uuid, + pub name: String, + pub description: Option<String>, + pub step_type: String, + pub contract_type: String, + pub initial_phase: Option<String>, + pub task_plan: Option<String>, + pub phases: Option<Vec<String>>, + pub depends_on: Option<Vec<Uuid>>, + pub parallel_group: Option<String>, + pub requirement_ids: Option<Vec<String>>, + pub acceptance_criteria_ids: Option<Vec<String>>, + #[sqlx(json)] + pub verifier_config: serde_json::Value, + pub status: String, + pub contract_id: Option<Uuid>, + pub supervisor_task_id: Option<Uuid>, + pub confidence_score: Option<f64>, + pub confidence_level: Option<String>, + pub evaluation_count: i32, + pub rework_count: i32, + pub last_evaluation_id: Option<Uuid>, + pub editor_x: Option<f64>, + pub editor_y: Option<f64>, + pub order_index: i32, + pub started_at: Option<DateTime<Utc>>, + pub completed_at: Option<DateTime<Utc>>, + pub created_at: DateTime<Utc>, +} + +/// Chain with its steps for detail view. +#[derive(Debug, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct ChainWithSteps { + #[serde(flatten)] + pub chain: DirectiveChain, + pub steps: Vec<ChainStep>, +} diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs index 863d927..5949079 100644 --- a/makima/src/db/repository.rs +++ b/makima/src/db/repository.rs @@ -6,16 +6,17 @@ use sqlx::PgPool; use uuid::Uuid; use super::models::{ - CheckpointPatch, CheckpointPatchInfo, Contract, ContractChatConversation, + ChainStep, CheckpointPatch, CheckpointPatchInfo, Contract, ContractChatConversation, ContractChatMessageRecord, ContractEvent, ContractRepository, ContractSummary, ContractTypeTemplateRecord, ConversationMessage, ConversationSnapshot, - CreateContractRequest, CreateFileRequest, CreateTaskRequest, CreateTemplateRequest, - Daemon, DaemonTaskAssignment, DaemonWithCapacity, DeliverableDefinition, + CreateContractRequest, CreateDirectiveRequest, CreateFileRequest, CreateTaskRequest, + CreateTemplateRequest, Daemon, DaemonTaskAssignment, DaemonWithCapacity, + DeliverableDefinition, Directive, DirectiveChain, DirectiveSummary, File, FileSummary, FileVersion, HistoryEvent, HistoryQueryFilters, MeshChatConversation, MeshChatMessageRecord, PhaseChangeResult, PhaseConfig, PhaseDefinition, SupervisorHeartbeatRecord, SupervisorState, Task, TaskCheckpoint, - TaskEvent, TaskSummary, UpdateContractRequest, UpdateFileRequest, UpdateTaskRequest, - UpdateTemplateRequest, + TaskEvent, TaskSummary, UpdateContractRequest, UpdateDirectiveRequest, + UpdateFileRequest, UpdateTaskRequest, UpdateTemplateRequest, }; /// Repository error types. @@ -4910,3 +4911,275 @@ fn truncate_string(s: &str, max_len: usize) -> String { format!("{}...", &s[..max_len - 3]) } } + +// ============================================================================= +// Directive CRUD +// ============================================================================= + +/// Create a new directive, scoped to owner. +pub async fn create_directive_for_owner( + pool: &PgPool, + owner_id: Uuid, + req: CreateDirectiveRequest, +) -> Result<Directive, sqlx::Error> { + sqlx::query_as::<_, Directive>( + r#" + INSERT INTO directives ( + owner_id, title, goal, + requirements, acceptance_criteria, constraints, external_dependencies, + autonomy_level, confidence_threshold_green, confidence_threshold_yellow, + max_total_cost_usd, max_wall_time_minutes, max_rework_cycles, max_chain_regenerations, + repository_url, local_path, base_branch + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) + RETURNING * + "#, + ) + .bind(owner_id) + .bind(&req.title) + .bind(&req.goal) + .bind(&req.requirements) + .bind(&req.acceptance_criteria) + .bind(&req.constraints) + .bind(&req.external_dependencies) + .bind(&req.autonomy_level) + .bind(req.confidence_threshold_green) + .bind(req.confidence_threshold_yellow) + .bind(req.max_total_cost_usd) + .bind(req.max_wall_time_minutes) + .bind(req.max_rework_cycles) + .bind(req.max_chain_regenerations) + .bind(&req.repository_url) + .bind(&req.local_path) + .bind(&req.base_branch) + .fetch_one(pool) + .await +} + +/// Get a directive by ID, scoped to owner. +pub async fn get_directive_for_owner( + pool: &PgPool, + id: Uuid, + owner_id: Uuid, +) -> Result<Option<Directive>, sqlx::Error> { + sqlx::query_as::<_, Directive>( + r#" + SELECT * + FROM directives + WHERE id = $1 AND owner_id = $2 + "#, + ) + .bind(id) + .bind(owner_id) + .fetch_optional(pool) + .await +} + +/// List all directives for an owner, ordered by created_at DESC. +pub async fn list_directives_for_owner( + pool: &PgPool, + owner_id: Uuid, +) -> Result<Vec<DirectiveSummary>, sqlx::Error> { + sqlx::query_as::<_, DirectiveSummary>( + r#" + SELECT + d.id, d.title, d.goal, d.status, d.autonomy_level, + (SELECT COUNT(*) FROM directive_chains WHERE directive_id = d.id) as chain_count, + (SELECT COUNT(*) FROM chain_steps cs JOIN directive_chains dc ON cs.chain_id = dc.id WHERE dc.directive_id = d.id) as step_count, + d.total_cost_usd, d.version, d.created_at, d.updated_at + FROM directives d + WHERE d.owner_id = $1 + ORDER BY d.created_at DESC + "#, + ) + .bind(owner_id) + .fetch_all(pool) + .await +} + +/// Update a directive by ID with optimistic locking, scoped to owner. +pub async fn update_directive_for_owner( + pool: &PgPool, + id: Uuid, + owner_id: Uuid, + req: UpdateDirectiveRequest, +) -> Result<Option<Directive>, RepositoryError> { + let existing = get_directive_for_owner(pool, id, owner_id).await?; + let Some(existing) = existing else { + return Ok(None); + }; + + // Check version if provided (optimistic locking) + if let Some(expected_version) = req.version { + if existing.version != expected_version { + return Err(RepositoryError::VersionConflict { + expected: expected_version, + actual: existing.version, + }); + } + } + + // Apply updates + let title = req.title.unwrap_or(existing.title); + let goal = req.goal.unwrap_or(existing.goal); + let requirements = req.requirements.unwrap_or(existing.requirements); + let acceptance_criteria = req.acceptance_criteria.unwrap_or(existing.acceptance_criteria); + let constraints = req.constraints.unwrap_or(existing.constraints); + let external_dependencies = req.external_dependencies.unwrap_or(existing.external_dependencies); + let status = req.status.unwrap_or(existing.status); + let autonomy_level = req.autonomy_level.unwrap_or(existing.autonomy_level); + let confidence_threshold_green = req.confidence_threshold_green.unwrap_or(existing.confidence_threshold_green); + let confidence_threshold_yellow = req.confidence_threshold_yellow.unwrap_or(existing.confidence_threshold_yellow); + let max_total_cost_usd = req.max_total_cost_usd.or(existing.max_total_cost_usd); + let max_wall_time_minutes = req.max_wall_time_minutes.or(existing.max_wall_time_minutes); + let max_rework_cycles = req.max_rework_cycles.or(existing.max_rework_cycles); + let max_chain_regenerations = req.max_chain_regenerations.or(existing.max_chain_regenerations); + let repository_url = req.repository_url.or(existing.repository_url); + let local_path = req.local_path.or(existing.local_path); + let base_branch = req.base_branch.or(existing.base_branch); + + let result = if req.version.is_some() { + sqlx::query_as::<_, Directive>( + r#" + UPDATE directives + SET title = $3, goal = $4, + requirements = $5, acceptance_criteria = $6, constraints = $7, external_dependencies = $8, + status = $9, autonomy_level = $10, + confidence_threshold_green = $11, confidence_threshold_yellow = $12, + max_total_cost_usd = $13, max_wall_time_minutes = $14, + max_rework_cycles = $15, max_chain_regenerations = $16, + repository_url = $17, local_path = $18, base_branch = $19, + version = version + 1, updated_at = NOW() + WHERE id = $1 AND owner_id = $2 AND version = $20 + RETURNING * + "#, + ) + .bind(id) + .bind(owner_id) + .bind(&title) + .bind(&goal) + .bind(&requirements) + .bind(&acceptance_criteria) + .bind(&constraints) + .bind(&external_dependencies) + .bind(&status) + .bind(&autonomy_level) + .bind(confidence_threshold_green) + .bind(confidence_threshold_yellow) + .bind(max_total_cost_usd) + .bind(max_wall_time_minutes) + .bind(max_rework_cycles) + .bind(max_chain_regenerations) + .bind(&repository_url) + .bind(&local_path) + .bind(&base_branch) + .bind(req.version.unwrap()) + .fetch_optional(pool) + .await? + } else { + sqlx::query_as::<_, Directive>( + r#" + UPDATE directives + SET title = $3, goal = $4, + requirements = $5, acceptance_criteria = $6, constraints = $7, external_dependencies = $8, + status = $9, autonomy_level = $10, + confidence_threshold_green = $11, confidence_threshold_yellow = $12, + max_total_cost_usd = $13, max_wall_time_minutes = $14, + max_rework_cycles = $15, max_chain_regenerations = $16, + repository_url = $17, local_path = $18, base_branch = $19, + version = version + 1, updated_at = NOW() + WHERE id = $1 AND owner_id = $2 + RETURNING * + "#, + ) + .bind(id) + .bind(owner_id) + .bind(&title) + .bind(&goal) + .bind(&requirements) + .bind(&acceptance_criteria) + .bind(&constraints) + .bind(&external_dependencies) + .bind(&status) + .bind(&autonomy_level) + .bind(confidence_threshold_green) + .bind(confidence_threshold_yellow) + .bind(max_total_cost_usd) + .bind(max_wall_time_minutes) + .bind(max_rework_cycles) + .bind(max_chain_regenerations) + .bind(&repository_url) + .bind(&local_path) + .bind(&base_branch) + .fetch_optional(pool) + .await? + }; + + // If versioned update returned None, there was a race condition + if result.is_none() && req.version.is_some() { + if let Some(current) = get_directive_for_owner(pool, id, owner_id).await? { + return Err(RepositoryError::VersionConflict { + expected: req.version.unwrap(), + actual: current.version, + }); + } + } + + Ok(result) +} + +/// Delete a directive by ID, scoped to owner. +pub async fn delete_directive_for_owner( + pool: &PgPool, + id: Uuid, + owner_id: Uuid, +) -> Result<bool, sqlx::Error> { + let result = sqlx::query( + r#" + DELETE FROM directives + WHERE id = $1 AND owner_id = $2 + "#, + ) + .bind(id) + .bind(owner_id) + .execute(pool) + .await?; + + Ok(result.rows_affected() > 0) +} + +/// List chains for a directive (read-only). +pub async fn list_chains_for_directive( + pool: &PgPool, + directive_id: Uuid, +) -> Result<Vec<DirectiveChain>, sqlx::Error> { + sqlx::query_as::<_, DirectiveChain>( + r#" + SELECT * + FROM directive_chains + WHERE directive_id = $1 + ORDER BY generation DESC, created_at DESC + "#, + ) + .bind(directive_id) + .fetch_all(pool) + .await +} + +/// List steps for a chain (read-only). +pub async fn list_steps_for_chain( + pool: &PgPool, + chain_id: Uuid, +) -> Result<Vec<ChainStep>, sqlx::Error> { + sqlx::query_as::<_, ChainStep>( + r#" + SELECT * + FROM chain_steps + WHERE chain_id = $1 + ORDER BY order_index ASC, created_at ASC + "#, + ) + .bind(chain_id) + .fetch_all(pool) + .await +} |
