1use crate::receipt::Receipt;
8use crate::selected::SelectedList;
9use crate::tick::{Check, Ground};
10
11#[derive(Debug, Clone, PartialEq)]
12pub enum Verdict {
13 Green,
14 Red,
15 GrayRed,
16 Unproven,
17 NotRun { missing_platforms: Vec<String> },
18 Stale { reason: String },
19 SilentlyUnbound,
20 Exempt, NotApplicable, }
23
24impl Verdict {
25 pub fn label(&self) -> &'static str {
27 match self {
28 Verdict::Green => "green",
29 Verdict::Red => "red",
30 Verdict::GrayRed => "gray->red",
31 Verdict::Unproven => "unproven",
32 Verdict::NotRun { .. } => "not-run",
33 Verdict::Stale { .. } => "stale",
34 Verdict::SilentlyUnbound => "silently-unbound",
35 Verdict::Exempt => "exempt",
36 Verdict::NotApplicable => "n/a",
37 }
38 }
39}
40
41use time::{format_description::well_known::Rfc3339, OffsetDateTime};
42
43pub struct Ctx {
46 pub live_origin_sha: Option<String>, pub selected: Option<SelectedList>, pub now_unix: i64, pub staleness_secs: i64, pub attest: Option<Vec<String>>, }
52
53pub fn verdict_for(
55 ground: &Ground,
56 receipts: &[Receipt],
57 ctx: &Ctx,
58 triggered_since: bool,
59) -> Verdict {
60 let (reference, verified_at_sha, liveness) = match &ground.check {
61 Some(Check::Test {
62 reference,
63 verified_at_sha,
64 liveness,
65 ..
66 }) => (reference.as_str(), verified_at_sha.as_str(), liveness),
67 _ => return Verdict::NotApplicable,
68 };
69
70 if let Some(origin) = ctx.live_origin_sha.as_deref() {
71 if origin != verified_at_sha {
72 return Verdict::Stale {
73 reason: "verified_at_sha behind live origin".into(),
74 };
75 }
76 }
77
78 let attested: Vec<&String> = match &ctx.attest {
81 Some(set) => liveness
82 .platforms
83 .iter()
84 .filter(|p| set.contains(p))
85 .collect(),
86 None => liveness.platforms.iter().collect(),
87 };
88 if ctx.attest.is_some() && attested.is_empty() {
89 return Verdict::Exempt; }
91 let mut missing = Vec::new();
92 let mut deciding: Vec<&Receipt> = Vec::new();
93 for p in attested {
94 let latest = receipts
96 .iter()
97 .filter(|r| r.test == reference && &r.platform == p)
98 .max_by(|a, b| a.ran_at.cmp(&b.ran_at));
99 match latest {
100 None => missing.push(p.clone()),
101 Some(r) => deciding.push(r),
102 }
103 }
104 if !missing.is_empty() {
105 return Verdict::NotRun {
106 missing_platforms: missing,
107 };
108 }
109
110 if triggered_since {
114 return Verdict::Stale {
115 reason: "a triggering change landed after the last run".into(),
116 };
117 }
118
119 let stale_by_age = deciding.iter().any(|r| {
122 OffsetDateTime::parse(&r.ran_at, &Rfc3339)
123 .map(|dt| ctx.now_unix - dt.unix_timestamp() > ctx.staleness_secs)
124 .unwrap_or(false)
125 });
126 if stale_by_age {
127 return Verdict::Stale {
128 reason: "deciding receipt older than the staleness window".into(),
129 };
130 }
131
132 if deciding.iter().any(|r| r.falsifiable == Some(false)) {
135 return Verdict::Unproven;
136 }
137
138 if deciding.iter().any(|r| r.result == "gray") {
139 return Verdict::GrayRed;
140 }
141 if deciding.iter().any(|r| r.result == "red") {
142 return Verdict::Red;
143 }
144
145 if let Some(sl) = ctx.selected.as_ref() {
148 let touched = liveness
149 .triggered_by
150 .iter()
151 .any(|t| sl.changed.iter().any(|c| c == t));
152 let was_selected = sl.selected.iter().any(|s| s == reference);
153 if touched && !was_selected {
154 return Verdict::SilentlyUnbound;
155 }
156 }
157
158 Verdict::Green
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164 use crate::receipt::Receipt;
165 use crate::tick::{Check, Ground, Liveness};
166
167 fn test_ground(platforms: &[&str]) -> Ground {
168 Ground {
169 claim: "no Redis".into(),
170 supports: "chosen".into(),
171 check: Some(Check::Test {
172 reference: "pytest x".into(),
173 verified_at_sha: "d308afac1b2c3d4e5f60718293a4b5c6d7e8f901".into(),
174 counter_test: "pytest x::flips".into(),
175 liveness: Liveness {
176 platforms: platforms.iter().map(|s| s.to_string()).collect(),
177 triggered_by: vec!["pyproject.toml".into()],
178 surfaces: vec!["pyproject-deps".into()],
179 },
180 }),
181 }
182 }
183 fn rcpt(platform: &str, ran_at: &str, result: &str) -> Receipt {
184 Receipt {
185 test: "pytest x".into(),
186 platform: platform.into(),
187 commit: "d308afac1b2c3d4e5f60718293a4b5c6d7e8f901".into(),
188 ran_at: ran_at.into(),
189 result: result.into(),
190 falsifiable: None,
191 }
192 }
193 fn ctx(live_origin_sha: Option<&str>, selected: Option<SelectedList>) -> Ctx {
195 Ctx {
196 live_origin_sha: live_origin_sha.map(|s| s.to_string()),
197 selected,
198 now_unix: 0,
199 staleness_secs: i64::MAX,
200 attest: None,
201 }
202 }
203 fn ctx_attest(attest: Option<&[&str]>) -> Ctx {
204 Ctx {
205 live_origin_sha: None,
206 selected: None,
207 now_unix: 0,
208 staleness_secs: i64::MAX,
209 attest: attest.map(|a| a.iter().map(|s| s.to_string()).collect()),
210 }
211 }
212
213 #[test]
214 fn verdict_for_should_be_not_applicable_when_the_ground_has_a_person_check() {
215 let g = Ground {
217 claim: "c".into(),
218 supports: "chosen".into(),
219 check: Some(Check::Person {
220 reference: "Q3".into(),
221 }),
222 };
223
224 let v = verdict_for(&g, &[], &ctx(None, None), false);
226
227 assert_eq!(v, Verdict::NotApplicable);
229 }
230
231 #[test]
232 fn verdict_for_should_be_not_run_when_a_declared_platform_has_no_receipt() {
233 let g = test_ground(&["linux-ci", "mac"]);
235 let receipts = vec![rcpt("linux-ci", "2026-01-01T00:00:00Z", "green")];
236
237 let v = verdict_for(&g, &receipts, &ctx(None, None), false);
239
240 assert_eq!(
242 v,
243 Verdict::NotRun {
244 missing_platforms: vec!["mac".into()]
245 }
246 );
247 }
248
249 #[test]
250 fn verdict_for_should_promote_gray_to_red_when_the_deciding_receipt_is_gray() {
251 let g = test_ground(&["linux-ci"]);
253 let receipts = vec![rcpt("linux-ci", "2026-01-01T00:00:00Z", "gray")];
254
255 let v = verdict_for(&g, &receipts, &ctx(None, None), false);
257
258 assert_eq!(v, Verdict::GrayRed);
260 }
261
262 #[test]
263 fn verdict_for_should_be_red_when_the_latest_receipt_is_red() {
264 let g = test_ground(&["linux-ci"]);
266 let receipts = vec![
267 rcpt("linux-ci", "2026-01-01T00:00:00Z", "green"),
268 rcpt("linux-ci", "2026-02-01T00:00:00Z", "red"),
269 ];
270
271 let v = verdict_for(&g, &receipts, &ctx(None, None), false);
273
274 assert_eq!(v, Verdict::Red);
276 }
277
278 #[test]
279 fn verdict_for_should_be_green_when_every_platform_has_a_fresh_green_receipt() {
280 let g = test_ground(&["linux-ci", "mac"]);
282 let receipts = vec![
283 rcpt("linux-ci", "2026-01-01T00:00:00Z", "green"),
284 rcpt("mac", "2026-01-01T00:00:00Z", "green"),
285 ];
286
287 let v = verdict_for(&g, &receipts, &ctx(None, None), false);
289
290 assert_eq!(v, Verdict::Green);
292 }
293
294 #[test]
295 fn verdict_for_should_be_stale_when_verified_at_sha_is_behind_the_live_origin() {
296 let g = test_ground(&["linux-ci"]);
298 let receipts = vec![rcpt("linux-ci", "2026-01-01T00:00:00Z", "green")];
299 let origin = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
300
301 let v = verdict_for(&g, &receipts, &ctx(Some(origin), None), false);
303
304 assert!(matches!(v, Verdict::Stale { .. }));
306 }
307
308 #[test]
309 fn verdict_for_should_be_silently_unbound_when_a_touched_trigger_was_not_selected() {
310 let g = test_ground(&["linux-ci"]);
312 let receipts = vec![rcpt("linux-ci", "2026-01-01T00:00:00Z", "green")];
313 let sl = crate::selected::SelectedList {
314 commit: "d308afac1b2c3d4e5f60718293a4b5c6d7e8f901".into(),
315 changed: vec!["pyproject.toml".into()],
316 selected: vec![],
317 };
318
319 let v = verdict_for(&g, &receipts, &ctx(None, Some(sl)), false);
321
322 assert_eq!(v, Verdict::SilentlyUnbound);
324 }
325
326 #[test]
327 fn verdict_for_should_be_green_when_the_touched_trigger_was_selected() {
328 let g = test_ground(&["linux-ci"]);
330 let receipts = vec![rcpt("linux-ci", "2026-01-01T00:00:00Z", "green")];
331 let sl = crate::selected::SelectedList {
332 commit: "d308afac1b2c3d4e5f60718293a4b5c6d7e8f901".into(),
333 changed: vec!["pyproject.toml".into()],
334 selected: vec!["pytest x".into()],
335 };
336
337 let v = verdict_for(&g, &receipts, &ctx(None, Some(sl)), false);
339
340 assert_eq!(v, Verdict::Green);
342 }
343
344 #[test]
345 fn verdict_for_should_be_stale_when_the_deciding_receipt_is_older_than_the_window() {
346 let g = test_ground(&["linux-ci"]);
348 let receipts = vec![rcpt("linux-ci", "2026-01-01T00:00:00Z", "green")];
349 let c = Ctx {
350 live_origin_sha: None,
351 selected: None,
352 now_unix: OffsetDateTime::parse("2026-06-01T00:00:00Z", &Rfc3339)
353 .unwrap()
354 .unix_timestamp(),
355 staleness_secs: 7 * 86_400,
356 attest: None,
357 };
358
359 let v = verdict_for(&g, &receipts, &c, false);
361
362 assert!(matches!(v, Verdict::Stale { .. }));
364 }
365
366 #[test]
367 fn verdict_for_should_be_green_when_the_deciding_receipt_is_within_the_window() {
368 let g = test_ground(&["linux-ci"]);
370 let receipts = vec![rcpt("linux-ci", "2026-06-01T00:00:00Z", "green")];
371 let c = Ctx {
372 live_origin_sha: None,
373 selected: None,
374 now_unix: OffsetDateTime::parse("2026-06-01T01:00:00Z", &Rfc3339)
375 .unwrap()
376 .unix_timestamp(),
377 staleness_secs: 7 * 86_400,
378 attest: None,
379 };
380
381 let v = verdict_for(&g, &receipts, &c, false);
383
384 assert_eq!(v, Verdict::Green);
386 }
387
388 #[test]
389 fn verdict_for_should_be_stale_when_a_triggering_change_landed_after_the_last_run() {
390 let g = test_ground(&["linux-ci"]);
392 let receipts = vec![rcpt("linux-ci", "2026-01-01T00:00:00Z", "green")];
393
394 let v = verdict_for(&g, &receipts, &ctx(None, None), true);
396
397 assert!(matches!(v, Verdict::Stale { .. }));
399 }
400
401 #[test]
402 fn verdict_for_should_ignore_triggered_since_when_a_platform_is_already_not_run() {
403 let g = test_ground(&["linux-ci", "mac"]);
405 let receipts = vec![rcpt("linux-ci", "2026-01-01T00:00:00Z", "green")];
406
407 let v = verdict_for(&g, &receipts, &ctx(None, None), true);
409
410 assert_eq!(
412 v,
413 Verdict::NotRun {
414 missing_platforms: vec!["mac".into()]
415 }
416 );
417 }
418
419 #[test]
420 fn verdict_for_should_exempt_a_binding_whose_platforms_this_runner_does_not_attest() {
421 let g = test_ground(&["mac"]);
423 assert_eq!(
425 verdict_for(&g, &[], &ctx_attest(Some(&["linux-ci"])), false),
426 Verdict::Exempt
427 );
428 }
429
430 #[test]
431 fn verdict_for_should_ignore_an_unattested_platform_when_an_attested_one_is_green() {
432 let g = test_ground(&["linux-ci", "mac"]);
434 let receipts = vec![rcpt("linux-ci", "2026-01-01T00:00:00Z", "green")];
435 assert_eq!(
437 verdict_for(&g, &receipts, &ctx_attest(Some(&["linux-ci"])), false),
438 Verdict::Green
439 );
440 }
441
442 #[test]
443 fn verdict_for_should_be_unproven_when_a_deciding_receipt_is_not_falsifiable() {
444 let g = test_ground(&["linux-ci"]);
446 let receipts = vec![Receipt {
447 test: "pytest x".into(),
448 platform: "linux-ci".into(),
449 commit: "d308afac1b2c3d4e5f60718293a4b5c6d7e8f901".into(),
450 ran_at: "2026-01-01T00:00:00Z".into(),
451 result: "green".into(),
452 falsifiable: Some(false),
453 }];
454 let v = verdict_for(&g, &receipts, &ctx(None, None), false);
456 assert_eq!(v, Verdict::Unproven);
458 }
459
460 #[test]
461 fn verdict_for_should_be_green_when_a_deciding_receipt_is_proven_falsifiable() {
462 let g = test_ground(&["linux-ci"]);
464 let receipts = vec![Receipt {
465 test: "pytest x".into(),
466 platform: "linux-ci".into(),
467 commit: "d308afac1b2c3d4e5f60718293a4b5c6d7e8f901".into(),
468 ran_at: "2026-01-01T00:00:00Z".into(),
469 result: "green".into(),
470 falsifiable: Some(true),
471 }];
472 let v = verdict_for(&g, &receipts, &ctx(None, None), false);
474 assert_eq!(v, Verdict::Green);
476 }
477}