Skip to main content

ev/
state.rs

1//! The verdict-cache read contract: results/state/<tick_id>.json — a per-host, gitignored
2//! snapshot of each tick's per-ground verdicts that a consumer hook reads WITHOUT shelling
3//! `ev check`. Facts, no scores; one row per ground.
4use crate::store::Store;
5use crate::tick::{Check, Ground};
6use crate::verdict::Verdict;
7use serde_json::{json, Map, Value};
8use time::format_description::well_known::Rfc3339;
9use time::OffsetDateTime;
10
11/// Write `results/state/<tick_id>.json`: one row per `(ground, verdict)` pair, the staleness
12/// reference, and the time of computation. Pairing at the boundary keeps grounds and verdicts
13/// from drifting out of alignment.
14pub fn write_state(
15    store: &Store,
16    tick_id: &str,
17    rows: &[(&Ground, Verdict)],
18    staleness_policy: &str,
19    staleness_sha: Option<&str>,
20) -> std::io::Result<()> {
21    let computed_at = OffsetDateTime::now_utc()
22        .format(&Rfc3339)
23        .unwrap_or_default();
24    let grounds: Vec<Value> = rows
25        .iter()
26        .map(|(g, v)| {
27            let mut row = Map::new();
28            row.insert("claim".into(), Value::String(g.claim.clone()));
29            row.insert("supports".into(), Value::String(g.supports.clone()));
30            let check = match &g.check {
31                Some(Check::Test {
32                    reference,
33                    verified_at_sha,
34                    ..
35                }) => {
36                    row.insert("ref".into(), Value::String(reference.clone()));
37                    row.insert(
38                        "verified_at_sha".into(),
39                        Value::String(verified_at_sha.clone()),
40                    );
41                    "test"
42                }
43                Some(Check::Person { .. }) => "person",
44                None => "none",
45            };
46            row.insert("check".into(), Value::String(check.into()));
47            row.insert("verdict".into(), Value::String(v.label().into()));
48            if let Verdict::NotRun { missing_platforms } = v {
49                row.insert("missing_platforms".into(), json!(missing_platforms));
50            }
51            Value::Object(row)
52        })
53        .collect();
54    let doc = json!({
55        "tick_id": tick_id,
56        "computed_at": computed_at,
57        "staleness_ref": { "policy": staleness_policy, "sha": staleness_sha },
58        "grounds": grounds,
59    });
60    let dir = store.root.join("results").join("state");
61    std::fs::create_dir_all(&dir)?;
62    std::fs::write(
63        dir.join(format!("{tick_id}.json")),
64        serde_json::to_string_pretty(&doc).expect("serializable"),
65    )
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71    use crate::tick::{Ground, Liveness, Tick};
72
73    fn store() -> (std::path::PathBuf, Store) {
74        use std::sync::atomic::{AtomicU64, Ordering};
75        static N: AtomicU64 = AtomicU64::new(0);
76        let p = std::env::temp_dir().join(format!(
77            "ev-state-{}-{}",
78            std::process::id(),
79            N.fetch_add(1, Ordering::Relaxed)
80        ));
81        let _ = std::fs::remove_dir_all(&p);
82        std::fs::create_dir_all(&p).unwrap();
83        let s = Store::at(&p);
84        s.init().unwrap();
85        (p, s)
86    }
87
88    #[test]
89    fn write_state_should_record_each_ground_verdict_when_a_tick_is_evaluated() {
90        // given: a tick with a not-run test ground and a person ground, and their verdicts
91        let (_p, s) = store();
92        let tick = Tick {
93            id: "abcabcabcabc".into(),
94            parent_id: "".into(),
95            observe: "o".into(),
96            decision: "d".into(),
97            grounds: vec![
98                Ground {
99                    claim: "no Redis".into(),
100                    supports: "chosen".into(),
101                    check: Some(Check::Test {
102                        reference: "pytest x".into(),
103                        verified_at_sha: "d308afac1b2c3d4e5f60718293a4b5c6d7e8f901".into(),
104                        counter_test: Some("ct".into()),
105                        liveness: Liveness {
106                            platforms: vec!["linux-ci".into()],
107                            triggered_by: vec!["f".into()],
108                            surfaces: vec!["s".into()],
109                        },
110                    }),
111                },
112                Ground {
113                    claim: "team ok".into(),
114                    supports: "chosen".into(),
115                    check: Some(Check::Person {
116                        reference: "Q3".into(),
117                    }),
118                },
119            ],
120            status: "live".into(),
121            held_since: "".into(),
122            blame: "Wang Yu".into(),
123            authority: None,
124            jurisdiction: None,
125            round_id: None,
126        };
127        let rows = vec![
128            (
129                &tick.grounds[0],
130                Verdict::NotRun {
131                    missing_platforms: vec!["linux-ci".into()],
132                },
133            ),
134            (&tick.grounds[1], Verdict::NotApplicable),
135        ];
136
137        // when: the state file is written
138        write_state(&s, &tick.id, &rows, "live-origin", None).unwrap();
139
140        // then: results/state/<id>.json records the tick id and each ground's verdict
141        let text = std::fs::read_to_string(
142            s.root
143                .join("results")
144                .join("state")
145                .join("abcabcabcabc.json"),
146        )
147        .unwrap();
148        let v: Value = serde_json::from_str(&text).unwrap();
149        assert_eq!(v["tick_id"], "abcabcabcabc");
150        assert_eq!(v["grounds"][0]["check"], "test");
151        assert_eq!(v["grounds"][0]["ref"], "pytest x");
152        assert_eq!(v["grounds"][0]["verdict"], "not-run");
153        assert_eq!(v["grounds"][0]["missing_platforms"][0], "linux-ci");
154        assert_eq!(v["grounds"][1]["check"], "person");
155        assert_eq!(v["grounds"][1]["verdict"], "n/a");
156    }
157}