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}