diff options
| author | soryu <soryu@soryu.co> | 2026-01-15 00:23:44 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-01-15 00:23:47 +0000 |
| commit | eff0d844ca6e35bfbc2d5fdaa2d2f92177611f2e (patch) | |
| tree | 90d87d6daf9dd78c31e4b816bb1d282db73821dd /makima/src | |
| parent | 87044a747b47bd83249d61a45842c7f7b2eae56d (diff) | |
| download | soryu-eff0d844ca6e35bfbc2d5fdaa2d2f92177611f2e.tar.gz soryu-eff0d844ca6e35bfbc2d5fdaa2d2f92177611f2e.zip | |
Contract type system
Diffstat (limited to 'makima/src')
| -rw-r--r-- | makima/src/db/models.rs | 78 | ||||
| -rw-r--r-- | makima/src/db/repository.rs | 39 | ||||
| -rw-r--r-- | makima/src/server/handlers/contracts.rs | 4 |
3 files changed, 110 insertions, 11 deletions
diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs index e16c43f..ca12eb2 100644 --- a/makima/src/db/models.rs +++ b/makima/src/db/models.rs @@ -991,6 +991,50 @@ pub struct MergeCompleteCheckResponse { // Contract Types // ============================================================================= +/// Contract type determines the workflow and required documents +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "lowercase")] +pub enum ContractType { + /// Simple Plan -> Execute workflow (default) + /// - Plan phase: requires a "Plan" document + /// - Execute phase: no documents, fulfills the plan + Simple, + /// Specification-based development with TDD + /// - Research: gather requirements and context + /// - Specify: write specifications and test cases + /// - Plan: create implementation plan + /// - Execute: implement according to specs + /// - Review: verify against specifications + Specification, +} + +impl Default for ContractType { + fn default() -> Self { + ContractType::Simple + } +} + +impl std::fmt::Display for ContractType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ContractType::Simple => write!(f, "simple"), + ContractType::Specification => write!(f, "specification"), + } + } +} + +impl std::str::FromStr for ContractType { + type Err = String; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s.to_lowercase().as_str() { + "simple" => Ok(ContractType::Simple), + "specification" => Ok(ContractType::Specification), + _ => Err(format!("Unknown contract type: {}", s)), + } + } +} + /// Contract phase for workflow progression #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "lowercase")] @@ -1143,6 +1187,8 @@ pub struct Contract { pub owner_id: Uuid, pub name: String, pub description: Option<String>, + /// Contract type: "simple" or "specification" + pub contract_type: String, pub phase: String, pub status: String, /// The long-running supervisor task that orchestrates this contract @@ -1154,6 +1200,11 @@ pub struct Contract { } impl Contract { + /// Parse contract_type string to ContractType enum + pub fn contract_type_enum(&self) -> Result<ContractType, String> { + self.contract_type.parse() + } + /// Parse phase string to ContractPhase enum pub fn phase_enum(&self) -> Result<ContractPhase, String> { self.phase.parse() @@ -1163,6 +1214,21 @@ impl Contract { pub fn status_enum(&self) -> Result<ContractStatus, String> { self.status.parse() } + + /// Get valid phases for this contract type + pub fn valid_phases(&self) -> Vec<ContractPhase> { + match self.contract_type.as_str() { + "simple" => vec![ContractPhase::Plan, ContractPhase::Execute], + "specification" => vec![ + ContractPhase::Research, + ContractPhase::Specify, + ContractPhase::Plan, + ContractPhase::Execute, + ContractPhase::Review, + ], + _ => vec![ContractPhase::Plan, ContractPhase::Execute], // Default to simple + } + } } /// Contract repository record from the database @@ -1200,6 +1266,8 @@ pub struct ContractSummary { pub id: Uuid, pub name: String, pub description: Option<String>, + /// Contract type: "simple" or "specification" + pub contract_type: String, pub phase: String, pub status: String, pub file_count: i64, @@ -1236,8 +1304,14 @@ pub struct CreateContractRequest { pub name: String, /// Optional description pub description: Option<String>, - /// Initial phase to start in (defaults to "research") - /// Valid values: "research", "specify", "plan", "execute", "review" + /// Contract type: "simple" (default) or "specification" + /// - simple: Plan -> Execute workflow + /// - specification: Research -> Specify -> Plan -> Execute -> Review + #[serde(default)] + pub contract_type: Option<String>, + /// Initial phase to start in (defaults based on contract_type) + /// - simple: defaults to "plan" + /// - specification: defaults to "research" #[serde(default)] pub initial_phase: Option<String>, } diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs index 3b911c2..7933f1e 100644 --- a/makima/src/db/repository.rs +++ b/makima/src/db/repository.rs @@ -2019,29 +2019,50 @@ pub async fn create_contract_for_owner( owner_id: Uuid, req: CreateContractRequest, ) -> Result<Contract, sqlx::Error> { - // Use provided initial_phase or default to "research" - let phase = req.initial_phase.as_deref().unwrap_or("research"); + // Default contract type is "simple" + let contract_type = req.contract_type.as_deref().unwrap_or("simple"); - // Validate the phase - let valid_phases = ["research", "specify", "plan", "execute", "review"]; + // Validate contract type + let valid_types = ["simple", "specification"]; + if !valid_types.contains(&contract_type) { + return Err(sqlx::Error::Protocol(format!( + "Invalid contract_type '{}'. Must be one of: {}", + 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"), + _ => (&["plan", "execute"], "plan"), + }; + + // Use provided initial_phase or default based on contract type + let phase = req.initial_phase.as_deref().unwrap_or(default_phase); + + // Validate the phase is valid for this contract type if !valid_phases.contains(&phase) { return Err(sqlx::Error::Protocol(format!( - "Invalid initial_phase '{}'. Must be one of: {}", + "Invalid initial_phase '{}' for contract type '{}'. Must be one of: {}", phase, + contract_type, valid_phases.join(", ") ))); } sqlx::query_as::<_, Contract>( r#" - INSERT INTO contracts (owner_id, name, description, phase) - VALUES ($1, $2, $3, $4) + INSERT INTO contracts (owner_id, name, description, contract_type, phase) + VALUES ($1, $2, $3, $4, $5) RETURNING * "#, ) .bind(owner_id) .bind(&req.name) .bind(&req.description) + .bind(contract_type) .bind(phase) .fetch_one(pool) .await @@ -2074,7 +2095,7 @@ pub async fn list_contracts_for_owner( sqlx::query_as::<_, ContractSummary>( r#" SELECT - c.id, c.name, c.description, c.phase, c.status, + c.id, c.name, c.description, c.contract_type, c.phase, c.status, c.version, c.created_at, (SELECT COUNT(*) FROM files WHERE contract_id = c.id) as file_count, (SELECT COUNT(*) FROM tasks WHERE contract_id = c.id) as task_count, @@ -2098,7 +2119,7 @@ pub async fn get_contract_summary_for_owner( sqlx::query_as::<_, ContractSummary>( r#" SELECT - c.id, c.name, c.description, c.phase, c.status, + c.id, c.name, c.description, c.contract_type, c.phase, c.status, c.version, c.created_at, (SELECT COUNT(*) FROM files WHERE contract_id = c.id) as file_count, (SELECT COUNT(*) FROM tasks WHERE contract_id = c.id) as task_count, diff --git a/makima/src/server/handlers/contracts.rs b/makima/src/server/handlers/contracts.rs index 3d726df..a3aa00a 100644 --- a/makima/src/server/handlers/contracts.rs +++ b/makima/src/server/handlers/contracts.rs @@ -342,6 +342,7 @@ pub async fn create_contract( id: contract.id, name: contract.name, description: contract.description, + contract_type: contract.contract_type, phase: contract.phase, status: contract.status, file_count: 0, @@ -361,6 +362,7 @@ pub async fn create_contract( id: contract.id, name: contract.name, description: contract.description, + contract_type: contract.contract_type, phase: contract.phase, status: contract.status, file_count: 0, @@ -464,6 +466,7 @@ pub async fn update_contract( id: contract.id, name: contract.name, description: contract.description, + contract_type: contract.contract_type, phase: contract.phase, status: contract.status, file_count: 0, @@ -1186,6 +1189,7 @@ pub async fn change_phase( id: contract.id, name: contract.name, description: contract.description, + contract_type: contract.contract_type, phase: contract.phase, status: contract.status, file_count: 0, |
