From f19acd400cc5bbe1fe51c004c50ee90d704240d8 Mon Sep 17 00:00:00 2001 From: soryu Date: Thu, 29 Jan 2026 02:56:44 +0000 Subject: Fix contract type selection --- makima/src/db/models.rs | 213 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 193 insertions(+), 20 deletions(-) (limited to 'makima/src/db/models.rs') diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs index 9e624c9..2eeba87 100644 --- a/makima/src/db/models.rs +++ b/makima/src/db/models.rs @@ -1113,6 +1113,108 @@ pub struct MergeCompleteCheckResponse { pub skipped_count: u32, } +// ============================================================================= +// Contract Type Templates (User-defined) +// ============================================================================= + +/// A phase definition within a contract template +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct PhaseDefinition { + /// Phase identifier (e.g., "research", "plan", "execute") + pub id: String, + /// Display name for the phase + pub name: String, + /// Order in the workflow (0-indexed) + pub order: i32, +} + +/// A deliverable definition within a phase +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct DeliverableDefinition { + /// Deliverable identifier (e.g., "plan-document", "pull-request") + pub id: String, + /// Display name for the deliverable + pub name: String, + /// Priority: "required", "recommended", or "optional" + #[serde(default = "default_priority")] + pub priority: String, +} + +fn default_priority() -> String { + "required".to_string() +} + +/// Phase configuration stored on a contract (copied from template at creation) +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct PhaseConfig { + /// Ordered list of phases in the workflow + pub phases: Vec, + /// Default starting phase + pub default_phase: String, + /// Deliverables per phase: { "phase_id": [deliverables] } + #[serde(default)] + pub deliverables: std::collections::HashMap>, +} + +/// Contract type template record from the database +#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct ContractTypeTemplateRecord { + pub id: Uuid, + pub owner_id: Uuid, + pub name: String, + pub description: Option, + #[sqlx(json)] + pub phases: Vec, + pub default_phase: String, + #[sqlx(json)] + pub deliverables: Option>>, + pub version: i32, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +/// Request to create a new contract type template +#[derive(Debug, Clone, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct CreateTemplateRequest { + pub name: String, + pub description: Option, + pub phases: Vec, + pub default_phase: String, + pub deliverables: Option>>, +} + +/// Request to update a contract type template +#[derive(Debug, Clone, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct UpdateTemplateRequest { + pub name: Option, + pub description: Option, + pub phases: Option>, + pub default_phase: Option, + pub deliverables: Option>>, + /// Version for optimistic locking + pub version: Option, +} + +/// Summary of a contract type template for list views +#[derive(Debug, Clone, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct ContractTypeTemplateSummary { + pub id: Uuid, + pub name: String, + pub description: Option, + pub phases: Vec, + pub default_phase: String, + pub is_builtin: bool, + pub version: i32, + pub created_at: DateTime, +} + // ============================================================================= // Contract Types // ============================================================================= @@ -1355,6 +1457,11 @@ pub struct Contract { /// when evaluating task outputs. #[serde(skip_serializing_if = "Option::is_none")] pub red_team_prompt: Option, + /// Phase configuration copied from template at contract creation. + /// When present, this overrides the built-in contract type phases. + #[sqlx(json)] + #[serde(skip_serializing_if = "Option::is_none")] + pub phase_config: Option, pub version: i32, pub created_at: DateTime, pub updated_at: DateTime, @@ -1376,37 +1483,96 @@ impl Contract { self.status.parse() } - /// Get valid phases for this contract type - pub fn valid_phases(&self) -> Vec { + /// Get valid phase IDs for this contract (as strings) + pub fn valid_phase_ids(&self) -> Vec { + // Check phase_config first (for custom templates) + if let Some(ref config) = self.phase_config { + let mut phases: Vec<_> = config.phases.iter().collect(); + phases.sort_by_key(|p| p.order); + return phases.iter().map(|p| p.id.clone()).collect(); + } + + // Fall back to built-in contract types match self.contract_type.as_str() { - "simple" => vec![ContractPhase::Plan, ContractPhase::Execute], + "simple" => vec!["plan".to_string(), "execute".to_string()], "specification" => vec![ - ContractPhase::Research, - ContractPhase::Specify, - ContractPhase::Plan, - ContractPhase::Execute, - ContractPhase::Review, + "research".to_string(), + "specify".to_string(), + "plan".to_string(), + "execute".to_string(), + "review".to_string(), ], - "execute" => vec![ContractPhase::Execute], // Execute-only, single phase - _ => vec![ContractPhase::Plan, ContractPhase::Execute], // Default to simple + "execute" => vec!["execute".to_string()], + _ => vec!["plan".to_string(), "execute".to_string()], + } + } + + /// Get valid phases for this contract type (as ContractPhase enums) + /// Note: For custom templates with non-standard phases, this only returns + /// phases that map to the ContractPhase enum. + pub fn valid_phases(&self) -> Vec { + self.valid_phase_ids() + .iter() + .filter_map(|id| id.parse::().ok()) + .collect() + } + + /// Get the initial phase ID for this contract type (as string) + pub fn initial_phase_id(&self) -> String { + // Check phase_config first (for custom templates) + if let Some(ref config) = self.phase_config { + return config.default_phase.clone(); + } + + // Fall back to built-in contract types + match self.contract_type.as_str() { + "specification" => "research".to_string(), + "execute" => "execute".to_string(), + _ => "plan".to_string(), } } - /// Get the initial phase for this contract type + /// Get the initial phase for this contract type (as ContractPhase enum) pub fn initial_phase(&self) -> ContractPhase { + self.initial_phase_id() + .parse() + .unwrap_or(ContractPhase::Plan) + } + + /// Get the terminal phase ID for this contract type (as string) + pub fn terminal_phase_id(&self) -> String { + // Check phase_config first (for custom templates) + if let Some(ref config) = self.phase_config { + // Last phase in sorted order is the terminal phase + let mut phases: Vec<_> = config.phases.iter().collect(); + phases.sort_by_key(|p| p.order); + if let Some(last) = phases.last() { + return last.id.clone(); + } + } + + // Fall back to built-in contract types match self.contract_type.as_str() { - "specification" => ContractPhase::Research, - "execute" => ContractPhase::Execute, - _ => ContractPhase::Plan, // simple and default + "specification" => "review".to_string(), + _ => "execute".to_string(), } } /// Get the terminal phase for this contract type (phase where contract can be completed) pub fn terminal_phase(&self) -> ContractPhase { - match self.contract_type.as_str() { - "specification" => ContractPhase::Review, - _ => ContractPhase::Execute, // simple and execute both end at execute - } + self.terminal_phase_id() + .parse() + .unwrap_or(ContractPhase::Execute) + } + + /// Check if a phase ID is valid for this contract + pub fn is_valid_phase(&self, phase_id: &str) -> bool { + self.valid_phase_ids().contains(&phase_id.to_string()) + } + + /// Get the phase configuration for custom templates + pub fn get_phase_config(&self) -> Option<&PhaseConfig> { + self.phase_config.as_ref() } /// Get completed deliverable IDs for a specific phase @@ -1507,12 +1673,19 @@ pub struct CreateContractRequest { pub name: String, /// Optional description pub description: Option, - /// Contract type: "simple" (default) or "specification" + /// Contract type: "simple" (default), "specification", "execute", or a custom template name. + /// For built-in types: /// - simple: Plan -> Execute workflow /// - specification: Research -> Specify -> Plan -> Execute -> Review + /// - execute: Execute only + /// For custom templates, use the template name or provide template_id. #[serde(default)] pub contract_type: Option, - /// Initial phase to start in (defaults based on contract_type) + /// UUID of a custom template to use. If provided, this takes precedence over contract_type. + /// The template's phase configuration will be copied to the contract. + #[serde(default)] + pub template_id: Option, + /// Initial phase to start in (defaults based on contract_type or template) /// - simple: defaults to "plan" /// - specification: defaults to "research" #[serde(default)] -- cgit v1.2.3