Skip to main content

telltale_bridge/
protocol_machine_trace.rs

1//! Semantic-audit normalization helpers for cross-runtime comparison.
2//!
3//! Bridge normalization ledger for semantic-audit comparison:
4//! - `tick`: semantically normalized per extracted session id so traces can be
5//!   compared independent of cross-session scheduler interleavings.
6//! - `session`: used only to derive the per-session normalization bucket and
7//!   then compared exactly.
8//! - all other event payload fields are compared exactly after normalization.
9//! - events without any extractable session identifier are left untouched.
10//!
11//! This module intentionally does not perform schema backfills or display/debug
12//! cleanup. Schema-compatibility backfills live in
13//! `protocol_machine_runner_json_parsing.rs`.
14
15use serde::{Deserialize, Serialize};
16use serde_json::Value;
17use std::collections::{BTreeMap, HashMap};
18
19/// Normalized event for cross-ProtocolMachine comparison.
20/// Drops absolute session IDs (may differ), normalizes to session index.
21#[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
30/// Per-session trace: the subsequence of events for one session.
31pub type SessionTrace = Vec<NormalizedEvent>;
32
33/// Topology perturbation kinds recorded in effect traces.
34#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
35pub enum TopologyPerturbationKind {
36    Crash,
37    Partition,
38    Heal,
39}
40
41/// Optional topology perturbation payload associated with an effect.
42#[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/// Effect-trace record used for replay and determinism checks.
54#[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/// Output-condition verification record in replay traces.
67#[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/// Replay bundle combining semantic-audit events with effect/output-condition traces.
77#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
78pub struct ProtocolMachineReplayBundle {
79    /// Schema version for this payload.
80    #[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/// Try to extract a session identifier from an event payload.
90///
91/// Supports common shapes used by protocol-machine semantic audit events:
92/// - `{ "session": <usize>, ... }`
93/// - `{ "sid": <usize>, ... }`
94/// - nested `{ "edge": { "sid": <usize> } }`
95/// - nested `{ "endpoint": { "sid": <usize> } }`
96#[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/// Normalize tick values per session so traces can be compared independent of
151/// cross-session scheduling interleavings.
152///
153/// This is the only semantic normalization applied to protocol-machine audit
154/// events. The event payload itself is preserved exactly.
155#[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/// Compare two traces after per-session normalization.
182#[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/// Minimal shared observational equivalence relation for Lean/Rust semantic audits.
194///
195/// Equivalence is defined as equality after per-session tick normalization.
196#[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/// Extract per-session traces from a flat event list.
208#[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}