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}