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,
280 Yellow {
282 at: Timestamp,
283 },
284 Green {
285 at: Timestamp,
286 },
287 Red {
288 at: Timestamp,
289 },
290 Error {
291 at: Timestamp,
292 error: Option<String>,
293 },
294 Incomplete {
295 at: Timestamp,
296 error: Option<String>,
297 },
298}
299
300impl RunState {
301 pub(super) fn from_parts(
302 status: RunStatus,
303 at: Option<Timestamp>,
304 error: Option<String>,
305 ) -> Result<Self, String> {
306 match status {
307 RunStatus::Running | RunStatus::WaitingHuman if at.is_some() || error.is_some() => {
308 Err("An open Run cannot be completed or carry an error.".into())
309 }
310 RunStatus::Running => Ok(Self::Running),
311 RunStatus::WaitingHuman => Ok(Self::WaitingHuman),
312 RunStatus::Green | RunStatus::Red | RunStatus::Yellow
313 if error.is_some() || at.is_none() =>
314 {
315 Err(
316 "A completed verdict Run needs a completion time and cannot carry an error."
317 .into(),
318 )
319 }
320 RunStatus::Yellow => Ok(Self::Yellow {
321 at: at.expect("validated completion time"),
322 }),
323 RunStatus::Green => Ok(Self::Green {
324 at: at.expect("validated completion time"),
325 }),
326 RunStatus::Red => Ok(Self::Red {
327 at: at.expect("validated completion time"),
328 }),
329 RunStatus::Error => Ok(Self::Error {
330 at: at.ok_or("A finished Run needs its completion time.")?,
331 error,
332 }),
333 RunStatus::Incomplete => Ok(Self::Incomplete {
334 at: at.ok_or("A finished Run needs its completion time.")?,
335 error,
336 }),
337 }
338 }
339 pub fn status(&self) -> RunStatus {
340 match self {
341 Self::Running => RunStatus::Running,
342 Self::WaitingHuman => RunStatus::WaitingHuman,
343 Self::Yellow { .. } => RunStatus::Yellow,
344 Self::Green { .. } => RunStatus::Green,
345 Self::Red { .. } => RunStatus::Red,
346 Self::Error { .. } => RunStatus::Error,
347 Self::Incomplete { .. } => RunStatus::Incomplete,
348 }
349 }
350 pub fn with_signoffs(self, signoff_open: bool) -> Self {
353 match self.status().with_signoffs(signoff_open) {
354 RunStatus::WaitingHuman => Self::WaitingHuman,
355 RunStatus::Running => Self::Running,
356 _ => self,
357 }
358 }
359 pub fn completed_at(&self) -> Option<Timestamp> {
360 match self {
361 Self::Running | Self::WaitingHuman => None,
362 Self::Yellow { at }
363 | Self::Green { at }
364 | Self::Red { at }
365 | Self::Error { at, .. }
366 | Self::Incomplete { at, .. } => Some(*at),
367 }
368 }
369 pub fn error(&self) -> Option<&str> {
370 match self {
371 Self::Error { error, .. } | Self::Incomplete { error, .. } => error.as_deref(),
372 _ => None,
373 }
374 }
375}
376
377#[cfg(test)]
378mod tests {
379 use super::*;
380 use serde_json::json;
381
382 #[test]
383 fn provisional_constructor_rejects_red_and_preserves_green_evidence_when_promoted() {
384 let red = ExecutionResult::try_from(json!({"verdict":"RED","reason":"failed"})).unwrap();
385 assert!(GreenResult::try_from(red).is_err());
386 let expected = json!({"verdict":"GREEN","summary":"criteria met"});
387 let green = ExecutionResult::try_from(expected.clone()).unwrap();
388 let provisional = RequestState::Provisional {
389 result: green.try_into().unwrap(),
390 at: "2026-01-01T00:00:00Z".parse().unwrap(),
391 };
392 assert_eq!(provisional.status(), RequestStatus::Provisional);
393 assert_eq!(provisional.result().unwrap().to_json(), expected);
394 let execution = ExecutionState::try_from(provisional).unwrap();
395 assert_eq!(execution.status(), ExecutionStatus::Green);
396 assert_eq!(execution.result().unwrap().to_json(), expected);
397 }
398}