diff options
Diffstat (limited to 'makima/src/db/repository.rs')
| -rw-r--r-- | makima/src/db/repository.rs | 200 |
1 files changed, 67 insertions, 133 deletions
diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs index e58f58c..20f3268 100644 --- a/makima/src/db/repository.rs +++ b/makima/src/db/repository.rs @@ -12,7 +12,7 @@ use super::models::{ CreateContractRequest, CreateFileRequest, CreateTaskRequest, CreateTemplateRequest, Daemon, DaemonTaskAssignment, DaemonWithCapacity, DeliverableDefinition, Directive, DirectiveDocument, DirectiveStep, DirectiveSummary, - CreateDirectiveRequest, CreateDirectiveStepRequest, DirectiveGoalHistory, + CreateDirectiveRequest, CreateDirectiveStepRequest, UpdateDirectiveRequest, UpdateDirectiveStepRequest, CreateOrderRequest, Order, UpdateOrderRequest, CreateDirectiveOrderGroupRequest, DirectiveOrderGroup, UpdateDirectiveOrderGroupRequest, @@ -5125,27 +5125,78 @@ fn truncate_string(s: &str, max_len: usize) -> String { // ============================================================================= /// Create a new directive for an owner. +/// +/// If `req.contract_body` is set, also auto-creates a first contract +/// with that body so the directive is immediately ready to start. Both +/// inserts run in the same transaction. pub async fn create_directive_for_owner( pool: &PgPool, owner_id: Uuid, req: CreateDirectiveRequest, ) -> Result<Directive, sqlx::Error> { - sqlx::query_as::<_, Directive>( + let mut tx = pool.begin().await?; + + let directive = sqlx::query_as::<_, Directive>( r#" - INSERT INTO directives (owner_id, title, goal, repository_url, local_path, base_branch, reconcile_mode) - VALUES ($1, $2, $3, $4, $5, $6, $7) + INSERT INTO directives (owner_id, title, repository_url, local_path, base_branch, reconcile_mode) + VALUES ($1, $2, $3, $4, $5, $6) RETURNING * "#, ) .bind(owner_id) .bind(&req.title) - .bind(&req.goal) .bind(&req.repository_url) .bind(&req.local_path) .bind(&req.base_branch) .bind(req.reconcile_mode.as_deref().unwrap_or("auto")) - .fetch_one(pool) - .await + .fetch_one(&mut *tx) + .await?; + + if let Some(body) = &req.contract_body { + sqlx::query( + r#" + INSERT INTO directive_documents (directive_id, title, body, status, position) + VALUES ($1, '', $2, 'draft', 0) + "#, + ) + .bind(directive.id) + .bind(body) + .execute(&mut *tx) + .await?; + } + + tx.commit().await?; + Ok(directive) +} + +/// Resolve the body of the directive's "current spec" — the active +/// contract's body, falling back to the most-recently-updated draft if +/// none is active. Returns empty string when the directive has no +/// usable contracts (orchestrator should refuse to spawn in that case). +pub async fn get_active_contract_body( + pool: &PgPool, + directive_id: Uuid, +) -> Result<String, sqlx::Error> { + let row: Option<(String,)> = sqlx::query_as( + r#" + SELECT body FROM directive_documents + WHERE directive_id = $1 + AND status IN ('active', 'queued', 'draft') + ORDER BY + CASE status + WHEN 'active' THEN 0 + WHEN 'queued' THEN 1 + WHEN 'draft' THEN 2 + ELSE 3 + END, + updated_at DESC + LIMIT 1 + "#, + ) + .bind(directive_id) + .fetch_optional(pool) + .await?; + Ok(row.map(|r| r.0).unwrap_or_default()) } /// Get a single directive for an owner. @@ -5212,7 +5263,7 @@ pub async fn list_directives_for_owner( sqlx::query_as::<_, DirectiveSummary>( r#" SELECT - d.id, d.owner_id, d.title, d.goal, d.status, d.repository_url, + d.id, d.owner_id, d.title, d.status, d.repository_url, d.orchestrator_task_id, d.pr_url, d.completion_task_id, d.reconcile_mode, d.version, d.created_at, d.updated_at, @@ -5271,8 +5322,6 @@ pub async fn update_directive_for_owner( } let title = req.title.as_deref().unwrap_or(¤t.title); - let goal = req.goal.as_deref().unwrap_or(¤t.goal); - let goal_changed = goal != current.goal; let status = req.status.as_deref().unwrap_or(¤t.status); let repository_url = req.repository_url.as_deref().or(current.repository_url.as_deref()); let local_path = req.local_path.as_deref().or(current.local_path.as_deref()); @@ -5285,10 +5334,9 @@ pub async fn update_directive_for_owner( let result = sqlx::query_as::<_, Directive>( r#" UPDATE directives - SET title = $3, goal = $4, status = $5, repository_url = $6, local_path = $7, - base_branch = $8, orchestrator_task_id = $9, pr_url = $10, pr_branch = $11, - reconcile_mode = $12, - goal_updated_at = CASE WHEN $13 THEN NOW() ELSE goal_updated_at END, + SET title = $3, status = $4, repository_url = $5, local_path = $6, + base_branch = $7, orchestrator_task_id = $8, pr_url = $9, pr_branch = $10, + reconcile_mode = $11, version = version + 1, updated_at = NOW() WHERE id = $1 AND owner_id = $2 RETURNING * @@ -5297,7 +5345,6 @@ pub async fn update_directive_for_owner( .bind(id) .bind(owner_id) .bind(title) - .bind(goal) .bind(status) .bind(repository_url) .bind(local_path) @@ -5306,7 +5353,6 @@ pub async fn update_directive_for_owner( .bind(pr_url) .bind(pr_branch) .bind(reconcile_mode) - .bind(goal_changed) .fetch_optional(pool) .await .map_err(RepositoryError::Database)?; @@ -6454,45 +6500,6 @@ pub async fn check_directive_idle( Ok(result.rows_affected() > 0) } -/// Update a directive's goal and bump goal_updated_at. -/// Reactivates draft/idle/paused/inactive directives and clears any stale -/// orchestrator task so that planning/replanning triggers on the next -/// reconciler tick. -/// -/// `draft` flips because the document-mode UI treats the first goal save as -/// the implicit "start". `inactive` flips because editing a contract whose -/// last revision was already shipped is the way the user kicks off an -/// amendment — the planner picks it up via phase_planning/replanning and -/// uses get_latest_merged_revision to learn the BEFORE→AFTER diff. -pub async fn update_directive_goal( - pool: &PgPool, - owner_id: Uuid, - directive_id: Uuid, - goal: &str, -) -> Result<Option<Directive>, sqlx::Error> { - sqlx::query_as::<_, Directive>( - r#" - UPDATE directives - SET goal = $3, - goal_updated_at = NOW(), - status = CASE - WHEN status IN ('draft', 'idle', 'paused', 'inactive') THEN 'active' - ELSE status - END, - orchestrator_task_id = NULL, - updated_at = NOW(), - version = version + 1 - WHERE id = $1 AND owner_id = $2 - RETURNING * - "#, - ) - .bind(directive_id) - .bind(owner_id) - .bind(goal) - .fetch_optional(pool) - .await -} - /// Mark a directive 'inactive'. Used at the moment a PR is raised — at that /// point the contract's current iteration is "shipped" and editing the goal /// (Stage 4) starts an amendment cycle. Idempotent: no-op if status is @@ -6517,11 +6524,10 @@ pub async fn set_directive_inactive( Ok(()) } -/// Reset a directive for a "new draft" cycle: clear the goal back to empty, -/// flip status to 'draft', and detach the current pr_url / pr_branch / -/// orchestrator linkage so the next goal save starts fresh. Prior revisions -/// remain in `directive_revisions` as the historical record. Used by the -/// sidebar's "New draft" right-click on inactive contracts. +/// Reset a directive for a "new draft" cycle: flip status to 'draft' and +/// detach the current pr_url / pr_branch / orchestrator linkage so the +/// next contract activation starts fresh. Prior revisions remain in +/// `directive_revisions` as the historical record. pub async fn reset_directive_for_new_draft( pool: &PgPool, owner_id: Uuid, @@ -6530,9 +6536,7 @@ pub async fn reset_directive_for_new_draft( sqlx::query_as::<_, Directive>( r#" UPDATE directives - SET goal = '', - goal_updated_at = NOW(), - status = 'draft', + SET status = 'draft', pr_url = NULL, pr_branch = NULL, orchestrator_task_id = NULL, @@ -6549,40 +6553,6 @@ pub async fn reset_directive_for_new_draft( .await } -/// Update a directive's goal WITHOUT clearing the orchestrator task id. -/// -/// This is the path used by the goal-edit interrupt cycle: when a small goal -/// edit arrives while a planner is already running, we want to keep the -/// planner attached so a `SendMessage` can summarise the change in-flight -/// instead of cancelling and respawning. We still bump `goal_updated_at` so -/// the timestamp reflects the edit, but we do NOT trigger replanning by -/// clearing the orchestrator task. We also do not flip status from -/// idle/paused → active here, since by definition a planner is already -/// running. -pub async fn update_directive_goal_keep_orchestrator( - pool: &PgPool, - owner_id: Uuid, - directive_id: Uuid, - goal: &str, -) -> Result<Option<Directive>, sqlx::Error> { - sqlx::query_as::<_, Directive>( - r#" - UPDATE directives - SET goal = $3, - goal_updated_at = NOW(), - updated_at = NOW(), - version = version + 1 - WHERE id = $1 AND owner_id = $2 - RETURNING * - "#, - ) - .bind(directive_id) - .bind(owner_id) - .bind(goal) - .fetch_optional(pool) - .await -} - // ============================================================================= // Directive Revisions — per-PR snapshots of the contract content. // ============================================================================= @@ -6701,42 +6671,6 @@ pub async fn get_latest_merged_revision( .await } -/// Save a goal to the directive goal history. -pub async fn save_directive_goal_history( - pool: &PgPool, - directive_id: Uuid, - goal: &str, -) -> Result<(), sqlx::Error> { - sqlx::query( - r#"INSERT INTO directive_goal_history (directive_id, goal) - VALUES ($1, $2)"#, - ) - .bind(directive_id) - .bind(goal) - .execute(pool) - .await?; - Ok(()) -} - -/// Get recent goal history for a directive (most recent first), limited to limit entries. -pub async fn get_directive_goal_history( - pool: &PgPool, - directive_id: Uuid, - limit: i64, -) -> Result<Vec<DirectiveGoalHistory>, sqlx::Error> { - sqlx::query_as::<_, DirectiveGoalHistory>( - r#"SELECT id, directive_id, goal, created_at - FROM directive_goal_history - WHERE directive_id = $1 - ORDER BY created_at DESC - LIMIT $2"#, - ) - .bind(directive_id) - .bind(limit) - .fetch_all(pool) - .await -} - /// Set a directive's status (used for start/pause/archive transitions). pub async fn set_directive_status( pool: &PgPool, |
