diff options
| author | soryu <soryu@soryu.co> | 2026-02-05 23:42:48 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-02-05 23:42:48 +0000 |
| commit | 88a4f15ce1310f8ee8693835be14aa5280233f17 (patch) | |
| tree | 5c1a0417e02071d2198d13478ffa85533b19f891 /makima/src/db | |
| parent | f1a50b80f3969d150bd1c31edde0aff05369157e (diff) | |
| download | soryu-88a4f15ce1310f8ee8693835be14aa5280233f17.tar.gz soryu-88a4f15ce1310f8ee8693835be14aa5280233f17.zip | |
Add directive-first chain system redesign
Redesigns the chain system with a directive-first architecture where
Directive is the top-level entity (the "why/what") and Chains are
generated execution plans (the "how") that can be dynamically modified.
Backend:
- Add database migration for directive system tables
- Add Directive, DirectiveChain, ChainStep, DirectiveEvent models
- Add DirectiveVerifier and DirectiveApproval models
- Add orchestration module with engine, planner, and verifier
- Add comprehensive API handlers for directives
- Add daemon CLI commands for directive management
- Add directive skill documentation
- Integrate contract completion with directive engine
- Add SSE endpoint for real-time directive events
Frontend:
- Add directives route with split-view layout
- Add 6-tab detail view (Overview, Chain, Events, Evaluations, Approvals, Verifiers)
- Add React Flow DAG visualization for chain steps
- Add SSE subscription hook for real-time event updates
- Add useDirectives and useDirectiveEventSubscription hooks
- Add directive types and API functions
Fixes:
- Fix test failures in ws/protocol, task_output, completion_gate, patch
- Fix word boundary matching in looks_like_task()
- Fix parse_last() to find actual last completion gate
- Fix create_export_patch when merge-base equals HEAD
- Clean up clippy warnings in new code
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'makima/src/db')
| -rw-r--r-- | makima/src/db/models.rs | 1178 | ||||
| -rw-r--r-- | makima/src/db/repository.rs | 2420 |
2 files changed, 1295 insertions, 2303 deletions
diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs index e861f1d..3a96165 100644 --- a/makima/src/db/models.rs +++ b/makima/src/db/models.rs @@ -1446,16 +1446,16 @@ pub struct Contract { /// Use `get_phase_config()` to get the parsed PhaseConfig. #[serde(skip_serializing_if = "Option::is_none")] pub phase_config: Option<serde_json::Value>, - /// Chain ID if this contract is part of a chain (DAG of contracts) + /// Directive ID if this contract is part of a directive's chain #[serde(skip_serializing_if = "Option::is_none")] - pub chain_id: Option<Uuid>, - /// Reference to chain spawned by this directive contract - #[serde(skip_serializing_if = "Option::is_none")] - pub spawned_chain_id: Option<Uuid>, - /// Whether this contract is a chain directive orchestrator + pub directive_id: Option<Uuid>, + /// Whether this contract is a directive orchestrator #[serde(default)] #[sqlx(default)] - pub is_chain_directive: bool, + pub is_directive_orchestrator: bool, + /// Reference to directive spawned by this orchestrator contract + #[serde(skip_serializing_if = "Option::is_none")] + pub spawned_directive_id: Option<Uuid>, pub version: i32, pub created_at: DateTime<Utc>, pub updated_at: DateTime<Utc>, @@ -2596,914 +2596,648 @@ pub struct HeartbeatHistoryQuery { } // ============================================================================= -// Chains (DAG of contracts for multi-contract orchestration) +// Directives (Goal-driven orchestration with chains of steps) // ============================================================================= -/// Chain status determines the overall state of the chain +/// Directive status #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "lowercase")] -pub enum ChainStatus { - /// Chain is actively running +#[serde(rename_all = "snake_case")] +pub enum DirectiveStatus { + Draft, + Planning, Active, - /// All contracts completed successfully + Paused, Completed, - /// Chain was manually archived Archived, + Failed, } -impl Default for ChainStatus { +impl Default for DirectiveStatus { fn default() -> Self { - ChainStatus::Active + DirectiveStatus::Draft } } -impl std::fmt::Display for ChainStatus { +impl std::fmt::Display for DirectiveStatus { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - ChainStatus::Active => write!(f, "active"), - ChainStatus::Completed => write!(f, "completed"), - ChainStatus::Archived => write!(f, "archived"), + DirectiveStatus::Draft => write!(f, "draft"), + DirectiveStatus::Planning => write!(f, "planning"), + DirectiveStatus::Active => write!(f, "active"), + DirectiveStatus::Paused => write!(f, "paused"), + DirectiveStatus::Completed => write!(f, "completed"), + DirectiveStatus::Archived => write!(f, "archived"), + DirectiveStatus::Failed => write!(f, "failed"), } } } -impl std::str::FromStr for ChainStatus { +impl std::str::FromStr for DirectiveStatus { type Err = String; fn from_str(s: &str) -> Result<Self, Self::Err> { match s.to_lowercase().as_str() { - "active" => Ok(ChainStatus::Active), - "completed" => Ok(ChainStatus::Completed), - "archived" => Ok(ChainStatus::Archived), - _ => Err(format!("Invalid chain status: {}", s)), + "draft" => Ok(DirectiveStatus::Draft), + "planning" => Ok(DirectiveStatus::Planning), + "active" => Ok(DirectiveStatus::Active), + "paused" => Ok(DirectiveStatus::Paused), + "completed" => Ok(DirectiveStatus::Completed), + "archived" => Ok(DirectiveStatus::Archived), + "failed" => Ok(DirectiveStatus::Failed), + _ => Err(format!("Invalid directive status: {}", s)), } } } -/// Chain - a directed acyclic graph (DAG) of contracts -/// Fits Makima's control theme - she controls through invisible chains +/// Directive - the top-level goal-driven orchestration entity #[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] -pub struct Chain { +pub struct Directive { pub id: Uuid, pub owner_id: Uuid, - pub name: String, - pub description: Option<String>, + pub title: String, + pub goal: String, + /// Structured requirements: [{ id, title, description, priority, category }] + #[sqlx(json)] + pub requirements: serde_json::Value, + /// Acceptance criteria: [{ id, requirementIds, description, testable, verificationMethod }] + #[sqlx(json)] + pub acceptance_criteria: serde_json::Value, + /// Constraints: [{ id, type, description, impact }] + #[sqlx(json)] + pub constraints: serde_json::Value, + /// External dependencies: [{ id, name, type, status, requiredBy }] + #[sqlx(json)] + pub external_dependencies: serde_json::Value, pub status: String, - /// Whether loop mode is enabled for iterative execution - #[serde(default)] - pub loop_enabled: bool, - /// Maximum loop iterations (default: 10) - pub loop_max_iterations: Option<i32>, - /// Current loop iteration count - pub loop_current_iteration: Option<i32>, - /// Progress check prompt/criteria for evaluating loop completion - pub loop_progress_check: Option<String>, - /// Reference to the directive contract that created/orchestrates this chain - pub directive_contract_id: Option<Uuid>, - /// The directive document text (formal specification) - pub directive_document: Option<String>, - /// Whether LLM evaluation is enabled after contract completion - #[serde(default = "default_evaluation_enabled")] - #[sqlx(default)] - pub evaluation_enabled: bool, - /// Default pass threshold for evaluations (0.0-1.0) - pub default_pass_threshold: Option<f64>, - /// Default max retry attempts for evaluations - pub default_max_retries: Option<i32>, - /// Version for optimistic locking + pub autonomy_level: String, + pub confidence_threshold_green: f64, + pub confidence_threshold_yellow: f64, + pub max_total_cost_usd: Option<f64>, + pub max_wall_time_minutes: Option<i32>, + pub max_rework_cycles: Option<i32>, + pub max_chain_regenerations: Option<i32>, + pub repository_url: Option<String>, + pub local_path: Option<String>, + pub base_branch: Option<String>, + pub orchestrator_contract_id: Option<Uuid>, + pub current_chain_id: Option<Uuid>, + pub chain_generation_count: i32, + pub total_cost_usd: f64, + pub started_at: Option<DateTime<Utc>>, + pub completed_at: Option<DateTime<Utc>>, pub version: i32, pub created_at: DateTime<Utc>, pub updated_at: DateTime<Utc>, } -fn default_evaluation_enabled() -> bool { - true +impl Directive { + /// Parse status string to DirectiveStatus enum + pub fn status_enum(&self) -> Result<DirectiveStatus, String> { + self.status.parse() + } } -/// Chain repository record from the database +/// Directive chain - a generated execution plan (DAG) for a directive #[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] -pub struct ChainRepository { +pub struct DirectiveChain { pub id: Uuid, - pub chain_id: Uuid, + pub directive_id: Uuid, + pub generation: i32, pub name: String, - pub repository_url: Option<String>, - pub local_path: Option<String>, - pub source_type: String, + pub description: Option<String>, + pub rationale: Option<String>, + pub planning_model: Option<String>, pub status: String, - pub is_primary: bool, + pub total_steps: i32, + pub completed_steps: i32, + pub failed_steps: i32, + pub current_confidence: Option<f64>, + pub started_at: Option<DateTime<Utc>>, + pub completed_at: Option<DateTime<Utc>>, + pub version: i32, pub created_at: DateTime<Utc>, pub updated_at: DateTime<Utc>, } -impl ChainRepository { - /// Parse source_type string to RepositorySourceType enum - pub fn source_type_enum(&self) -> Result<RepositorySourceType, String> { - self.source_type.parse() +/// Chain step status +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum StepStatus { + Pending, + Ready, + Running, + Evaluating, + Passed, + Failed, + Rework, + Skipped, + Blocked, +} + +impl Default for StepStatus { + fn default() -> Self { + StepStatus::Pending } +} - /// Parse status string to RepositoryStatus enum - pub fn status_enum(&self) -> Result<RepositoryStatus, String> { - self.status.parse() +impl std::fmt::Display for StepStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + StepStatus::Pending => write!(f, "pending"), + StepStatus::Ready => write!(f, "ready"), + StepStatus::Running => write!(f, "running"), + StepStatus::Evaluating => write!(f, "evaluating"), + StepStatus::Passed => write!(f, "passed"), + StepStatus::Failed => write!(f, "failed"), + StepStatus::Rework => write!(f, "rework"), + StepStatus::Skipped => write!(f, "skipped"), + StepStatus::Blocked => write!(f, "blocked"), + } } } -impl Chain { - /// Parse status string to ChainStatus enum - pub fn status_enum(&self) -> Result<ChainStatus, String> { - self.status.parse() +impl std::str::FromStr for StepStatus { + type Err = String; + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s.to_lowercase().as_str() { + "pending" => Ok(StepStatus::Pending), + "ready" => Ok(StepStatus::Ready), + "running" => Ok(StepStatus::Running), + "evaluating" => Ok(StepStatus::Evaluating), + "passed" => Ok(StepStatus::Passed), + "failed" => Ok(StepStatus::Failed), + "rework" => Ok(StepStatus::Rework), + "skipped" => Ok(StepStatus::Skipped), + "blocked" => Ok(StepStatus::Blocked), + _ => Err(format!("Invalid step status: {}", s)), + } } } -/// Chain contract link - links contracts to chains with DAG dependency info +/// Chain step - a node in the DAG execution plan #[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] -pub struct ChainContract { +pub struct ChainStep { pub id: Uuid, pub chain_id: Uuid, - pub contract_id: Uuid, - /// Contract IDs this contract depends on (DAG edges) + pub name: String, + pub description: Option<String>, + pub step_type: String, + pub contract_type: String, + pub initial_phase: Option<String>, + pub task_plan: Option<String>, #[sqlx(default)] - pub depends_on: Vec<Uuid>, - /// Order for display/processing (topological sort order) - pub order_index: i32, - /// X position for GUI editor - pub editor_x: Option<f64>, - /// Y position for GUI editor - pub editor_y: Option<f64>, - /// Evaluation status: pending, evaluating, passed, failed, rework, escalated - #[serde(default = "default_evaluation_status")] + pub phases: Vec<String>, #[sqlx(default)] - pub evaluation_status: String, - /// Number of evaluation retry attempts - #[serde(default)] + pub depends_on: Vec<Uuid>, + pub parallel_group: Option<String>, #[sqlx(default)] - pub evaluation_retry_count: i32, - /// Maximum evaluation retry attempts (default: 3) - #[serde(default = "default_max_evaluation_retries")] + pub requirement_ids: Vec<String>, #[sqlx(default)] - pub max_evaluation_retries: i32, - /// Reference to the last evaluation result + pub acceptance_criteria_ids: Vec<String>, + #[sqlx(json)] + #[serde(default)] + pub verifier_config: serde_json::Value, + pub status: String, + pub contract_id: Option<Uuid>, + pub supervisor_task_id: Option<Uuid>, + pub confidence_score: Option<f64>, + pub confidence_level: Option<String>, + pub evaluation_count: i32, + pub rework_count: i32, pub last_evaluation_id: Option<Uuid>, - /// Rework feedback/instructions from failed evaluation - pub rework_feedback: Option<String>, - /// When rework was started - pub rework_started_at: Option<DateTime<Utc>>, - /// When contract originally completed (before rework) - pub original_completion_at: Option<DateTime<Utc>>, + pub editor_x: Option<f64>, + pub editor_y: Option<f64>, + pub order_index: i32, + pub started_at: Option<DateTime<Utc>>, + pub completed_at: Option<DateTime<Utc>>, pub created_at: DateTime<Utc>, } -fn default_evaluation_status() -> String { - "pending".to_string() +impl ChainStep { + /// Parse status string to StepStatus enum + pub fn status_enum(&self) -> Result<StepStatus, String> { + self.status.parse() + } } -fn default_max_evaluation_retries() -> i32 { - 3 +/// Confidence level (traffic light) +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum ConfidenceLevel { + Green, + Yellow, + Red, +} + +impl ConfidenceLevel { + pub fn from_score(score: f64, green_threshold: f64, yellow_threshold: f64) -> Self { + if score >= green_threshold { + Self::Green + } else if score >= yellow_threshold { + Self::Yellow + } else { + Self::Red + } + } } -/// Chain event for audit trail +impl std::fmt::Display for ConfidenceLevel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ConfidenceLevel::Green => write!(f, "green"), + ConfidenceLevel::Yellow => write!(f, "yellow"), + ConfidenceLevel::Red => write!(f, "red"), + } + } +} + +/// Directive evaluation - composite programmatic + LLM evaluation result #[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] -pub struct ChainEvent { +pub struct DirectiveEvaluation { pub id: Uuid, - pub chain_id: Uuid, - pub event_type: String, + pub directive_id: Uuid, + pub chain_id: Option<Uuid>, + pub step_id: Option<Uuid>, pub contract_id: Option<Uuid>, + pub evaluation_type: String, + pub evaluation_number: i32, + pub evaluator: Option<String>, + pub passed: bool, + pub overall_score: Option<f64>, + pub confidence_level: Option<String>, #[sqlx(json)] - pub event_data: Option<serde_json::Value>, + #[serde(default)] + pub programmatic_results: serde_json::Value, + #[sqlx(json)] + #[serde(default)] + pub llm_results: serde_json::Value, + #[sqlx(json)] + #[serde(default)] + pub criteria_results: serde_json::Value, + pub summary_feedback: String, + pub rework_instructions: Option<String>, + #[sqlx(json)] + pub directive_snapshot: Option<serde_json::Value>, + #[sqlx(json)] + pub deliverables_snapshot: Option<serde_json::Value>, + pub started_at: DateTime<Utc>, + pub completed_at: Option<DateTime<Utc>>, pub created_at: DateTime<Utc>, } -/// Summary of a chain for list views -#[derive(Debug, Clone, FromRow, Serialize, ToSchema)] +/// Directive event - audit stream entry +#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] -pub struct ChainSummary { +pub struct DirectiveEvent { pub id: Uuid, - pub name: String, - pub description: Option<String>, - pub status: String, - pub loop_enabled: bool, - pub loop_current_iteration: Option<i32>, - pub contract_count: i64, - pub completed_count: i64, - pub version: i32, + pub directive_id: Uuid, + pub chain_id: Option<Uuid>, + pub step_id: Option<Uuid>, + pub event_type: String, + pub severity: String, + #[sqlx(json)] + pub event_data: Option<serde_json::Value>, + pub actor_type: String, + pub actor_id: Option<Uuid>, pub created_at: DateTime<Utc>, } -/// Chain with contracts for detail view -#[derive(Debug, Serialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct ChainWithContracts { - #[serde(flatten)] - pub chain: Chain, - pub contracts: Vec<ChainContractDetail>, - pub repositories: Vec<ChainRepository>, -} - -/// Contract detail within a chain (includes contract info + chain link info) -#[derive(Debug, Clone, FromRow, Serialize, ToSchema)] +/// Directive verifier - pluggable verification configuration +#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] -pub struct ChainContractDetail { - pub chain_contract_id: Uuid, - pub contract_id: Uuid, - pub contract_name: String, - pub contract_status: String, - pub contract_phase: String, - #[sqlx(default)] - pub depends_on: Vec<Uuid>, - pub order_index: i32, - pub editor_x: Option<f64>, - pub editor_y: Option<f64>, - /// Evaluation status: pending, passed, failed, rework - #[sqlx(default)] - pub evaluation_status: Option<String>, - /// Number of evaluation retries - #[sqlx(default)] - pub evaluation_retry_count: i32, - /// Maximum evaluation retry attempts +pub struct DirectiveVerifier { + pub id: Uuid, + pub directive_id: Uuid, + pub name: String, + pub verifier_type: String, + pub command: Option<String>, + pub working_directory: Option<String>, + pub timeout_seconds: Option<i32>, + #[sqlx(json)] + #[serde(default)] + pub environment: serde_json::Value, + pub auto_detect: bool, #[sqlx(default)] - pub max_evaluation_retries: i32, - /// When the chain contract was created + pub detect_files: Vec<String>, + pub weight: f64, + pub required: bool, + pub enabled: bool, + pub last_run_at: Option<DateTime<Utc>>, + #[sqlx(json)] + pub last_result: Option<serde_json::Value>, pub created_at: DateTime<Utc>, + pub updated_at: DateTime<Utc>, } -/// DAG graph structure for visualization -#[derive(Debug, Serialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct ChainGraphResponse { - pub chain_id: Uuid, - pub chain_name: String, - pub chain_status: String, - pub nodes: Vec<ChainGraphNode>, - pub edges: Vec<ChainGraphEdge>, -} - -/// Node in chain DAG graph -#[derive(Debug, Serialize, ToSchema)] +/// Directive approval - human-in-the-loop gate +#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] -pub struct ChainGraphNode { +pub struct DirectiveApproval { pub id: Uuid, - pub contract_id: Uuid, - pub name: String, + pub directive_id: Uuid, + pub step_id: Option<Uuid>, + pub approval_type: String, + pub description: String, + #[sqlx(json)] + pub context: Option<serde_json::Value>, + pub urgency: String, pub status: String, - pub phase: String, - pub x: f64, - pub y: f64, + pub response: Option<String>, + pub responded_by: Option<Uuid>, + pub responded_at: Option<DateTime<Utc>>, + pub expires_at: Option<DateTime<Utc>>, + pub created_at: DateTime<Utc>, } -/// Edge in chain DAG graph -#[derive(Debug, Clone, Serialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct ChainGraphEdge { - pub from: Uuid, - pub to: Uuid, -} +// ============================================================================= +// Directive Request/Response Types +// ============================================================================= -/// Response for chain list endpoint -#[derive(Debug, Serialize, ToSchema)] +/// Request to create a directive from a goal +#[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] -pub struct ChainListResponse { - pub chains: Vec<ChainSummary>, - pub total: i64, +pub struct CreateDirectiveRequest { + pub goal: String, + pub title: Option<String>, + pub repository_url: Option<String>, + pub local_path: Option<String>, + pub base_branch: Option<String>, + pub autonomy_level: Option<String>, + pub requirements: Option<serde_json::Value>, + pub acceptance_criteria: Option<serde_json::Value>, + pub confidence_threshold_green: Option<f64>, + pub confidence_threshold_yellow: Option<f64>, + pub max_total_cost_usd: Option<f64>, + pub max_wall_time_minutes: Option<i32>, } -/// Request payload for creating a new chain -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +/// Request to update a directive +#[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] -pub struct CreateChainRequest { - /// Name of the chain - pub name: String, - /// Optional description - pub description: Option<String>, - /// Repositories for this chain - pub repositories: Option<Vec<AddChainRepositoryRequest>>, - /// Enable loop mode for iterative execution - #[serde(default)] - pub loop_enabled: Option<bool>, - /// Maximum loop iterations (default: 10) - pub loop_max_iterations: Option<i32>, - /// Progress check prompt for evaluating loop completion - pub loop_progress_check: Option<String>, - /// Contracts to create within this chain - pub contracts: Option<Vec<CreateChainContractRequest>>, +pub struct UpdateDirectiveRequest { + pub title: Option<String>, + pub goal: Option<String>, + pub requirements: Option<serde_json::Value>, + pub acceptance_criteria: Option<serde_json::Value>, + pub constraints: Option<serde_json::Value>, + pub external_dependencies: Option<serde_json::Value>, + pub autonomy_level: Option<String>, + pub confidence_threshold_green: Option<f64>, + pub confidence_threshold_yellow: Option<f64>, + pub max_total_cost_usd: Option<f64>, + pub max_wall_time_minutes: Option<i32>, + pub max_rework_cycles: Option<i32>, + pub max_chain_regenerations: Option<i32>, + pub version: i32, } -/// Request to add a repository to a chain -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +/// Directive summary for list views +#[derive(Debug, Clone, Serialize, FromRow, ToSchema)] #[serde(rename_all = "camelCase")] -pub struct AddChainRepositoryRequest { - /// Display name for the repository - pub name: String, - /// Remote repository URL (for remote repos) - pub repository_url: Option<String>, - /// Local filesystem path (for local repos) - pub local_path: Option<String>, - /// Source type: remote, local, or managed - #[serde(default = "default_source_type")] - pub source_type: String, - /// Whether this is the primary repository - #[serde(default)] - pub is_primary: bool, +pub struct DirectiveSummary { + pub id: Uuid, + pub title: String, + pub goal: String, + pub status: String, + pub autonomy_level: String, + pub current_confidence: Option<f64>, + pub completed_steps: i32, + pub total_steps: i32, + pub chain_generation_count: i32, + pub started_at: Option<DateTime<Utc>>, + pub created_at: DateTime<Utc>, } -fn default_source_type() -> String { - "remote".to_string() +/// Directive with progress, chain, events, and approvals +#[derive(Debug, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct DirectiveWithProgress { + #[serde(flatten)] + pub directive: Directive, + pub chain: Option<DirectiveChain>, + pub steps: Vec<ChainStep>, + pub recent_events: Vec<DirectiveEvent>, + pub pending_approvals: Vec<DirectiveApproval>, } -/// Request to create a contract within a chain -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +/// Request to add a step to a chain +#[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] -pub struct CreateChainContractRequest { - /// Name of the contract +pub struct AddStepRequest { pub name: String, - /// Optional description pub description: Option<String>, - /// Contract type - #[serde(default)] + pub step_type: Option<String>, pub contract_type: Option<String>, - /// Initial phase pub initial_phase: Option<String>, - /// Phases for the contract + pub task_plan: Option<String>, pub phases: Option<Vec<String>>, - /// Names of contracts this depends on (resolved to IDs) - pub depends_on: Option<Vec<String>>, - /// Tasks to create in this contract - pub tasks: Option<Vec<CreateChainTaskRequest>>, - /// Deliverables for this contract - pub deliverables: Option<Vec<CreateChainDeliverableRequest>>, - /// Position in GUI editor + pub depends_on: Option<Vec<Uuid>>, + pub parallel_group: Option<String>, + pub requirement_ids: Option<Vec<String>>, + pub acceptance_criteria_ids: Option<Vec<String>>, + pub verifier_config: Option<serde_json::Value>, pub editor_x: Option<f64>, pub editor_y: Option<f64>, } -/// Task definition within a chain contract -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct CreateChainTaskRequest { - pub name: String, - pub plan: String, -} - -/// Deliverable definition within a chain contract -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct CreateChainDeliverableRequest { - pub id: String, - pub name: String, - pub priority: Option<String>, -} - -/// Validation configuration for checkpoint contracts. -/// Checkpoint contracts validate the outputs of their upstream dependencies -/// before allowing downstream contracts to proceed. -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct CheckpointValidation { - /// Check that all required deliverables from upstream contracts exist - #[serde(default)] - pub check_deliverables: bool, - - /// Run tests in the repository (requires repository to be configured) - #[serde(default)] - pub run_tests: bool, - - /// Custom validation instructions for Claude to execute. - /// Claude will review the outputs of upstream contracts and verify they meet these criteria. - pub check_content: Option<String>, - - /// Action to take on validation failure: "block" (default), "retry", "warn" - /// - block: Fail the checkpoint and block downstream contracts - /// - retry: Mark upstream contracts for retry (up to max_retries) - /// - warn: Log warning but allow downstream to proceed - #[serde(default = "default_checkpoint_on_failure")] - pub on_failure: String, - - /// Maximum retry attempts for upstream contracts (when on_failure = "retry") - #[serde(default = "default_checkpoint_max_retries")] - pub max_retries: i32, -} - -fn default_checkpoint_on_failure() -> String { - "block".to_string() -} - -fn default_checkpoint_max_retries() -> i32 { - 3 -} - -impl Default for CheckpointValidation { - fn default() -> Self { - Self { - check_deliverables: false, - run_tests: false, - check_content: None, - on_failure: default_checkpoint_on_failure(), - max_retries: default_checkpoint_max_retries(), - } - } -} - -/// Request to update an existing chain -#[derive(Debug, Clone, Deserialize, ToSchema)] +/// Request to update a step +#[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] -pub struct UpdateChainRequest { +pub struct UpdateStepRequest { pub name: Option<String>, pub description: Option<String>, - pub status: Option<String>, - pub loop_enabled: Option<bool>, - pub loop_max_iterations: Option<i32>, - pub loop_progress_check: Option<String>, - /// Version for optimistic locking - pub version: Option<i32>, -} - -/// Request to add a contract to a chain -#[derive(Debug, Clone, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct AddContractToChainRequest { - /// Existing contract ID to add - pub contract_id: Option<Uuid>, - /// Or create a new contract with this definition - pub new_contract: Option<CreateChainContractRequest>, - /// Contract IDs this depends on + pub task_plan: Option<String>, pub depends_on: Option<Vec<Uuid>>, - /// Position in GUI editor + pub requirement_ids: Option<Vec<String>>, + pub acceptance_criteria_ids: Option<Vec<String>>, + pub verifier_config: Option<serde_json::Value>, pub editor_x: Option<f64>, pub editor_y: Option<f64>, } -/// Editor data model for GUI chain editor -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct ChainEditorData { - pub id: Option<Uuid>, - pub name: String, - pub description: Option<String>, - pub repositories: Vec<ChainRepository>, - pub loop_enabled: bool, - pub loop_max_iterations: Option<i32>, - pub loop_progress_check: Option<String>, - pub nodes: Vec<ChainEditorNode>, - pub edges: Vec<ChainEditorEdge>, -} - -/// Node in chain editor -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct ChainEditorNode { - pub id: String, - pub x: f64, - pub y: f64, - pub contract: ChainEditorContract, -} - -/// Contract data in chain editor node -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct ChainEditorContract { - pub name: String, - pub description: Option<String>, - #[serde(rename = "type")] - pub contract_type: String, - pub phases: Vec<String>, - pub tasks: Vec<ChainEditorTask>, - pub deliverables: Vec<ChainEditorDeliverable>, -} - -/// Task in chain editor -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct ChainEditorTask { - pub name: String, - pub plan: String, -} - -/// Deliverable in chain editor -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct ChainEditorDeliverable { - pub id: String, - pub name: String, - pub priority: String, -} - -/// Edge in chain editor -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct ChainEditorEdge { - pub from: String, - pub to: String, -} - -// ============================================================================= -// 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)] +/// Chain graph response for DAG visualization +#[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] -pub struct ChainContractDefinition { - pub id: Uuid, +pub struct DirectiveChainGraphResponse { 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>, - /// Validation configuration for checkpoint contracts (JSON) - pub validation: Option<serde_json::Value>, - /// Requirement IDs this contract addresses (for traceability) - #[sqlx(default)] - #[serde(default)] - pub requirement_ids: Vec<String>, - /// Acceptance criteria for this contract (JSON array) - #[serde(default)] - pub acceptance_criteria: Option<serde_json::Value>, - /// Whether LLM evaluation is enabled for this contract - #[serde(default = "default_evaluation_enabled")] - #[sqlx(default)] - pub evaluation_enabled: bool, - /// Pass threshold for evaluation (0.0-1.0) - pub pass_threshold: Option<f64>, - /// Position in GUI editor - pub editor_x: Option<f64>, - pub editor_y: Option<f64>, - pub order_index: i32, - pub created_at: DateTime<Utc>, + pub directive_id: Uuid, + pub nodes: Vec<DirectiveChainGraphNode>, + pub edges: Vec<DirectiveChainGraphEdge>, } -/// Request to add a contract definition to a chain -#[derive(Debug, Clone, Deserialize, ToSchema)] +/// Node in directive chain graph +#[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] -pub struct AddContractDefinitionRequest { +pub struct DirectiveChainGraphNode { + pub id: Uuid, 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>>, - /// Validation configuration (for checkpoint contracts) - pub validation: Option<CheckpointValidation>, - /// 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>>, - /// Validation configuration (for checkpoint contracts) - pub validation: Option<CheckpointValidation>, + pub step_type: String, + pub status: String, + pub confidence_score: Option<f64>, + pub confidence_level: Option<String>, + pub contract_id: Option<Uuid>, pub editor_x: Option<f64>, pub editor_y: Option<f64>, } -/// Request to start a chain (kept for backwards compatibility) -#[derive(Debug, Clone, Deserialize, ToSchema)] +/// Edge in directive chain graph +#[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] -pub struct StartChainRequest { - /// Repository URL (reserved for future use) - pub repository_url: Option<String>, +pub struct DirectiveChainGraphEdge { + pub source: Uuid, + pub target: Uuid, } -/// Response when starting a chain -#[derive(Debug, Clone, Serialize, ToSchema)] +/// Start directive response +#[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] -pub struct StartChainResponse { +pub struct StartDirectiveResponse { + pub directive_id: Uuid, pub chain_id: Uuid, - /// Root contracts created (those with no dependencies) - pub contracts_created: Vec<Uuid>, + pub chain_generation: i32, + pub steps: Vec<ChainStep>, pub status: String, } -/// Graph node for definitions (before contracts are created) -#[derive(Debug, Clone, Serialize, ToSchema)] +/// Request to create a verifier +#[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] -pub struct ChainDefinitionGraphNode { - pub id: Uuid, +pub struct CreateVerifierRequest { 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>, + pub verifier_type: String, + pub command: Option<String>, + pub working_directory: Option<String>, + pub timeout_seconds: Option<i32>, + pub environment: Option<serde_json::Value>, + pub weight: Option<f64>, + pub required: Option<bool>, } -/// Graph response for definitions -#[derive(Debug, Clone, Serialize, ToSchema)] +/// Request to update a verifier +#[derive(Debug, Deserialize, 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>, +pub struct UpdateVerifierRequest { + pub name: Option<String>, + pub command: Option<String>, + pub working_directory: Option<String>, + pub timeout_seconds: Option<i32>, + pub weight: Option<f64>, + pub required: Option<bool>, + pub enabled: Option<bool>, } -// ============================================================================= -// Chain Directives (formal specification documents for directive-driven chains) -// ============================================================================= - -/// Chain directive - formal specification document that drives chain creation and evaluation -#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] +/// Approval action request +#[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] -pub struct ChainDirective { - pub id: Uuid, - pub chain_id: Uuid, - pub version: i32, - /// Requirements as JSON: [{ id, title, description, priority, category, parentId? }] - #[sqlx(json)] - pub requirements: serde_json::Value, - /// Acceptance criteria as JSON: [{ id, requirementIds[], description, testable, verificationMethod }] - #[sqlx(json)] - pub acceptance_criteria: serde_json::Value, - /// Constraints as JSON: [{ id, type, description, impact }] - #[sqlx(json)] - pub constraints: serde_json::Value, - /// External dependencies as JSON: [{ id, name, type, status, requiredBy[] }] - #[sqlx(json)] - pub external_dependencies: serde_json::Value, - /// Source type: 'manual', 'llm_generated', 'imported' - pub source_type: String, - pub created_at: DateTime<Utc>, - pub updated_at: DateTime<Utc>, +pub struct ApprovalActionRequest { + pub response: Option<String>, } -/// Requirement in a directive +/// Directive requirement (shared type used in directive specification) #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct DirectiveRequirement { pub id: String, pub title: String, pub description: String, - /// Priority: 'must', 'should', 'could', 'wont' pub priority: String, - /// Category: 'feature', 'infrastructure', 'testing', etc. pub category: Option<String>, - /// Parent requirement ID for hierarchical requirements + #[serde(skip_serializing_if = "Option::is_none")] pub parent_id: Option<String>, } -/// Acceptance criterion in a directive +/// Directive acceptance criterion #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct DirectiveAcceptanceCriterion { pub id: String, - /// Requirement IDs this criterion validates + #[serde(default)] pub requirement_ids: Vec<String>, pub description: String, + #[serde(default = "default_true")] pub testable: bool, - /// Verification method: 'automated', 'manual', 'review', 'llm' - pub verification_method: String, + pub verification_method: Option<String>, } -/// Constraint in a directive -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct DirectiveConstraint { - pub id: String, - /// Type: 'technical', 'business', 'time', 'resource' - #[serde(rename = "type")] - pub constraint_type: String, - pub description: String, - /// Impact: 'high', 'medium', 'low' - pub impact: String, +fn default_true() -> bool { + true } -/// External dependency in a directive -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct DirectiveExternalDependency { - pub id: String, - pub name: String, - /// Type: 'api', 'service', 'library', 'data' - #[serde(rename = "type")] - pub dependency_type: String, - /// Status: 'available', 'pending', 'blocked' - pub status: String, - /// Requirement IDs that need this dependency - pub required_by: Vec<String>, -} +// Old chain types (Chain, ChainContract, ChainContractDefinition, ChainDirective, +// ContractEvaluation, ChainEvent, ChainRepository, etc.) have been replaced by +// the directive system above: Directive, DirectiveChain, ChainStep, +// DirectiveEvaluation, DirectiveEvent, DirectiveVerifier, DirectiveApproval. -/// Request to create or update a chain directive -#[derive(Debug, Clone, Deserialize, ToSchema)] +// Legacy types kept temporarily for chain runner/parser compatibility during migration. +// These will be removed once the chain daemon module is replaced. + +/// Request payload for creating a new chain (legacy - used by chain runner) +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct CreateChainDirectiveRequest { - pub requirements: Option<Vec<DirectiveRequirement>>, - pub acceptance_criteria: Option<Vec<DirectiveAcceptanceCriterion>>, - pub constraints: Option<Vec<DirectiveConstraint>>, - pub external_dependencies: Option<Vec<DirectiveExternalDependency>>, - pub source_type: Option<String>, +pub struct CreateChainRequest { + pub name: String, + pub description: Option<String>, + pub repository_url: Option<String>, + pub repositories: Option<Vec<AddChainRepositoryRequest>>, + pub loop_enabled: Option<bool>, + pub loop_max_iterations: Option<i32>, + pub loop_progress_check: Option<String>, + pub contracts: Option<Vec<CreateChainContractRequest>>, } -/// Request to initialize a directive-driven chain -#[derive(Debug, Clone, Deserialize, ToSchema)] +/// Request to add a repository to a chain (legacy - used by chain runner) +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct InitChainRequest { - /// High-level goal/description for the directive contract - pub goal: String, - /// Repository URL for chain contracts +pub struct AddChainRepositoryRequest { + pub name: String, pub repository_url: Option<String>, - /// Local path for chain contracts pub local_path: Option<String>, - /// Whether to enable phase guard (user approval between phases) + #[serde(default = "default_source_type")] + pub source_type: String, #[serde(default)] - pub phase_guard: bool, -} - -/// Response from initializing a directive-driven chain -#[derive(Debug, Clone, Serialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct InitChainResponse { - pub chain_id: Uuid, - pub directive_contract_id: Uuid, - pub supervisor_task_id: Option<Uuid>, -} - -// ============================================================================= -// Contract Evaluations (LLM evaluation results for completed contracts) -// ============================================================================= - -/// Evaluation status for chain contracts -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "snake_case")] -pub enum EvaluationStatus { - /// Not yet evaluated - Pending, - /// Currently being evaluated - Evaluating, - /// Evaluation passed - Passed, - /// Evaluation failed - Failed, - /// Contract is being reworked after failed evaluation - Rework, - /// Max retries exceeded, escalated to user - Escalated, - /// User approved despite partial failure - ApprovedWithIssues, -} - -impl std::fmt::Display for EvaluationStatus { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Pending => write!(f, "pending"), - Self::Evaluating => write!(f, "evaluating"), - Self::Passed => write!(f, "passed"), - Self::Failed => write!(f, "failed"), - Self::Rework => write!(f, "rework"), - Self::Escalated => write!(f, "escalated"), - Self::ApprovedWithIssues => write!(f, "approved_with_issues"), - } - } -} - -impl std::str::FromStr for EvaluationStatus { - type Err = String; - - fn from_str(s: &str) -> Result<Self, Self::Err> { - match s.to_lowercase().as_str() { - "pending" => Ok(Self::Pending), - "evaluating" => Ok(Self::Evaluating), - "passed" => Ok(Self::Passed), - "failed" => Ok(Self::Failed), - "rework" => Ok(Self::Rework), - "escalated" => Ok(Self::Escalated), - "approved_with_issues" => Ok(Self::ApprovedWithIssues), - _ => Err(format!("Unknown evaluation status: {}", s)), - } - } -} - -/// Contract evaluation - LLM evaluation result after contract completion -#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct ContractEvaluation { - pub id: Uuid, - pub contract_id: Uuid, - pub chain_id: Option<Uuid>, - pub chain_contract_id: Option<Uuid>, - /// Evaluation attempt number (1-based) - pub evaluation_number: i32, - /// Model used for evaluation - pub evaluator_model: Option<String>, - /// Whether the evaluation passed - pub passed: bool, - /// Overall score (0.0-1.0) - pub overall_score: Option<f64>, - /// Per-criterion results as JSON - #[sqlx(json)] - pub criteria_results: serde_json::Value, - /// Summary feedback from the evaluator - pub summary_feedback: String, - /// Instructions for rework if evaluation failed - pub rework_instructions: Option<String>, - /// Snapshot of directive at evaluation time - pub directive_snapshot: Option<serde_json::Value>, - /// Snapshot of deliverables at evaluation time - pub deliverables_snapshot: Option<serde_json::Value>, - pub started_at: DateTime<Utc>, - pub completed_at: Option<DateTime<Utc>>, - pub created_at: DateTime<Utc>, -} - -/// Per-criterion evaluation result -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct EvaluationCriterionResult { - pub criterion_id: String, - pub criterion_text: String, - pub passed: bool, - /// Score (0.0-1.0) - pub score: f64, - pub feedback: String, - /// Evidence supporting the evaluation - pub evidence: Vec<String>, -} - -/// Request to create a contract evaluation -#[derive(Debug, Clone, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct CreateContractEvaluationRequest { - pub contract_id: Uuid, - pub chain_id: Option<Uuid>, - pub chain_contract_id: Option<Uuid>, - pub evaluator_model: Option<String>, - pub passed: bool, - pub overall_score: Option<f64>, - pub criteria_results: Vec<EvaluationCriterionResult>, - pub summary_feedback: String, - pub rework_instructions: Option<String>, + pub is_primary: bool, } -/// Summary of contract evaluation for list views -#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct ContractEvaluationSummary { - pub id: Uuid, - pub contract_id: Uuid, - pub evaluation_number: i32, - pub passed: bool, - pub overall_score: Option<f64>, - pub summary_feedback: String, - pub created_at: DateTime<Utc>, +fn default_source_type() -> String { + "remote".to_string() } -/// Response listing evaluations for a chain or contract -#[derive(Debug, Clone, Serialize, ToSchema)] +/// Request to create a contract within a chain (legacy - used by chain runner) +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct ContractEvaluationsResponse { - pub evaluations: Vec<ContractEvaluationSummary>, - pub total: i64, +pub struct CreateChainContractRequest { + pub name: String, + pub description: Option<String>, + #[serde(default)] + pub contract_type: Option<String>, + pub initial_phase: Option<String>, + pub phases: Option<Vec<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>, } -/// Traceability matrix entry - maps requirements to contracts -#[derive(Debug, Clone, Serialize, ToSchema)] +/// Task definition within a chain contract (legacy - used by chain runner) +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct TraceabilityEntry { - pub requirement_id: String, - pub requirement_title: String, - pub contract_definition_ids: Vec<Uuid>, - pub contract_definition_names: Vec<String>, - pub acceptance_criteria_ids: Vec<String>, +pub struct CreateChainTaskRequest { + pub name: String, + pub plan: String, } -/// Response for directive traceability -#[derive(Debug, Clone, Serialize, ToSchema)] +/// Deliverable definition within a chain contract (legacy - used by chain runner) +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct DirectiveTraceabilityResponse { - pub chain_id: Uuid, - pub entries: Vec<TraceabilityEntry>, - /// Requirements not mapped to any contract - pub uncovered_requirements: Vec<String>, +pub struct CreateChainDeliverableRequest { + pub id: String, + pub name: String, + pub priority: Option<String>, } // ============================================================================= diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs index 7be7bc8..eeda4a5 100644 --- a/makima/src/db/repository.rs +++ b/makima/src/db/repository.rs @@ -6,23 +6,22 @@ use sqlx::PgPool; use uuid::Uuid; use super::models::{ - AddChainRepositoryRequest, AddContractDefinitionRequest, AddContractToChainRequest, Chain, - ChainContract, ChainContractDefinition, ChainContractDetail, ChainDefinitionGraphNode, - ChainDefinitionGraphResponse, ChainDirective, ChainEditorContract, ChainEditorData, - ChainEditorDeliverable, ChainEditorEdge, ChainEditorNode, ChainEditorTask, ChainEvent, - ChainGraphEdge, ChainGraphNode, ChainGraphResponse, ChainRepository, ChainSummary, - ChainWithContracts, CheckpointPatch, CheckpointPatchInfo, Contract, ContractChatConversation, - ContractChatMessageRecord, ContractEvaluation, ContractEvaluationSummary, ContractEvent, - ContractRepository, ContractSummary, ContractTypeTemplateRecord, ConversationMessage, - ConversationSnapshot, CreateChainDirectiveRequest, CreateChainRequest, - CreateContractEvaluationRequest, CreateContractRequest, CreateFileRequest, CreateTaskRequest, - CreateTemplateRequest, Daemon, DaemonTaskAssignment, DaemonWithCapacity, DeliverableDefinition, - DirectiveTraceabilityResponse, EvaluationCriterionResult, File, FileSummary, FileVersion, - HistoryEvent, HistoryQueryFilters, InitChainRequest, InitChainResponse, MeshChatConversation, - MeshChatMessageRecord, PhaseChangeResult, PhaseConfig, PhaseDefinition, - SupervisorHeartbeatRecord, SupervisorState, Task, TaskCheckpoint, TaskEvent, TaskSummary, - TraceabilityEntry, UpdateChainRequest, UpdateContractDefinitionRequest, UpdateContractRequest, - UpdateFileRequest, UpdateTaskRequest, UpdateTemplateRequest, + // Core types + CheckpointPatch, CheckpointPatchInfo, Contract, ContractChatConversation, + ContractChatMessageRecord, ContractEvent, ContractRepository, ContractSummary, + ContractTypeTemplateRecord, ConversationMessage, ConversationSnapshot, + CreateContractRequest, CreateFileRequest, CreateTaskRequest, CreateTemplateRequest, + Daemon, DaemonTaskAssignment, DaemonWithCapacity, DeliverableDefinition, + File, FileSummary, FileVersion, HistoryEvent, HistoryQueryFilters, + MeshChatConversation, MeshChatMessageRecord, PhaseChangeResult, PhaseConfig, + PhaseDefinition, SupervisorHeartbeatRecord, SupervisorState, Task, TaskCheckpoint, + TaskEvent, TaskSummary, UpdateContractRequest, UpdateFileRequest, UpdateTaskRequest, + UpdateTemplateRequest, + // Directive types + AddStepRequest, ChainStep, CreateDirectiveRequest, Directive, DirectiveApproval, + DirectiveChain, DirectiveChainGraphEdge, DirectiveChainGraphNode, DirectiveChainGraphResponse, + DirectiveEvaluation, DirectiveEvent, DirectiveSummary, DirectiveVerifier, + DirectiveWithProgress, UpdateDirectiveRequest, UpdateStepRequest, }; /// Repository error types. @@ -4906,46 +4905,140 @@ pub async fn sync_supervisor_state( } // ============================================================================= -// Chain Operations (DAG of contracts for multi-contract orchestration) +// Directive Operations (top-level orchestration entity) // ============================================================================= +// TODO: Implement directive CRUD functions +// - create_directive_for_owner +// - get_directive_for_owner +// - list_directives_for_owner +// - update_directive_for_owner +// - archive_directive_for_owner +// - update_directive_status -/// Create a new chain for a specific owner. -pub async fn create_chain_for_owner( +// ============================================================================= +// Directive Chain Operations (generated execution plans) +// ============================================================================= +// TODO: Implement chain CRUD functions +// - create_directive_chain +// - get_current_chain +// - supersede_chain + +// ============================================================================= +// Chain Step Operations (nodes in the DAG) +// ============================================================================= +// TODO: Implement step CRUD functions +// - create_chain_step +// - update_chain_step +// - delete_chain_step +// - find_ready_steps +// - update_step_status +// - update_step_contract +// - update_step_confidence +// - increment_step_rework_count + +// ============================================================================= +// Directive Evaluation Operations +// ============================================================================= +// TODO: Implement evaluation functions +// - create_directive_evaluation +// - list_step_evaluations +// - list_directive_evaluations + +// ============================================================================= +// Directive Event Operations (audit stream) +// ============================================================================= +// TODO: Implement event functions +// - emit_directive_event +// - list_directive_events + +// ============================================================================= +// Directive Verifier Operations +// ============================================================================= +// TODO: Implement verifier CRUD functions +// - create_directive_verifier +// - list_directive_verifiers +// - update_directive_verifier + +// ============================================================================= +// Directive Approval Operations (human-in-the-loop) +// ============================================================================= +// TODO: Implement approval functions +// - create_approval_request +// - resolve_approval +// - list_pending_approvals + +// NOTE: Old chain functions removed. See git history for reference. +// Old functions included: create_chain_for_owner, get_chain_for_owner, +// list_chains_for_owner, update_chain_for_owner, delete_chain_for_owner, +// add_contract_to_chain, remove_contract_from_chain, list_chain_contracts, +// get_chain_with_contracts, list_chain_repositories, add_chain_repository, +// delete_chain_repository, set_chain_repository_primary, get_chain_graph, +// record_chain_event, list_chain_events, increment_chain_loop, complete_chain, +// get_ready_chain_contracts, is_chain_complete, get_chain_editor_data, +// create_chain_contract_definition, list_chain_contract_definitions, +// update_chain_contract_definition, delete_chain_contract_definition, +// get_chain_definition_graph, update_chain_status, progress_chain, +// create_chain_directive, get_chain_directive, update_chain_directive, +// delete_chain_directive, create_contract_evaluation, get_contract_evaluation, +// list_chain_evaluations, update_chain_contract_evaluation_status, +// mark_chain_contract_original_completion, get_chain_contract_by_contract_id, +// init_chain_for_owner. + +// ============================================================================= +// Directive Operations +// ============================================================================= + +/// Create a new directive for an owner. +pub async fn create_directive_for_owner( pool: &PgPool, owner_id: Uuid, - req: CreateChainRequest, -) -> Result<Chain, sqlx::Error> { - let loop_enabled = req.loop_enabled.unwrap_or(false); - let loop_max_iterations = req.loop_max_iterations.unwrap_or(10); - - sqlx::query_as::<_, Chain>( - r#" - INSERT INTO chains (owner_id, name, description, loop_enabled, loop_max_iterations, loop_progress_check) - VALUES ($1, $2, $3, $4, $5, $6) + req: CreateDirectiveRequest, +) -> Result<Directive, sqlx::Error> { + let title = req.title.unwrap_or_else(|| truncate_string(&req.goal, 100)); + let autonomy_level = req.autonomy_level.unwrap_or_else(|| "guardrails".to_string()); + let green_threshold = req.confidence_threshold_green.unwrap_or(0.85); + let yellow_threshold = req.confidence_threshold_yellow.unwrap_or(0.60); + let requirements = req.requirements.unwrap_or(serde_json::json!([])); + let acceptance_criteria = req.acceptance_criteria.unwrap_or(serde_json::json!([])); + + sqlx::query_as::<_, Directive>( + r#" + INSERT INTO directives ( + owner_id, title, goal, requirements, acceptance_criteria, + autonomy_level, confidence_threshold_green, confidence_threshold_yellow, + repository_url, local_path, base_branch, + max_total_cost_usd, max_wall_time_minutes + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING * "#, ) .bind(owner_id) - .bind(&req.name) - .bind(&req.description) - .bind(loop_enabled) - .bind(loop_max_iterations) - .bind(&req.loop_progress_check) + .bind(&title) + .bind(&req.goal) + .bind(&requirements) + .bind(&acceptance_criteria) + .bind(&autonomy_level) + .bind(green_threshold) + .bind(yellow_threshold) + .bind(&req.repository_url) + .bind(&req.local_path) + .bind(&req.base_branch) + .bind(req.max_total_cost_usd) + .bind(req.max_wall_time_minutes) .fetch_one(pool) .await } -/// Get a chain by ID, scoped to owner. -pub async fn get_chain_for_owner( +/// Get a directive by ID, scoped to owner. +pub async fn get_directive_for_owner( pool: &PgPool, id: Uuid, owner_id: Uuid, -) -> Result<Option<Chain>, sqlx::Error> { - sqlx::query_as::<_, Chain>( +) -> Result<Option<Directive>, sqlx::Error> { + sqlx::query_as::<_, Directive>( r#" - SELECT * - FROM chains - WHERE id = $1 AND owner_id = $2 + SELECT * FROM directives WHERE id = $1 AND owner_id = $2 "#, ) .bind(id) @@ -4954,817 +5047,485 @@ pub async fn get_chain_for_owner( .await } -/// Get a chain by ID (no owner check - for internal use). -pub async fn get_chain(pool: &PgPool, id: Uuid) -> Result<Option<Chain>, sqlx::Error> { - sqlx::query_as::<_, Chain>( - r#" - SELECT * - FROM chains - WHERE id = $1 - "#, +/// Get a directive by ID (no owner check - for internal use). +pub async fn get_directive(pool: &PgPool, id: Uuid) -> Result<Option<Directive>, sqlx::Error> { + sqlx::query_as::<_, Directive>( + r#"SELECT * FROM directives WHERE id = $1"#, ) .bind(id) .fetch_optional(pool) .await } -/// List chains for a specific owner. -pub async fn list_chains_for_owner( +/// List directives for an owner. +pub async fn list_directives_for_owner( pool: &PgPool, owner_id: Uuid, -) -> Result<Vec<ChainSummary>, sqlx::Error> { - sqlx::query_as::<_, ChainSummary>( - r#" - SELECT - c.id, - c.name, - c.description, - c.status, - c.loop_enabled, - c.loop_current_iteration, - COUNT(DISTINCT cc.contract_id) as contract_count, - COUNT(DISTINCT CASE WHEN con.status = 'completed' THEN cc.contract_id END) as completed_count, - c.version, - c.created_at - FROM chains c - LEFT JOIN chain_contracts cc ON cc.chain_id = c.id - LEFT JOIN contracts con ON con.id = cc.contract_id - WHERE c.owner_id = $1 - GROUP BY c.id - ORDER BY c.created_at DESC - "#, - ) - .bind(owner_id) - .fetch_all(pool) - .await + status_filter: Option<&str>, +) -> Result<Vec<DirectiveSummary>, sqlx::Error> { + let query = if let Some(status) = status_filter { + sqlx::query_as::<_, DirectiveSummary>( + r#" + SELECT + d.id, d.title, d.goal, d.status, d.autonomy_level, + dc.current_confidence, + COALESCE(dc.completed_steps, 0) as completed_steps, + COALESCE(dc.total_steps, 0) as total_steps, + d.chain_generation_count, d.started_at, d.created_at + FROM directives d + LEFT JOIN directive_chains dc ON dc.id = d.current_chain_id + WHERE d.owner_id = $1 AND d.status = $2 + ORDER BY d.created_at DESC + "#, + ) + .bind(owner_id) + .bind(status) + } else { + sqlx::query_as::<_, DirectiveSummary>( + r#" + SELECT + d.id, d.title, d.goal, d.status, d.autonomy_level, + dc.current_confidence, + COALESCE(dc.completed_steps, 0) as completed_steps, + COALESCE(dc.total_steps, 0) as total_steps, + d.chain_generation_count, d.started_at, d.created_at + FROM directives d + LEFT JOIN directive_chains dc ON dc.id = d.current_chain_id + WHERE d.owner_id = $1 + ORDER BY d.created_at DESC + "#, + ) + .bind(owner_id) + }; + query.fetch_all(pool).await } -/// Update a chain. -pub async fn update_chain_for_owner( +/// Update a directive with optimistic locking. +pub async fn update_directive_for_owner( pool: &PgPool, id: Uuid, owner_id: Uuid, - req: UpdateChainRequest, -) -> Result<Chain, RepositoryError> { - // First get current version if optimistic locking requested - if let Some(expected_version) = req.version { - let current: Option<(i32,)> = sqlx::query_as( - "SELECT version FROM chains WHERE id = $1 AND owner_id = $2", - ) - .bind(id) - .bind(owner_id) - .fetch_optional(pool) - .await?; + req: UpdateDirectiveRequest, +) -> Result<Directive, RepositoryError> { + // First get current version + let current = sqlx::query_scalar::<_, i32>( + "SELECT version FROM directives WHERE id = $1 AND owner_id = $2" + ) + .bind(id) + .bind(owner_id) + .fetch_optional(pool) + .await? + .ok_or_else(|| RepositoryError::Database(sqlx::Error::RowNotFound))?; - if let Some((actual_version,)) = current { - if actual_version != expected_version { - return Err(RepositoryError::VersionConflict { - expected: expected_version, - actual: actual_version, - }); - } - } + if current != req.version { + return Err(RepositoryError::VersionConflict { + expected: req.version, + actual: current, + }); } - let result = sqlx::query_as::<_, Chain>( - r#" - UPDATE chains - SET - name = COALESCE($3, name), - description = COALESCE($4, description), - status = COALESCE($5, status), - loop_enabled = COALESCE($6, loop_enabled), - loop_max_iterations = COALESCE($7, loop_max_iterations), - loop_progress_check = COALESCE($8, loop_progress_check), + let directive = sqlx::query_as::<_, Directive>( + r#" + UPDATE directives SET + title = COALESCE($3, title), + goal = COALESCE($4, goal), + requirements = COALESCE($5, requirements), + acceptance_criteria = COALESCE($6, acceptance_criteria), + constraints = COALESCE($7, constraints), + external_dependencies = COALESCE($8, external_dependencies), + autonomy_level = COALESCE($9, autonomy_level), + confidence_threshold_green = COALESCE($10, confidence_threshold_green), + confidence_threshold_yellow = COALESCE($11, confidence_threshold_yellow), + max_total_cost_usd = COALESCE($12, max_total_cost_usd), + max_wall_time_minutes = COALESCE($13, max_wall_time_minutes), + max_rework_cycles = COALESCE($14, max_rework_cycles), + max_chain_regenerations = COALESCE($15, max_chain_regenerations), version = version + 1, updated_at = NOW() - WHERE id = $1 AND owner_id = $2 + WHERE id = $1 AND owner_id = $2 AND version = $16 RETURNING * "#, ) .bind(id) .bind(owner_id) - .bind(&req.name) - .bind(&req.description) - .bind(&req.status) - .bind(req.loop_enabled) - .bind(req.loop_max_iterations) - .bind(&req.loop_progress_check) + .bind(&req.title) + .bind(&req.goal) + .bind(&req.requirements) + .bind(&req.acceptance_criteria) + .bind(&req.constraints) + .bind(&req.external_dependencies) + .bind(&req.autonomy_level) + .bind(req.confidence_threshold_green) + .bind(req.confidence_threshold_yellow) + .bind(req.max_total_cost_usd) + .bind(req.max_wall_time_minutes) + .bind(req.max_rework_cycles) + .bind(req.max_chain_regenerations) + .bind(req.version) .fetch_one(pool) .await?; - Ok(result) + Ok(directive) } -/// Delete (archive) a chain. -pub async fn delete_chain_for_owner( +/// Update directive status. +pub async fn update_directive_status( pool: &PgPool, id: Uuid, - owner_id: Uuid, -) -> Result<bool, sqlx::Error> { - let result = sqlx::query( - r#" - UPDATE chains - SET status = 'archived', updated_at = NOW() - WHERE id = $1 AND owner_id = $2 - "#, - ) - .bind(id) - .bind(owner_id) - .execute(pool) - .await?; - - Ok(result.rows_affected() > 0) -} - -/// Add a contract to a chain. -pub async fn add_contract_to_chain( - pool: &PgPool, - chain_id: Uuid, - contract_id: Uuid, - depends_on: Vec<Uuid>, - order_index: i32, - editor_x: Option<f64>, - editor_y: Option<f64>, -) -> Result<ChainContract, sqlx::Error> { - // Also update the contract's chain_id - sqlx::query("UPDATE contracts SET chain_id = $1 WHERE id = $2") - .bind(chain_id) - .bind(contract_id) - .execute(pool) - .await?; - - sqlx::query_as::<_, ChainContract>( + status: &str, +) -> Result<Directive, sqlx::Error> { + sqlx::query_as::<_, Directive>( r#" - INSERT INTO chain_contracts (chain_id, contract_id, depends_on, order_index, editor_x, editor_y) - VALUES ($1, $2, $3, $4, $5, $6) - ON CONFLICT (chain_id, contract_id) DO UPDATE SET - depends_on = EXCLUDED.depends_on, - order_index = EXCLUDED.order_index, - editor_x = EXCLUDED.editor_x, - editor_y = EXCLUDED.editor_y + UPDATE directives SET + status = $2, + started_at = CASE WHEN $2 = 'active' AND started_at IS NULL THEN NOW() ELSE started_at END, + completed_at = CASE WHEN $2 IN ('completed', 'failed', 'archived') THEN NOW() ELSE completed_at END, + updated_at = NOW() + WHERE id = $1 RETURNING * "#, ) - .bind(chain_id) - .bind(contract_id) - .bind(&depends_on) - .bind(order_index) - .bind(editor_x) - .bind(editor_y) + .bind(id) + .bind(status) .fetch_one(pool) .await } -/// Remove a contract from a chain. -pub async fn remove_contract_from_chain( +/// Archive a directive (soft delete). +pub async fn archive_directive_for_owner( pool: &PgPool, - chain_id: Uuid, - contract_id: Uuid, + id: Uuid, + owner_id: Uuid, ) -> Result<bool, sqlx::Error> { - // Clear the contract's chain_id - sqlx::query("UPDATE contracts SET chain_id = NULL WHERE id = $1 AND chain_id = $2") - .bind(contract_id) - .bind(chain_id) - .execute(pool) - .await?; - let result = sqlx::query( r#" - DELETE FROM chain_contracts - WHERE chain_id = $1 AND contract_id = $2 + UPDATE directives SET status = 'archived', updated_at = NOW() + WHERE id = $1 AND owner_id = $2 "#, ) - .bind(chain_id) - .bind(contract_id) + .bind(id) + .bind(owner_id) .execute(pool) .await?; - Ok(result.rows_affected() > 0) } -/// List contracts in a chain with their details. -pub async fn list_chain_contracts( - pool: &PgPool, - chain_id: Uuid, -) -> Result<Vec<ChainContractDetail>, sqlx::Error> { - sqlx::query_as::<_, ChainContractDetail>( - r#" - SELECT - cc.id as chain_contract_id, - cc.contract_id, - c.name as contract_name, - c.status as contract_status, - c.phase as contract_phase, - cc.depends_on, - cc.order_index, - cc.editor_x, - cc.editor_y, - cc.evaluation_status, - cc.evaluation_retry_count, - cc.max_evaluation_retries, - cc.created_at - FROM chain_contracts cc - JOIN contracts c ON c.id = cc.contract_id - WHERE cc.chain_id = $1 - ORDER BY cc.order_index ASC - "#, - ) - .bind(chain_id) - .fetch_all(pool) - .await -} - -/// Get chain with all contracts for detail view. -pub async fn get_chain_with_contracts( +/// Get directive with full progress info. +pub async fn get_directive_with_progress( pool: &PgPool, - chain_id: Uuid, + id: Uuid, owner_id: Uuid, -) -> Result<Option<ChainWithContracts>, sqlx::Error> { - let chain = get_chain_for_owner(pool, chain_id, owner_id).await?; - - match chain { - Some(chain) => { - let contracts = list_chain_contracts(pool, chain_id).await?; - let repositories = list_chain_repositories(pool, chain_id).await?; - Ok(Some(ChainWithContracts { - chain, - contracts, - repositories, - })) - } - None => Ok(None), - } -} +) -> Result<Option<DirectiveWithProgress>, sqlx::Error> { + let directive = match get_directive_for_owner(pool, id, owner_id).await? { + Some(d) => d, + None => return Ok(None), + }; -// ============================================================================= -// Chain Repository Operations -// ============================================================================= + let chain = if let Some(chain_id) = directive.current_chain_id { + get_directive_chain(pool, chain_id).await? + } else { + None + }; -/// List all repositories for a chain. -pub async fn list_chain_repositories( - pool: &PgPool, - chain_id: Uuid, -) -> Result<Vec<ChainRepository>, sqlx::Error> { - sqlx::query_as::<_, ChainRepository>( - r#" - SELECT * - FROM chain_repositories - WHERE chain_id = $1 - ORDER BY is_primary DESC, created_at ASC - "#, - ) - .bind(chain_id) - .fetch_all(pool) - .await -} + let steps = if let Some(ref c) = chain { + list_chain_steps(pool, c.id).await? + } else { + vec![] + }; -/// Get a chain repository by ID. -pub async fn get_chain_repository( - pool: &PgPool, - chain_id: Uuid, - repository_id: Uuid, -) -> Result<Option<ChainRepository>, sqlx::Error> { - sqlx::query_as::<_, ChainRepository>( - r#" - SELECT * - FROM chain_repositories - WHERE id = $1 AND chain_id = $2 - "#, - ) - .bind(repository_id) - .bind(chain_id) - .fetch_optional(pool) - .await + let recent_events = list_directive_events(pool, id, Some(20)).await?; + let pending_approvals = list_pending_approvals(pool, id).await?; + + Ok(Some(DirectiveWithProgress { + directive, + chain, + steps, + recent_events, + pending_approvals, + })) } -/// Add a repository to a chain. -pub async fn add_chain_repository( +// ============================================================================= +// Directive Chain Operations +// ============================================================================= + +/// Create a new chain generation for a directive. +pub async fn create_directive_chain( pool: &PgPool, - chain_id: Uuid, - req: &AddChainRepositoryRequest, -) -> Result<ChainRepository, sqlx::Error> { - // If is_primary, clear other primaries first - if req.is_primary { - sqlx::query( - r#" - UPDATE chain_repositories - SET is_primary = false, updated_at = NOW() - WHERE chain_id = $1 AND is_primary = true - "#, - ) - .bind(chain_id) - .execute(pool) - .await?; - } + directive_id: Uuid, + name: &str, + description: Option<&str>, + rationale: Option<&str>, + planning_model: Option<&str>, +) -> Result<DirectiveChain, sqlx::Error> { + // Get next generation number + let generation = sqlx::query_scalar::<_, i32>( + "SELECT COALESCE(MAX(generation), 0) + 1 FROM directive_chains WHERE directive_id = $1" + ) + .bind(directive_id) + .fetch_one(pool) + .await?; - sqlx::query_as::<_, ChainRepository>( + let chain = sqlx::query_as::<_, DirectiveChain>( r#" - INSERT INTO chain_repositories (chain_id, name, repository_url, local_path, source_type, status, is_primary) - VALUES ($1, $2, $3, $4, $5, 'ready', $6) + INSERT INTO directive_chains (directive_id, generation, name, description, rationale, planning_model) + VALUES ($1, $2, $3, $4, $5, $6) RETURNING * "#, ) - .bind(chain_id) - .bind(&req.name) - .bind(&req.repository_url) - .bind(&req.local_path) - .bind(&req.source_type) - .bind(req.is_primary) + .bind(directive_id) + .bind(generation) + .bind(name) + .bind(description) + .bind(rationale) + .bind(planning_model) .fetch_one(pool) - .await -} - -/// Delete a repository from a chain. -pub async fn delete_chain_repository( - pool: &PgPool, - chain_id: Uuid, - repository_id: Uuid, -) -> Result<bool, sqlx::Error> { - let result = sqlx::query( - r#" - DELETE FROM chain_repositories - WHERE id = $1 AND chain_id = $2 - "#, - ) - .bind(repository_id) - .bind(chain_id) - .execute(pool) .await?; - Ok(result.rows_affected() > 0) -} - -/// Set a repository as primary for a chain. -pub async fn set_chain_repository_primary( - pool: &PgPool, - chain_id: Uuid, - repository_id: Uuid, -) -> Result<ChainRepository, sqlx::Error> { - // Clear existing primary + // Update directive to point to new chain and increment generation count sqlx::query( r#" - UPDATE chain_repositories - SET is_primary = false, updated_at = NOW() - WHERE chain_id = $1 AND is_primary = true + UPDATE directives SET + current_chain_id = $2, + chain_generation_count = chain_generation_count + 1, + updated_at = NOW() + WHERE id = $1 "#, ) - .bind(chain_id) + .bind(directive_id) + .bind(chain.id) .execute(pool) .await?; - // Set new primary - sqlx::query_as::<_, ChainRepository>( - r#" - UPDATE chain_repositories - SET is_primary = true, updated_at = NOW() - WHERE id = $1 AND chain_id = $2 - RETURNING * - "#, - ) - .bind(repository_id) - .bind(chain_id) - .fetch_one(pool) - .await + Ok(chain) } -/// Get the primary repository for a chain. -pub async fn get_chain_primary_repository( - pool: &PgPool, - chain_id: Uuid, -) -> Result<Option<ChainRepository>, sqlx::Error> { - sqlx::query_as::<_, ChainRepository>( - r#" - SELECT * - FROM chain_repositories - WHERE chain_id = $1 AND is_primary = true - "#, +/// Get a directive chain by ID. +pub async fn get_directive_chain(pool: &PgPool, id: Uuid) -> Result<Option<DirectiveChain>, sqlx::Error> { + sqlx::query_as::<_, DirectiveChain>( + "SELECT * FROM directive_chains WHERE id = $1" ) - .bind(chain_id) + .bind(id) .fetch_optional(pool) .await } -/// Get chain graph structure for visualization. -pub async fn get_chain_graph( - pool: &PgPool, - chain_id: Uuid, -) -> Result<Option<ChainGraphResponse>, sqlx::Error> { - let chain = get_chain(pool, chain_id).await?; - - match chain { - Some(chain) => { - let contracts = list_chain_contracts(pool, chain_id).await?; - - let nodes: Vec<ChainGraphNode> = contracts - .iter() - .map(|c| ChainGraphNode { - id: c.chain_contract_id, - contract_id: c.contract_id, - name: c.contract_name.clone(), - status: c.contract_status.clone(), - phase: c.contract_phase.clone(), - x: c.editor_x.unwrap_or(0.0), - y: c.editor_y.unwrap_or(0.0), - }) - .collect(); - - let mut edges: Vec<ChainGraphEdge> = Vec::new(); - for contract in &contracts { - for dep_id in &contract.depends_on { - // Find the chain_contract_id for this dependency - if let Some(dep) = contracts.iter().find(|c| c.contract_id == *dep_id) { - edges.push(ChainGraphEdge { - from: dep.chain_contract_id, - to: contract.chain_contract_id, - }); - } - } - } - - Ok(Some(ChainGraphResponse { - chain_id: chain.id, - chain_name: chain.name, - chain_status: chain.status, - nodes, - edges, - })) - } - None => Ok(None), - } -} - -/// Record a chain event. -pub async fn record_chain_event( - pool: &PgPool, - chain_id: Uuid, - event_type: &str, - contract_id: Option<Uuid>, - event_data: Option<serde_json::Value>, -) -> Result<ChainEvent, sqlx::Error> { - sqlx::query_as::<_, ChainEvent>( +/// Get the current chain for a directive. +pub async fn get_current_chain(pool: &PgPool, directive_id: Uuid) -> Result<Option<DirectiveChain>, sqlx::Error> { + sqlx::query_as::<_, DirectiveChain>( r#" - INSERT INTO chain_events (chain_id, event_type, contract_id, event_data) - VALUES ($1, $2, $3, $4) - RETURNING * + SELECT dc.* FROM directive_chains dc + JOIN directives d ON d.current_chain_id = dc.id + WHERE d.id = $1 "#, ) - .bind(chain_id) - .bind(event_type) - .bind(contract_id) - .bind(event_data) - .fetch_one(pool) + .bind(directive_id) + .fetch_optional(pool) .await } -/// List chain events. -pub async fn list_chain_events( +/// Update chain status. +pub async fn update_chain_status( pool: &PgPool, chain_id: Uuid, -) -> Result<Vec<ChainEvent>, sqlx::Error> { - sqlx::query_as::<_, ChainEvent>( - r#" - SELECT * - FROM chain_events - WHERE chain_id = $1 - ORDER BY created_at DESC - "#, - ) - .bind(chain_id) - .fetch_all(pool) - .await -} - -/// Increment chain loop iteration. -pub async fn increment_chain_loop(pool: &PgPool, chain_id: Uuid) -> Result<Chain, sqlx::Error> { - sqlx::query_as::<_, Chain>( + status: &str, +) -> Result<DirectiveChain, sqlx::Error> { + sqlx::query_as::<_, DirectiveChain>( r#" - UPDATE chains - SET loop_current_iteration = COALESCE(loop_current_iteration, 0) + 1, + UPDATE directive_chains SET + status = $2, + started_at = CASE WHEN $2 = 'active' AND started_at IS NULL THEN NOW() ELSE started_at END, + completed_at = CASE WHEN $2 IN ('completed', 'failed', 'superseded') THEN NOW() ELSE completed_at END, updated_at = NOW() WHERE id = $1 RETURNING * "#, ) .bind(chain_id) + .bind(status) .fetch_one(pool) .await } -/// Mark a chain as completed. -pub async fn complete_chain(pool: &PgPool, chain_id: Uuid) -> Result<Chain, sqlx::Error> { - sqlx::query_as::<_, Chain>( +/// Supersede a chain (mark as superseded and update directive). +pub async fn supersede_chain(pool: &PgPool, chain_id: Uuid) -> Result<(), sqlx::Error> { + sqlx::query( r#" - UPDATE chains - SET status = 'completed', - updated_at = NOW() + UPDATE directive_chains SET status = 'superseded', completed_at = NOW(), updated_at = NOW() WHERE id = $1 - RETURNING * - "#, - ) - .bind(chain_id) - .fetch_one(pool) - .await -} - -/// Get contracts in a chain that have no pending dependencies (ready to start). -/// Returns contracts where all depends_on contracts are completed. -pub async fn get_ready_chain_contracts( - pool: &PgPool, - chain_id: Uuid, -) -> Result<Vec<ChainContractDetail>, sqlx::Error> { - sqlx::query_as::<_, ChainContractDetail>( - r#" - SELECT - cc.id as chain_contract_id, - cc.contract_id, - c.name as contract_name, - c.status as contract_status, - c.phase as contract_phase, - cc.depends_on, - cc.order_index, - cc.editor_x, - cc.editor_y - FROM chain_contracts cc - JOIN contracts c ON c.id = cc.contract_id - WHERE cc.chain_id = $1 - AND c.status = 'active' - AND ( - -- No dependencies - cc.depends_on IS NULL - OR array_length(cc.depends_on, 1) IS NULL - OR array_length(cc.depends_on, 1) = 0 - -- Or all dependencies completed - OR NOT EXISTS ( - SELECT 1 - FROM unnest(cc.depends_on) AS dep_id - JOIN contracts dep ON dep.id = dep_id - WHERE dep.status != 'completed' - ) - ) - ORDER BY cc.order_index ASC "#, ) .bind(chain_id) - .fetch_all(pool) - .await -} - -/// Check if all contracts in a chain are completed. -pub async fn is_chain_complete(pool: &PgPool, chain_id: Uuid) -> Result<bool, sqlx::Error> { - let result: (i64,) = sqlx::query_as( - r#" - SELECT COUNT(*) - FROM chain_contracts cc - JOIN contracts c ON c.id = cc.contract_id - WHERE cc.chain_id = $1 - AND c.status != 'completed' - "#, - ) - .bind(chain_id) - .fetch_one(pool) + .execute(pool) .await?; - - Ok(result.0 == 0) -} - -/// Get chain editor data for the GUI editor. -pub async fn get_chain_editor_data( - pool: &PgPool, - chain_id: Uuid, - owner_id: Uuid, -) -> Result<Option<ChainEditorData>, sqlx::Error> { - let chain = get_chain_for_owner(pool, chain_id, owner_id).await?; - - match chain { - Some(chain) => { - let contracts = list_chain_contracts(pool, chain_id).await?; - let repositories = list_chain_repositories(pool, chain_id).await?; - - // Build nodes - let nodes: Vec<ChainEditorNode> = contracts - .iter() - .map(|c| ChainEditorNode { - id: c.contract_id.to_string(), - x: c.editor_x.unwrap_or(0.0), - y: c.editor_y.unwrap_or(0.0), - contract: ChainEditorContract { - name: c.contract_name.clone(), - description: None, // Would need to join with full contract data - contract_type: "simple".to_string(), - phases: vec!["plan".to_string(), "execute".to_string()], - tasks: vec![], - deliverables: vec![], - }, - }) - .collect(); - - // Build edges - let edges: Vec<ChainEditorEdge> = contracts - .iter() - .flat_map(|c| { - c.depends_on.iter().map(move |dep_id| ChainEditorEdge { - from: dep_id.to_string(), - to: c.contract_id.to_string(), - }) - }) - .collect(); - - Ok(Some(ChainEditorData { - id: Some(chain.id), - name: chain.name, - description: chain.description, - repositories, - loop_enabled: chain.loop_enabled, - loop_max_iterations: chain.loop_max_iterations, - loop_progress_check: chain.loop_progress_check, - nodes, - edges, - })) - } - None => Ok(None), - } + Ok(()) } // ============================================================================= -// Chain Contract Definition Operations +// Chain Step Operations // ============================================================================= -/// Create a new contract definition in a chain. -pub async fn create_chain_contract_definition( +/// Create a new step in a chain. +pub async fn create_chain_step( 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", + req: AddStepRequest, +) -> Result<ChainStep, sqlx::Error> { + let step_type = req.step_type.unwrap_or_else(|| "execute".to_string()); + let contract_type = req.contract_type.unwrap_or_else(|| "simple".to_string()); + let phases = req.phases.unwrap_or_default(); + let depends_on = req.depends_on.unwrap_or_default(); + let requirement_ids = req.requirement_ids.unwrap_or_default(); + let acceptance_criteria_ids = req.acceptance_criteria_ids.unwrap_or_default(); + let verifier_config = req.verifier_config.unwrap_or(serde_json::json!({})); + + // Get next order index + let order_index = sqlx::query_scalar::<_, i32>( + "SELECT COALESCE(MAX(order_index), 0) + 1 FROM chain_steps WHERE chain_id = $1" ) .bind(chain_id) .fetch_one(pool) .await?; - let order_index = max_order.unwrap_or(-1) + 1; - - // Convert tasks, deliverables, and validation 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 validation_json = req - .validation - .as_ref() - .map(|v| serde_json::to_value(v).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, validation, editor_x, editor_y, order_index) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) + let step = sqlx::query_as::<_, ChainStep>( + r#" + INSERT INTO chain_steps ( + chain_id, name, description, step_type, contract_type, + initial_phase, task_plan, phases, depends_on, parallel_group, + requirement_ids, acceptance_criteria_ids, verifier_config, + editor_x, editor_y, order_index + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) RETURNING * "#, ) .bind(chain_id) .bind(&req.name) .bind(&req.description) - .bind(&req.contract_type) + .bind(&step_type) + .bind(&contract_type) .bind(&req.initial_phase) - .bind(&depends_on_names) - .bind(&tasks_json) - .bind(&deliverables_json) - .bind(&validation_json) - .bind(req.editor_x) - .bind(req.editor_y) + .bind(&req.task_plan) + .bind(&phases) + .bind(&depends_on) + .bind(&req.parallel_group) + .bind(&requirement_ids) + .bind(&acceptance_criteria_ids) + .bind(&verifier_config) + .bind(req.editor_x.unwrap_or(0.0)) + .bind(req.editor_y.unwrap_or(0.0)) .bind(order_index) .fetch_one(pool) - .await -} + .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 - "#, + // Update chain total_steps count + sqlx::query( + "UPDATE directive_chains SET total_steps = total_steps + 1, updated_at = NOW() WHERE id = $1" ) .bind(chain_id) - .fetch_all(pool) - .await + .execute(pool) + .await?; + + Ok(step) } -/// 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", +/// Get a chain step by ID. +pub async fn get_chain_step(pool: &PgPool, id: Uuid) -> Result<Option<ChainStep>, sqlx::Error> { + sqlx::query_as::<_, ChainStep>( + "SELECT * FROM chain_steps WHERE id = $1" ) - .bind(definition_id) + .bind(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()); - let validation_json = req - .validation - .as_ref() - .map(|v| serde_json::to_value(v).unwrap()); +/// List all steps in a chain. +pub async fn list_chain_steps(pool: &PgPool, chain_id: Uuid) -> Result<Vec<ChainStep>, sqlx::Error> { + sqlx::query_as::<_, ChainStep>( + "SELECT * FROM chain_steps WHERE chain_id = $1 ORDER BY order_index" + ) + .bind(chain_id) + .fetch_all(pool) + .await +} - sqlx::query_as::<_, ChainContractDefinition>( +/// Update a chain step. +pub async fn update_chain_step( + pool: &PgPool, + step_id: Uuid, + req: UpdateStepRequest, +) -> Result<ChainStep, sqlx::Error> { + sqlx::query_as::<_, ChainStep>( r#" - UPDATE chain_contract_definitions SET + UPDATE chain_steps 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), - validation = COALESCE($9, validation), - editor_x = COALESCE($10, editor_x), - editor_y = COALESCE($11, editor_y) + task_plan = COALESCE($4, task_plan), + depends_on = COALESCE($5, depends_on), + requirement_ids = COALESCE($6, requirement_ids), + acceptance_criteria_ids = COALESCE($7, acceptance_criteria_ids), + verifier_config = COALESCE($8, verifier_config), + editor_x = COALESCE($9, editor_x), + editor_y = COALESCE($10, editor_y) WHERE id = $1 RETURNING * "#, ) - .bind(definition_id) + .bind(step_id) .bind(&req.name) .bind(&req.description) - .bind(&req.contract_type) - .bind(&req.initial_phase) + .bind(&req.task_plan) .bind(&req.depends_on) - .bind(&tasks_json) - .bind(&deliverables_json) - .bind(&validation_json) + .bind(&req.requirement_ids) + .bind(&req.acceptance_criteria_ids) + .bind(&req.verifier_config) .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) +/// Delete a chain step. +pub async fn delete_chain_step(pool: &PgPool, step_id: Uuid) -> Result<bool, sqlx::Error> { + // Get chain_id first for updating count + let chain_id = sqlx::query_scalar::<_, Uuid>( + "SELECT chain_id FROM chain_steps WHERE id = $1" + ) + .bind(step_id) + .fetch_optional(pool) + .await?; + + let result = sqlx::query("DELETE FROM chain_steps WHERE id = $1") + .bind(step_id) .execute(pool) .await?; + + // Update chain total_steps count + if let Some(cid) = chain_id { + sqlx::query( + "UPDATE directive_chains SET total_steps = total_steps - 1, updated_at = NOW() WHERE id = $1" + ) + .bind(cid) + .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 +/// Find steps that are ready to execute (all dependencies met, status=pending). +pub async fn find_ready_steps(pool: &PgPool, chain_id: Uuid) -> Result<Vec<ChainStep>, sqlx::Error> { + sqlx::query_as::<_, ChainStep>( + r#" + SELECT s.* FROM chain_steps s + WHERE s.chain_id = $1 + AND s.status = 'pending' + AND NOT EXISTS ( + SELECT 1 FROM chain_steps dep + WHERE dep.id = ANY(s.depends_on) + AND dep.status NOT IN ('passed', 'skipped') + ) + ORDER BY s.order_index "#, ) .bind(chain_id) @@ -5772,909 +5533,500 @@ pub async fn get_ready_definitions( .await } -/// Get the definition graph for visualization. -pub async fn get_chain_definition_graph( +/// Update step status. +pub async fn update_step_status( 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, - })) -} - -/// Update chain status. -pub async fn update_chain_status( - pool: &PgPool, - chain_id: Uuid, + step_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(()) -} - -// ============================================================================= -// Chain Progression -// ============================================================================= - -/// Result of chain progression check -#[derive(Debug)] -pub struct ChainProgressionResult { - /// Contracts created from ready definitions - pub contracts_created: Vec<Uuid>, - /// Whether all definitions are instantiated and completed (chain is done) - pub chain_completed: bool, -} - -/// Progress a chain by creating contracts from ready definitions. -/// -/// This is called when a contract in the chain completes. It: -/// 1. Finds definitions whose dependencies are all satisfied (completed) -/// 2. Creates contracts from those definitions -/// 3. Links them to the chain -/// 4. Checks if chain is complete (all definitions instantiated and completed) -pub async fn progress_chain( - pool: &PgPool, - chain_id: Uuid, - owner_id: Uuid, -) -> Result<ChainProgressionResult, sqlx::Error> { - let mut contracts_created = Vec::new(); - - // Get all definitions for this chain - let definitions = list_chain_contract_definitions(pool, chain_id).await?; - if definitions.is_empty() { - return Ok(ChainProgressionResult { - contracts_created: vec![], - chain_completed: true, - }); - } - - // Get existing chain contracts to know what's already instantiated - let chain_contracts = list_chain_contracts(pool, chain_id).await?; - - // Build a map of definition name -> instantiated contract status - let instantiated: std::collections::HashMap<String, Option<String>> = chain_contracts - .iter() - .map(|cc| (cc.contract_name.clone(), Some(cc.contract_status.clone()))) - .collect(); - - // Find definitions that are ready to be instantiated: - // - Not yet instantiated - // - All dependencies are instantiated AND completed - for def in &definitions { - // Skip if already instantiated - if instantiated.contains_key(&def.name) { - continue; - } - - // Check if all dependencies are completed - let deps_satisfied = def.depends_on_names.iter().all(|dep_name| { - instantiated - .get(dep_name) - .map(|status| status.as_deref() == Some("completed")) - .unwrap_or(false) - }); - - // Root definitions (no dependencies) are always ready - let is_root = def.depends_on_names.is_empty(); - - if is_root || deps_satisfied { - // Create contract from definition - match create_contract_from_definition(pool, chain_id, owner_id, def).await { - Ok(contract_id) => { - contracts_created.push(contract_id); - tracing::info!( - chain_id = %chain_id, - definition_name = %def.name, - contract_id = %contract_id, - "Created contract from chain definition" - ); - } - Err(e) => { - tracing::error!( - chain_id = %chain_id, - definition_name = %def.name, - error = %e, - "Failed to create contract from chain definition" - ); - } - } - } - } - - // Check if chain is complete (all definitions instantiated and completed) - let updated_contracts = list_chain_contracts(pool, chain_id).await?; - let all_instantiated = definitions.len() == updated_contracts.len(); - let all_completed = updated_contracts - .iter() - .all(|cc| cc.contract_status == "completed"); - let chain_completed = all_instantiated && all_completed; - - if chain_completed { - update_chain_status(pool, chain_id, "completed").await?; - tracing::info!(chain_id = %chain_id, "Chain completed - all contracts done"); - } - - Ok(ChainProgressionResult { - contracts_created, - chain_completed, - }) -} - -/// Task definition parsed from JSON (matches chain YAML format) -#[derive(Debug, Clone, serde::Deserialize)] -struct ChainTaskDef { - name: String, - plan: String, -} - -/// Validation config parsed from definition JSON -#[derive(Debug, Clone, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -struct ValidationConfig { - #[serde(default)] - check_deliverables: bool, - #[serde(default)] - run_tests: bool, - check_content: Option<String>, - #[serde(default = "default_on_failure_str")] - on_failure: String, - #[serde(default = "default_max_retries_val")] - max_retries: i32, -} - -fn default_on_failure_str() -> String { - "block".to_string() -} - -fn default_max_retries_val() -> i32 { - 3 -} - -/// Generate a validation plan for a checkpoint contract. -fn generate_checkpoint_plan( - def: &ChainContractDefinition, - upstream_contracts: &[&ChainContractDetail], - validation: &ValidationConfig, -) -> String { - let upstream_names: Vec<&str> = upstream_contracts.iter().map(|c| c.contract_name.as_str()).collect(); - - let mut plan = format!( - r#"# Checkpoint Validation: {} - -You are validating the outputs of upstream contracts before allowing downstream work to proceed. - -## Upstream Contracts to Validate -{} - -"#, - def.name, - upstream_names.iter().map(|n| format!("- {}", n)).collect::<Vec<_>>().join("\n") - ); - - // Add deliverables check section - if validation.check_deliverables { - plan.push_str(r#"## Deliverables Check -Verify that all required deliverables from upstream contracts exist and are properly completed. - -Use the makima CLI to check contract status: -```bash -makima contract status <contract_id> -``` - -For each upstream contract, verify: -1. Contract status is "completed" -2. All required deliverables are marked as complete -3. Deliverable content exists and is not empty - -"#); - } - - // Add tests check section - if validation.run_tests { - plan.push_str(r#"## Tests Check -Run the test suite to verify the codebase is in a good state. - -```bash -# Run tests appropriate for the project type -npm test # for Node.js projects -cargo test # for Rust projects -pytest # for Python projects -go test ./... # for Go projects -``` - -Verify: -1. All tests pass -2. No new test failures introduced -3. Test coverage is acceptable - -"#); - } - - // Add custom content check section - if let Some(content_check) = &validation.check_content { - plan.push_str(&format!(r#"## Custom Validation Criteria -{} - -"#, content_check)); - } - - // Add validation result section - plan.push_str(&format!(r#"## Reporting Results - -After completing all validation checks, you must report the result: - -**If ALL checks pass:** -Mark this checkpoint contract as completed using: -```bash -makima supervisor complete -``` - -**If ANY check fails (on_failure: "{}"):** -"#, validation.on_failure)); - - match validation.on_failure.as_str() { - "block" => plan.push_str(r#" -- Document the failure reason clearly -- Do NOT mark the contract as complete -- The chain will be blocked until issues are resolved manually -"#), - "retry" => plan.push_str(&format!(r#" -- Document the failure reason -- Request retry of the failed upstream contract (max {} retries) -- Use: `makima supervisor ask "Upstream validation failed. Retry?" --choices "Yes,No"` -"#, validation.max_retries)), - "warn" => plan.push_str(r#" -- Document the warning/issue found -- Mark the contract as complete anyway (downstream will proceed) -- Log the warning for visibility -"#), - _ => plan.push_str(r#" -- Document the failure reason -- Do NOT mark the contract as complete -"#), - } - - plan.push_str(r#" -## Begin Validation - -Start by checking the status of each upstream contract, then proceed with the validation criteria above. -"#); - - plan -} - -/// Create a contract from a chain definition. -async fn create_contract_from_definition( - pool: &PgPool, - chain_id: Uuid, - owner_id: Uuid, - def: &ChainContractDefinition, -) -> Result<Uuid, sqlx::Error> { - // Get the existing contracts to find dependency info - let existing_contracts = list_chain_contracts(pool, chain_id).await?; - let name_to_contract: std::collections::HashMap<&str, &ChainContractDetail> = existing_contracts - .iter() - .map(|cc| (cc.contract_name.as_str(), cc)) - .collect(); - - // Resolve dependency names to contract details - let upstream_contracts: Vec<&ChainContractDetail> = def - .depends_on_names - .iter() - .filter_map(|name| name_to_contract.get(name.as_str()).copied()) - .collect(); - - // Create the contract request with basic fields - let req = CreateContractRequest { - name: def.name.clone(), - description: def.description.clone(), - contract_type: Some(def.contract_type.clone()), - initial_phase: def.initial_phase.clone(), - template_id: None, - autonomous_loop: None, - phase_guard: None, - local_only: None, - auto_merge_local: None, - }; - - // Create the contract - let contract = create_contract_for_owner(pool, owner_id, req).await?; - - // For checkpoint contracts, generate a validation plan - if def.contract_type == "checkpoint" { - // Parse validation config - let validation: ValidationConfig = def - .validation - .as_ref() - .and_then(|v| serde_json::from_value(v.clone()).ok()) - .unwrap_or(ValidationConfig { - check_deliverables: true, - run_tests: false, - check_content: None, - on_failure: default_on_failure_str(), - max_retries: default_max_retries_val(), - }); - - // Generate validation plan - let validation_plan = generate_checkpoint_plan(def, &upstream_contracts, &validation); - - // Create a supervisor task with the validation plan - let task_req = CreateTaskRequest { - contract_id: Some(contract.id), - name: format!("Validate: {}", def.name), - description: Some("Checkpoint validation task".to_string()), - plan: validation_plan, - parent_task_id: None, - is_supervisor: true, // Checkpoint uses supervisor task for validation - priority: 0, - repository_url: None, - base_branch: None, - target_branch: None, - merge_mode: None, - target_repo_path: None, - completion_action: None, - continue_from_task_id: None, - copy_files: None, - checkpoint_sha: None, - branched_from_task_id: None, - conversation_history: None, - supervisor_worktree_task_id: None, - }; - - if let Err(e) = create_task_for_owner(pool, owner_id, task_req).await { - tracing::warn!( - contract_id = %contract.id, - error = %e, - "Failed to create validation task for checkpoint contract" - ); - } +) -> Result<ChainStep, sqlx::Error> { + let step = sqlx::query_as::<_, ChainStep>( + r#" + UPDATE chain_steps SET + status = $2, + started_at = CASE WHEN $2 = 'running' AND started_at IS NULL THEN NOW() ELSE started_at END, + completed_at = CASE WHEN $2 IN ('passed', 'failed', 'skipped') THEN NOW() ELSE completed_at END + WHERE id = $1 + RETURNING * + "#, + ) + .bind(step_id) + .bind(status) + .fetch_one(pool) + .await?; - // Set initial validation status + // Update chain completed_steps and failed_steps counts + if status == "passed" || status == "skipped" { sqlx::query( - "UPDATE chain_contracts SET validation_status = 'pending' WHERE chain_id = $1 AND contract_id = $2", + "UPDATE directive_chains SET completed_steps = completed_steps + 1, updated_at = NOW() WHERE id = $1" ) - .bind(chain_id) - .bind(contract.id) + .bind(step.chain_id) .execute(pool) .await?; - } else { - // Parse and create tasks from definition for regular contracts - if let Some(tasks_json) = &def.tasks { - if let Ok(tasks) = serde_json::from_value::<Vec<ChainTaskDef>>(tasks_json.clone()) { - for task_def in tasks { - let task_req = CreateTaskRequest { - contract_id: Some(contract.id), - name: task_def.name, - description: None, - plan: task_def.plan, - parent_task_id: None, - is_supervisor: false, - priority: 0, - repository_url: None, - base_branch: None, - target_branch: None, - merge_mode: None, - target_repo_path: None, - completion_action: None, - continue_from_task_id: None, - copy_files: None, - checkpoint_sha: None, - branched_from_task_id: None, - conversation_history: None, - supervisor_worktree_task_id: None, - }; - if let Err(e) = create_task_for_owner(pool, owner_id, task_req).await { - tracing::warn!( - contract_id = %contract.id, - error = %e, - "Failed to create task from chain definition" - ); - } - } - } - } - } - - // Resolve dependency names to contract IDs - let depends_on: Vec<Uuid> = upstream_contracts.iter().map(|c| c.contract_id).collect(); - - // Link contract to chain - add_contract_to_chain( - pool, - chain_id, - contract.id, - depends_on, - def.order_index, - def.editor_x, - def.editor_y, - ) - .await?; - - // Update chain_contracts with definition_id link - sqlx::query( - "UPDATE chain_contracts SET definition_id = $1 WHERE chain_id = $2 AND contract_id = $3", - ) - .bind(def.id) - .bind(chain_id) - .bind(contract.id) - .execute(pool) - .await?; - - // Copy repositories from chain to contract - let chain_repos = list_chain_repositories(pool, chain_id).await.unwrap_or_default(); - for repo in chain_repos { - if let Some(url) = &repo.repository_url { - // Remote repository - if let Err(e) = add_remote_repository(pool, contract.id, &repo.name, url, repo.is_primary).await { - tracing::warn!( - contract_id = %contract.id, - repo_name = %repo.name, - error = %e, - "Failed to copy repository from chain to contract" - ); - } - } else if let Some(path) = &repo.local_path { - // Local repository - if let Err(e) = add_local_repository(pool, contract.id, &repo.name, path, repo.is_primary).await { - tracing::warn!( - contract_id = %contract.id, - repo_name = %repo.name, - error = %e, - "Failed to copy local repository from chain to contract" - ); - } - } - } - - // Activate the contract so it can start - sqlx::query("UPDATE contracts SET status = 'active' WHERE id = $1") - .bind(contract.id) + } else if status == "failed" { + sqlx::query( + "UPDATE directive_chains SET failed_steps = failed_steps + 1, updated_at = NOW() WHERE id = $1" + ) + .bind(step.chain_id) .execute(pool) .await?; + } - tracing::info!( - contract_id = %contract.id, - contract_name = %def.name, - chain_id = %chain_id, - "Contract created and activated from chain definition" - ); - - Ok(contract.id) + Ok(step) } -// ============================================================================= -// Chain Directives -// ============================================================================= - -/// Create a directive for a chain. -pub async fn create_chain_directive( +/// Link a step to a contract. +pub async fn update_step_contract( pool: &PgPool, - chain_id: Uuid, - req: CreateChainDirectiveRequest, -) -> Result<ChainDirective, sqlx::Error> { - let requirements = serde_json::to_value(&req.requirements.unwrap_or_default()) - .unwrap_or(serde_json::json!([])); - let acceptance_criteria = serde_json::to_value(&req.acceptance_criteria.unwrap_or_default()) - .unwrap_or(serde_json::json!([])); - let constraints = - serde_json::to_value(&req.constraints.unwrap_or_default()).unwrap_or(serde_json::json!([])); - let external_dependencies = - serde_json::to_value(&req.external_dependencies.unwrap_or_default()) - .unwrap_or(serde_json::json!([])); - let source_type = req.source_type.unwrap_or_else(|| "llm_generated".to_string()); - - sqlx::query_as::<_, ChainDirective>( - r#" - INSERT INTO chain_directives (chain_id, requirements, acceptance_criteria, constraints, external_dependencies, source_type) - VALUES ($1, $2, $3, $4, $5, $6) + step_id: Uuid, + contract_id: Uuid, + supervisor_task_id: Option<Uuid>, +) -> Result<ChainStep, sqlx::Error> { + sqlx::query_as::<_, ChainStep>( + r#" + UPDATE chain_steps SET contract_id = $2, supervisor_task_id = $3 + WHERE id = $1 RETURNING * "#, ) - .bind(chain_id) - .bind(&requirements) - .bind(&acceptance_criteria) - .bind(&constraints) - .bind(&external_dependencies) - .bind(&source_type) + .bind(step_id) + .bind(contract_id) + .bind(supervisor_task_id) .fetch_one(pool) .await } -/// Get the directive for a chain. -pub async fn get_chain_directive( +/// Update step confidence score and level. +pub async fn update_step_confidence( pool: &PgPool, - chain_id: Uuid, -) -> Result<Option<ChainDirective>, sqlx::Error> { - sqlx::query_as::<_, ChainDirective>( + step_id: Uuid, + score: f64, + level: &str, + evaluation_id: Uuid, +) -> Result<ChainStep, sqlx::Error> { + sqlx::query_as::<_, ChainStep>( r#" - SELECT * - FROM chain_directives - WHERE chain_id = $1 + UPDATE chain_steps SET + confidence_score = $2, + confidence_level = $3, + last_evaluation_id = $4, + evaluation_count = evaluation_count + 1 + WHERE id = $1 + RETURNING * "#, ) - .bind(chain_id) - .fetch_optional(pool) + .bind(step_id) + .bind(score) + .bind(level) + .bind(evaluation_id) + .fetch_one(pool) .await } -/// Update a chain directive. -pub async fn update_chain_directive( - pool: &PgPool, - chain_id: Uuid, - req: CreateChainDirectiveRequest, -) -> Result<ChainDirective, sqlx::Error> { - let requirements = req - .requirements - .map(|r| serde_json::to_value(&r).unwrap_or(serde_json::json!([]))); - let acceptance_criteria = req - .acceptance_criteria - .map(|ac| serde_json::to_value(&ac).unwrap_or(serde_json::json!([]))); - let constraints = req - .constraints - .map(|c| serde_json::to_value(&c).unwrap_or(serde_json::json!([]))); - let external_dependencies = req - .external_dependencies - .map(|ed| serde_json::to_value(&ed).unwrap_or(serde_json::json!([]))); - - sqlx::query_as::<_, ChainDirective>( - r#" - UPDATE chain_directives SET - requirements = COALESCE($2, requirements), - acceptance_criteria = COALESCE($3, acceptance_criteria), - constraints = COALESCE($4, constraints), - external_dependencies = COALESCE($5, external_dependencies), - source_type = COALESCE($6, source_type), - version = version + 1, - updated_at = NOW() - WHERE chain_id = $1 +/// Increment step rework count. +pub async fn increment_step_rework_count(pool: &PgPool, step_id: Uuid) -> Result<ChainStep, sqlx::Error> { + sqlx::query_as::<_, ChainStep>( + r#" + UPDATE chain_steps SET rework_count = rework_count + 1, status = 'rework' + WHERE id = $1 RETURNING * "#, ) - .bind(chain_id) - .bind(&requirements) - .bind(&acceptance_criteria) - .bind(&constraints) - .bind(&external_dependencies) - .bind(&req.source_type) + .bind(step_id) .fetch_one(pool) .await } -/// Delete a chain directive. -pub async fn delete_chain_directive(pool: &PgPool, chain_id: Uuid) -> Result<bool, sqlx::Error> { - let result = sqlx::query("DELETE FROM chain_directives WHERE chain_id = $1") - .bind(chain_id) - .execute(pool) - .await?; - Ok(result.rows_affected() > 0) -} - -/// Get directive traceability (requirement -> contract mapping). -pub async fn get_directive_traceability( +/// Get chain graph for visualization. +pub async fn get_chain_graph( pool: &PgPool, chain_id: Uuid, -) -> Result<DirectiveTraceabilityResponse, sqlx::Error> { - // Get the directive - let directive = get_chain_directive(pool, chain_id).await?; - - // Get all contract definitions with their requirement mappings - let definitions = list_chain_contract_definitions(pool, chain_id).await?; - - // Parse requirements from directive - let requirements: Vec<super::models::DirectiveRequirement> = directive - .as_ref() - .and_then(|d| serde_json::from_value(d.requirements.clone()).ok()) - .unwrap_or_default(); - - // Build traceability entries - let mut entries: Vec<TraceabilityEntry> = Vec::new(); - let mut covered_requirements: std::collections::HashSet<String> = - std::collections::HashSet::new(); - - for req in &requirements { - let mut contract_def_ids: Vec<Uuid> = Vec::new(); - let mut contract_def_names: Vec<String> = Vec::new(); - - for def in &definitions { - if def.requirement_ids.contains(&req.id) { - contract_def_ids.push(def.id); - contract_def_names.push(def.name.clone()); - covered_requirements.insert(req.id.clone()); - } +) -> Result<DirectiveChainGraphResponse, sqlx::Error> { + let chain = get_directive_chain(pool, chain_id).await? + .ok_or_else(|| sqlx::Error::RowNotFound)?; + + let steps = list_chain_steps(pool, chain_id).await?; + + let nodes: Vec<DirectiveChainGraphNode> = steps.iter().map(|s| { + DirectiveChainGraphNode { + id: s.id, + name: s.name.clone(), + step_type: s.step_type.clone(), + status: s.status.clone(), + confidence_score: s.confidence_score, + confidence_level: s.confidence_level.clone(), + contract_id: s.contract_id, + editor_x: s.editor_x, + editor_y: s.editor_y, + } + }).collect(); + + let mut edges = Vec::new(); + for step in &steps { + for dep_id in &step.depends_on { + edges.push(DirectiveChainGraphEdge { + source: *dep_id, + target: step.id, + }); } - - // Get acceptance criteria for this requirement - let acceptance_criteria: Vec<super::models::DirectiveAcceptanceCriterion> = directive - .as_ref() - .and_then(|d| serde_json::from_value(d.acceptance_criteria.clone()).ok()) - .unwrap_or_default(); - - let ac_ids: Vec<String> = acceptance_criteria - .iter() - .filter(|ac| ac.requirement_ids.contains(&req.id)) - .map(|ac| ac.id.clone()) - .collect(); - - entries.push(TraceabilityEntry { - requirement_id: req.id.clone(), - requirement_title: req.title.clone(), - contract_definition_ids: contract_def_ids, - contract_definition_names: contract_def_names, - acceptance_criteria_ids: ac_ids, - }); } - // Find uncovered requirements - let uncovered: Vec<String> = requirements - .iter() - .filter(|r| !covered_requirements.contains(&r.id)) - .map(|r| r.id.clone()) - .collect(); - - Ok(DirectiveTraceabilityResponse { + Ok(DirectiveChainGraphResponse { chain_id, - entries, - uncovered_requirements: uncovered, + directive_id: chain.directive_id, + nodes, + edges, }) } // ============================================================================= -// Contract Evaluations +// Directive Evaluation Operations // ============================================================================= -/// Create a contract evaluation record. -pub async fn create_contract_evaluation( +/// Create a directive evaluation. +pub async fn create_directive_evaluation( pool: &PgPool, - req: CreateContractEvaluationRequest, -) -> Result<ContractEvaluation, sqlx::Error> { - let criteria_results = serde_json::to_value(&req.criteria_results).unwrap_or(serde_json::json!([])); + directive_id: Uuid, + chain_id: Option<Uuid>, + step_id: Option<Uuid>, + contract_id: Option<Uuid>, + evaluation_type: &str, + evaluator: Option<&str>, + passed: bool, + overall_score: Option<f64>, + confidence_level: Option<&str>, + programmatic_results: serde_json::Value, + llm_results: serde_json::Value, + criteria_results: serde_json::Value, + summary_feedback: &str, + rework_instructions: Option<&str>, +) -> Result<DirectiveEvaluation, sqlx::Error> { + // Get next evaluation number for this step/directive + let evaluation_number = if let Some(sid) = step_id { + sqlx::query_scalar::<_, i32>( + "SELECT COALESCE(MAX(evaluation_number), 0) + 1 FROM directive_evaluations WHERE step_id = $1" + ) + .bind(sid) + .fetch_one(pool) + .await? + } else { + sqlx::query_scalar::<_, i32>( + "SELECT COALESCE(MAX(evaluation_number), 0) + 1 FROM directive_evaluations WHERE directive_id = $1 AND step_id IS NULL" + ) + .bind(directive_id) + .fetch_one(pool) + .await? + }; - sqlx::query_as::<_, ContractEvaluation>( + sqlx::query_as::<_, DirectiveEvaluation>( r#" - INSERT INTO contract_evaluations ( - contract_id, chain_id, chain_contract_id, - evaluator_model, passed, overall_score, - criteria_results, summary_feedback, rework_instructions, + INSERT INTO directive_evaluations ( + directive_id, chain_id, step_id, contract_id, + evaluation_type, evaluation_number, evaluator, + passed, overall_score, confidence_level, + programmatic_results, llm_results, criteria_results, + summary_feedback, rework_instructions, completed_at ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW()) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, NOW()) RETURNING * "#, ) - .bind(req.contract_id) - .bind(req.chain_id) - .bind(req.chain_contract_id) - .bind(&req.evaluator_model) - .bind(req.passed) - .bind(req.overall_score) + .bind(directive_id) + .bind(chain_id) + .bind(step_id) + .bind(contract_id) + .bind(evaluation_type) + .bind(evaluation_number) + .bind(evaluator) + .bind(passed) + .bind(overall_score) + .bind(confidence_level) + .bind(&programmatic_results) + .bind(&llm_results) .bind(&criteria_results) - .bind(&req.summary_feedback) - .bind(&req.rework_instructions) + .bind(summary_feedback) + .bind(rework_instructions) .fetch_one(pool) .await } -/// Get a contract evaluation by ID. -pub async fn get_contract_evaluation( +/// List evaluations for a step. +pub async fn list_step_evaluations( pool: &PgPool, - id: Uuid, -) -> Result<Option<ContractEvaluation>, sqlx::Error> { - sqlx::query_as::<_, ContractEvaluation>( - r#" - SELECT * - FROM contract_evaluations - WHERE id = $1 - "#, + step_id: Uuid, +) -> Result<Vec<DirectiveEvaluation>, sqlx::Error> { + sqlx::query_as::<_, DirectiveEvaluation>( + "SELECT * FROM directive_evaluations WHERE step_id = $1 ORDER BY evaluation_number DESC" ) - .bind(id) - .fetch_optional(pool) + .bind(step_id) + .fetch_all(pool) .await } -/// List evaluations for a contract. -pub async fn list_contract_evaluations( +/// List evaluations for a directive. +pub async fn list_directive_evaluations( pool: &PgPool, - contract_id: Uuid, -) -> Result<Vec<ContractEvaluationSummary>, sqlx::Error> { - sqlx::query_as::<_, ContractEvaluationSummary>( - r#" - SELECT id, contract_id, evaluation_number, passed, overall_score, summary_feedback, created_at - FROM contract_evaluations - WHERE contract_id = $1 - ORDER BY evaluation_number DESC - "#, + directive_id: Uuid, + limit: Option<i64>, +) -> Result<Vec<DirectiveEvaluation>, sqlx::Error> { + let limit = limit.unwrap_or(100); + sqlx::query_as::<_, DirectiveEvaluation>( + "SELECT * FROM directive_evaluations WHERE directive_id = $1 ORDER BY created_at DESC LIMIT $2" ) - .bind(contract_id) + .bind(directive_id) + .bind(limit) .fetch_all(pool) .await } -/// List evaluations for a chain. -pub async fn list_chain_evaluations( +// ============================================================================= +// Directive Event Operations +// ============================================================================= + +/// Emit a directive event. +pub async fn emit_directive_event( pool: &PgPool, - chain_id: Uuid, -) -> Result<Vec<ContractEvaluationSummary>, sqlx::Error> { - sqlx::query_as::<_, ContractEvaluationSummary>( + directive_id: Uuid, + chain_id: Option<Uuid>, + step_id: Option<Uuid>, + event_type: &str, + severity: &str, + event_data: Option<serde_json::Value>, + actor_type: &str, + actor_id: Option<Uuid>, +) -> Result<DirectiveEvent, sqlx::Error> { + sqlx::query_as::<_, DirectiveEvent>( r#" - SELECT id, contract_id, evaluation_number, passed, overall_score, summary_feedback, created_at - FROM contract_evaluations - WHERE chain_id = $1 - ORDER BY created_at DESC + INSERT INTO directive_events ( + directive_id, chain_id, step_id, event_type, severity, event_data, actor_type, actor_id + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + RETURNING * "#, ) + .bind(directive_id) .bind(chain_id) + .bind(step_id) + .bind(event_type) + .bind(severity) + .bind(event_data) + .bind(actor_type) + .bind(actor_id) + .fetch_one(pool) + .await +} + +/// List directive events. +pub async fn list_directive_events( + pool: &PgPool, + directive_id: Uuid, + limit: Option<i64>, +) -> Result<Vec<DirectiveEvent>, sqlx::Error> { + let limit = limit.unwrap_or(100); + sqlx::query_as::<_, DirectiveEvent>( + "SELECT * FROM directive_events WHERE directive_id = $1 ORDER BY created_at DESC LIMIT $2" + ) + .bind(directive_id) + .bind(limit) + .fetch_all(pool) + .await +} + +// ============================================================================= +// Directive Verifier Operations +// ============================================================================= + +/// Create a directive verifier. +pub async fn create_directive_verifier( + pool: &PgPool, + directive_id: Uuid, + name: &str, + verifier_type: &str, + command: Option<&str>, + working_directory: Option<&str>, + auto_detect: bool, + detect_files: Vec<String>, + weight: f64, + required: bool, +) -> Result<DirectiveVerifier, sqlx::Error> { + sqlx::query_as::<_, DirectiveVerifier>( + r#" + INSERT INTO directive_verifiers ( + directive_id, name, verifier_type, command, working_directory, + auto_detect, detect_files, weight, required + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + RETURNING * + "#, + ) + .bind(directive_id) + .bind(name) + .bind(verifier_type) + .bind(command) + .bind(working_directory) + .bind(auto_detect) + .bind(&detect_files) + .bind(weight) + .bind(required) + .fetch_one(pool) + .await +} + +/// List verifiers for a directive. +pub async fn list_directive_verifiers( + pool: &PgPool, + directive_id: Uuid, +) -> Result<Vec<DirectiveVerifier>, sqlx::Error> { + sqlx::query_as::<_, DirectiveVerifier>( + "SELECT * FROM directive_verifiers WHERE directive_id = $1 ORDER BY name" + ) + .bind(directive_id) .fetch_all(pool) .await } -/// Get the latest evaluation for a chain contract. -pub async fn get_latest_chain_contract_evaluation( +/// Update a directive verifier. +pub async fn update_directive_verifier( pool: &PgPool, - chain_contract_id: Uuid, -) -> Result<Option<ContractEvaluation>, sqlx::Error> { - sqlx::query_as::<_, ContractEvaluation>( + verifier_id: Uuid, + enabled: Option<bool>, + command: Option<&str>, + weight: Option<f64>, + required: Option<bool>, +) -> Result<DirectiveVerifier, sqlx::Error> { + sqlx::query_as::<_, DirectiveVerifier>( r#" - SELECT * - FROM contract_evaluations - WHERE chain_contract_id = $1 - ORDER BY evaluation_number DESC - LIMIT 1 + UPDATE directive_verifiers SET + enabled = COALESCE($2, enabled), + command = COALESCE($3, command), + weight = COALESCE($4, weight), + required = COALESCE($5, required), + updated_at = NOW() + WHERE id = $1 + RETURNING * "#, ) - .bind(chain_contract_id) - .fetch_optional(pool) + .bind(verifier_id) + .bind(enabled) + .bind(command) + .bind(weight) + .bind(required) + .fetch_one(pool) .await } -/// Get the next evaluation number for a chain contract. -pub async fn get_next_evaluation_number( +/// Update verifier last run result. +pub async fn update_verifier_result( pool: &PgPool, - chain_contract_id: Uuid, -) -> Result<i32, sqlx::Error> { - let result: Option<(i32,)> = sqlx::query_as( + verifier_id: Uuid, + result: serde_json::Value, +) -> Result<DirectiveVerifier, sqlx::Error> { + sqlx::query_as::<_, DirectiveVerifier>( r#" - SELECT COALESCE(MAX(evaluation_number), 0) + 1 as next_number - FROM contract_evaluations - WHERE chain_contract_id = $1 + UPDATE directive_verifiers SET last_run_at = NOW(), last_result = $2, updated_at = NOW() + WHERE id = $1 + RETURNING * "#, ) - .bind(chain_contract_id) - .fetch_optional(pool) - .await?; + .bind(verifier_id) + .bind(result) + .fetch_one(pool) + .await +} - Ok(result.map(|(n,)| n).unwrap_or(1)) +// ============================================================================= +// Directive Approval Operations +// ============================================================================= + +/// Create an approval request. +pub async fn create_approval_request( + pool: &PgPool, + directive_id: Uuid, + step_id: Option<Uuid>, + approval_type: &str, + description: &str, + context: Option<serde_json::Value>, + urgency: &str, + expires_at: Option<chrono::DateTime<Utc>>, +) -> Result<DirectiveApproval, sqlx::Error> { + sqlx::query_as::<_, DirectiveApproval>( + r#" + INSERT INTO directive_approvals ( + directive_id, step_id, approval_type, description, context, urgency, expires_at + ) + VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING * + "#, + ) + .bind(directive_id) + .bind(step_id) + .bind(approval_type) + .bind(description) + .bind(context) + .bind(urgency) + .bind(expires_at) + .fetch_one(pool) + .await } -/// Update chain contract evaluation status. -pub async fn update_chain_contract_evaluation_status( +/// Resolve an approval request. +pub async fn resolve_approval( pool: &PgPool, - chain_contract_id: Uuid, + approval_id: Uuid, status: &str, - evaluation_id: Option<Uuid>, - rework_feedback: Option<&str>, -) -> Result<ChainContract, sqlx::Error> { - sqlx::query_as::<_, ChainContract>( - r#" - UPDATE chain_contracts SET - evaluation_status = $2, - last_evaluation_id = COALESCE($3, last_evaluation_id), - rework_feedback = COALESCE($4, rework_feedback), - evaluation_retry_count = CASE - WHEN $2 = 'rework' THEN evaluation_retry_count + 1 - ELSE evaluation_retry_count - END, - rework_started_at = CASE - WHEN $2 = 'rework' THEN NOW() - ELSE rework_started_at - END + response: Option<&str>, + responded_by: Uuid, +) -> Result<DirectiveApproval, sqlx::Error> { + sqlx::query_as::<_, DirectiveApproval>( + r#" + UPDATE directive_approvals SET + status = $2, + response = $3, + responded_by = $4, + responded_at = NOW() WHERE id = $1 RETURNING * "#, ) - .bind(chain_contract_id) + .bind(approval_id) .bind(status) - .bind(evaluation_id) - .bind(rework_feedback) + .bind(response) + .bind(responded_by) .fetch_one(pool) .await } -/// Mark a chain contract's original completion time (before rework). -pub async fn mark_chain_contract_original_completion( +/// List pending approvals for a directive. +pub async fn list_pending_approvals( pool: &PgPool, - chain_contract_id: Uuid, -) -> Result<(), sqlx::Error> { - sqlx::query( + directive_id: Uuid, +) -> Result<Vec<DirectiveApproval>, sqlx::Error> { + sqlx::query_as::<_, DirectiveApproval>( r#" - UPDATE chain_contracts SET - original_completion_at = COALESCE(original_completion_at, NOW()) - WHERE id = $1 + SELECT * FROM directive_approvals + WHERE directive_id = $1 AND status = 'pending' + ORDER BY + CASE urgency + WHEN 'critical' THEN 1 + WHEN 'high' THEN 2 + WHEN 'normal' THEN 3 + ELSE 4 + END, + created_at "#, ) - .bind(chain_contract_id) - .execute(pool) - .await?; - Ok(()) + .bind(directive_id) + .fetch_all(pool) + .await } -/// Get chain contract by contract ID. -pub async fn get_chain_contract_by_contract_id( +/// Get step by contract ID. +pub async fn get_step_by_contract_id( pool: &PgPool, contract_id: Uuid, -) -> Result<Option<ChainContract>, sqlx::Error> { - sqlx::query_as::<_, ChainContract>( - r#" - SELECT * - FROM chain_contracts - WHERE contract_id = $1 - "#, +) -> Result<Option<ChainStep>, sqlx::Error> { + sqlx::query_as::<_, ChainStep>( + "SELECT * FROM chain_steps WHERE contract_id = $1" ) .bind(contract_id) .fetch_optional(pool) @@ -6682,103 +6034,9 @@ pub async fn get_chain_contract_by_contract_id( } // ============================================================================= -// Init Chain (Directive-Driven Chain Creation) +// Helper Functions // ============================================================================= -/// Initialize a directive-driven chain. -/// Creates a directive contract and an empty chain linked to it. -pub async fn init_chain_for_owner( - pool: &PgPool, - owner_id: Uuid, - req: InitChainRequest, -) -> Result<InitChainResponse, sqlx::Error> { - // Create the directive contract - // Note: "directive" contract type uses the "specification" phases by default - let contract_req = CreateContractRequest { - name: format!("Directive: {}", truncate_string(&req.goal, 50)), - description: Some(req.goal.clone()), - contract_type: Some("specification".to_string()), // Directive uses spec workflow - template_id: None, - initial_phase: Some("research".to_string()), - phase_guard: Some(req.phase_guard), - autonomous_loop: Some(false), - local_only: Some(false), - auto_merge_local: Some(false), - }; - - let contract = create_contract_for_owner(pool, owner_id, contract_req).await?; - - // Mark it as a chain directive - sqlx::query("UPDATE contracts SET is_chain_directive = true WHERE id = $1") - .bind(contract.id) - .execute(pool) - .await?; - - // Build repositories list from request - let repositories = match (req.repository_url.as_ref(), req.local_path.as_ref()) { - (Some(url), _) => Some(vec![AddChainRepositoryRequest { - name: "Primary".to_string(), - repository_url: Some(url.clone()), - local_path: None, - source_type: "remote".to_string(), - is_primary: true, - }]), - (None, Some(path)) => Some(vec![AddChainRepositoryRequest { - name: "Primary".to_string(), - repository_url: None, - local_path: Some(path.clone()), - source_type: "local".to_string(), - is_primary: true, - }]), - (None, None) => None, - }; - - // Create the chain with directive contract reference - let chain_req = CreateChainRequest { - name: truncate_string(&req.goal, 100), - description: Some(req.goal), - repositories, - loop_enabled: Some(false), - loop_max_iterations: None, - loop_progress_check: None, - contracts: None, - }; - - let chain = create_chain_for_owner(pool, owner_id, chain_req).await?; - - // Link directive contract to chain - sqlx::query( - r#" - UPDATE chains SET directive_contract_id = $2 WHERE id = $1; - UPDATE contracts SET spawned_chain_id = $1 WHERE id = $2; - "#, - ) - .bind(chain.id) - .bind(contract.id) - .execute(pool) - .await?; - - // Create empty directive document - create_chain_directive( - pool, - chain.id, - CreateChainDirectiveRequest { - requirements: Some(vec![]), - acceptance_criteria: Some(vec![]), - constraints: Some(vec![]), - external_dependencies: Some(vec![]), - source_type: Some("llm_generated".to_string()), - }, - ) - .await?; - - Ok(InitChainResponse { - chain_id: chain.id, - directive_contract_id: contract.id, - supervisor_task_id: contract.supervisor_task_id, - }) -} - /// Helper to truncate string to max length fn truncate_string(s: &str, max_len: usize) -> String { if s.len() <= max_len { |
