1use super::ExecutionResult;
4use crate::types::{ExecutionStatus, FailureCode, RequestStatus, RunStatus, Timestamp};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum PendingRequest {
9 Queued,
10 Blocked,
11 BudgetExhausted,
12 Stale,
13 Unreviewed,
14 WaitDependency,
15}
16
17#[derive(Debug, Clone)]
18pub enum ExecutionState {
19 Running,
20 WaitingHuman,
21 Completed {
22 result: ExecutionResult,
23 at: Timestamp,
24 },
25 Failed {
26 error: String,
27 code: Option<FailureCode>,
28 at: Timestamp,
29 },
30}
31
32#[derive(Debug, Clone, PartialEq)]
35pub struct GreenResult(ExecutionResult);
36
37impl TryFrom<ExecutionResult> for GreenResult {
38 type Error = String;
39 fn try_from(result: ExecutionResult) -> Result<Self, Self::Error> {
40 if result.verdict() != crate::runtime::Verdict::Green {
41 return Err("A provisional request requires GREEN evidence.".into());
42 }
43 Ok(Self(result))
44 }
45}
46impl AsRef<ExecutionResult> for GreenResult {
47 fn as_ref(&self) -> &ExecutionResult {
48 &self.0
49 }
50}
51impl From<GreenResult> for ExecutionResult {
52 fn from(result: GreenResult) -> Self {
53 result.0
54 }
55}
56
57#[derive(Debug, Clone)]
58pub enum RequestState {
59 Running,
60 WaitingHuman,
61 Deferred,
62 Provisional {
63 result: GreenResult,
64 at: Timestamp,
65 },
66 Pending {
67 kind: PendingRequest,
68 at: Option<Timestamp>,
69 },
70 Completed {
71 result: ExecutionResult,
72 at: Timestamp,
73 },
74 Failed {
75 error: String,
76 code: Option<FailureCode>,
77 at: Timestamp,
78 },
79}
80
81impl RequestState {
82 pub fn pending(status: RequestStatus, at: Option<Timestamp>) -> Result<Self, String> {
84 Self::from_parts(status, None, None, None, at)
85 }
86
87 pub fn completed(result: ExecutionResult, at: Timestamp) -> Self {
88 Self::Completed { result, at }
89 }
90
91 pub fn failed(error: String, code: Option<FailureCode>, at: Timestamp) -> Self {
92 Self::Failed { error, code, at }
93 }
94
95 pub(super) fn from_parts(
96 status: RequestStatus,
97 result: Option<ExecutionResult>,
98 error: Option<String>,
99 code: Option<FailureCode>,
100 at: Option<Timestamp>,
101 ) -> Result<Self, String> {
102 super::validation::result(
103 status,
104 result.as_ref(),
105 error.as_deref(),
106 code.map(FailureCode::as_str),
107 )?;
108 match status {
109 RequestStatus::Provisional => Ok(Self::Provisional {
110 result: result.expect("validated provisional GREEN").try_into()?,
111 at: at.ok_or("A provisional request needs its completion time.")?,
112 }),
113 RequestStatus::Green | RequestStatus::Red => Ok(Self::Completed {
114 result: result.expect("validated verdict"),
115 at: at.ok_or("A finished request needs its completion time.")?,
116 }),
117 RequestStatus::Error => Ok(Self::Failed {
118 error: error.expect("validated error"),
119 code,
120 at: at.ok_or("A finished request needs its completion time.")?,
121 }),
122 RequestStatus::Running | RequestStatus::WaitingHuman | RequestStatus::Deferred
123 if at.is_some() =>
124 {
125 Err("An active request cannot have a completion time.".into())
126 }
127 RequestStatus::Running => Ok(Self::Running),
128 RequestStatus::WaitingHuman => Ok(Self::WaitingHuman),
129 RequestStatus::Deferred => Ok(Self::Deferred),
130 _ => Ok(Self::Pending {
131 kind: match status {
132 RequestStatus::Queued => PendingRequest::Queued,
133 RequestStatus::Blocked => PendingRequest::Blocked,
134 RequestStatus::BudgetExhausted => PendingRequest::BudgetExhausted,
135 RequestStatus::Stale => PendingRequest::Stale,
136 RequestStatus::Unreviewed => PendingRequest::Unreviewed,
137 RequestStatus::WaitDependency => PendingRequest::WaitDependency,
138 _ => unreachable!("terminal and active states handled above"),
139 },
140 at,
141 }),
142 }
143 }
144
145 pub fn status(&self) -> RequestStatus {
146 match self {
147 Self::Running => RequestStatus::Running,
148 Self::WaitingHuman => RequestStatus::WaitingHuman,
149 Self::Deferred => RequestStatus::Deferred,
150 Self::Provisional { .. } => RequestStatus::Provisional,
151 Self::Completed { result, .. } => match result.verdict() {
152 crate::runtime::Verdict::Green => RequestStatus::Green,
153 crate::runtime::Verdict::Red => RequestStatus::Red,
154 },
155 Self::Failed { .. } => RequestStatus::Error,
156 Self::Pending { kind, .. } => match kind {
157 PendingRequest::Queued => RequestStatus::Queued,
158 PendingRequest::Blocked => RequestStatus::Blocked,
159 PendingRequest::BudgetExhausted => RequestStatus::BudgetExhausted,
160 PendingRequest::Stale => RequestStatus::Stale,
161 PendingRequest::Unreviewed => RequestStatus::Unreviewed,
162 PendingRequest::WaitDependency => RequestStatus::WaitDependency,
163 },
164 }
165 }
166 pub fn result(&self) -> Option<&ExecutionResult> {
167 match self {
168 Self::Completed { result, .. } => Some(result),
169 Self::Provisional { result, .. } => Some(result.as_ref()),
170 _ => None,
171 }
172 }
173 pub fn error(&self) -> Option<&str> {
174 match self {
175 Self::Failed { error, .. } => Some(error),
176 _ => None,
177 }
178 }
179 pub fn error_code(&self) -> Option<FailureCode> {
180 match self {
181 Self::Failed { code, .. } => *code,
182 _ => None,
183 }
184 }
185 pub fn completed_at(&self) -> Option<Timestamp> {
186 match self {
187 Self::Completed { at, .. } | Self::Provisional { at, .. } | Self::Failed { at, .. } => {
188 Some(*at)
189 }
190 Self::Pending { at, .. } => *at,
191 _ => None,
192 }
193 }
194}
195
196impl ExecutionState {
197 pub(super) fn from_parts(
198 status: ExecutionStatus,
199 result: Option<ExecutionResult>,
200 error: Option<String>,
201 code: Option<FailureCode>,
202 at: Option<Timestamp>,
203 ) -> Result<Self, String> {
204 let state = RequestState::from_parts(status.into(), result, error, code, at)?;
205 Self::try_from(state)
206 }
207 pub fn status(&self) -> ExecutionStatus {
208 match self {
209 Self::Running => ExecutionStatus::Running,
210 Self::WaitingHuman => ExecutionStatus::WaitingHuman,
211 Self::Completed { result, .. } => match result.verdict() {
212 crate::runtime::Verdict::Green => ExecutionStatus::Green,
213 crate::runtime::Verdict::Red => ExecutionStatus::Red,
214 },
215 Self::Failed { .. } => ExecutionStatus::Error,
216 }
217 }
218 pub fn result(&self) -> Option<&ExecutionResult> {
219 match self {
220 Self::Completed { result, .. } => Some(result),
221 _ => None,
222 }
223 }
224 pub fn error(&self) -> Option<&str> {
225 match self {
226 Self::Failed { error, .. } => Some(error),
227 _ => None,
228 }
229 }
230 pub fn error_code(&self) -> Option<FailureCode> {
231 match self {
232 Self::Failed { code, .. } => *code,
233 _ => None,
234 }
235 }
236 pub fn completed_at(&self) -> Option<Timestamp> {
237 match self {
238 Self::Completed { at, .. } | Self::Failed { at, .. } => Some(*at),
239 _ => None,
240 }
241 }
242}
243impl TryFrom<RequestState> for ExecutionState {
244 type Error = String;
245 fn try_from(state: RequestState) -> Result<Self, String> {
246 match state {
247 RequestState::Running => Ok(Self::Running),
248 RequestState::WaitingHuman | RequestState::Deferred => Ok(Self::WaitingHuman),
249 RequestState::Provisional { result, at } => Ok(Self::Completed {
250 result: result.into(),
251 at,
252 }),
253 RequestState::Completed { result, at } => Ok(Self::Completed { result, at }),
254 RequestState::Failed { error, code, at } => Ok(Self::Failed { error, code, at }),
255 RequestState::Pending { .. } => {
256 Err("A pending request has no execution lifecycle state.".into())
257 }
258 }
259 }
260}
261impl From<ExecutionState> for RequestState {
262 fn from(state: ExecutionState) -> Self {
263 match state {
264 ExecutionState::Running => Self::Running,
265 ExecutionState::WaitingHuman => Self::WaitingHuman,
266 ExecutionState::Completed { result, at } => Self::Completed { result, at },
267 ExecutionState::Failed { error, code, at } => Self::Failed { error, code, at },
268 }
269 }
270}
271
272#[derive(Debug, Clone)]
274pub enum RunState {
275 Running,
276 WaitingHuman,
279 Yellow {
281 at: Timestamp,
282 },
283 Green {
284 at: Timestamp,
285 },
286 Red {
287 at: Timestamp,
288 },
289 Error {
290 at: Timestamp,
291 error: Option<String>,
292 },
293 Incomplete {
294 at: Timestamp,
295 error: Option<String>,
296 },
297}
298
299impl RunState {
300 pub(super) fn from_parts(
301 status: RunStatus,
302 at: Option<Timestamp>,
303 error: Option<String>,
304 ) -> Result<Self, String> {
305 match status {
306 RunStatus::Running | RunStatus::WaitingHuman if at.is_some() || error.is_some() => {
307 Err("An open Run cannot be completed or carry an error.".into())
308 }
309 RunStatus::Running => Ok(Self::Running),
310 RunStatus::WaitingHuman => Ok(Self::WaitingHuman),
311 RunStatus::Green | RunStatus::Red | RunStatus::Yellow
312 if error.is_some() || at.is_none() =>
313 {
314 Err(
315 "A completed verdict Run needs a completion time and cannot carry an error."
316 .into(),
317 )
318 }
319 RunStatus::Yellow => Ok(Self::Yellow {
320 at: at.expect("validated completion time"),
321 }),
322 RunStatus::Green => Ok(Self::Green {
323 at: at.expect("validated completion time"),
324 }),
325 RunStatus::Red => Ok(Self::Red {
326 at: at.expect("validated completion time"),
327 }),
328 RunStatus::Error => Ok(Self::Error {
329 at: at.ok_or("A finished Run needs its completion time.")?,
330 error,
331 }),
332 RunStatus::Incomplete => Ok(Self::Incomplete {
333 at: at.ok_or("A finished Run needs its completion time.")?,
334 error,
335 }),
336 }
337 }
338 pub fn status(&self) -> RunStatus {
339 match self {
340 Self::Running => RunStatus::Running,
341 Self::WaitingHuman => RunStatus::WaitingHuman,
342 Self::Yellow { .. } => RunStatus::Yellow,
343 Self::Green { .. } => RunStatus::Green,
344 Self::Red { .. } => RunStatus::Red,
345 Self::Error { .. } => RunStatus::Error,
346 Self::Incomplete { .. } => RunStatus::Incomplete,
347 }
348 }
349 pub fn completed_at(&self) -> Option<Timestamp> {
350 match self {
351 Self::Running | Self::WaitingHuman => None,
352 Self::Yellow { at }
353 | Self::Green { at }
354 | Self::Red { at }
355 | Self::Error { at, .. }
356 | Self::Incomplete { at, .. } => Some(*at),
357 }
358 }
359 pub fn error(&self) -> Option<&str> {
360 match self {
361 Self::Error { error, .. } | Self::Incomplete { error, .. } => error.as_deref(),
362 _ => None,
363 }
364 }
365}
366
367#[cfg(test)]
368mod tests {
369 use super::*;
370 use serde_json::json;
371
372 #[test]
373 fn provisional_constructor_rejects_red_and_preserves_green_evidence_when_promoted() {
374 let red = ExecutionResult::try_from(json!({"verdict":"RED","reason":"failed"})).unwrap();
375 assert!(GreenResult::try_from(red).is_err());
376 let expected = json!({"verdict":"GREEN","summary":"criteria met"});
377 let green = ExecutionResult::try_from(expected.clone()).unwrap();
378 let provisional = RequestState::Provisional {
379 result: green.try_into().unwrap(),
380 at: "2026-01-01T00:00:00Z".parse().unwrap(),
381 };
382 assert_eq!(provisional.status(), RequestStatus::Provisional);
383 assert_eq!(provisional.result().unwrap().to_json(), expected);
384 let execution = ExecutionState::try_from(provisional).unwrap();
385 assert_eq!(execution.status(), ExecutionStatus::Green);
386 assert_eq!(execution.result().unwrap().to_json(), expected);
387 }
388}