Skip to main content

zenkey_fleet/report/
cutover.rs

1//! The cutover plane (RFC 09 §6): did a migration finish?
2//!
3//! [`CutoverVerdict`] has three states, not two, and that is the whole
4//! point: "the old family is quiet on a fleet that is provably speaking"
5//! and "everything is quiet" are different facts, and only the first is
6//! evidence.
7
8use serde::Serialize;
9
10/// The RFC 09 §6 cutover-acceptance verdict (issue #59). Three states, not
11/// two: "the old family is quiet on a fleet that is provably speaking" and
12/// "everything is quiet" are different facts, and only the first is
13/// evidence a migration finished.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
15#[serde(rename_all = "snake_case")]
16pub enum CutoverVerdict {
17    /// Old root silent, new plane carrying traffic — both halves held.
18    Pass,
19    /// The retired family still speaks — the migration is not done.
20    OldStillSpeaks,
21    /// The old root was silent but so was the new plane: a non-verdict
22    /// (RFC 05 §3.1) — a dead fleet passes the silence half for free.
23    Unproven,
24}
25
26impl CutoverVerdict {
27    /// The [`Judgement`](crate::report::Judgement) mapping (RFC 13,
28    /// v1.24). The judged claim is the finding — "the retired family still
29    /// speaks":
30    ///
31    /// | verdict | judgement | exit (RFC 13) |
32    /// |---|---|---|
33    /// | `OldStillSpeaks` | `Established` (finding) | 1 |
34    /// | `Pass` | `NotEstablished` (clean) | 0 |
35    /// | `Unproven` | `Unobservable` | 2 |
36    pub fn to_judgement(self) -> crate::report::Judgement {
37        use crate::report::Judgement;
38        match self {
39            CutoverVerdict::OldStillSpeaks => Judgement::Established,
40            CutoverVerdict::Pass => Judgement::NotEstablished {
41                reason: "the retired family is silent while the new plane carries traffic".into(),
42            },
43            CutoverVerdict::Unproven => Judgement::Unobservable {
44                reason: "both planes were silent — a dead fleet passes the silence half \
45                         for free (RFC 05 §3.1)"
46                    .into(),
47            },
48        }
49    }
50}
51
52/// The inverse of [`CutoverVerdict::to_judgement`]. Both unestablished poles
53/// fold to `Unproven`: a question that was not put (or could not be carried)
54/// proves no migration.
55impl From<crate::report::Judgement> for CutoverVerdict {
56    fn from(j: crate::report::Judgement) -> CutoverVerdict {
57        use crate::report::Judgement;
58        match j {
59            Judgement::Established => CutoverVerdict::OldStillSpeaks,
60            Judgement::NotEstablished { .. } => CutoverVerdict::Pass,
61            Judgement::NotAsked | Judgement::Unobservable { .. } => CutoverVerdict::Unproven,
62        }
63    }
64}
65
66/// The `zenctl cutover` report (issue #59; RFC 09 §6 half one).
67#[derive(Debug, Clone, Serialize)]
68pub struct CutoverReport {
69    pub old_root: String,
70    /// The stated meaning of "new plane": keys under this prefix. Stated,
71    /// not inferred — the version chunk is plain, so key algebra cannot
72    /// separate old from new (RFC 09 §6's note).
73    pub new_prefix: String,
74    pub window_s: f64,
75    /// Samples heard on the old root — every one is a failure fact.
76    pub old_samples: u64,
77    pub old_keys_seen: usize,
78    /// Up to a cap of offending keys, with per-key counts.
79    #[serde(skip_serializing_if = "Vec::is_empty")]
80    pub old_examples: Vec<String>,
81    /// Samples on the new plane over the window.
82    pub new_samples: u64,
83    /// Samples that were neither: outside `<base>/v1/` and not the old
84    /// root. Leaks by this check's stated definition.
85    pub leak_samples: u64,
86    pub leaked_keys_seen: usize,
87    #[serde(skip_serializing_if = "Vec::is_empty")]
88    pub leak_examples: Vec<String>,
89    /// Samples the bounded observer missed (O6): non-zero weakens the
90    /// silence claim and the report says so.
91    pub dropped: u64,
92    pub verdict: CutoverVerdict,
93}