summaryrefslogtreecommitdiff
path: root/makima/src/db
diff options
context:
space:
mode:
Diffstat (limited to 'makima/src/db')
-rw-r--r--makima/src/db/models.rs234
-rw-r--r--makima/src/db/repository.rs283
2 files changed, 512 insertions, 5 deletions
diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs
index 3b10cb5..ec4ee15 100644
--- a/makima/src/db/models.rs
+++ b/makima/src/db/models.rs
@@ -2690,3 +2690,237 @@ mod tests {
assert_eq!(deserialized.progress, 50);
}
}
+
+// =============================================================================
+// Directive Types
+// =============================================================================
+
+/// Default autonomy level for directives
+fn default_autonomy_level() -> String {
+ "guardrails".to_string()
+}
+
+/// Default empty JSON array
+fn default_json_array() -> serde_json::Value {
+ serde_json::json!([])
+}
+
+/// Default empty JSON object
+fn default_json_object() -> serde_json::Value {
+ serde_json::json!({})
+}
+
+/// Default confidence threshold (green)
+fn default_confidence_green() -> f64 {
+ 0.85
+}
+
+/// Default confidence threshold (yellow)
+fn default_confidence_yellow() -> f64 {
+ 0.60
+}
+
+/// Default max rework cycles
+fn default_max_rework_cycles() -> Option<i32> {
+ Some(3)
+}
+
+/// Default max chain regenerations
+fn default_max_chain_regenerations() -> Option<i32> {
+ Some(2)
+}
+
+/// Full directive row from the database.
+#[derive(Debug, Clone, FromRow, Serialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct Directive {
+ pub id: Uuid,
+ pub owner_id: Uuid,
+ pub title: String,
+ pub goal: String,
+ #[sqlx(json)]
+ pub requirements: serde_json::Value,
+ #[sqlx(json)]
+ pub acceptance_criteria: serde_json::Value,
+ #[sqlx(json)]
+ pub constraints: serde_json::Value,
+ #[sqlx(json)]
+ pub external_dependencies: serde_json::Value,
+ pub status: String,
+ pub autonomy_level: String,
+ pub confidence_threshold_green: f64,
+ pub confidence_threshold_yellow: f64,
+ pub max_total_cost_usd: Option<f64>,
+ pub max_wall_time_minutes: Option<i32>,
+ pub max_rework_cycles: Option<i32>,
+ pub max_chain_regenerations: Option<i32>,
+ pub repository_url: Option<String>,
+ pub local_path: Option<String>,
+ pub base_branch: Option<String>,
+ pub orchestrator_contract_id: Option<Uuid>,
+ pub current_chain_id: Option<Uuid>,
+ pub chain_generation_count: i32,
+ pub total_cost_usd: f64,
+ pub started_at: Option<DateTime<Utc>>,
+ pub completed_at: Option<DateTime<Utc>>,
+ pub version: i32,
+ pub created_at: DateTime<Utc>,
+ pub updated_at: DateTime<Utc>,
+}
+
+/// Summary of a directive for list views.
+#[derive(Debug, Clone, FromRow, Serialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct DirectiveSummary {
+ pub id: Uuid,
+ pub title: String,
+ pub goal: String,
+ pub status: String,
+ pub autonomy_level: String,
+ pub chain_count: i64,
+ pub step_count: i64,
+ pub total_cost_usd: f64,
+ pub version: i32,
+ pub created_at: DateTime<Utc>,
+ pub updated_at: DateTime<Utc>,
+}
+
+/// Response for directive list endpoint.
+#[derive(Debug, Serialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct DirectiveListResponse {
+ pub directives: Vec<DirectiveSummary>,
+ pub total: i64,
+}
+
+/// Request to create a new directive.
+#[derive(Debug, Clone, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct CreateDirectiveRequest {
+ pub title: String,
+ pub goal: String,
+ #[serde(default = "default_json_array")]
+ pub requirements: serde_json::Value,
+ #[serde(default = "default_json_array")]
+ pub acceptance_criteria: serde_json::Value,
+ #[serde(default = "default_json_array")]
+ pub constraints: serde_json::Value,
+ #[serde(default = "default_json_array")]
+ pub external_dependencies: serde_json::Value,
+ #[serde(default = "default_autonomy_level")]
+ pub autonomy_level: String,
+ #[serde(default = "default_confidence_green")]
+ pub confidence_threshold_green: f64,
+ #[serde(default = "default_confidence_yellow")]
+ pub confidence_threshold_yellow: f64,
+ pub max_total_cost_usd: Option<f64>,
+ pub max_wall_time_minutes: Option<i32>,
+ #[serde(default = "default_max_rework_cycles")]
+ pub max_rework_cycles: Option<i32>,
+ #[serde(default = "default_max_chain_regenerations")]
+ pub max_chain_regenerations: Option<i32>,
+ pub repository_url: Option<String>,
+ pub local_path: Option<String>,
+ pub base_branch: Option<String>,
+}
+
+/// Request to update an existing directive.
+#[derive(Debug, Clone, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct UpdateDirectiveRequest {
+ pub title: Option<String>,
+ pub goal: Option<String>,
+ pub requirements: Option<serde_json::Value>,
+ pub acceptance_criteria: Option<serde_json::Value>,
+ pub constraints: Option<serde_json::Value>,
+ pub external_dependencies: Option<serde_json::Value>,
+ pub status: Option<String>,
+ pub autonomy_level: Option<String>,
+ pub confidence_threshold_green: Option<f64>,
+ pub confidence_threshold_yellow: Option<f64>,
+ pub max_total_cost_usd: Option<f64>,
+ pub max_wall_time_minutes: Option<i32>,
+ pub max_rework_cycles: Option<i32>,
+ pub max_chain_regenerations: Option<i32>,
+ pub repository_url: Option<String>,
+ pub local_path: Option<String>,
+ pub base_branch: Option<String>,
+ /// Version for optimistic locking
+ pub version: Option<i32>,
+}
+
+/// Directive with its chains for detail view.
+#[derive(Debug, Serialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct DirectiveWithChains {
+ #[serde(flatten)]
+ pub directive: Directive,
+ pub chains: Vec<DirectiveChain>,
+}
+
+/// Full row from directive_chains table.
+#[derive(Debug, Clone, FromRow, Serialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct DirectiveChain {
+ pub id: Uuid,
+ pub directive_id: Uuid,
+ pub generation: i32,
+ pub name: String,
+ pub description: Option<String>,
+ pub rationale: Option<String>,
+ pub planning_model: Option<String>,
+ pub status: String,
+ pub total_steps: i32,
+ pub completed_steps: i32,
+ pub failed_steps: i32,
+ pub current_confidence: Option<f64>,
+ pub started_at: Option<DateTime<Utc>>,
+ pub completed_at: Option<DateTime<Utc>>,
+ pub version: i32,
+ pub created_at: DateTime<Utc>,
+ pub updated_at: DateTime<Utc>,
+}
+
+/// Full row from chain_steps table.
+#[derive(Debug, Clone, FromRow, Serialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct ChainStep {
+ pub id: Uuid,
+ pub chain_id: Uuid,
+ pub name: String,
+ pub description: Option<String>,
+ pub step_type: String,
+ pub contract_type: String,
+ pub initial_phase: Option<String>,
+ pub task_plan: Option<String>,
+ pub phases: Option<Vec<String>>,
+ pub depends_on: Option<Vec<Uuid>>,
+ pub parallel_group: Option<String>,
+ pub requirement_ids: Option<Vec<String>>,
+ pub acceptance_criteria_ids: Option<Vec<String>>,
+ #[sqlx(json)]
+ pub verifier_config: serde_json::Value,
+ pub status: String,
+ pub contract_id: Option<Uuid>,
+ pub supervisor_task_id: Option<Uuid>,
+ pub confidence_score: Option<f64>,
+ pub confidence_level: Option<String>,
+ pub evaluation_count: i32,
+ pub rework_count: i32,
+ pub last_evaluation_id: Option<Uuid>,
+ pub editor_x: Option<f64>,
+ pub editor_y: Option<f64>,
+ pub order_index: i32,
+ pub started_at: Option<DateTime<Utc>>,
+ pub completed_at: Option<DateTime<Utc>>,
+ pub created_at: DateTime<Utc>,
+}
+
+/// Chain with its steps for detail view.
+#[derive(Debug, Serialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct ChainWithSteps {
+ #[serde(flatten)]
+ pub chain: DirectiveChain,
+ pub steps: Vec<ChainStep>,
+}
diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs
index 863d927..5949079 100644
--- a/makima/src/db/repository.rs
+++ b/makima/src/db/repository.rs
@@ -6,16 +6,17 @@ use sqlx::PgPool;
use uuid::Uuid;
use super::models::{
- CheckpointPatch, CheckpointPatchInfo, Contract, ContractChatConversation,
+ ChainStep, CheckpointPatch, CheckpointPatchInfo, Contract, ContractChatConversation,
ContractChatMessageRecord, ContractEvent, ContractRepository, ContractSummary,
ContractTypeTemplateRecord, ConversationMessage, ConversationSnapshot,
- CreateContractRequest, CreateFileRequest, CreateTaskRequest, CreateTemplateRequest,
- Daemon, DaemonTaskAssignment, DaemonWithCapacity, DeliverableDefinition,
+ CreateContractRequest, CreateDirectiveRequest, CreateFileRequest, CreateTaskRequest,
+ CreateTemplateRequest, Daemon, DaemonTaskAssignment, DaemonWithCapacity,
+ DeliverableDefinition, Directive, DirectiveChain, DirectiveSummary,
File, FileSummary, FileVersion, HistoryEvent, HistoryQueryFilters,
MeshChatConversation, MeshChatMessageRecord, PhaseChangeResult, PhaseConfig,
PhaseDefinition, SupervisorHeartbeatRecord, SupervisorState, Task, TaskCheckpoint,
- TaskEvent, TaskSummary, UpdateContractRequest, UpdateFileRequest, UpdateTaskRequest,
- UpdateTemplateRequest,
+ TaskEvent, TaskSummary, UpdateContractRequest, UpdateDirectiveRequest,
+ UpdateFileRequest, UpdateTaskRequest, UpdateTemplateRequest,
};
/// Repository error types.
@@ -4910,3 +4911,275 @@ fn truncate_string(s: &str, max_len: usize) -> String {
format!("{}...", &s[..max_len - 3])
}
}
+
+// =============================================================================
+// Directive CRUD
+// =============================================================================
+
+/// Create a new directive, scoped to owner.
+pub async fn create_directive_for_owner(
+ pool: &PgPool,
+ owner_id: Uuid,
+ req: CreateDirectiveRequest,
+) -> Result<Directive, sqlx::Error> {
+ sqlx::query_as::<_, Directive>(
+ r#"
+ INSERT INTO directives (
+ owner_id, title, goal,
+ requirements, acceptance_criteria, constraints, external_dependencies,
+ autonomy_level, confidence_threshold_green, confidence_threshold_yellow,
+ max_total_cost_usd, max_wall_time_minutes, max_rework_cycles, max_chain_regenerations,
+ repository_url, local_path, base_branch
+ )
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
+ RETURNING *
+ "#,
+ )
+ .bind(owner_id)
+ .bind(&req.title)
+ .bind(&req.goal)
+ .bind(&req.requirements)
+ .bind(&req.acceptance_criteria)
+ .bind(&req.constraints)
+ .bind(&req.external_dependencies)
+ .bind(&req.autonomy_level)
+ .bind(req.confidence_threshold_green)
+ .bind(req.confidence_threshold_yellow)
+ .bind(req.max_total_cost_usd)
+ .bind(req.max_wall_time_minutes)
+ .bind(req.max_rework_cycles)
+ .bind(req.max_chain_regenerations)
+ .bind(&req.repository_url)
+ .bind(&req.local_path)
+ .bind(&req.base_branch)
+ .fetch_one(pool)
+ .await
+}
+
+/// Get a directive by ID, scoped to owner.
+pub async fn get_directive_for_owner(
+ pool: &PgPool,
+ id: Uuid,
+ owner_id: Uuid,
+) -> Result<Option<Directive>, sqlx::Error> {
+ sqlx::query_as::<_, Directive>(
+ r#"
+ SELECT *
+ FROM directives
+ WHERE id = $1 AND owner_id = $2
+ "#,
+ )
+ .bind(id)
+ .bind(owner_id)
+ .fetch_optional(pool)
+ .await
+}
+
+/// List all directives for an owner, ordered by created_at DESC.
+pub async fn list_directives_for_owner(
+ pool: &PgPool,
+ owner_id: Uuid,
+) -> Result<Vec<DirectiveSummary>, sqlx::Error> {
+ sqlx::query_as::<_, DirectiveSummary>(
+ r#"
+ SELECT
+ d.id, d.title, d.goal, d.status, d.autonomy_level,
+ (SELECT COUNT(*) FROM directive_chains WHERE directive_id = d.id) as chain_count,
+ (SELECT COUNT(*) FROM chain_steps cs JOIN directive_chains dc ON cs.chain_id = dc.id WHERE dc.directive_id = d.id) as step_count,
+ d.total_cost_usd, d.version, d.created_at, d.updated_at
+ FROM directives d
+ WHERE d.owner_id = $1
+ ORDER BY d.created_at DESC
+ "#,
+ )
+ .bind(owner_id)
+ .fetch_all(pool)
+ .await
+}
+
+/// Update a directive by ID with optimistic locking, scoped to owner.
+pub async fn update_directive_for_owner(
+ pool: &PgPool,
+ id: Uuid,
+ owner_id: Uuid,
+ req: UpdateDirectiveRequest,
+) -> Result<Option<Directive>, RepositoryError> {
+ let existing = get_directive_for_owner(pool, id, owner_id).await?;
+ let Some(existing) = existing else {
+ return Ok(None);
+ };
+
+ // Check version if provided (optimistic locking)
+ if let Some(expected_version) = req.version {
+ if existing.version != expected_version {
+ return Err(RepositoryError::VersionConflict {
+ expected: expected_version,
+ actual: existing.version,
+ });
+ }
+ }
+
+ // Apply updates
+ let title = req.title.unwrap_or(existing.title);
+ let goal = req.goal.unwrap_or(existing.goal);
+ let requirements = req.requirements.unwrap_or(existing.requirements);
+ let acceptance_criteria = req.acceptance_criteria.unwrap_or(existing.acceptance_criteria);
+ let constraints = req.constraints.unwrap_or(existing.constraints);
+ let external_dependencies = req.external_dependencies.unwrap_or(existing.external_dependencies);
+ let status = req.status.unwrap_or(existing.status);
+ let autonomy_level = req.autonomy_level.unwrap_or(existing.autonomy_level);
+ let confidence_threshold_green = req.confidence_threshold_green.unwrap_or(existing.confidence_threshold_green);
+ let confidence_threshold_yellow = req.confidence_threshold_yellow.unwrap_or(existing.confidence_threshold_yellow);
+ let max_total_cost_usd = req.max_total_cost_usd.or(existing.max_total_cost_usd);
+ let max_wall_time_minutes = req.max_wall_time_minutes.or(existing.max_wall_time_minutes);
+ let max_rework_cycles = req.max_rework_cycles.or(existing.max_rework_cycles);
+ let max_chain_regenerations = req.max_chain_regenerations.or(existing.max_chain_regenerations);
+ let repository_url = req.repository_url.or(existing.repository_url);
+ let local_path = req.local_path.or(existing.local_path);
+ let base_branch = req.base_branch.or(existing.base_branch);
+
+ let result = if req.version.is_some() {
+ sqlx::query_as::<_, Directive>(
+ r#"
+ UPDATE directives
+ SET title = $3, goal = $4,
+ requirements = $5, acceptance_criteria = $6, constraints = $7, external_dependencies = $8,
+ status = $9, autonomy_level = $10,
+ confidence_threshold_green = $11, confidence_threshold_yellow = $12,
+ max_total_cost_usd = $13, max_wall_time_minutes = $14,
+ max_rework_cycles = $15, max_chain_regenerations = $16,
+ repository_url = $17, local_path = $18, base_branch = $19,
+ version = version + 1, updated_at = NOW()
+ WHERE id = $1 AND owner_id = $2 AND version = $20
+ RETURNING *
+ "#,
+ )
+ .bind(id)
+ .bind(owner_id)
+ .bind(&title)
+ .bind(&goal)
+ .bind(&requirements)
+ .bind(&acceptance_criteria)
+ .bind(&constraints)
+ .bind(&external_dependencies)
+ .bind(&status)
+ .bind(&autonomy_level)
+ .bind(confidence_threshold_green)
+ .bind(confidence_threshold_yellow)
+ .bind(max_total_cost_usd)
+ .bind(max_wall_time_minutes)
+ .bind(max_rework_cycles)
+ .bind(max_chain_regenerations)
+ .bind(&repository_url)
+ .bind(&local_path)
+ .bind(&base_branch)
+ .bind(req.version.unwrap())
+ .fetch_optional(pool)
+ .await?
+ } else {
+ sqlx::query_as::<_, Directive>(
+ r#"
+ UPDATE directives
+ SET title = $3, goal = $4,
+ requirements = $5, acceptance_criteria = $6, constraints = $7, external_dependencies = $8,
+ status = $9, autonomy_level = $10,
+ confidence_threshold_green = $11, confidence_threshold_yellow = $12,
+ max_total_cost_usd = $13, max_wall_time_minutes = $14,
+ max_rework_cycles = $15, max_chain_regenerations = $16,
+ repository_url = $17, local_path = $18, base_branch = $19,
+ version = version + 1, updated_at = NOW()
+ WHERE id = $1 AND owner_id = $2
+ RETURNING *
+ "#,
+ )
+ .bind(id)
+ .bind(owner_id)
+ .bind(&title)
+ .bind(&goal)
+ .bind(&requirements)
+ .bind(&acceptance_criteria)
+ .bind(&constraints)
+ .bind(&external_dependencies)
+ .bind(&status)
+ .bind(&autonomy_level)
+ .bind(confidence_threshold_green)
+ .bind(confidence_threshold_yellow)
+ .bind(max_total_cost_usd)
+ .bind(max_wall_time_minutes)
+ .bind(max_rework_cycles)
+ .bind(max_chain_regenerations)
+ .bind(&repository_url)
+ .bind(&local_path)
+ .bind(&base_branch)
+ .fetch_optional(pool)
+ .await?
+ };
+
+ // If versioned update returned None, there was a race condition
+ if result.is_none() && req.version.is_some() {
+ if let Some(current) = get_directive_for_owner(pool, id, owner_id).await? {
+ return Err(RepositoryError::VersionConflict {
+ expected: req.version.unwrap(),
+ actual: current.version,
+ });
+ }
+ }
+
+ Ok(result)
+}
+
+/// Delete a directive by ID, scoped to owner.
+pub async fn delete_directive_for_owner(
+ pool: &PgPool,
+ id: Uuid,
+ owner_id: Uuid,
+) -> Result<bool, sqlx::Error> {
+ let result = sqlx::query(
+ r#"
+ DELETE FROM directives
+ WHERE id = $1 AND owner_id = $2
+ "#,
+ )
+ .bind(id)
+ .bind(owner_id)
+ .execute(pool)
+ .await?;
+
+ Ok(result.rows_affected() > 0)
+}
+
+/// List chains for a directive (read-only).
+pub async fn list_chains_for_directive(
+ pool: &PgPool,
+ directive_id: Uuid,
+) -> Result<Vec<DirectiveChain>, sqlx::Error> {
+ sqlx::query_as::<_, DirectiveChain>(
+ r#"
+ SELECT *
+ FROM directive_chains
+ WHERE directive_id = $1
+ ORDER BY generation DESC, created_at DESC
+ "#,
+ )
+ .bind(directive_id)
+ .fetch_all(pool)
+ .await
+}
+
+/// List steps for a chain (read-only).
+pub async fn list_steps_for_chain(
+ pool: &PgPool,
+ chain_id: Uuid,
+) -> Result<Vec<ChainStep>, sqlx::Error> {
+ sqlx::query_as::<_, ChainStep>(
+ r#"
+ SELECT *
+ FROM chain_steps
+ WHERE chain_id = $1
+ ORDER BY order_index ASC, created_at ASC
+ "#,
+ )
+ .bind(chain_id)
+ .fetch_all(pool)
+ .await
+}