summaryrefslogtreecommitdiff
path: root/makima/src/db/repository.rs
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-02-07 01:11:26 +0000
committersoryu <soryu@soryu.co>2026-02-07 01:11:26 +0000
commit9e9f18884c78c21f5785908fb7ccd00e2fa5436b (patch)
treef2ca7c2a3db5350186282ae0be0e539aa77c0320 /makima/src/db/repository.rs
parentb8d563d45f14a2b1db1f684aa0a8dcd7e5b6ad56 (diff)
downloadsoryu-9e9f18884c78c21f5785908fb7ccd00e2fa5436b.tar.gz
soryu-9e9f18884c78c21f5785908fb7ccd00e2fa5436b.zip
Add new directive initial implementation
Diffstat (limited to 'makima/src/db/repository.rs')
-rw-r--r--makima/src/db/repository.rs283
1 files changed, 278 insertions, 5 deletions
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
+}