summaryrefslogtreecommitdiff
path: root/makima/src/db/repository.rs
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-02-03 23:49:08 +0000
committersoryu <soryu@soryu.co>2026-02-03 23:49:19 +0000
commitc732dd048128808cd9f67f6e1176a5b565df5678 (patch)
tree6ebf359c9c3f2d8aca264c53da6367b7f0af5fc8 /makima/src/db/repository.rs
parent9ebc9724afcc0482a8e7cd2369c06208fedbcbd1 (diff)
downloadsoryu-c732dd048128808cd9f67f6e1176a5b565df5678.tar.gz
soryu-c732dd048128808cd9f67f6e1176a5b565df5678.zip
Allow chain creation via web interface
Diffstat (limited to 'makima/src/db/repository.rs')
-rw-r--r--makima/src/db/repository.rs299
1 files changed, 289 insertions, 10 deletions
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(())
+}