1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
//! The field plane (#223): per-path observations inside a payload — what
//! moved, what never did, and which paths the cap turned away.
use super::asked::u64_is_zero;
use super::doctor::DoctorFinding;
use serde::Serialize;
/// One dotted path's statistics over a `zenctl field` window (#223) — the
/// per-field arrival story validation cannot tell.
#[derive(Debug, Clone, Serialize)]
pub struct FieldRow {
/// The concrete wire key the path was observed under.
pub key: String,
/// The dotted path inside the structural value (`$` = a non-object root).
pub path: String,
/// Document samples in which the path was present.
pub seen: u64,
/// The key's document samples — the presence ratio's denominator.
pub documents: u64,
/// JSON kinds observed (one entry = type-stable).
pub kinds: Vec<String>,
/// Times the value differed from its previous observation.
pub changes: u64,
/// Window-relative seconds of the last change; absent = never changed
/// within the window (which the stated window scopes — not "never").
#[serde(skip_serializing_if = "Option::is_none")]
pub last_change_s: Option<f64>,
/// Numeric min/max/last, present only when the path carried numbers.
#[serde(skip_serializing_if = "Option::is_none")]
pub min: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last: Option<f64>,
/// Small-domain distinct values, total when present; absent = the domain
/// outgrew the cap (stated, never silently partial).
#[serde(skip_serializing_if = "Option::is_none")]
pub values: Option<Vec<String>>,
}
/// The `zenctl field` report (#223): bounded per-path statistics over one
/// window, plus the `field-vanished`/`field-stuck`/`field-new` findings.
/// Every bound states its cost (RFC 09 §5.1 O6), and "not asked" — no
/// registry, no served schema, no structural document — never reads as "no"
/// (O4).
#[derive(Debug, Clone, Serialize)]
pub struct FieldReport {
pub selector: String,
pub window_s: f64,
pub samples: u64,
pub keys_seen: usize,
/// Samples the bounded observer missed (O6).
pub dropped: u64,
/// Samples carrying no structural document — fields unobservable for
/// them, counted apart from absence (O4).
pub undocumented: u64,
/// Samples whose payload was past the observation limit and therefore
/// never read — distinct from `undocumented`, which means the payload was
/// read and carried no document (RFC 09 §5.1 O6).
#[serde(skip_serializing_if = "u64_is_zero")]
pub unread: u64,
/// Whether a registry was loaded: without one, declared `ttl_s` and type
/// names are unknown and `field-stuck`/`field-new` are unjudgeable.
pub registry_loaded: bool,
/// Distinct (key, path) pairs tracked, against the bound they ran under.
pub paths: usize,
pub max_paths: usize,
/// Path observations refused to stay within the bound (O6) — the table
/// never truncates silently.
pub paths_dropped: u64,
/// Up to a handful of `key · path` names among the refused.
#[serde(skip_serializing_if = "Vec::is_empty")]
pub paths_dropped_examples: Vec<String>,
/// Key projections the bounded facts cache (#107) retired during the
/// window — non-zero means the declared-ttl/type context covers the
/// retained keys only (RFC 09 §5.1 O6). Absent when zero.
#[serde(skip_serializing_if = "u64_is_zero", default)]
pub facts_evicted: u64,
pub rows: Vec<FieldRow>,
pub findings: Vec<DoctorFinding>,
}
#[cfg(test)]
mod tests {
use super::*;
/// Same contract again for `zenctl field --format json` (#223): the
/// document changes only deliberately, and every "not asked" is an
/// absent field, never a null or a zero (RFC 09 §5.1 O4).
#[test]
fn field_report_json_shape_is_pinned() {
let report = FieldReport {
selector: "v1/*/state/demo/health".into(),
window_s: 30.0,
samples: 40,
keys_seen: 1,
dropped: 0,
undocumented: 2,
unread: 0,
registry_loaded: true,
paths: 2,
max_paths: 512,
paths_dropped: 0,
paths_dropped_examples: vec![],
facts_evicted: 0,
rows: vec![FieldRow {
key: "v1/h-3fa9c2d41b7e/state/demo/health".into(),
path: "temperature_c".into(),
seen: 38,
documents: 38,
kinds: vec!["number".into()],
changes: 0,
last_change_s: None,
min: Some(21.5),
max: Some(21.5),
last: Some(21.5),
values: Some(vec!["21.5".into()]),
}],
findings: vec![],
};
let json = serde_json::to_value(&report).unwrap();
assert_eq!(
json,
serde_json::json!({
"selector": "v1/*/state/demo/health",
"window_s": 30.0,
"samples": 40,
"keys_seen": 1,
"dropped": 0,
"undocumented": 2,
"registry_loaded": true,
"paths": 2,
"max_paths": 512,
"paths_dropped": 0,
"rows": [{
"key": "v1/h-3fa9c2d41b7e/state/demo/health",
"path": "temperature_c",
"seen": 38,
"documents": 38,
"kinds": ["number"],
"changes": 0,
"min": 21.5,
"max": 21.5,
"last": 21.5,
"values": ["21.5"],
}],
"findings": [],
}),
"`last_change_s` and dropped-path examples are absent when there \
is nothing to say, never null (O4); `values` absent would mean \
the domain overflowed the cap"
);
}
}