summaryrefslogtreecommitdiff
path: root/makima/src/db
diff options
context:
space:
mode:
Diffstat (limited to 'makima/src/db')
-rw-r--r--makima/src/db/models.rs40
-rw-r--r--makima/src/db/repository.rs200
2 files changed, 82 insertions, 158 deletions
diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs
index fcccd05..3fb9667 100644
--- a/makima/src/db/models.rs
+++ b/makima/src/db/models.rs
@@ -2704,14 +2704,16 @@ mod tests {
// Directive Types
// =============================================================================
-/// A directive — a long-lived top-level entity for managing projects via a DAG of steps.
+/// A directive — a long-lived top-level entity that owns a sequence of
+/// contracts (see `directive_documents`). The directive itself is a
+/// folder; the active contract's body is the spec the orchestrator
+/// daemon reads when planning.
#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct Directive {
pub id: Uuid,
pub owner_id: Uuid,
pub title: String,
- pub goal: String,
/// Status: draft, active, idle, paused, archived
pub status: String,
pub repository_url: Option<String>,
@@ -2723,7 +2725,6 @@ pub struct Directive {
pub completion_task_id: Option<Uuid>,
/// Question timeout mode: "auto" (30s timeout), "semi-auto" (block indefinitely), "manual" (block + ask many questions)
pub reconcile_mode: String,
- pub goal_updated_at: DateTime<Utc>,
pub started_at: Option<DateTime<Utc>>,
pub version: i32,
pub created_at: DateTime<Utc>,
@@ -2736,16 +2737,6 @@ pub struct Directive {
pub is_tmp: bool,
}
-/// A historical record of a directive goal change.
-#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)]
-#[serde(rename_all = "camelCase")]
-pub struct DirectiveGoalHistory {
- pub id: Uuid,
- pub directive_id: Uuid,
- pub goal: String,
- pub created_at: DateTime<Utc>,
-}
-
/// Per-PR snapshot of a directive's goal — the immutable record of what the
/// contract said at the moment a PR was raised. Frozen at PR-creation time;
/// `pr_state` mirrors the PR's GitHub lifecycle ('open' | 'merged' | 'closed').
@@ -2808,7 +2799,6 @@ pub struct DirectiveSummary {
pub id: Uuid,
pub owner_id: Uuid,
pub title: String,
- pub goal: String,
pub status: String,
pub repository_url: Option<String>,
pub orchestrator_task_id: Option<Uuid>,
@@ -2833,12 +2823,18 @@ pub struct DirectiveListResponse {
pub total: i64,
}
-/// Request to create a new directive.
+/// Request to create a new directive. The directive itself has no spec
+/// text — pass `contractBody` to auto-create a first contract whose
+/// body is the spec; if omitted, the directive is created empty and
+/// the user will create a contract from the UI.
#[derive(Debug, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct CreateDirectiveRequest {
pub title: String,
- pub goal: String,
+ /// Optional. When provided, a first contract is auto-created with
+ /// this body so the directive is immediately ready to start.
+ #[serde(default)]
+ pub contract_body: Option<String>,
pub repository_url: Option<String>,
pub local_path: Option<String>,
pub base_branch: Option<String>,
@@ -2846,12 +2842,13 @@ pub struct CreateDirectiveRequest {
pub reconcile_mode: Option<String>,
}
-/// Request to update a directive.
+/// Request to update a directive's metadata. Spec edits go through the
+/// contracts API now — this endpoint only mutates directive-level
+/// fields (title, repo, status, etc.).
#[derive(Debug, Default, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct UpdateDirectiveRequest {
pub title: Option<String>,
- pub goal: Option<String>,
pub status: Option<String>,
pub repository_url: Option<String>,
pub local_path: Option<String>,
@@ -2864,13 +2861,6 @@ pub struct UpdateDirectiveRequest {
pub version: Option<i32>,
}
-/// Request to update a directive's goal (triggers re-planning).
-#[derive(Debug, Deserialize, ToSchema)]
-#[serde(rename_all = "camelCase")]
-pub struct UpdateGoalRequest {
- pub goal: String,
-}
-
/// Response for cleanup_directive_tasks (legacy).
#[derive(Debug, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
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(&current.title);
- let goal = req.goal.as_deref().unwrap_or(&current.goal);
- let goal_changed = goal != current.goal;
let status = req.status.as_deref().unwrap_or(&current.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,