summaryrefslogtreecommitdiff
path: root/makima/src
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-15 00:23:44 +0000
committersoryu <soryu@soryu.co>2026-01-15 00:23:47 +0000
commiteff0d844ca6e35bfbc2d5fdaa2d2f92177611f2e (patch)
tree90d87d6daf9dd78c31e4b816bb1d282db73821dd /makima/src
parent87044a747b47bd83249d61a45842c7f7b2eae56d (diff)
downloadsoryu-eff0d844ca6e35bfbc2d5fdaa2d2f92177611f2e.tar.gz
soryu-eff0d844ca6e35bfbc2d5fdaa2d2f92177611f2e.zip
Contract type system
Diffstat (limited to 'makima/src')
-rw-r--r--makima/src/db/models.rs78
-rw-r--r--makima/src/db/repository.rs39
-rw-r--r--makima/src/server/handlers/contracts.rs4
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,