From 745b6f1b794e3d18f0ed42b1d261fc2bbcddb27e Mon Sep 17 00:00:00 2001 From: soryu Date: Thu, 5 Mar 2026 23:16:25 +0000 Subject: WIP: heartbeat checkpoint --- ...0260303000000_create_directive_order_groups.sql | 19 +++ makima/src/db/models.rs | 53 ++++++ makima/src/db/repository.rs | 187 ++++++++++++++++++++- makima/src/server/handlers/directives.rs | 3 + 4 files changed, 259 insertions(+), 3 deletions(-) create mode 100644 makima/migrations/20260303000000_create_directive_order_groups.sql diff --git a/makima/migrations/20260303000000_create_directive_order_groups.sql b/makima/migrations/20260303000000_create_directive_order_groups.sql new file mode 100644 index 0000000..8a382e5 --- /dev/null +++ b/makima/migrations/20260303000000_create_directive_order_groups.sql @@ -0,0 +1,19 @@ +-- Directive Order Groups (DOGs): Epic-like groupings of orders within a directive. +CREATE TABLE directive_order_groups ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + directive_id UUID NOT NULL REFERENCES directives(id) ON DELETE CASCADE, + owner_id UUID NOT NULL REFERENCES owners(id) ON DELETE CASCADE, + name VARCHAR(500) NOT NULL, + description TEXT, + status VARCHAR(32) NOT NULL DEFAULT 'open' + CHECK (status IN ('open', 'in_progress', 'done', 'archived')), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_dog_directive_id ON directive_order_groups(directive_id); +CREATE INDEX IF NOT EXISTS idx_dog_owner_id ON directive_order_groups(owner_id); + +-- Add optional dog_id to orders +ALTER TABLE orders ADD COLUMN dog_id UUID REFERENCES directive_order_groups(id) ON DELETE SET NULL; +CREATE INDEX IF NOT EXISTS idx_orders_dog_id ON orders(dog_id); diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs index 32e55f0..97657dc 100644 --- a/makima/src/db/models.rs +++ b/makima/src/db/models.rs @@ -2925,6 +2925,8 @@ pub struct Order { pub directive_name: Option, /// Repository context pub repository_url: Option, + /// Optional DOG (Directive Order Group) this order belongs to + pub dog_id: Option, pub created_at: DateTime, pub updated_at: DateTime, } @@ -2943,6 +2945,8 @@ pub struct CreateOrderRequest { /// Directive ID is required for new orders. pub directive_id: Uuid, pub repository_url: Option, + /// Optional DOG (Directive Order Group) to assign this order to. + pub dog_id: Option, } /// Default empty JSON array for labels. @@ -2963,6 +2967,8 @@ pub struct UpdateOrderRequest { pub directive_id: Option, pub directive_step_id: Option, pub repository_url: Option, + /// Optional DOG (Directive Order Group) to assign/reassign this order to. + pub dog_id: Option, } /// Response for order list endpoint. @@ -2986,6 +2992,8 @@ pub struct OrderListQuery { pub priority: Option, /// Filter by linked directive ID pub directive_id: Option, + /// Filter by DOG (Directive Order Group) ID + pub dog_id: Option, /// Text search across title, description, and directive_name (case-insensitive) pub search: Option, } @@ -2997,4 +3005,49 @@ pub struct LinkDirectiveRequest { pub directive_id: Uuid, } +// ============================================================================= +// Directive Order Group (DOG) Types +// ============================================================================= + +/// A Directive Order Group (DOG) — an epic-like grouping of orders within a directive. +/// DOGs allow organizing related orders under a common theme or goal. +#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct DirectiveOrderGroup { + pub id: Uuid, + pub directive_id: Uuid, + pub owner_id: Uuid, + pub name: String, + pub description: Option, + /// Status: open, in_progress, done, archived + pub status: String, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +/// Request to create a new Directive Order Group (DOG). +#[derive(Debug, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct CreateDirectiveOrderGroupRequest { + pub name: String, + pub description: Option, +} + +/// Request to update a Directive Order Group (DOG). +#[derive(Debug, Default, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct UpdateDirectiveOrderGroupRequest { + pub name: Option, + pub description: Option, + pub status: Option, +} + +/// Response for DOG list endpoint. +#[derive(Debug, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct DirectiveOrderGroupListResponse { + pub dogs: Vec, + pub total: i64, +} + diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs index f14bc66..57e8a78 100644 --- a/makima/src/db/repository.rs +++ b/makima/src/db/repository.rs @@ -15,6 +15,7 @@ use super::models::{ CreateDirectiveRequest, CreateDirectiveStepRequest, DirectiveGoalHistory, UpdateDirectiveRequest, UpdateDirectiveStepRequest, CreateOrderRequest, Order, UpdateOrderRequest, + CreateDirectiveOrderGroupRequest, DirectiveOrderGroup, UpdateDirectiveOrderGroupRequest, File, FileSummary, FileVersion, HistoryEvent, HistoryQueryFilters, MeshChatConversation, MeshChatMessageRecord, PhaseChangeResult, PhaseConfig, PhaseDefinition, SupervisorHeartbeatRecord, SupervisorState, @@ -6122,8 +6123,8 @@ pub async fn create_order( sqlx::query_as::<_, Order>( r#" - INSERT INTO orders (owner_id, title, description, priority, status, order_type, labels, directive_id, repository_url) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + INSERT INTO orders (owner_id, title, description, priority, status, order_type, labels, directive_id, repository_url, dog_id) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING * "#, ) @@ -6136,6 +6137,7 @@ pub async fn create_order( .bind(&req.labels) .bind(req.directive_id) .bind(&req.repository_url) + .bind(req.dog_id) .fetch_one(pool) .await } @@ -6148,6 +6150,7 @@ pub async fn list_orders( type_filter: Option<&str>, priority_filter: Option<&str>, directive_id_filter: Option, + dog_id_filter: Option, search_filter: Option<&str>, ) -> Result, sqlx::Error> { // Build dynamic query with optional filters @@ -6170,6 +6173,10 @@ pub async fn list_orders( query.push_str(&format!(" AND directive_id = ${}", param_idx)); param_idx += 1; } + if dog_id_filter.is_some() { + query.push_str(&format!(" AND dog_id = ${}", param_idx)); + param_idx += 1; + } if search_filter.is_some() { query.push_str(&format!( " AND (title ILIKE ${p} OR description ILIKE ${p} OR directive_name ILIKE ${p})", @@ -6193,6 +6200,9 @@ pub async fn list_orders( if let Some(d) = directive_id_filter { q = q.bind(d); } + if let Some(d) = dog_id_filter { + q = q.bind(d); + } if let Some(s) = search_filter { q = q.bind(format!("%{}%", s)); } @@ -6244,13 +6254,14 @@ pub async fn update_order( let directive_id = req.directive_id.or(current.directive_id); let directive_step_id = req.directive_step_id.or(current.directive_step_id); let repository_url = req.repository_url.as_deref().or(current.repository_url.as_deref()); + let dog_id = req.dog_id.or(current.dog_id); sqlx::query_as::<_, Order>( r#" UPDATE orders SET title = $3, description = $4, priority = $5, status = $6, order_type = $7, labels = $8, directive_id = $9, directive_step_id = $10, - repository_url = $11, updated_at = NOW() + repository_url = $11, dog_id = $12, updated_at = NOW() WHERE id = $1 AND owner_id = $2 RETURNING * "#, @@ -6266,6 +6277,7 @@ pub async fn update_order( .bind(directive_id) .bind(directive_step_id) .bind(repository_url) + .bind(dog_id) .fetch_optional(pool) .await } @@ -6517,3 +6529,172 @@ pub async fn reconcile_directive_orders( Ok(rows.len() as i64) } +// ============================================================================= +// Directive Order Group (DOG) CRUD +// ============================================================================= + +/// Create a new Directive Order Group (DOG) for the given owner and directive. +pub async fn create_directive_order_group( + pool: &PgPool, + directive_id: Uuid, + owner_id: Uuid, + req: CreateDirectiveOrderGroupRequest, +) -> Result { + sqlx::query_as::<_, DirectiveOrderGroup>( + r#" + INSERT INTO directive_order_groups (directive_id, owner_id, name, description) + VALUES ($1, $2, $3, $4) + RETURNING * + "#, + ) + .bind(directive_id) + .bind(owner_id) + .bind(&req.name) + .bind(&req.description) + .fetch_one(pool) + .await +} + +/// List all DOGs for a given directive (owner-scoped). +pub async fn list_directive_order_groups( + pool: &PgPool, + directive_id: Uuid, + owner_id: Uuid, +) -> Result, sqlx::Error> { + sqlx::query_as::<_, DirectiveOrderGroup>( + r#" + SELECT * FROM directive_order_groups + WHERE directive_id = $1 AND owner_id = $2 + ORDER BY created_at DESC + "#, + ) + .bind(directive_id) + .bind(owner_id) + .fetch_all(pool) + .await +} + +/// Get a single DOG by ID (owner-scoped). +pub async fn get_directive_order_group( + pool: &PgPool, + id: Uuid, + owner_id: Uuid, +) -> Result, sqlx::Error> { + sqlx::query_as::<_, DirectiveOrderGroup>( + r#"SELECT * FROM directive_order_groups WHERE id = $1 AND owner_id = $2"#, + ) + .bind(id) + .bind(owner_id) + .fetch_optional(pool) + .await +} + +/// Update a DOG (owner-scoped). Uses fetch-then-update pattern for partial updates. +pub async fn update_directive_order_group( + pool: &PgPool, + id: Uuid, + owner_id: Uuid, + req: UpdateDirectiveOrderGroupRequest, +) -> Result, sqlx::Error> { + let current = sqlx::query_as::<_, DirectiveOrderGroup>( + r#"SELECT * FROM directive_order_groups WHERE id = $1 AND owner_id = $2"#, + ) + .bind(id) + .bind(owner_id) + .fetch_optional(pool) + .await?; + + let current = match current { + Some(c) => c, + None => return Ok(None), + }; + + let name = req.name.as_deref().unwrap_or(¤t.name); + let description = req.description.as_deref().or(current.description.as_deref()); + let status = req.status.as_deref().unwrap_or(¤t.status); + + sqlx::query_as::<_, DirectiveOrderGroup>( + r#" + UPDATE directive_order_groups + SET name = $3, description = $4, status = $5, updated_at = NOW() + WHERE id = $1 AND owner_id = $2 + RETURNING * + "#, + ) + .bind(id) + .bind(owner_id) + .bind(name) + .bind(description) + .bind(status) + .fetch_optional(pool) + .await +} + +/// Delete a DOG (owner-scoped). Returns true if a row was deleted. +pub async fn delete_directive_order_group( + pool: &PgPool, + id: Uuid, + owner_id: Uuid, +) -> Result { + let result = sqlx::query( + r#"DELETE FROM directive_order_groups WHERE id = $1 AND owner_id = $2"#, + ) + .bind(id) + .bind(owner_id) + .execute(pool) + .await?; + + Ok(result.rows_affected() > 0) +} + +/// List orders belonging to a specific DOG (owner-scoped). +pub async fn list_orders_by_dog( + pool: &PgPool, + dog_id: Uuid, + owner_id: Uuid, +) -> Result, sqlx::Error> { + sqlx::query_as::<_, Order>( + r#" + SELECT * FROM orders + WHERE dog_id = $1 AND owner_id = $2 + ORDER BY created_at DESC + "#, + ) + .bind(dog_id) + .bind(owner_id) + .fetch_all(pool) + .await +} + +/// Get available orders for pickup filtered to a specific DOG. +/// Like `get_available_orders_for_pickup` but only returns orders belonging to the given DOG. +pub async fn get_available_orders_for_dog_pickup( + pool: &PgPool, + owner_id: Uuid, + directive_id: Uuid, + dog_id: Uuid, +) -> Result, sqlx::Error> { + sqlx::query_as::<_, Order>( + r#" + SELECT * + FROM orders + WHERE owner_id = $1 + AND dog_id = $3 + AND status IN ('open', 'in_progress') + AND (directive_id IS NULL OR directive_id = $2) + ORDER BY CASE priority + WHEN 'critical' THEN 0 + WHEN 'high' THEN 1 + WHEN 'medium' THEN 2 + WHEN 'low' THEN 3 + ELSE 4 + END ASC, created_at ASC + "#, + ) + .bind(owner_id) + .bind(directive_id) + .bind(dog_id) + .fetch_all(pool) + .await +} + diff --git a/makima/src/server/handlers/directives.rs b/makima/src/server/handlers/directives.rs index 992affe..e6ccfa3 100644 --- a/makima/src/server/handlers/directives.rs +++ b/makima/src/server/handlers/directives.rs @@ -14,6 +14,9 @@ use crate::db::models::{ DirectiveStep, DirectiveWithSteps, PickUpOrdersResponse, UpdateDirectiveRequest, UpdateDirectiveStepRequest, UpdateGoalRequest, UpdateOrderRequest, + CreateDirectiveOrderGroupRequest, DirectiveOrderGroup, + DirectiveOrderGroupListResponse, UpdateDirectiveOrderGroupRequest, + OrderListResponse, }; use crate::db::repository; use crate::orchestration::directive::{build_cleanup_prompt, build_order_pickup_prompt}; -- cgit v1.2.3