Skip to main content

zenkey_fleet/report/
impact.rs

1//! Impact attribution as a wire shape (#389, RFC 06 §5.6): which down
2//! entities are **roots** and which firing sites are **symptoms** of one —
3//! what a notifier writes beside an inhibited notification and what a
4//! script reads to ask "cause or symptom". Computed by
5//! [`crate::model::impact::attribute`].
6
7use serde::Serialize;
8
9/// A down entity with no down containment ancestor.
10#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
11pub struct Root {
12    pub entity: String,
13    /// Every entity the bounded walk reached from this root, ordered.
14    pub reached: Vec<String>,
15}
16
17/// A firing or down site that a root explains.
18#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
19pub struct Symptom {
20    pub entity: String,
21    /// The nearest root (ties by root id, ascending).
22    pub explained_by: String,
23}
24
25/// The whole attribution, ordered (RFC 06 §5.6: two consumers with the
26/// same inputs render the same thing), with what the bounds cost (RFC 13
27/// §3 O6): a walk cut at the depth cap, a walk that came back to its own
28/// root.
29#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
30pub struct ImpactReport {
31    pub roots: Vec<Root>,
32    pub symptoms: Vec<Symptom>,
33    /// Walks that still had unvisited edges at the depth cap.
34    pub walks_capped: u64,
35    /// Downward walks that returned to their own root.
36    pub cycles_seen: u64,
37    pub depth_cap: usize,
38}
39
40#[cfg(test)]
41mod tests {
42    use super::*;
43
44    /// The JSON a script reads: ordered arrays, snake_case counters.
45    #[test]
46    fn impact_report_json_shape_is_pinned() {
47        let r = ImpactReport {
48            roots: vec![Root {
49                entity: "ent-a".into(),
50                reached: vec!["ent-b".into(), "ent-c".into()],
51            }],
52            symptoms: vec![Symptom {
53                entity: "ent-c".into(),
54                explained_by: "ent-a".into(),
55            }],
56            walks_capped: 0,
57            cycles_seen: 1,
58            depth_cap: 4,
59        };
60        assert_eq!(
61            serde_json::to_value(&r).unwrap(),
62            serde_json::json!({
63                "roots": [{"entity": "ent-a", "reached": ["ent-b", "ent-c"]}],
64                "symptoms": [{"entity": "ent-c", "explained_by": "ent-a"}],
65                "walks_capped": 0,
66                "cycles_seen": 1,
67                "depth_cap": 4,
68            })
69        );
70    }
71}