summaryrefslogtreecommitdiff
path: root/makima/src/server/handlers/directives.rs
diff options
context:
space:
mode:
Diffstat (limited to 'makima/src/server/handlers/directives.rs')
-rw-r--r--makima/src/server/handlers/directives.rs100
1 files changed, 98 insertions, 2 deletions
diff --git a/makima/src/server/handlers/directives.rs b/makima/src/server/handlers/directives.rs
index 44bf4ac..91f5892 100644
--- a/makima/src/server/handlers/directives.rs
+++ b/makima/src/server/handlers/directives.rs
@@ -11,12 +11,14 @@ use uuid::Uuid;
use crate::db::models::{
CleanupResponse, CreateDirectiveRequest, CreateTaskRequest,
CreateDirectiveStepRequest, Directive, DirectiveListResponse,
- DirectiveStep, DirectiveWithSteps, PickUpOrdersResponse,
+ DirectiveRevision, DirectiveStep, DirectiveWithSteps, PickUpOrdersResponse,
UpdateDirectiveRequest, UpdateDirectiveStepRequest, UpdateGoalRequest,
CreateDirectiveOrderGroupRequest, DirectiveOrderGroup,
DirectiveOrderGroupListResponse, UpdateDirectiveOrderGroupRequest,
OrderListResponse,
};
+use serde::Serialize;
+use utoipa::ToSchema;
use crate::db::repository;
use crate::orchestration::directive::{
build_cleanup_prompt, build_order_pickup_prompt, classify_goal_change,
@@ -185,8 +187,50 @@ pub async fn update_directive(
.into_response();
};
+ // Capture the BEFORE state so we can detect a pr_url transition (null
+ // → some-value), which is when the orchestrator's completion task has
+ // raised a PR for this directive. That transition is the trigger for
+ // freezing a directive_revisions snapshot.
+ let before_pr_url = match repository::get_directive_for_owner(pool, auth.owner_id, id).await {
+ Ok(Some(d)) => d.pr_url.clone(),
+ _ => None,
+ };
+
match repository::update_directive_for_owner(pool, auth.owner_id, id, req).await {
- Ok(Some(directive)) => Json(directive).into_response(),
+ Ok(Some(directive)) => {
+ // Detect "PR was just raised" — pr_url went from None to Some.
+ // Snapshot the current goal as a revision tied to this PR.
+ // Best-effort: a snapshot failure should not fail the update,
+ // because the directive's pr_url has already been written.
+ if before_pr_url.is_none() {
+ if let Some(ref new_pr_url) = directive.pr_url {
+ if let Err(e) = repository::create_directive_revision(
+ pool,
+ directive.id,
+ &directive.goal,
+ new_pr_url,
+ directive.pr_branch.as_deref(),
+ )
+ .await
+ {
+ tracing::warn!(
+ directive_id = %directive.id,
+ pr_url = %new_pr_url,
+ error = %e,
+ "Failed to snapshot directive revision on PR creation — \
+ continuing; revision history will be incomplete"
+ );
+ } else {
+ tracing::info!(
+ directive_id = %directive.id,
+ pr_url = %new_pr_url,
+ "Snapshotted directive revision on PR creation"
+ );
+ }
+ }
+ }
+ Json(directive).into_response()
+ }
Ok(None) => (
StatusCode::NOT_FOUND,
Json(ApiError::new("NOT_FOUND", "Directive not found")),
@@ -2038,3 +2082,55 @@ pub async fn pick_up_dog_orders(
})
.into_response()
}
+
+// =============================================================================
+// Directive Revisions (per-PR snapshots)
+// =============================================================================
+
+#[derive(Debug, Serialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct DirectiveRevisionListResponse {
+ pub revisions: Vec<DirectiveRevision>,
+ pub total: i64,
+}
+
+/// List all per-PR revisions for a directive, newest first.
+#[utoipa::path(
+ get,
+ path = "/api/v1/directives/{id}/revisions",
+ params(("id" = Uuid, Path, description = "Directive ID")),
+ responses(
+ (status = 200, description = "Revision history", body = DirectiveRevisionListResponse),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directives"
+)]
+pub async fn list_directive_revisions(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path(id): Path<Uuid>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ match repository::list_directive_revisions_for_owner(pool, auth.owner_id, id).await {
+ Ok(revisions) => {
+ let total = revisions.len() as i64;
+ Json(DirectiveRevisionListResponse { revisions, total }).into_response()
+ }
+ Err(e) => {
+ tracing::error!("Failed to list directive revisions: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("LIST_FAILED", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}