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.rs344
1 files changed, 313 insertions, 31 deletions
diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs
index b947cdd..1ab4165 100644
--- a/makima/src/db/repository.rs
+++ b/makima/src/db/repository.rs
@@ -8,11 +8,12 @@ use uuid::Uuid;
use super::models::{
CheckpointPatch, CheckpointPatchInfo, Contract, ContractChatConversation,
ContractChatMessageRecord, ContractEvent, ContractRepository, ContractSummary,
- ConversationMessage, ConversationSnapshot, CreateContractRequest, CreateFileRequest,
- CreateTaskRequest, Daemon, DaemonTaskAssignment, DaemonWithCapacity, File, FileSummary,
- FileVersion, HistoryEvent, HistoryQueryFilters, MeshChatConversation, MeshChatMessageRecord,
+ ContractTypeTemplateRecord, ConversationMessage, ConversationSnapshot, CreateContractRequest,
+ CreateFileRequest, CreateTaskRequest, CreateTemplateRequest, Daemon, DaemonTaskAssignment,
+ DaemonWithCapacity, DeliverableDefinition, File, FileSummary, FileVersion, HistoryEvent,
+ HistoryQueryFilters, MeshChatConversation, MeshChatMessageRecord, PhaseConfig, PhaseDefinition,
RedTeamNotification, SupervisorState, Task, TaskCheckpoint, TaskEvent, TaskSummary,
- UpdateContractRequest, UpdateFileRequest, UpdateTaskRequest,
+ UpdateContractRequest, UpdateFileRequest, UpdateTaskRequest, UpdateTemplateRequest,
};
/// Repository error types.
@@ -2141,68 +2142,349 @@ pub async fn clear_contract_conversation(
}
// =============================================================================
+// Contract Type Template Functions (Owner-Scoped)
+// =============================================================================
+
+/// Create a new contract type template for a specific owner.
+pub async fn create_template_for_owner(
+ pool: &PgPool,
+ owner_id: Uuid,
+ req: CreateTemplateRequest,
+) -> Result<ContractTypeTemplateRecord, sqlx::Error> {
+ sqlx::query_as::<_, ContractTypeTemplateRecord>(
+ r#"
+ INSERT INTO contract_type_templates (owner_id, name, description, phases, default_phase, deliverables)
+ VALUES ($1, $2, $3, $4, $5, $6)
+ RETURNING *
+ "#,
+ )
+ .bind(owner_id)
+ .bind(&req.name)
+ .bind(&req.description)
+ .bind(serde_json::to_value(&req.phases).unwrap_or_default())
+ .bind(&req.default_phase)
+ .bind(req.deliverables.as_ref().map(|d| serde_json::to_value(d).unwrap_or_default()))
+ .fetch_one(pool)
+ .await
+}
+
+/// Get a contract type template by ID, scoped to owner.
+pub async fn get_template_for_owner(
+ pool: &PgPool,
+ id: Uuid,
+ owner_id: Uuid,
+) -> Result<Option<ContractTypeTemplateRecord>, sqlx::Error> {
+ sqlx::query_as::<_, ContractTypeTemplateRecord>(
+ r#"
+ SELECT *
+ FROM contract_type_templates
+ WHERE id = $1 AND owner_id = $2
+ "#,
+ )
+ .bind(id)
+ .bind(owner_id)
+ .fetch_optional(pool)
+ .await
+}
+
+/// Get a contract type template by ID (internal use, no owner scoping).
+pub async fn get_template_by_id(
+ pool: &PgPool,
+ id: Uuid,
+) -> Result<Option<ContractTypeTemplateRecord>, sqlx::Error> {
+ sqlx::query_as::<_, ContractTypeTemplateRecord>(
+ r#"
+ SELECT *
+ FROM contract_type_templates
+ WHERE id = $1
+ "#,
+ )
+ .bind(id)
+ .fetch_optional(pool)
+ .await
+}
+
+/// List all contract type templates for an owner, ordered by name.
+pub async fn list_templates_for_owner(
+ pool: &PgPool,
+ owner_id: Uuid,
+) -> Result<Vec<ContractTypeTemplateRecord>, sqlx::Error> {
+ sqlx::query_as::<_, ContractTypeTemplateRecord>(
+ r#"
+ SELECT *
+ FROM contract_type_templates
+ WHERE owner_id = $1
+ ORDER BY name ASC
+ "#,
+ )
+ .bind(owner_id)
+ .fetch_all(pool)
+ .await
+}
+
+/// Update a contract type template for an owner.
+pub async fn update_template_for_owner(
+ pool: &PgPool,
+ id: Uuid,
+ owner_id: Uuid,
+ req: UpdateTemplateRequest,
+) -> Result<Option<ContractTypeTemplateRecord>, RepositoryError> {
+ // Build dynamic update query
+ let mut query = String::from("UPDATE contract_type_templates SET updated_at = NOW()");
+ let mut param_idx = 3; // $1 = id, $2 = owner_id
+
+ if req.name.is_some() {
+ query.push_str(&format!(", name = ${}", param_idx));
+ param_idx += 1;
+ }
+ if req.description.is_some() {
+ query.push_str(&format!(", description = ${}", param_idx));
+ param_idx += 1;
+ }
+ if req.phases.is_some() {
+ query.push_str(&format!(", phases = ${}", param_idx));
+ param_idx += 1;
+ }
+ if req.default_phase.is_some() {
+ query.push_str(&format!(", default_phase = ${}", param_idx));
+ param_idx += 1;
+ }
+ if req.deliverables.is_some() {
+ query.push_str(&format!(", deliverables = ${}", param_idx));
+ param_idx += 1;
+ }
+
+ // Optimistic locking
+ if req.version.is_some() {
+ query.push_str(&format!(", version = version + 1 WHERE id = $1 AND owner_id = $2 AND version = ${}", param_idx));
+ } else {
+ query.push_str(", version = version + 1 WHERE id = $1 AND owner_id = $2");
+ }
+ query.push_str(" RETURNING *");
+
+ let mut sql_query = sqlx::query_as::<_, ContractTypeTemplateRecord>(&query);
+ sql_query = sql_query.bind(id).bind(owner_id);
+
+ if let Some(ref name) = req.name {
+ sql_query = sql_query.bind(name);
+ }
+ if let Some(ref description) = req.description {
+ sql_query = sql_query.bind(description);
+ }
+ if let Some(ref phases) = req.phases {
+ sql_query = sql_query.bind(serde_json::to_value(phases).unwrap_or_default());
+ }
+ if let Some(ref default_phase) = req.default_phase {
+ sql_query = sql_query.bind(default_phase);
+ }
+ if let Some(ref deliverables) = req.deliverables {
+ sql_query = sql_query.bind(serde_json::to_value(deliverables).unwrap_or_default());
+ }
+ if let Some(version) = req.version {
+ sql_query = sql_query.bind(version);
+ }
+
+ match sql_query.fetch_optional(pool).await {
+ Ok(result) => {
+ if result.is_none() && req.version.is_some() {
+ // Check if it's a version conflict
+ if let Some(current) = get_template_for_owner(pool, id, owner_id).await? {
+ return Err(RepositoryError::VersionConflict {
+ expected: req.version.unwrap(),
+ actual: current.version,
+ });
+ }
+ }
+ Ok(result)
+ }
+ Err(e) => Err(RepositoryError::Database(e)),
+ }
+}
+
+/// Delete a contract type template for an owner.
+pub async fn delete_template_for_owner(
+ pool: &PgPool,
+ id: Uuid,
+ owner_id: Uuid,
+) -> Result<bool, sqlx::Error> {
+ let result = sqlx::query(
+ r#"
+ DELETE FROM contract_type_templates
+ WHERE id = $1 AND owner_id = $2
+ "#,
+ )
+ .bind(id)
+ .bind(owner_id)
+ .execute(pool)
+ .await?;
+
+ Ok(result.rows_affected() > 0)
+}
+
+/// Helper function to build PhaseConfig from a template.
+pub fn build_phase_config_from_template(template: &ContractTypeTemplateRecord) -> PhaseConfig {
+ PhaseConfig {
+ phases: template.phases.clone(),
+ default_phase: template.default_phase.clone(),
+ deliverables: template.deliverables.clone().unwrap_or_default(),
+ }
+}
+
+/// Helper function to build PhaseConfig for built-in contract types.
+pub fn build_phase_config_for_builtin(contract_type: &str) -> PhaseConfig {
+ match contract_type {
+ "simple" => PhaseConfig {
+ phases: vec![
+ PhaseDefinition { id: "plan".to_string(), name: "Plan".to_string(), order: 0 },
+ PhaseDefinition { id: "execute".to_string(), name: "Execute".to_string(), order: 1 },
+ ],
+ default_phase: "plan".to_string(),
+ deliverables: [
+ ("plan".to_string(), vec![DeliverableDefinition {
+ id: "plan-document".to_string(),
+ name: "Plan".to_string(),
+ priority: "required".to_string(),
+ }]),
+ ("execute".to_string(), vec![DeliverableDefinition {
+ id: "pull-request".to_string(),
+ name: "Pull Request".to_string(),
+ priority: "required".to_string(),
+ }]),
+ ].into_iter().collect(),
+ },
+ "specification" => PhaseConfig {
+ phases: vec![
+ PhaseDefinition { id: "research".to_string(), name: "Research".to_string(), order: 0 },
+ PhaseDefinition { id: "specify".to_string(), name: "Specify".to_string(), order: 1 },
+ PhaseDefinition { id: "plan".to_string(), name: "Plan".to_string(), order: 2 },
+ PhaseDefinition { id: "execute".to_string(), name: "Execute".to_string(), order: 3 },
+ PhaseDefinition { id: "review".to_string(), name: "Review".to_string(), order: 4 },
+ ],
+ default_phase: "research".to_string(),
+ deliverables: [
+ ("research".to_string(), vec![DeliverableDefinition {
+ id: "research-notes".to_string(),
+ name: "Research Notes".to_string(),
+ priority: "required".to_string(),
+ }]),
+ ("specify".to_string(), vec![DeliverableDefinition {
+ id: "requirements-document".to_string(),
+ name: "Requirements Document".to_string(),
+ priority: "required".to_string(),
+ }]),
+ ("plan".to_string(), vec![DeliverableDefinition {
+ id: "plan-document".to_string(),
+ name: "Plan".to_string(),
+ priority: "required".to_string(),
+ }]),
+ ("execute".to_string(), vec![DeliverableDefinition {
+ id: "pull-request".to_string(),
+ name: "Pull Request".to_string(),
+ priority: "required".to_string(),
+ }]),
+ ("review".to_string(), vec![DeliverableDefinition {
+ id: "release-notes".to_string(),
+ name: "Release Notes".to_string(),
+ priority: "required".to_string(),
+ }]),
+ ].into_iter().collect(),
+ },
+ "execute" | _ => PhaseConfig {
+ phases: vec![
+ PhaseDefinition { id: "execute".to_string(), name: "Execute".to_string(), order: 0 },
+ ],
+ default_phase: "execute".to_string(),
+ deliverables: std::collections::HashMap::new(),
+ },
+ }
+}
+
+// =============================================================================
// Contract Functions (Owner-Scoped)
// =============================================================================
/// Create a new contract for a specific owner.
+/// Supports both built-in contract types (simple, specification, execute) and custom templates.
pub async fn create_contract_for_owner(
pool: &PgPool,
owner_id: Uuid,
req: CreateContractRequest,
) -> Result<Contract, sqlx::Error> {
- // Default contract type is "simple"
- let contract_type = req.contract_type.as_deref().unwrap_or("simple");
+ // Determine phase configuration based on template_id or contract_type
+ let (phase_config, contract_type_str, default_phase): (PhaseConfig, String, String) =
+ if let Some(template_id) = req.template_id {
+ // Look up the custom template
+ let template = get_template_by_id(pool, template_id)
+ .await?
+ .ok_or_else(|| {
+ sqlx::Error::Protocol(format!("Template not found: {}", template_id))
+ })?;
+
+ let config = build_phase_config_from_template(&template);
+ let default = config.default_phase.clone();
+ // For custom templates, store the template name as the contract_type
+ (config, template.name.clone(), default)
+ } else {
+ // Use built-in contract type
+ let contract_type = req.contract_type.as_deref().unwrap_or("simple");
- // Validate contract type
- let valid_types = ["simple", "specification", "execute"];
- if !valid_types.contains(&contract_type) {
- return Err(sqlx::Error::Protocol(format!(
- "Invalid contract_type '{}'. Must be one of: {}",
- contract_type,
- valid_types.join(", ")
- )));
- }
+ // Validate contract type
+ let valid_types = ["simple", "specification", "execute"];
+ if !valid_types.contains(&contract_type) {
+ return Err(sqlx::Error::Protocol(format!(
+ "Invalid contract_type '{}'. Must be one of: {} or provide a template_id",
+ contract_type,
+ valid_types.join(", ")
+ )));
+ }
- // Determine valid phases based on contract type
- let (valid_phases, default_phase): (&[&str], &str) = match contract_type {
- "simple" => (&["plan", "execute"], "plan"),
- "specification" => (&["research", "specify", "plan", "execute", "review"], "research"),
- "execute" => (&["execute"], "execute"),
- _ => (&["plan", "execute"], "plan"),
- };
+ let config = build_phase_config_for_builtin(contract_type);
+ let default = config.default_phase.clone();
+ (config, contract_type.to_string(), default)
+ };
- // Use provided initial_phase or default based on contract type
- let phase = req.initial_phase.as_deref().unwrap_or(default_phase);
+ // Get valid phase IDs from the configuration
+ let valid_phase_ids: Vec<String> = phase_config.phases.iter().map(|p| p.id.clone()).collect();
- // Validate the phase is valid for this contract type
- if !valid_phases.contains(&phase) {
+ // Use provided initial_phase or default based on contract type/template
+ let phase = req.initial_phase.as_deref().unwrap_or(&default_phase);
+
+ // Validate the phase is valid for this contract type/template
+ if !valid_phase_ids.contains(&phase.to_string()) {
return Err(sqlx::Error::Protocol(format!(
"Invalid initial_phase '{}' for contract type '{}'. Must be one of: {}",
phase,
- contract_type,
- valid_phases.join(", ")
+ contract_type_str,
+ valid_phase_ids.join(", ")
)));
}
let autonomous_loop = req.autonomous_loop.unwrap_or(false);
let phase_guard = req.phase_guard.unwrap_or(false);
let local_only = req.local_only.unwrap_or(false);
+ let red_team_enabled = req.red_team_enabled.unwrap_or(false);
+
+ // Serialize phase_config to JSON
+ let phase_config_json = serde_json::to_value(&phase_config).ok();
sqlx::query_as::<_, Contract>(
r#"
- INSERT INTO contracts (owner_id, name, description, contract_type, phase, autonomous_loop, phase_guard, local_only)
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
+ INSERT INTO contracts (owner_id, name, description, contract_type, phase, autonomous_loop, phase_guard, local_only, red_team_enabled, red_team_prompt, phase_config)
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
RETURNING *
"#,
)
.bind(owner_id)
.bind(&req.name)
.bind(&req.description)
- .bind(contract_type)
+ .bind(&contract_type_str)
.bind(phase)
.bind(autonomous_loop)
.bind(phase_guard)
.bind(local_only)
+ .bind(red_team_enabled)
+ .bind(&req.red_team_prompt)
+ .bind(phase_config_json)
.fetch_one(pool)
.await
}