1use serde::{Deserialize, Serialize};
16use serde_json::Value;
17use std::collections::{BTreeMap, HashMap};
18
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
22pub struct NormalizedEvent {
23 pub kind: String,
24 pub session_index: usize,
25 pub sender: String,
26 pub receiver: String,
27 pub label: Option<String>,
28}
29
30pub type SessionTrace = Vec<NormalizedEvent>;
32
33#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
35pub enum TopologyPerturbationKind {
36 Crash,
37 Partition,
38 Heal,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
43pub struct TopologyPerturbationEvent {
44 pub kind: TopologyPerturbationKind,
45 #[serde(default)]
46 pub site: Option<String>,
47 #[serde(default)]
48 pub from: Option<String>,
49 #[serde(default)]
50 pub to: Option<String>,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
55pub struct EffectTraceEvent {
56 pub effect_id: u64,
57 pub effect_kind: String,
58 pub inputs: Value,
59 pub outputs: Value,
60 pub handler_identity: String,
61 pub ordering_key: u64,
62 #[serde(default)]
63 pub topology: Option<TopologyPerturbationEvent>,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
68pub struct OutputConditionTraceEvent {
69 pub predicate_ref: String,
70 #[serde(default)]
71 pub witness_ref: Option<String>,
72 pub output_digest: String,
73 pub passed: bool,
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
78pub struct ProtocolMachineReplayBundle {
79 #[serde(deserialize_with = "crate::schema::deserialize_schema_version")]
81 pub schema_version: String,
82 pub semantic_audit: Vec<NormalizedEvent>,
83 #[serde(default)]
84 pub effect_trace: Vec<EffectTraceEvent>,
85 #[serde(default)]
86 pub output_condition_trace: Vec<OutputConditionTraceEvent>,
87}
88
89#[must_use]
97pub fn event_session<E>(event: &E) -> Option<usize>
98where
99 E: Serialize,
100{
101 let value = serde_json::to_value(event).ok()?;
102 event_session_from_value(&value)
103}
104
105fn event_session_from_value(value: &Value) -> Option<usize> {
106 match value {
107 Value::Object(map) => {
108 if let Some(sid) = map.get("session").and_then(Value::as_u64) {
109 return usize::try_from(sid).ok();
110 }
111 if let Some(sid) = map.get("sid").and_then(Value::as_u64) {
112 return usize::try_from(sid).ok();
113 }
114 if let Some(edge_sid) = map
115 .get("edge")
116 .and_then(Value::as_object)
117 .and_then(|edge| edge.get("sid"))
118 .and_then(Value::as_u64)
119 {
120 return usize::try_from(edge_sid).ok();
121 }
122 if let Some(endpoint_sid) = map
123 .get("endpoint")
124 .and_then(Value::as_object)
125 .and_then(|endpoint| endpoint.get("sid"))
126 .and_then(Value::as_u64)
127 {
128 return usize::try_from(endpoint_sid).ok();
129 }
130
131 for nested in map.values() {
132 if let Some(sid) = event_session_from_value(nested) {
133 return Some(sid);
134 }
135 }
136 None
137 }
138 Value::Array(items) => {
139 for nested in items {
140 if let Some(sid) = event_session_from_value(nested) {
141 return Some(sid);
142 }
143 }
144 None
145 }
146 _ => None,
147 }
148}
149
150#[must_use]
156pub fn normalize_semantic_audit<E>(
157 trace: &[crate::semantic_objects::TickedObsEvent<E>],
158) -> Vec<crate::semantic_objects::TickedObsEvent<E>>
159where
160 E: Serialize + Clone,
161{
162 let mut counters: HashMap<usize, u64> = HashMap::new();
163 let mut out = Vec::with_capacity(trace.len());
164
165 for ev in trace {
166 if let Some(sid) = event_session(&ev.event) {
167 let counter = counters.entry(sid).or_insert(0);
168 out.push(crate::semantic_objects::TickedObsEvent {
169 tick: *counter,
170 event: ev.event.clone(),
171 });
172 *counter += 1;
173 } else {
174 out.push(ev.clone());
175 }
176 }
177
178 out
179}
180
181#[must_use]
183pub fn semantic_audits_equivalent<E>(
184 rust_trace: &[crate::semantic_objects::TickedObsEvent<E>],
185 lean_trace: &[crate::semantic_objects::TickedObsEvent<E>],
186) -> bool
187where
188 E: Serialize + Clone + PartialEq,
189{
190 normalize_semantic_audit(rust_trace) == normalize_semantic_audit(lean_trace)
191}
192
193#[must_use]
197pub fn observationally_equivalent<E>(
198 left: &[crate::semantic_objects::TickedObsEvent<E>],
199 right: &[crate::semantic_objects::TickedObsEvent<E>],
200) -> bool
201where
202 E: Serialize + Clone + PartialEq,
203{
204 semantic_audits_equivalent(left, right)
205}
206
207#[must_use]
209pub fn partition_by_session(events: &[NormalizedEvent]) -> BTreeMap<usize, SessionTrace> {
210 let mut map: BTreeMap<usize, SessionTrace> = BTreeMap::new();
211 for ev in events {
212 map.entry(ev.session_index).or_default().push(ev.clone());
213 }
214 map
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220 use crate::semantic_objects::TickedObsEvent;
221 use serde_json::json;
222
223 #[test]
224 fn event_session_extracts_common_shapes() {
225 assert_eq!(
226 event_session(&json!({"session": 3, "kind": "sent"})),
227 Some(3)
228 );
229 assert_eq!(event_session(&json!({"sid": 4, "kind": "opened"})), Some(4));
230 assert_eq!(
231 event_session(&json!({"edge": {"sid": 5, "from": "A", "to": "B"}})),
232 Some(5)
233 );
234 assert_eq!(
235 event_session(&json!({"event": {"endpoint": {"sid": 6, "role": "A"}}})),
236 Some(6)
237 );
238 assert_eq!(event_session(&json!({"kind": "halted"})), None);
239 }
240
241 #[test]
242 fn normalize_semantic_audit_rewrites_ticks_per_session() {
243 let trace = vec![
244 TickedObsEvent {
245 tick: 10,
246 event: json!({"session": 1, "kind": "sent"}),
247 },
248 TickedObsEvent {
249 tick: 11,
250 event: json!({"session": 2, "kind": "sent"}),
251 },
252 TickedObsEvent {
253 tick: 12,
254 event: json!({"session": 1, "kind": "received"}),
255 },
256 ];
257
258 let normalized = normalize_semantic_audit(&trace);
259 assert_eq!(normalized[0].tick, 0);
260 assert_eq!(normalized[1].tick, 0);
261 assert_eq!(normalized[2].tick, 1);
262 }
263
264 #[test]
265 fn normalize_semantic_audit_preserves_event_payloads() {
266 let trace = vec![
267 TickedObsEvent {
268 tick: 9,
269 event: json!({
270 "session": 7,
271 "kind": "sent",
272 "sender": "A",
273 "receiver": "B",
274 "label": "ping"
275 }),
276 },
277 TickedObsEvent {
278 tick: 12,
279 event: json!({
280 "session": 7,
281 "kind": "received",
282 "sender": "A",
283 "receiver": "B",
284 "label": "ping"
285 }),
286 },
287 ];
288
289 let normalized = normalize_semantic_audit(&trace);
290 assert_eq!(normalized[0].tick, 0);
291 assert_eq!(normalized[1].tick, 1);
292 assert_eq!(normalized[0].event, trace[0].event);
293 assert_eq!(normalized[1].event, trace[1].event);
294 }
295
296 #[test]
297 fn normalize_semantic_audit_leaves_non_session_events_untouched() {
298 let trace = vec![TickedObsEvent {
299 tick: 42,
300 event: json!({"kind": "halted", "target": "3"}),
301 }];
302
303 let normalized = normalize_semantic_audit(&trace);
304 assert_eq!(normalized, trace);
305 }
306
307 #[test]
308 fn semantic_audits_equivalent_compares_normalized_ticks() {
309 let rust_trace = vec![
310 TickedObsEvent {
311 tick: 50,
312 event: json!({"session": 1, "kind": "sent"}),
313 },
314 TickedObsEvent {
315 tick: 99,
316 event: json!({"session": 1, "kind": "received"}),
317 },
318 ];
319 let lean_trace = vec![
320 TickedObsEvent {
321 tick: 1,
322 event: json!({"session": 1, "kind": "sent"}),
323 },
324 TickedObsEvent {
325 tick: 2,
326 event: json!({"session": 1, "kind": "received"}),
327 },
328 ];
329 assert!(semantic_audits_equivalent(&rust_trace, &lean_trace));
330 }
331}