zenkey_fleet/report/generate.rs
1//! The generator plane (#162): what a mock producer was asked to publish,
2//! and what it actually did.
3//!
4//! Every plan entry and every report here describes traffic that **did not
5//! happen on a real fleet**. The synthetic marker rides the samples
6//! themselves; these shapes are the paper trail that says a run was a
7//! rehearsal.
8
9use serde::Serialize;
10use zenkey::schema::TypeSchema;
11
12/// A single deliberate deviation from a known-valid synthesized sample (#163).
13///
14/// Fault injection is a *mode of the generator*, not a sibling tool: it reuses
15/// the whole registry walk, synthesis, scheduling, and guard machinery, then
16/// perturbs one dimension of the output **after synthesis** — so the delta
17/// from valid is always known, printable (`Fault::delta`), and stamped into
18/// the marker (`"fault": "<kind>"`, RFC 09 §5.3). The point is
19/// consumer-robustness testing: a consumer that crashes on a truncated payload
20/// fails RFC 09 §5.1 O1's spirit — a non-conforming sample is a fact to
21/// report, not an error to die on.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
23#[serde(rename_all = "kebab-case")]
24pub enum Fault {
25 /// Cut the encoded payload to half its bytes — a partial frame the
26 /// decoder meets mid-value.
27 Truncate,
28 /// Replace the body with a JSON value of the wrong shape for the declared
29 /// type (a bare string where a structured type is declared).
30 WrongType,
31 /// Add an undeclared field to the (JSON) body — the extra key a strict
32 /// schema must reject or a lenient one must ignore, never choke on.
33 ExtraField,
34 /// Publish on a key that matches no registered subject (a trailing chunk
35 /// the registry never declared).
36 UnregisteredKey,
37 /// Publish under a QoS profile other than the subject's declared one
38 /// (RFC 04 §3) — the observed-vs-declared mismatch a doctor listen flags.
39 WrongQos,
40 /// Publish with no wire `Encoding` set, though the subject declares one —
41 /// a consumer keyed on the encoding meets a blank.
42 MissingEncoding,
43 /// Publish a state sample carrying no HLC timestamp: LWW cannot order it
44 /// (RFC 04 §4), and freshness is unjudgeable.
45 Unstamped,
46}
47
48/// One subject the run will publish, fully resolved — the plan is printed
49/// before anything touches the bus (the replay dry-run precedent).
50#[derive(Debug, Clone, Serialize)]
51pub struct GenPlanEntry {
52 pub key: String,
53 pub class: String,
54 pub producer: String,
55 pub type_name: String,
56 pub qos: String,
57 /// `declared` or `default` — where the profile came from (#158's rule:
58 /// a generator that picks QoS silently is the write-side O4 mistake).
59 pub qos_source: &'static str,
60 pub rate_hz: f64,
61 /// `describe` / `schema-set` / `placeholder` — where the body shape
62 /// came from.
63 pub body_source: &'static str,
64 #[serde(skip_serializing_if = "Option::is_none")]
65 pub encoding: Option<String>,
66 /// For `events`: the hard cap on sends within the run (the declared
67 /// budget, RFC 04 §1.3) — a generator must not out-shout the registry
68 /// it claims to follow.
69 #[serde(skip_serializing_if = "Option::is_none")]
70 pub events_cap: Option<u64>,
71 #[serde(skip_serializing_if = "Option::is_none")]
72 pub note: Option<String>,
73 /// The fault this entry injects (#163), `None` for a conforming entry —
74 /// stamped into every one of its samples' markers.
75 #[serde(skip_serializing_if = "Option::is_none")]
76 pub fault: Option<Fault>,
77 /// The printable delta from valid this fault introduces (#163) — stated
78 /// in the plan before a byte moves.
79 #[serde(skip_serializing_if = "Option::is_none")]
80 pub fault_delta: Option<String>,
81 #[serde(skip)]
82 pub schema: Option<TypeSchema>,
83 /// Absolute chunk index (into the full wire key) of the per-send unique
84 /// id — set for events, whose keys MUST be write-once (RFC 04 §1.3).
85 #[serde(skip)]
86 pub unique_chunk: Option<usize>,
87}
88
89/// What one run did.
90#[derive(Debug, Clone, Serialize)]
91pub struct GenReport {
92 pub duration_s: f64,
93 pub entries: usize,
94 pub sent: u64,
95 /// Bodies the schema refused to encode — counted, never silently
96 /// skipped (these are a bug in the synthesizer or the schema, and
97 /// either way the operator hears about it).
98 pub refused: u64,
99 #[serde(skip_serializing_if = "Vec::is_empty")]
100 pub first_errors: Vec<String>,
101}