Skip to main content

loong_contracts/
workflow_types.rs

1use std::fmt;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
6#[serde(rename_all = "snake_case")]
7pub enum GovernedSessionMode {
8    AdvisoryOnly,
9    MutatingCapable,
10}
11
12impl GovernedSessionMode {
13    #[must_use]
14    pub const fn as_str(self) -> &'static str {
15        match self {
16            Self::AdvisoryOnly => "advisory_only",
17            Self::MutatingCapable => "mutating_capable",
18        }
19    }
20}
21
22impl fmt::Display for GovernedSessionMode {
23    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24        f.write_str(self.as_str())
25    }
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
29#[serde(rename_all = "snake_case")]
30pub enum WorkflowOperationKind {
31    Plan,
32    Task,
33    Worktree,
34    Approval,
35}
36
37impl WorkflowOperationKind {
38    #[must_use]
39    pub const fn as_str(self) -> &'static str {
40        match self {
41            Self::Plan => "plan",
42            Self::Task => "task",
43            Self::Worktree => "worktree",
44            Self::Approval => "approval",
45        }
46    }
47}
48
49impl fmt::Display for WorkflowOperationKind {
50    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51        f.write_str(self.as_str())
52    }
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
56#[serde(rename_all = "snake_case")]
57pub enum WorkflowOperationScope {
58    Session,
59    Task,
60    Worktree,
61    Approval,
62}
63
64impl WorkflowOperationScope {
65    #[must_use]
66    pub const fn as_str(self) -> &'static str {
67        match self {
68            Self::Session => "session",
69            Self::Task => "task",
70            Self::Worktree => "worktree",
71            Self::Approval => "approval",
72        }
73    }
74}
75
76impl fmt::Display for WorkflowOperationScope {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        f.write_str(self.as_str())
79    }
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
83#[serde(rename_all = "snake_case")]
84pub enum GovernedWorkflowPhase {
85    Plan,
86    Spec,
87    Execute,
88    Verify,
89    Fix,
90    Complete,
91    Failed,
92    Cancelled,
93}
94
95impl GovernedWorkflowPhase {
96    #[must_use]
97    pub const fn as_str(self) -> &'static str {
98        match self {
99            Self::Plan => "plan",
100            Self::Spec => "spec",
101            Self::Execute => "execute",
102            Self::Verify => "verify",
103            Self::Fix => "fix",
104            Self::Complete => "complete",
105            Self::Failed => "failed",
106            Self::Cancelled => "cancelled",
107        }
108    }
109}
110
111impl fmt::Display for GovernedWorkflowPhase {
112    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113        f.write_str(self.as_str())
114    }
115}
116
117#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
118#[serde(deny_unknown_fields)]
119pub struct TaskScopeDescriptor {
120    pub task_id: String,
121}
122
123#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
124#[serde(deny_unknown_fields)]
125pub struct WorktreeBindingDescriptor {
126    pub worktree_id: String,
127    pub workspace_root: String,
128}
129
130#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
131#[serde(deny_unknown_fields)]
132pub struct GovernedSessionBindingDescriptor {
133    pub session_id: String,
134    pub task_scope: TaskScopeDescriptor,
135    pub turn_id: String,
136    pub worktree: WorktreeBindingDescriptor,
137    pub policy_snapshot: String,
138    pub audit_correlation_id: String,
139    pub execution_surface: String,
140    pub mode: GovernedSessionMode,
141}
142
143#[cfg(test)]
144mod tests {
145    use serde::Deserialize;
146    use serde_json::json;
147
148    use super::*;
149
150    #[derive(Debug, Deserialize)]
151    #[serde(deny_unknown_fields)]
152    struct WorkflowOperationHolder {
153        #[serde(rename = "kind")]
154        _kind: WorkflowOperationKind,
155    }
156
157    #[test]
158    fn governed_workflow_contract_governed_session_binding_serializes_with_stable_shape() {
159        let descriptor = GovernedSessionBindingDescriptor {
160            session_id: "session-001".to_owned(),
161            task_scope: TaskScopeDescriptor {
162                task_id: "task-001".to_owned(),
163            },
164            turn_id: "turn-001".to_owned(),
165            worktree: WorktreeBindingDescriptor {
166                worktree_id: "worktree-001".to_owned(),
167                workspace_root: "/repo/.worktrees/worktree-001".to_owned(),
168            },
169            policy_snapshot: "policy-snapshot-001".to_owned(),
170            audit_correlation_id: "audit-001".to_owned(),
171            execution_surface: "conversation_turn".to_owned(),
172            mode: GovernedSessionMode::MutatingCapable,
173        };
174
175        let serialized = serde_json::to_value(&descriptor).expect("binding descriptor serializes");
176
177        assert_eq!(
178            serialized,
179            json!({
180                "session_id": "session-001",
181                "task_scope": {
182                    "task_id": "task-001",
183                },
184                "turn_id": "turn-001",
185                "worktree": {
186                    "worktree_id": "worktree-001",
187                    "workspace_root": "/repo/.worktrees/worktree-001",
188                },
189                "policy_snapshot": "policy-snapshot-001",
190                "audit_correlation_id": "audit-001",
191                "execution_surface": "conversation_turn",
192                "mode": "mutating_capable",
193            })
194        );
195    }
196
197    #[test]
198    fn governed_workflow_contract_session_modes_are_distinguishable() {
199        let advisory =
200            serde_json::to_value(GovernedSessionMode::AdvisoryOnly).expect("advisory serializes");
201        let mutating = serde_json::to_value(GovernedSessionMode::MutatingCapable)
202            .expect("mutating serializes");
203
204        assert_eq!(advisory, json!("advisory_only"));
205        assert_eq!(mutating, json!("mutating_capable"));
206        assert_ne!(advisory, mutating);
207    }
208
209    #[test]
210    fn governed_workflow_contract_operation_kind_and_scope_serialize_deterministically() {
211        let kind = serde_json::to_value(WorkflowOperationKind::Worktree).expect("kind serializes");
212        let scope =
213            serde_json::to_value(WorkflowOperationScope::Worktree).expect("scope serializes");
214        let phase = serde_json::to_value(GovernedWorkflowPhase::Execute).expect("phase serializes");
215
216        assert_eq!(kind, json!("worktree"));
217        assert_eq!(scope, json!("worktree"));
218        assert_eq!(phase, json!("execute"));
219    }
220
221    #[test]
222    fn governed_workflow_contract_operation_kind_rejects_missing_or_unknown_values() {
223        let missing_kind = serde_json::from_value::<WorkflowOperationHolder>(json!({}))
224            .expect_err("missing kind should fail closed");
225        assert!(missing_kind.to_string().contains("missing field `kind`"));
226
227        let unknown_kind = serde_json::from_value::<WorkflowOperationHolder>(json!({
228            "kind": "shell",
229        }))
230        .expect_err("unknown kind should fail closed");
231        assert!(unknown_kind.to_string().contains("unknown variant"));
232    }
233
234    #[test]
235    fn governed_workflow_contract_binding_rejects_missing_required_fields() {
236        let error = serde_json::from_value::<GovernedSessionBindingDescriptor>(json!({
237            "session_id": "session-001",
238            "task_scope": {
239                "task_id": "task-001",
240            },
241            "turn_id": "turn-001",
242            "worktree": {
243                "worktree_id": "worktree-001",
244                "workspace_root": "/repo/.worktrees/worktree-001",
245            },
246            "policy_snapshot": "policy-snapshot-001",
247            "audit_correlation_id": "audit-001",
248            "execution_surface": "conversation_turn"
249        }))
250        .expect_err("missing mode should fail closed");
251
252        assert!(error.to_string().contains("missing field `mode`"));
253    }
254}