diff options
| author | soryu <soryu@soryu.co> | 2026-02-03 23:49:08 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-02-03 23:49:19 +0000 |
| commit | c732dd048128808cd9f67f6e1176a5b565df5678 (patch) | |
| tree | 6ebf359c9c3f2d8aca264c53da6367b7f0af5fc8 /makima/src/db | |
| parent | 9ebc9724afcc0482a8e7cd2369c06208fedbcbd1 (diff) | |
| download | soryu-c732dd048128808cd9f67f6e1176a5b565df5678.tar.gz soryu-c732dd048128808cd9f67f6e1176a5b565df5678.zip | |
Allow chain creation via web interface
Diffstat (limited to 'makima/src/db')
| -rw-r--r-- | makima/src/db/models.rs | 107 | ||||
| -rw-r--r-- | makima/src/db/repository.rs | 299 |
2 files changed, 395 insertions, 11 deletions
diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs index 45ddb52..eeb30e4 100644 --- a/makima/src/db/models.rs +++ b/makima/src/db/models.rs @@ -2767,7 +2767,7 @@ pub struct ChainGraphNode { } /// Edge in chain DAG graph -#[derive(Debug, Serialize, ToSchema)] +#[derive(Debug, Clone, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct ChainGraphEdge { pub from: Uuid, @@ -2942,6 +2942,111 @@ pub struct ChainEditorEdge { } // ============================================================================= +// Chain Contract Definitions (stored specs for on-demand contract creation) +// ============================================================================= + +/// Contract definition within a chain - stored spec before actual contract is created +#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct ChainContractDefinition { + pub id: Uuid, + pub chain_id: Uuid, + pub name: String, + pub description: Option<String>, + pub contract_type: String, + pub initial_phase: Option<String>, + /// Names of other definitions this depends on + #[sqlx(default)] + pub depends_on_names: Vec<String>, + /// Task definitions as JSON: [{name, plan}, ...] + pub tasks: Option<serde_json::Value>, + /// Deliverable definitions as JSON: [{id, name, priority}, ...] + pub deliverables: Option<serde_json::Value>, + /// Position in GUI editor + pub editor_x: Option<f64>, + pub editor_y: Option<f64>, + pub order_index: i32, + pub created_at: DateTime<Utc>, +} + +/// Request to add a contract definition to a chain +#[derive(Debug, Clone, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct AddContractDefinitionRequest { + pub name: String, + pub description: Option<String>, + #[serde(default = "default_contract_type")] + pub contract_type: String, + pub initial_phase: Option<String>, + /// Names of other definitions this depends on + pub depends_on: Option<Vec<String>>, + /// Task definitions + pub tasks: Option<Vec<CreateChainTaskRequest>>, + /// Deliverable definitions + pub deliverables: Option<Vec<CreateChainDeliverableRequest>>, + /// Position in GUI editor + pub editor_x: Option<f64>, + pub editor_y: Option<f64>, +} + +fn default_contract_type() -> String { + "simple".to_string() +} + +/// Request to update a contract definition +#[derive(Debug, Clone, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct UpdateContractDefinitionRequest { + pub name: Option<String>, + pub description: Option<String>, + pub contract_type: Option<String>, + pub initial_phase: Option<String>, + pub depends_on: Option<Vec<String>>, + pub tasks: Option<Vec<CreateChainTaskRequest>>, + pub deliverables: Option<Vec<CreateChainDeliverableRequest>>, + pub editor_x: Option<f64>, + pub editor_y: Option<f64>, +} + +/// Response when starting a chain +#[derive(Debug, Clone, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct StartChainResponse { + pub chain_id: Uuid, + pub supervisor_task_id: Option<Uuid>, + /// Root contracts created (those with no dependencies) + pub contracts_created: Vec<Uuid>, + pub status: String, +} + +/// Graph node for definitions (before contracts are created) +#[derive(Debug, Clone, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct ChainDefinitionGraphNode { + pub id: Uuid, + pub name: String, + pub contract_type: String, + pub x: f64, + pub y: f64, + /// Whether this definition has been instantiated as a contract + pub is_instantiated: bool, + /// The contract ID if instantiated + pub contract_id: Option<Uuid>, + pub contract_status: Option<String>, +} + +/// Graph response for definitions +#[derive(Debug, Clone, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct ChainDefinitionGraphResponse { + pub chain_id: Uuid, + pub chain_name: String, + pub chain_status: String, + pub nodes: Vec<ChainDefinitionGraphNode>, + pub edges: Vec<ChainGraphEdge>, +} + +// ============================================================================= // Unit Tests // ============================================================================= diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs index 48b0714..85af178 100644 --- a/makima/src/db/repository.rs +++ b/makima/src/db/repository.rs @@ -6,19 +6,20 @@ use sqlx::PgPool; use uuid::Uuid; use super::models::{ - AddContractToChainRequest, Chain, ChainContract, ChainContractDetail, ChainEditorContract, - ChainEditorData, ChainEditorDeliverable, ChainEditorEdge, ChainEditorNode, ChainEditorTask, - ChainEvent, ChainGraphEdge, ChainGraphNode, ChainGraphResponse, ChainSummary, - ChainWithContracts, CheckpointPatch, CheckpointPatchInfo, Contract, - ContractChatConversation, ContractChatMessageRecord, ContractEvent, ContractRepository, - ContractSummary, ContractTypeTemplateRecord, ConversationMessage, ConversationSnapshot, - CreateChainRequest, CreateContractRequest, CreateFileRequest, CreateTaskRequest, - CreateTemplateRequest, Daemon, DaemonTaskAssignment, DaemonWithCapacity, + AddContractDefinitionRequest, AddContractToChainRequest, Chain, ChainContract, + ChainContractDefinition, ChainContractDetail, ChainDefinitionGraphNode, + ChainDefinitionGraphResponse, ChainEditorContract, ChainEditorData, ChainEditorDeliverable, + ChainEditorEdge, ChainEditorNode, ChainEditorTask, ChainEvent, ChainGraphEdge, ChainGraphNode, + ChainGraphResponse, ChainSummary, ChainWithContracts, CheckpointPatch, CheckpointPatchInfo, + Contract, ContractChatConversation, ContractChatMessageRecord, ContractEvent, + ContractRepository, ContractSummary, ContractTypeTemplateRecord, ConversationMessage, + ConversationSnapshot, CreateChainRequest, CreateContractRequest, CreateFileRequest, + CreateTaskRequest, CreateTemplateRequest, Daemon, DaemonTaskAssignment, DaemonWithCapacity, DeliverableDefinition, File, FileSummary, FileVersion, HistoryEvent, HistoryQueryFilters, MeshChatConversation, MeshChatMessageRecord, PhaseChangeResult, PhaseConfig, PhaseDefinition, SupervisorHeartbeatRecord, SupervisorState, Task, TaskCheckpoint, TaskEvent, TaskSummary, - UpdateChainRequest, UpdateContractRequest, UpdateFileRequest, UpdateTaskRequest, - UpdateTemplateRequest, + UpdateChainRequest, UpdateContractDefinitionRequest, UpdateContractRequest, UpdateFileRequest, + UpdateTaskRequest, UpdateTemplateRequest, }; /// Repository error types. @@ -5426,3 +5427,281 @@ pub async fn get_chain_editor_data( None => Ok(None), } } + +// ============================================================================= +// Chain Contract Definition Operations +// ============================================================================= + +/// Create a new contract definition in a chain. +pub async fn create_chain_contract_definition( + pool: &PgPool, + chain_id: Uuid, + req: AddContractDefinitionRequest, +) -> Result<ChainContractDefinition, sqlx::Error> { + // Get the next order index + let max_order: Option<i32> = sqlx::query_scalar( + "SELECT MAX(order_index) FROM chain_contract_definitions WHERE chain_id = $1", + ) + .bind(chain_id) + .fetch_one(pool) + .await?; + + let order_index = max_order.unwrap_or(-1) + 1; + + // Convert tasks and deliverables to JSON + let tasks_json = req.tasks.as_ref().map(|t| serde_json::to_value(t).unwrap()); + let deliverables_json = req + .deliverables + .as_ref() + .map(|d| serde_json::to_value(d).unwrap()); + let depends_on_names: Vec<String> = req.depends_on.unwrap_or_default(); + + sqlx::query_as::<_, ChainContractDefinition>( + r#" + INSERT INTO chain_contract_definitions + (chain_id, name, description, contract_type, initial_phase, depends_on_names, tasks, deliverables, editor_x, editor_y, order_index) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) + RETURNING * + "#, + ) + .bind(chain_id) + .bind(&req.name) + .bind(&req.description) + .bind(&req.contract_type) + .bind(&req.initial_phase) + .bind(&depends_on_names) + .bind(&tasks_json) + .bind(&deliverables_json) + .bind(req.editor_x) + .bind(req.editor_y) + .bind(order_index) + .fetch_one(pool) + .await +} + +/// List all contract definitions in a chain. +pub async fn list_chain_contract_definitions( + pool: &PgPool, + chain_id: Uuid, +) -> Result<Vec<ChainContractDefinition>, sqlx::Error> { + sqlx::query_as::<_, ChainContractDefinition>( + r#" + SELECT * FROM chain_contract_definitions + WHERE chain_id = $1 + ORDER BY order_index ASC + "#, + ) + .bind(chain_id) + .fetch_all(pool) + .await +} + +/// Get a specific contract definition. +pub async fn get_chain_contract_definition( + pool: &PgPool, + definition_id: Uuid, +) -> Result<Option<ChainContractDefinition>, sqlx::Error> { + sqlx::query_as::<_, ChainContractDefinition>( + "SELECT * FROM chain_contract_definitions WHERE id = $1", + ) + .bind(definition_id) + .fetch_optional(pool) + .await +} + +/// Update a contract definition. +pub async fn update_chain_contract_definition( + pool: &PgPool, + definition_id: Uuid, + req: UpdateContractDefinitionRequest, +) -> Result<ChainContractDefinition, sqlx::Error> { + let tasks_json = req.tasks.as_ref().map(|t| serde_json::to_value(t).unwrap()); + let deliverables_json = req + .deliverables + .as_ref() + .map(|d| serde_json::to_value(d).unwrap()); + + sqlx::query_as::<_, ChainContractDefinition>( + r#" + UPDATE chain_contract_definitions SET + name = COALESCE($2, name), + description = COALESCE($3, description), + contract_type = COALESCE($4, contract_type), + initial_phase = COALESCE($5, initial_phase), + depends_on_names = COALESCE($6, depends_on_names), + tasks = COALESCE($7, tasks), + deliverables = COALESCE($8, deliverables), + editor_x = COALESCE($9, editor_x), + editor_y = COALESCE($10, editor_y) + WHERE id = $1 + RETURNING * + "#, + ) + .bind(definition_id) + .bind(&req.name) + .bind(&req.description) + .bind(&req.contract_type) + .bind(&req.initial_phase) + .bind(&req.depends_on) + .bind(&tasks_json) + .bind(&deliverables_json) + .bind(req.editor_x) + .bind(req.editor_y) + .fetch_one(pool) + .await +} + +/// Delete a contract definition. +pub async fn delete_chain_contract_definition( + pool: &PgPool, + definition_id: Uuid, +) -> Result<bool, sqlx::Error> { + let result = sqlx::query("DELETE FROM chain_contract_definitions WHERE id = $1") + .bind(definition_id) + .execute(pool) + .await?; + Ok(result.rows_affected() > 0) +} + +/// Get definitions that are ready to be instantiated (all dependencies are satisfied). +/// A definition is ready if all definitions it depends on have been instantiated as contracts +/// and those contracts have completed. +pub async fn get_ready_definitions( + pool: &PgPool, + chain_id: Uuid, +) -> Result<Vec<ChainContractDefinition>, sqlx::Error> { + sqlx::query_as::<_, ChainContractDefinition>( + r#" + SELECT d.* + FROM chain_contract_definitions d + WHERE d.chain_id = $1 + -- Not already instantiated + AND NOT EXISTS ( + SELECT 1 FROM chain_contracts cc + WHERE cc.definition_id = d.id + ) + -- All dependencies satisfied (either no deps, or all deps have completed contracts) + AND ( + cardinality(d.depends_on_names) = 0 + OR NOT EXISTS ( + SELECT 1 FROM unnest(d.depends_on_names) AS dep_name + WHERE NOT EXISTS ( + SELECT 1 FROM chain_contract_definitions dep_def + JOIN chain_contracts cc ON cc.definition_id = dep_def.id + JOIN contracts c ON c.id = cc.contract_id + WHERE dep_def.chain_id = d.chain_id + AND dep_def.name = dep_name + AND c.status = 'completed' + ) + ) + ) + ORDER BY d.order_index ASC + "#, + ) + .bind(chain_id) + .fetch_all(pool) + .await +} + +/// Get the definition graph for visualization. +pub async fn get_chain_definition_graph( + pool: &PgPool, + chain_id: Uuid, +) -> Result<Option<ChainDefinitionGraphResponse>, sqlx::Error> { + let chain = sqlx::query_as::<_, Chain>("SELECT * FROM chains WHERE id = $1") + .bind(chain_id) + .fetch_optional(pool) + .await?; + + let Some(chain) = chain else { + return Ok(None); + }; + + let definitions = list_chain_contract_definitions(pool, chain_id).await?; + + // Get instantiated contracts for each definition + let chain_contracts = list_chain_contracts(pool, chain_id).await?; + let instantiated: std::collections::HashMap<Uuid, &ChainContractDetail> = chain_contracts + .iter() + .filter_map(|cc| { + // Find definition_id from cc - we need to query this + // For now, match by name + definitions + .iter() + .find(|d| d.name == cc.contract_name) + .map(|d| (d.id, cc)) + }) + .collect(); + + let nodes: Vec<ChainDefinitionGraphNode> = definitions + .iter() + .map(|d| { + let cc = instantiated.get(&d.id); + ChainDefinitionGraphNode { + id: d.id, + name: d.name.clone(), + contract_type: d.contract_type.clone(), + x: d.editor_x.unwrap_or(0.0), + y: d.editor_y.unwrap_or(0.0), + is_instantiated: cc.is_some(), + contract_id: cc.map(|c| c.contract_id), + contract_status: cc.map(|c| c.contract_status.clone()), + } + }) + .collect(); + + // Build edges from depends_on_names + let name_to_id: std::collections::HashMap<&str, Uuid> = + definitions.iter().map(|d| (d.name.as_str(), d.id)).collect(); + + let edges: Vec<ChainGraphEdge> = definitions + .iter() + .flat_map(|d| { + let target_id = d.id; + let name_to_id = &name_to_id; + d.depends_on_names.iter().filter_map(move |dep_name| { + name_to_id + .get(dep_name.as_str()) + .map(|&from_id| ChainGraphEdge { from: from_id, to: target_id }) + }) + }) + .collect(); + + Ok(Some(ChainDefinitionGraphResponse { + chain_id: chain.id, + chain_name: chain.name, + chain_status: chain.status, + nodes, + edges, + })) +} + +/// Set the supervisor task ID for a chain. +pub async fn set_chain_supervisor_task( + pool: &PgPool, + chain_id: Uuid, + supervisor_task_id: Option<Uuid>, +) -> Result<(), sqlx::Error> { + sqlx::query( + "UPDATE chains SET supervisor_task_id = $2, updated_at = NOW() WHERE id = $1", + ) + .bind(chain_id) + .bind(supervisor_task_id) + .execute(pool) + .await?; + Ok(()) +} + +/// Update chain status. +pub async fn update_chain_status( + pool: &PgPool, + chain_id: Uuid, + status: &str, +) -> Result<(), sqlx::Error> { + sqlx::query("UPDATE chains SET status = $2, updated_at = NOW() WHERE id = $1") + .bind(chain_id) + .bind(status) + .execute(pool) + .await?; + Ok(()) +} |
