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}