metrale_governance/event.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-26: The event types a journey file holds, one JSON object per line.
4//!
5//! Owner: metrale-governance.
6//! Invariants: none beyond the types.
7
8use serde::{Deserialize, Serialize};
9
10/// 2026-09-26: The verdict a gate reached.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(rename_all = "lowercase")]
13pub enum Verdict {
14 Pass,
15 Fail,
16 /// 2026-09-26: No record covered the commit, as opposed to `Fail`, a run
17 /// whose numbers were out of bounds.
18 Missing,
19}
20
21/// 2026-09-26: What happened. Serialised with a `kind` tag, flattened into
22/// [`Event`].
23#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
24#[serde(tag = "kind", rename_all = "snake_case")]
25pub enum EventKind {
26 /// 2026-09-26: A lifecycle transition. `to` is meant to name a state of the
27 /// agent loop table in CONTRIBUTING.md (`branch` … `merge`); nothing
28 /// validates it.
29 State { to: String },
30 /// 2026-09-26: A gate was evaluated. `invalidated_by` names the paths that
31 /// re-opened it; it is omitted from the JSON when empty.
32 Gate {
33 id: String,
34 verdict: Verdict,
35 #[serde(default, skip_serializing_if = "Vec::is_empty")]
36 invalidated_by: Vec<String>,
37 #[serde(default, skip_serializing_if = "Option::is_none")]
38 detail: Option<String>,
39 },
40 /// 2026-09-26: The advisory classifier's opinion. `status` is free text;
41 /// the gate check's intent report counts only `ok` and `partial`.
42 Category { value: String, status: String },
43 /// 2026-09-26: A benchmark run's metrics.
44 Measurement {
45 benchmark: String,
46 #[serde(default)]
47 metrics: std::collections::BTreeMap<String, f64>,
48 },
49}
50
51/// 2026-09-26: One line of a journey. [`Event::identity`] is
52/// `(head_sha, run_id, attempt, kind)`: `pr` and `at` are left out, so the same
53/// event appended twice collapses when read.
54#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
55pub struct Event {
56 pub pr: u64,
57 pub head_sha: String,
58 /// 2026-09-26: The CI run that observed this; empty when absent from the
59 /// JSON.
60 #[serde(default)]
61 pub run_id: String,
62 /// 2026-09-26: Which attempt of that run. Part of the identity, so a re-run
63 /// is kept as a separate event.
64 #[serde(default)]
65 pub attempt: u32,
66 /// 2026-09-26: Unix seconds. Not part of the identity.
67 pub at: u64,
68 #[serde(flatten)]
69 pub kind: EventKind,
70}
71
72impl Event {
73 /// 2026-09-26: The deduplication key: head sha, run id, attempt and the
74 /// serialised kind.
75 pub fn identity(&self) -> String {
76 // 2026-09-26: The serialised kind carries the variant tag and all of its
77 // fields, so a new variant needs no change here.
78 let kind = serde_json::to_string(&self.kind).unwrap_or_default();
79 format!(
80 "{}|{}|{}|{}",
81 self.head_sha, self.run_id, self.attempt, kind
82 )
83 }
84
85 /// 2026-09-26: The `label` field of this event's node in
86 /// [`crate::ledger::materialize`].
87 pub fn node_label(&self) -> &'static str {
88 match self.kind {
89 EventKind::State { .. } => "state",
90 EventKind::Gate { .. } => "gate",
91 EventKind::Category { .. } => "category",
92 EventKind::Measurement { .. } => "measurement",
93 }
94 }
95}