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