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.rs185
1 files changed, 185 insertions, 0 deletions
diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs
index 20f3268..ee4b561 100644
--- a/makima/src/db/repository.rs
+++ b/makima/src/db/repository.rs
@@ -6132,6 +6132,11 @@ pub async fn archive_directive_document(
/// or queue it. Returns the updated row, or `Ok(None)` if the contract
/// doesn't exist. Errors with `RepositoryError::Validation` if the
/// contract is in any state other than `draft`.
+///
+/// Side effect: if the contract enters `active`, the parent directive
+/// is flipped to `active` (from `draft|paused|idle|inactive`). This is
+/// what makes the orchestrator reconciler pick the directive up — its
+/// gate is `directive.status = 'active' AND orchestrator_task_id IS NULL`.
pub async fn start_contract(
pool: &PgPool,
contract_id: Uuid,
@@ -6184,6 +6189,13 @@ pub async fn start_contract(
.fetch_optional(&mut *tx)
.await?;
+ // Flip the parent directive to active so the reconciler picks it up.
+ // Only when this contract is actually entering the active slot — a
+ // queued contract doesn't drive planning by itself.
+ if new_status == "active" {
+ activate_parent_directive(&mut tx, current.directive_id).await?;
+ }
+
tx.commit().await?;
Ok(updated)
}
@@ -6234,6 +6246,16 @@ pub async fn pause_contract(
// position, excluding the one we just paused).
promote_next_queued_contract(&mut tx, current.directive_id).await?;
+ // If no contract is active after the pause+promote, pause the
+ // directive too — stops the reconciler from spawning new planners
+ // on what is now an idle directive.
+ deactivate_parent_directive_if_no_active(
+ &mut tx,
+ current.directive_id,
+ "paused",
+ )
+ .await?;
+
tx.commit().await?;
Ok(updated)
}
@@ -6289,6 +6311,17 @@ pub async fn complete_contract(
promote_next_queued_contract(&mut tx, current.directive_id).await?;
+ // If the ship freed the active slot AND no queued contract was
+ // available to promote, the directive itself goes inactive — its
+ // iteration is shipped; the next cycle starts via reopen or a new
+ // contract.
+ deactivate_parent_directive_if_no_active(
+ &mut tx,
+ current.directive_id,
+ "inactive",
+ )
+ .await?;
+
tx.commit().await?;
Ok(updated)
}
@@ -6339,12 +6372,164 @@ pub async fn unlock_contract(
if was_active {
promote_next_queued_contract(&mut tx, current.directive_id).await?;
+ // If unlocking the active contract leaves no other active under
+ // the directive, pause the directive too.
+ deactivate_parent_directive_if_no_active(
+ &mut tx,
+ current.directive_id,
+ "paused",
+ )
+ .await?;
}
tx.commit().await?;
Ok(updated)
}
+/// Reopen a shipped contract for amendment. Flips the contract back to
+/// `active`, re-activates the parent directive, and clears the
+/// directive's PR linkage + orchestrator task so the reconciler spawns a
+/// fresh planner. The planner uses `get_latest_merged_revision` to
+/// detect the previously-shipped PR and frame the new plan as a delta.
+pub async fn reopen_contract(
+ pool: &PgPool,
+ contract_id: Uuid,
+) -> Result<Option<DirectiveDocument>, RepositoryError> {
+ let mut tx = pool.begin().await?;
+
+ let current = sqlx::query_as::<_, DirectiveDocument>(
+ r#"SELECT * FROM directive_documents WHERE id = $1"#,
+ )
+ .bind(contract_id)
+ .fetch_optional(&mut *tx)
+ .await?;
+
+ let current = match current {
+ Some(c) => c,
+ None => return Ok(None),
+ };
+
+ if current.status != "shipped" {
+ return Err(RepositoryError::Validation(format!(
+ "contract is in status '{}'; only 'shipped' contracts can be reopened",
+ current.status
+ )));
+ }
+
+ let updated = sqlx::query_as::<_, DirectiveDocument>(
+ r#"
+ UPDATE directive_documents
+ SET status = 'active',
+ version = version + 1,
+ updated_at = NOW()
+ WHERE id = $1
+ RETURNING *
+ "#,
+ )
+ .bind(contract_id)
+ .fetch_optional(&mut *tx)
+ .await?;
+
+ // Re-activate the directive and clear the prior PR + orchestrator
+ // linkage. Status is forced to `active` regardless of prior value
+ // (except archived — guard against re-opening under an archived
+ // directive).
+ sqlx::query(
+ r#"
+ UPDATE directives
+ SET status = 'active',
+ orchestrator_task_id = NULL,
+ pr_url = NULL,
+ pr_branch = NULL,
+ updated_at = NOW(),
+ version = version + 1
+ WHERE id = $1 AND status <> 'archived'
+ "#,
+ )
+ .bind(current.directive_id)
+ .execute(&mut *tx)
+ .await?;
+
+ tx.commit().await?;
+ Ok(updated)
+}
+
+/// Resolve the directive's currently-active contract id. Returns
+/// `Ok(None)` when no active contract exists. Used by the
+/// auto-complete-on-PR path so the contract row can be shipped at the
+/// same moment the directive registers its PR url.
+pub async fn get_active_contract_id_for_directive(
+ pool: &PgPool,
+ directive_id: Uuid,
+) -> Result<Option<Uuid>, sqlx::Error> {
+ let row: Option<(Uuid,)> = sqlx::query_as(
+ r#"
+ SELECT id FROM directive_documents
+ WHERE directive_id = $1 AND status = 'active'
+ ORDER BY position ASC, created_at ASC
+ LIMIT 1
+ "#,
+ )
+ .bind(directive_id)
+ .fetch_optional(pool)
+ .await?;
+ Ok(row.map(|r| r.0))
+}
+
+/// Flip the parent directive to `active` when a child contract just
+/// became active. Only promotes from `draft|paused|idle|inactive` —
+/// leaves `archived` directives untouched.
+async fn activate_parent_directive(
+ tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
+ directive_id: Uuid,
+) -> Result<(), sqlx::Error> {
+ sqlx::query(
+ r#"
+ UPDATE directives
+ SET status = 'active',
+ updated_at = NOW(),
+ version = version + 1
+ WHERE id = $1
+ AND status IN ('draft', 'paused', 'idle', 'inactive')
+ "#,
+ )
+ .bind(directive_id)
+ .execute(&mut **tx)
+ .await?;
+ Ok(())
+}
+
+/// After a contract lifecycle change that may have left no active
+/// contract under the directive, transition the directive to the
+/// supplied `new_status` (typically `'paused'` for unlock/pause flows,
+/// `'inactive'` for ship). No-op if the directive still has an active
+/// contract or is already past the destination state.
+async fn deactivate_parent_directive_if_no_active(
+ tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
+ directive_id: Uuid,
+ new_status: &str,
+) -> Result<(), sqlx::Error> {
+ sqlx::query(
+ r#"
+ UPDATE directives
+ SET status = $2,
+ updated_at = NOW(),
+ version = version + 1
+ WHERE id = $1
+ AND status = 'active'
+ AND NOT EXISTS (
+ SELECT 1 FROM directive_documents
+ WHERE directive_id = $1 AND status = 'active'
+ )
+ "#,
+ )
+ .bind(directive_id)
+ .bind(new_status)
+ .execute(&mut **tx)
+ .await?;
+ Ok(())
+}
+
/// Find the lowest-position `queued` contract under a directive and
/// flip it to `active`. No-op when no queued contract exists.
///