Skip to main content

asupersync/trace/distributed/
vclock.rs

1//! Vector clocks for causal ordering of distributed trace events.
2//!
3//! A vector clock maps each node in the system to a logical counter. It captures
4//! the causal partial order: events are either causally ordered (happens-before)
5//! or concurrent. This avoids imposing a false total order on distributed events.
6//!
7//! # Usage
8//!
9//! ```rust
10//! use asupersync::trace::distributed::vclock::{VectorClock, CausalOrder};
11//! use asupersync::remote::NodeId;
12//!
13//! let mut vc_a = VectorClock::new();
14//! let node_a = NodeId::new("node-a");
15//! let node_b = NodeId::new("node-b");
16//!
17//! vc_a.increment(&node_a);
18//! vc_a.increment(&node_a);
19//!
20//! let mut vc_b = VectorClock::new();
21//! vc_b.increment(&node_b);
22//!
23//! // These are concurrent — neither happened before the other.
24//! assert_eq!(vc_a.partial_cmp(&vc_b), None);
25//!
26//! // Merge to get the join (componentwise max).
27//! let merged = vc_a.merge(&vc_b);
28//! assert!(merged.get(&node_a) == 2);
29//! assert!(merged.get(&node_b) == 1);
30//! ```
31
32use crate::remote::NodeId;
33use crate::time::{TimeSource, TimerDriverHandle, WallClock};
34use crate::types::Time;
35use parking_lot::Mutex;
36use serde::{Deserialize, Serialize};
37use std::collections::BTreeMap;
38use std::fmt;
39use std::sync::Arc;
40use std::sync::atomic::{AtomicU64, Ordering};
41
42/// Logical clock trait for causally ordering distributed events.
43///
44/// Uses `PartialOrd` so vector clocks (partial order) are supported.
45pub trait LogicalClock: Send + Sync {
46    /// The time representation produced by this clock.
47    type Time: Clone + PartialOrd + Send + Sync + 'static;
48
49    /// Records a local event and returns the updated time.
50    #[must_use]
51    fn tick(&self) -> Self::Time;
52
53    /// Updates the clock based on a received time and returns the updated time.
54    #[must_use]
55    fn receive(&self, sender_time: &Self::Time) -> Self::Time;
56
57    /// Returns the current time without ticking.
58    #[must_use]
59    fn now(&self) -> Self::Time;
60}
61
62/// Logical time for Lamport clocks.
63#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
64pub struct LamportTime(u64);
65
66impl LamportTime {
67    /// Returns the raw counter value.
68    #[must_use]
69    pub const fn raw(self) -> u64 {
70        self.0
71    }
72
73    /// Creates a Lamport time from a raw counter value.
74    #[must_use]
75    pub const fn from_raw(value: u64) -> Self {
76        Self(value)
77    }
78}
79
80/// Lamport logical clock (single counter).
81#[derive(Default)]
82pub struct LamportClock {
83    counter: AtomicU64,
84}
85
86impl LamportClock {
87    /// Creates a new Lamport clock starting at zero.
88    #[must_use]
89    pub fn new() -> Self {
90        Self::default()
91    }
92
93    /// Creates a Lamport clock starting at the given value.
94    #[must_use]
95    pub fn with_start(start: u64) -> Self {
96        Self {
97            counter: AtomicU64::new(start),
98        }
99    }
100
101    /// Returns the current Lamport time without incrementing.
102    #[must_use]
103    pub fn now(&self) -> LamportTime {
104        LamportTime(self.counter.load(Ordering::Acquire))
105    }
106
107    /// Records a local event and returns the updated time.
108    #[must_use]
109    pub fn tick(&self) -> LamportTime {
110        let mut current = self.counter.load(Ordering::Acquire);
111        loop {
112            let next = current
113                .checked_add(1)
114                .expect("Lamport clock overflowed while ticking");
115            match self.counter.compare_exchange_weak(
116                current,
117                next,
118                Ordering::AcqRel,
119                Ordering::Acquire,
120            ) {
121                Ok(_) => return LamportTime(next),
122                Err(actual) => current = actual,
123            }
124        }
125    }
126
127    /// Merges a received Lamport time and returns the updated time.
128    #[must_use]
129    pub fn receive(&self, sender: LamportTime) -> LamportTime {
130        let mut current = self.counter.load(Ordering::Acquire);
131        loop {
132            let next = current
133                .max(sender.raw())
134                .checked_add(1)
135                .expect("Lamport clock overflowed while merging a received time");
136            match self.counter.compare_exchange_weak(
137                current,
138                next,
139                Ordering::AcqRel,
140                Ordering::Acquire,
141            ) {
142                Ok(_) => return LamportTime(next),
143                Err(actual) => current = actual,
144            }
145        }
146    }
147}
148
149impl fmt::Debug for LamportClock {
150    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151        f.debug_struct("LamportClock")
152            .field("counter", &self.counter.load(Ordering::Relaxed))
153            .finish()
154    }
155}
156
157impl LogicalClock for LamportClock {
158    type Time = LamportTime;
159
160    fn tick(&self) -> Self::Time {
161        Self::tick(self)
162    }
163
164    fn receive(&self, sender_time: &Self::Time) -> Self::Time {
165        Self::receive(self, *sender_time)
166    }
167
168    fn now(&self) -> Self::Time {
169        Self::now(self)
170    }
171}
172
173/// Logical time for hybrid clocks (physical + logical).
174#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
175pub struct HybridTime {
176    physical: Time,
177    logical: u64,
178}
179
180impl HybridTime {
181    /// Creates a new hybrid time.
182    #[must_use]
183    pub const fn new(physical: Time, logical: u64) -> Self {
184        Self { physical, logical }
185    }
186
187    /// Returns the physical component.
188    #[must_use]
189    pub const fn physical(self) -> Time {
190        self.physical
191    }
192
193    /// Returns the logical component.
194    #[must_use]
195    pub const fn logical(self) -> u64 {
196        self.logical
197    }
198}
199
200#[derive(Debug)]
201struct HybridState {
202    last_physical: Time,
203    logical: u64,
204}
205
206/// Hybrid logical clock (HLC) with a monotonic physical component.
207pub struct HybridClock {
208    time_source: Arc<dyn TimeSource>,
209    state: Mutex<HybridState>,
210}
211
212impl HybridClock {
213    /// Creates a new hybrid clock using the provided time source.
214    #[must_use]
215    pub fn new(time_source: Arc<dyn TimeSource>) -> Self {
216        let now = time_source.now();
217        Self {
218            time_source,
219            state: Mutex::new(HybridState {
220                last_physical: now,
221                logical: 0,
222            }),
223        }
224    }
225
226    /// Returns the current hybrid time without ticking.
227    #[must_use]
228    pub fn now(&self) -> HybridTime {
229        let state = self.state.lock();
230        let physical = self.physical_now(&state);
231        let logical = if physical == state.last_physical {
232            state.logical
233        } else {
234            0
235        };
236        HybridTime::new(physical, logical)
237    }
238
239    /// Records a local event and returns the updated time.
240    #[must_use]
241    pub fn tick(&self) -> HybridTime {
242        let mut state = self.state.lock();
243        let physical = self.physical_now(&state);
244        if physical == state.last_physical {
245            state.logical = state
246                .logical
247                .checked_add(1)
248                .expect("Hybrid clock logical counter overflowed while ticking");
249        } else {
250            state.last_physical = physical;
251            state.logical = 0;
252        }
253        HybridTime::new(state.last_physical, state.logical)
254    }
255
256    /// Merges a received hybrid time and returns the updated time.
257    #[must_use]
258    pub fn receive(&self, sender: HybridTime) -> HybridTime {
259        let mut state = self.state.lock();
260        let physical_now = self.physical_now(&state);
261        let max_physical = physical_now.max(state.last_physical).max(sender.physical);
262
263        let next_logical = if max_physical == state.last_physical && max_physical == sender.physical
264        {
265            state
266                .logical
267                .max(sender.logical)
268                .checked_add(1)
269                .expect("Hybrid clock logical counter overflowed while merging equal physical time")
270        } else if max_physical == state.last_physical {
271            state.logical.checked_add(1).expect(
272                "Hybrid clock logical counter overflowed while advancing local logical time",
273            )
274        } else if max_physical == sender.physical {
275            sender.logical.checked_add(1).expect(
276                "Hybrid clock logical counter overflowed while incorporating a remote physical time",
277            )
278        } else {
279            0
280        };
281
282        state.last_physical = max_physical;
283        state.logical = next_logical;
284        HybridTime::new(state.last_physical, state.logical)
285    }
286
287    fn physical_now(&self, state: &HybridState) -> Time {
288        let physical = self.time_source.now();
289        if physical < state.last_physical {
290            state.last_physical
291        } else {
292            physical
293        }
294    }
295}
296
297impl fmt::Debug for HybridClock {
298    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
299        let state = self.state.lock();
300        f.debug_struct("HybridClock")
301            .field("last_physical", &state.last_physical)
302            .field("logical", &state.logical)
303            .finish_non_exhaustive()
304    }
305}
306
307impl LogicalClock for HybridClock {
308    type Time = HybridTime;
309
310    fn tick(&self) -> Self::Time {
311        Self::tick(self)
312    }
313
314    fn receive(&self, sender_time: &Self::Time) -> Self::Time {
315        Self::receive(self, *sender_time)
316    }
317
318    fn now(&self) -> Self::Time {
319        Self::now(self)
320    }
321}
322
323/// Logical clock wrapper for vector clocks with a local node identity.
324pub struct VectorClockHandle {
325    /// Local node identity for this vector clock.
326    node: NodeId,
327    /// Internal vector clock state protected by a mutex.
328    clock: Mutex<VectorClock>,
329}
330
331impl VectorClockHandle {
332    /// Creates a new vector clock handle for the given node.
333    #[must_use]
334    pub fn new(node: NodeId) -> Self {
335        Self {
336            node,
337            clock: Mutex::new(VectorClock::new()),
338        }
339    }
340
341    /// Returns the current vector clock snapshot.
342    #[must_use]
343    pub fn current(&self) -> VectorClock {
344        self.clock.lock().clone()
345    }
346}
347
348impl fmt::Debug for VectorClockHandle {
349    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
350        f.debug_struct("VectorClockHandle")
351            .field("node", &self.node)
352            .field("clock", &self.clock.lock())
353            .finish()
354    }
355}
356
357impl LogicalClock for VectorClockHandle {
358    type Time = VectorClock;
359
360    fn tick(&self) -> Self::Time {
361        let mut clock = self.clock.lock();
362        clock.increment(&self.node);
363        clock.clone()
364    }
365
366    fn receive(&self, sender_time: &Self::Time) -> Self::Time {
367        let mut clock = self.clock.lock();
368        clock.receive(&self.node, sender_time);
369        clock.clone()
370    }
371
372    fn now(&self) -> Self::Time {
373        self.clock.lock().clone()
374    }
375}
376
377/// Logical time values for heterogeneous clock types.
378#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
379#[serde(rename_all = "snake_case")]
380pub enum LogicalTime {
381    /// Lamport clock time.
382    Lamport(LamportTime),
383    /// Vector clock time.
384    Vector(VectorClock),
385    /// Hybrid clock time.
386    Hybrid(HybridTime),
387}
388
389impl LogicalTime {
390    /// Returns the logical clock kind for this time value.
391    #[must_use]
392    pub const fn kind(&self) -> LogicalClockKind {
393        match self {
394            Self::Lamport(_) => LogicalClockKind::Lamport,
395            Self::Vector(_) => LogicalClockKind::Vector,
396            Self::Hybrid(_) => LogicalClockKind::Hybrid,
397        }
398    }
399
400    /// Compares two logical times for causal ordering.
401    ///
402    /// Returns the causal relationship between `self` and `other`.
403    ///
404    /// Only vector clocks can establish true happens-before: their componentwise
405    /// partial order *is* the causality relation, so it is reported directly.
406    ///
407    /// Scalar Lamport / Hybrid clocks cannot. They guarantee only the forward
408    /// implication `a → b ⟹ counter(a) < counter(b)`; the converse does not
409    /// hold. A bare counter comparison therefore never establishes causal order:
410    /// `counter(a) < counter(b)` is fully consistent with `a` and `b` being
411    /// concurrent, and equal counters do not mean the two events are the same
412    /// (distinct concurrent events routinely share a Lamport counter). Mapping
413    /// those comparisons onto `Before`/`After`/`Equal` asserted causality the
414    /// clock cannot support, so for scalar clocks — and for mismatched clock
415    /// kinds — we honestly report the relationship as undetermined
416    /// (`CausalOrder::Concurrent`) rather than fabricate a happens-before edge.
417    #[must_use]
418    pub fn causal_order(&self, other: &Self) -> CausalOrder {
419        match (self, other) {
420            (Self::Vector(a), Self::Vector(b)) => a.causal_order(b),
421            _ => CausalOrder::Concurrent,
422        }
423    }
424}
425
426impl PartialOrd for LogicalTime {
427    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
428        match (self, other) {
429            (Self::Lamport(a), Self::Lamport(b)) => a.partial_cmp(b),
430            (Self::Vector(a), Self::Vector(b)) => a.partial_cmp(b),
431            (Self::Hybrid(a), Self::Hybrid(b)) => a.partial_cmp(b),
432            _ => None,
433        }
434    }
435}
436
437/// Kind of logical clock in use.
438#[derive(Clone, Copy, Debug, PartialEq, Eq)]
439pub enum LogicalClockKind {
440    /// Lamport clock.
441    Lamport,
442    /// Vector clock.
443    Vector,
444    /// Hybrid clock.
445    Hybrid,
446}
447
448/// Runtime-selected logical clock configuration.
449#[derive(Clone, Debug)]
450pub enum LogicalClockMode {
451    /// Use a Lamport clock.
452    Lamport,
453    /// Use a vector clock with the provided local node id.
454    Vector {
455        /// Local node identity for vector clock tracking.
456        node: NodeId,
457    },
458    /// Use a hybrid logical clock.
459    Hybrid,
460}
461
462/// Opaque handle to a logical clock instance.
463#[derive(Clone)]
464pub enum LogicalClockHandle {
465    /// Lamport clock handle.
466    Lamport(Arc<LamportClock>),
467    /// Vector clock handle.
468    Vector(Arc<VectorClockHandle>),
469    /// Hybrid clock handle.
470    Hybrid(Arc<HybridClock>),
471}
472
473impl LogicalClockHandle {
474    /// Returns the kind of clock this handle wraps.
475    #[must_use]
476    pub const fn kind(&self) -> LogicalClockKind {
477        match self {
478            Self::Lamport(_) => LogicalClockKind::Lamport,
479            Self::Vector(_) => LogicalClockKind::Vector,
480            Self::Hybrid(_) => LogicalClockKind::Hybrid,
481        }
482    }
483
484    /// Records a local event and returns the updated logical time.
485    #[must_use]
486    pub fn tick(&self) -> LogicalTime {
487        match self {
488            Self::Lamport(clock) => LogicalTime::Lamport(clock.tick()),
489            Self::Vector(clock) => LogicalTime::Vector(clock.tick()),
490            Self::Hybrid(clock) => LogicalTime::Hybrid(clock.tick()),
491        }
492    }
493
494    /// Updates the clock using a received logical time and returns the updated time.
495    #[must_use]
496    pub fn receive(&self, sender_time: &LogicalTime) -> LogicalTime {
497        match (self, sender_time) {
498            (Self::Lamport(clock), LogicalTime::Lamport(time)) => {
499                LogicalTime::Lamport(clock.receive(*time))
500            }
501            (Self::Vector(clock), LogicalTime::Vector(time)) => {
502                LogicalTime::Vector(clock.receive(time))
503            }
504            (Self::Hybrid(clock), LogicalTime::Hybrid(time)) => {
505                LogicalTime::Hybrid(clock.receive(*time))
506            }
507            // Mismatched clock kinds: fall back to a local tick.
508            _ => self.tick(),
509        }
510    }
511
512    /// Returns the current logical time without ticking.
513    #[must_use]
514    pub fn now(&self) -> LogicalTime {
515        match self {
516            Self::Lamport(clock) => LogicalTime::Lamport(clock.now()),
517            Self::Vector(clock) => LogicalTime::Vector(clock.now()),
518            Self::Hybrid(clock) => LogicalTime::Hybrid(clock.now()),
519        }
520    }
521}
522
523impl fmt::Debug for LogicalClockHandle {
524    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
525        match self {
526            Self::Lamport(_) => f.write_str("LogicalClockHandle::Lamport"),
527            Self::Vector(_) => f.write_str("LogicalClockHandle::Vector"),
528            Self::Hybrid(_) => f.write_str("LogicalClockHandle::Hybrid"),
529        }
530    }
531}
532
533impl Default for LogicalClockHandle {
534    fn default() -> Self {
535        Self::Lamport(Arc::new(LamportClock::new()))
536    }
537}
538
539impl LogicalClockMode {
540    /// Builds a logical clock handle for the given timer driver context.
541    #[must_use]
542    pub fn build_handle(&self, timer_driver: Option<TimerDriverHandle>) -> LogicalClockHandle {
543        match self {
544            Self::Lamport => LogicalClockHandle::Lamport(Arc::new(LamportClock::new())),
545            Self::Vector { node } => {
546                LogicalClockHandle::Vector(Arc::new(VectorClockHandle::new(node.clone())))
547            }
548            Self::Hybrid => {
549                let time_source: Arc<dyn TimeSource> = match timer_driver {
550                    Some(driver) => Arc::new(TimerDriverSource::new(driver)),
551                    None => Arc::new(WallClock::new()),
552                };
553                LogicalClockHandle::Hybrid(Arc::new(HybridClock::new(time_source)))
554            }
555        }
556    }
557}
558
559#[derive(Clone)]
560struct TimerDriverSource {
561    timer: TimerDriverHandle,
562}
563
564impl TimerDriverSource {
565    fn new(timer: TimerDriverHandle) -> Self {
566        Self { timer }
567    }
568}
569
570impl fmt::Debug for TimerDriverSource {
571    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
572        f.debug_struct("TimerDriverSource").finish()
573    }
574}
575
576impl TimeSource for TimerDriverSource {
577    fn now(&self) -> Time {
578        self.timer.now()
579    }
580}
581
582/// A vector clock for causal ordering in a distributed system.
583///
584/// Maps `NodeId → u64` counters. The partial order is:
585/// - `a ≤ b` iff `∀ node: a[node] ≤ b[node]`
586/// - `a < b` (happens-before) iff `a ≤ b` and `a ≠ b`
587/// - `a ∥ b` (concurrent) iff `¬(a ≤ b)` and `¬(b ≤ a)`
588#[derive(Clone, Default, Serialize, Deserialize)]
589pub struct VectorClock {
590    /// BTreeMap for deterministic iteration order.
591    entries: BTreeMap<NodeId, u64>,
592}
593
594/// Equality treats an absent node the same as a node mapped to `0`.
595///
596/// The derived `PartialEq` compared the backing maps structurally, so a clock
597/// that carries an explicit zero entry (e.g. `{"A": 0}` materialized by a
598/// deserialization round-trip) was `!=` an empty clock `{}`. That diverged from
599/// [`VectorClock::partial_cmp`] / [`VectorClock::causal_order`], which read
600/// missing counters as `0` via [`VectorClock::get`] and report the two clocks as
601/// `Equal`. The mismatch broke `Ord`/`Eq` coherence (`a == b` no longer implied
602/// `partial_cmp(a, b) == Some(Equal)`). This manual implementation restores the
603/// invariant by comparing every key that appears in either clock under the same
604/// absent-is-zero rule the ordering uses.
605impl PartialEq for VectorClock {
606    fn eq(&self, other: &Self) -> bool {
607        self.entries
608            .keys()
609            .chain(other.entries.keys())
610            .all(|node| self.get(node) == other.get(node))
611    }
612}
613
614// Absent-is-zero equality is a genuine equivalence relation (reflexive,
615// symmetric, transitive), so `Eq` remains sound.
616impl Eq for VectorClock {}
617
618impl VectorClock {
619    /// Creates an empty vector clock (all components zero).
620    #[must_use]
621    pub fn new() -> Self {
622        Self::default()
623    }
624
625    /// Creates a vector clock with a single node initialized to 1.
626    #[must_use]
627    pub fn for_node(node: &NodeId) -> Self {
628        let mut vc = Self::new();
629        vc.entries.insert(node.clone(), 1);
630        vc
631    }
632
633    /// Returns the counter for the given node (0 if absent).
634    #[must_use]
635    pub fn get(&self, node: &NodeId) -> u64 {
636        self.entries.get(node).copied().unwrap_or(0)
637    }
638
639    /// Increments the counter for the given node and returns the new value.
640    pub fn increment(&mut self, node: &NodeId) -> u64 {
641        let entry = self.entries.entry(node.clone()).or_insert(0);
642        *entry = entry
643            .checked_add(1)
644            .expect("Vector clock counter overflowed while incrementing");
645        *entry
646    }
647
648    /// Sets the counter for a node to a specific value, monotone.
649    ///
650    /// Used when receiving a message: update local clock to be at least
651    /// as large as the sender's value for each node.
652    pub fn set(&mut self, node: &NodeId, value: u64) {
653        if value == 0 {
654            return;
655        }
656        let entry = self.entries.entry(node.clone()).or_insert(0);
657        if value > *entry {
658            *entry = value;
659        }
660    }
661
662    /// Returns the merge (join / componentwise max) of two vector clocks.
663    ///
664    /// This is the least upper bound in the partial order.
665    #[must_use]
666    pub fn merge(&self, other: &Self) -> Self {
667        let mut result = self.clone();
668        for (node, &value) in &other.entries {
669            let entry = result.entries.entry(node.clone()).or_insert(0);
670            if value > *entry {
671                *entry = value;
672            }
673        }
674        result
675    }
676
677    /// Merges another vector clock into `self` in place.
678    pub fn merge_in(&mut self, other: &Self) {
679        for (node, &value) in &other.entries {
680            let entry = self.entries.entry(node.clone()).or_insert(0);
681            if value > *entry {
682                *entry = value;
683            }
684        }
685    }
686
687    /// Increments the local node and merges the remote clock.
688    ///
689    /// This is the standard "on receive" operation:
690    /// 1. Merge the incoming clock
691    /// 2. Increment the local counter
692    pub fn receive(&mut self, local_node: &NodeId, remote_clock: &Self) {
693        self.merge_in(remote_clock);
694        self.increment(local_node);
695    }
696
697    /// Compares two vector clocks for causal ordering.
698    #[must_use]
699    pub fn causal_order(&self, other: &Self) -> CausalOrder {
700        let mut self_leq_other = true;
701        let mut other_leq_self = true;
702
703        // Check all nodes present in either clock.
704        let all_nodes: std::collections::BTreeSet<&NodeId> =
705            self.entries.keys().chain(other.entries.keys()).collect();
706
707        for node in all_nodes {
708            let a = self.get(node);
709            let b = other.get(node);
710            if a > b {
711                self_leq_other = false;
712            }
713            if b > a {
714                other_leq_self = false;
715            }
716            if !self_leq_other && !other_leq_self {
717                return CausalOrder::Concurrent;
718            }
719        }
720
721        match (self_leq_other, other_leq_self) {
722            (true, true) => CausalOrder::Equal,
723            (true, false) => CausalOrder::Before,
724            (false, true) => CausalOrder::After,
725            (false, false) => CausalOrder::Concurrent,
726        }
727    }
728
729    /// Returns true if `self` happens-before `other`.
730    #[must_use]
731    pub fn happens_before(&self, other: &Self) -> bool {
732        self.causal_order(other) == CausalOrder::Before
733    }
734
735    /// Returns true if `self` and `other` are concurrent.
736    #[must_use]
737    pub fn is_concurrent_with(&self, other: &Self) -> bool {
738        self.causal_order(other) == CausalOrder::Concurrent
739    }
740
741    /// Returns the number of nodes tracked by this clock.
742    #[must_use]
743    pub fn node_count(&self) -> usize {
744        self.entries.len()
745    }
746
747    /// Returns true if all counters are zero (empty clock).
748    #[must_use]
749    pub fn is_zero(&self) -> bool {
750        self.entries.is_empty()
751    }
752
753    /// Returns an iterator over (node, counter) pairs.
754    pub fn iter(&self) -> impl Iterator<Item = (&NodeId, &u64)> {
755        self.entries.iter()
756    }
757}
758
759/// Implements the partial order for vector clocks.
760///
761/// Returns `None` when the clocks are concurrent (incomparable).
762impl PartialOrd for VectorClock {
763    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
764        match self.causal_order(other) {
765            CausalOrder::Before => Some(std::cmp::Ordering::Less),
766            CausalOrder::After => Some(std::cmp::Ordering::Greater),
767            CausalOrder::Equal => Some(std::cmp::Ordering::Equal),
768            CausalOrder::Concurrent => None,
769        }
770    }
771}
772
773impl fmt::Debug for VectorClock {
774    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
775        write!(f, "VC{{")?;
776        for (i, (node, value)) in self.entries.iter().enumerate() {
777            if i > 0 {
778                write!(f, ", ")?;
779            }
780            write!(f, "{}:{}", node.as_str(), value)?;
781        }
782        write!(f, "}}")
783    }
784}
785
786impl fmt::Display for VectorClock {
787    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
788        write!(f, "[")?;
789        for (i, (node, value)) in self.entries.iter().enumerate() {
790            if i > 0 {
791                write!(f, ", ")?;
792            }
793            write!(f, "{}={}", node.as_str(), value)?;
794        }
795        write!(f, "]")
796    }
797}
798
799/// Result of comparing two vector clocks.
800#[derive(Debug, Clone, Copy, PartialEq, Eq)]
801pub enum CausalOrder {
802    /// `self` happened strictly before `other`.
803    Before,
804    /// `self` happened strictly after `other`.
805    After,
806    /// `self` and `other` are exactly equal.
807    Equal,
808    /// `self` and `other` are concurrent (neither happened before the other).
809    Concurrent,
810}
811
812/// A trace event annotated with causal metadata.
813///
814/// Wraps any event with the vector clock at the time the event was recorded,
815/// plus the originating node.
816#[derive(Clone, Debug)]
817pub struct CausalEvent<T> {
818    /// The originating node.
819    pub origin: NodeId,
820    /// The vector clock at event creation time.
821    pub clock: VectorClock,
822    /// The wrapped event.
823    pub event: T,
824}
825
826impl<T> CausalEvent<T> {
827    /// Creates a new causal event.
828    pub fn new(origin: NodeId, clock: VectorClock, event: T) -> Self {
829        Self {
830            origin,
831            clock,
832            event,
833        }
834    }
835
836    /// Returns true if this event causally precedes `other`.
837    pub fn happens_before<U>(&self, other: &CausalEvent<U>) -> bool {
838        self.clock.happens_before(&other.clock)
839    }
840
841    /// Returns true if this event is concurrent with `other`.
842    pub fn is_concurrent_with<U>(&self, other: &CausalEvent<U>) -> bool {
843        self.clock.is_concurrent_with(&other.clock)
844    }
845}
846
847/// A causal history tracker for a single node.
848///
849/// Manages the local vector clock, incrementing on local events and
850/// merging on message receive.
851#[derive(Clone, Debug)]
852pub struct CausalTracker {
853    /// The local node.
854    node: NodeId,
855    /// The current vector clock.
856    clock: VectorClock,
857}
858
859impl CausalTracker {
860    /// Creates a new tracker for the given node.
861    #[must_use]
862    pub fn new(node: NodeId) -> Self {
863        Self {
864            node,
865            clock: VectorClock::new(),
866        }
867    }
868
869    /// Records a local event, incrementing the local counter.
870    ///
871    /// Returns the vector clock at the time of the event.
872    pub fn record_local_event(&mut self) -> VectorClock {
873        self.clock.increment(&self.node);
874        self.clock.clone()
875    }
876
877    /// Records a local event, wrapping it with causal metadata.
878    pub fn record<T>(&mut self, event: T) -> CausalEvent<T> {
879        let clock = self.record_local_event();
880        CausalEvent::new(self.node.clone(), clock, event)
881    }
882
883    /// Records a message send. Increments the local clock and returns
884    /// the clock to attach to the outgoing message.
885    pub fn on_send(&mut self) -> VectorClock {
886        self.record_local_event()
887    }
888
889    /// Records a message receive. Merges the incoming clock and
890    /// increments the local counter.
891    pub fn on_receive(&mut self, remote_clock: &VectorClock) {
892        self.clock.receive(&self.node, remote_clock);
893    }
894
895    /// Returns the current vector clock (snapshot).
896    #[must_use]
897    pub fn current_clock(&self) -> &VectorClock {
898        &self.clock
899    }
900
901    /// Returns the local node ID.
902    #[must_use]
903    pub fn node(&self) -> &NodeId {
904        &self.node
905    }
906}
907
908#[cfg(test)]
909mod tests {
910    #![allow(
911        clippy::pedantic,
912        clippy::nursery,
913        clippy::expect_fun_call,
914        clippy::map_unwrap_or,
915        clippy::cast_possible_wrap,
916        clippy::future_not_send
917    )]
918    use super::*;
919    use crate::time::VirtualClock;
920    use serde_json::{Value, json};
921    use std::sync::Arc;
922
923    fn node(name: &str) -> NodeId {
924        NodeId::new(name)
925    }
926
927    fn scrub_vclock_output(value: Value) -> Value {
928        fn scrub_node_names(text: &str) -> String {
929            text.replace("alpha-node", "[NODE_A]")
930                .replace("beta-node", "[NODE_B]")
931        }
932
933        fn scrub_value(value: &mut Value) {
934            match value {
935                Value::String(text) => *text = scrub_node_names(text),
936                Value::Array(values) => {
937                    for entry in values {
938                        scrub_value(entry);
939                    }
940                }
941                Value::Object(map) => {
942                    for entry in map.values_mut() {
943                        scrub_value(entry);
944                    }
945                }
946                Value::Null | Value::Bool(_) | Value::Number(_) => {}
947            }
948        }
949
950        let mut scrubbed = value;
951        scrub_value(&mut scrubbed);
952        scrubbed
953    }
954
955    #[test]
956    fn empty_clocks_are_equal() {
957        let a = VectorClock::new();
958        let b = VectorClock::new();
959        assert_eq!(a.causal_order(&b), CausalOrder::Equal);
960        assert_eq!(a.partial_cmp(&b), Some(std::cmp::Ordering::Equal));
961    }
962
963    #[test]
964    fn increment_creates_happens_before() {
965        let n = node("A");
966        let mut a = VectorClock::new();
967        let b = a.clone();
968        a.increment(&n);
969        assert_eq!(b.causal_order(&a), CausalOrder::Before);
970        assert!(b.happens_before(&a));
971    }
972
973    #[test]
974    fn concurrent_detection() {
975        let na = node("A");
976        let nb = node("B");
977        let mut a = VectorClock::new();
978        let mut b = VectorClock::new();
979        a.increment(&na);
980        b.increment(&nb);
981        assert_eq!(a.causal_order(&b), CausalOrder::Concurrent);
982        assert!(a.is_concurrent_with(&b));
983        assert_eq!(a.partial_cmp(&b), None);
984    }
985
986    #[test]
987    fn lamport_tick_and_receive() {
988        let clock = LamportClock::new();
989        let t1 = clock.tick();
990        let t2 = clock.tick();
991        assert!(t2 > t1);
992
993        let remote = LamportTime::from_raw(10);
994        let merged = clock.receive(remote);
995        assert!(merged.raw() > remote.raw());
996    }
997
998    #[test]
999    #[should_panic(expected = "Lamport clock overflowed while ticking")]
1000    fn lamport_tick_panics_on_overflow() {
1001        let clock = LamportClock::with_start(u64::MAX);
1002        let _ = clock.tick();
1003    }
1004
1005    #[test]
1006    #[should_panic(expected = "Lamport clock overflowed while merging a received time")]
1007    fn lamport_receive_panics_on_overflow() {
1008        let clock = LamportClock::with_start(u64::MAX - 1);
1009        let _ = clock.receive(LamportTime::from_raw(u64::MAX));
1010    }
1011
1012    #[test]
1013    fn hybrid_clock_deterministic_with_virtual_time() {
1014        let virtual_clock = Arc::new(VirtualClock::new());
1015        let hlc = HybridClock::new(virtual_clock.clone());
1016
1017        let t1 = hlc.tick();
1018        let t2 = hlc.tick();
1019        assert!(t2 >= t1);
1020
1021        virtual_clock.advance(1_000);
1022        let t3 = hlc.tick();
1023        assert!(t3.physical() >= t2.physical());
1024    }
1025
1026    #[test]
1027    fn hybrid_now_resets_logical_when_physical_advances() {
1028        let virtual_clock = Arc::new(VirtualClock::new());
1029        let hlc = HybridClock::new(virtual_clock.clone());
1030
1031        let t1 = hlc.tick();
1032        assert_eq!(t1.logical(), 1);
1033
1034        virtual_clock.advance(1_000);
1035        let observed = hlc.now();
1036        assert!(observed.physical() > t1.physical());
1037        assert_eq!(observed.logical(), 0);
1038
1039        let t2 = hlc.tick();
1040        assert!(t2 >= observed);
1041    }
1042
1043    #[test]
1044    #[should_panic(expected = "Hybrid clock logical counter overflowed while ticking")]
1045    fn hybrid_tick_panics_on_logical_overflow() {
1046        let time_source: Arc<dyn TimeSource> = Arc::new(VirtualClock::new());
1047        let hlc = HybridClock {
1048            time_source,
1049            state: Mutex::new(HybridState {
1050                last_physical: Time::ZERO,
1051                logical: u64::MAX,
1052            }),
1053        };
1054
1055        let _ = hlc.tick();
1056    }
1057
1058    #[test]
1059    #[should_panic(
1060        expected = "Hybrid clock logical counter overflowed while merging equal physical time"
1061    )]
1062    fn hybrid_receive_panics_on_equal_physical_logical_overflow() {
1063        let time_source: Arc<dyn TimeSource> = Arc::new(VirtualClock::new());
1064        let hlc = HybridClock {
1065            time_source,
1066            state: Mutex::new(HybridState {
1067                last_physical: Time::ZERO,
1068                logical: u64::MAX,
1069            }),
1070        };
1071
1072        let _ = hlc.receive(HybridTime::new(Time::ZERO, u64::MAX));
1073    }
1074
1075    #[test]
1076    fn merge_is_least_upper_bound() {
1077        let na = node("A");
1078        let nb = node("B");
1079        #[allow(clippy::many_single_char_names)]
1080        let mut a = VectorClock::new();
1081        a.increment(&na);
1082        a.increment(&na);
1083        let mut b = VectorClock::new();
1084        b.increment(&nb);
1085        b.increment(&nb);
1086        b.increment(&nb);
1087
1088        let merged = a.merge(&b);
1089        assert_eq!(merged.get(&na), 2);
1090        assert_eq!(merged.get(&nb), 3);
1091        // Both original clocks happen-before the merge
1092        assert!(a.happens_before(&merged));
1093        assert!(b.happens_before(&merged));
1094    }
1095
1096    #[test]
1097    fn merge_is_commutative() {
1098        let na = node("A");
1099        let nb = node("B");
1100        #[allow(clippy::many_single_char_names)]
1101        let mut a = VectorClock::new();
1102        a.increment(&na);
1103        let mut b = VectorClock::new();
1104        b.increment(&nb);
1105
1106        assert_eq!(a.merge(&b), b.merge(&a));
1107    }
1108
1109    #[test]
1110    fn merge_is_associative() {
1111        let na = node("A");
1112        let nb = node("B");
1113        let nc = node("C");
1114        #[allow(clippy::many_single_char_names)]
1115        let mut a = VectorClock::new();
1116        a.increment(&na);
1117        let mut b = VectorClock::new();
1118        b.increment(&nb);
1119        let mut c = VectorClock::new();
1120        c.increment(&nc);
1121
1122        let ab_c = a.merge(&b).merge(&c);
1123        let a_bc = a.merge(&b.merge(&c));
1124        assert_eq!(ab_c, a_bc);
1125    }
1126
1127    #[test]
1128    fn merge_is_idempotent() {
1129        let na = node("A");
1130        #[allow(clippy::many_single_char_names)]
1131        let mut a = VectorClock::new();
1132        a.increment(&na);
1133        assert_eq!(a.merge(&a), a);
1134    }
1135
1136    #[test]
1137    fn receive_merges_and_increments() {
1138        let na = node("A");
1139        let nb = node("B");
1140        #[allow(clippy::many_single_char_names)]
1141        let mut a = VectorClock::new();
1142        a.increment(&na); // A: {A:1}
1143
1144        let mut b = VectorClock::new();
1145        b.increment(&nb); // B: {B:1}
1146        b.increment(&nb); // B: {B:2}
1147
1148        // A receives a message with B's clock
1149        a.receive(&na, &b); // merge → {A:1, B:2}, then increment → {A:2, B:2}
1150        assert_eq!(a.get(&na), 2);
1151        assert_eq!(a.get(&nb), 2);
1152    }
1153
1154    #[test]
1155    fn vector_clock_output_snapshot_scrubbed() {
1156        let na = node("alpha-node");
1157        let nb = node("beta-node");
1158        #[allow(clippy::many_single_char_names)]
1159        let mut a = VectorClock::new();
1160        a.increment(&na);
1161        a.increment(&na);
1162
1163        let mut b = VectorClock::new();
1164        b.increment(&nb);
1165
1166        let merged = a.merge(&b);
1167        insta::assert_json_snapshot!(
1168            "vector_clock_output_scrubbed",
1169            scrub_vclock_output(json!({
1170                "display": merged.to_string(),
1171                "debug": format!("{merged:?}"),
1172                "order_vs_a": format!("{:?}", merged.causal_order(&a)),
1173            }))
1174        );
1175    }
1176
1177    #[test]
1178    fn for_node_initializes_to_one() {
1179        let n = node("X");
1180        let vc = VectorClock::for_node(&n);
1181        assert_eq!(vc.get(&n), 1);
1182        assert_eq!(vc.node_count(), 1);
1183    }
1184
1185    #[test]
1186    fn set_is_monotone() {
1187        let n = node("A");
1188        let mut vc = VectorClock::new();
1189        vc.set(&n, 3);
1190        assert_eq!(vc.get(&n), 3);
1191
1192        // Lower value should not regress the clock.
1193        vc.set(&n, 1);
1194        assert_eq!(vc.get(&n), 3);
1195
1196        // Higher value should advance.
1197        vc.set(&n, 7);
1198        assert_eq!(vc.get(&n), 7);
1199    }
1200
1201    #[test]
1202    #[should_panic(expected = "Vector clock counter overflowed while incrementing")]
1203    fn vector_clock_increment_panics_on_overflow() {
1204        let n = node("A");
1205        let mut vc = VectorClock::new();
1206        vc.entries.insert(n.clone(), u64::MAX);
1207        let _ = vc.increment(&n);
1208    }
1209
1210    #[test]
1211    #[should_panic(expected = "Vector clock counter overflowed while incrementing")]
1212    fn vector_clock_receive_panics_on_local_overflow() {
1213        let local = node("A");
1214        let remote = node("B");
1215        let mut vc = VectorClock::new();
1216        vc.entries.insert(local.clone(), u64::MAX);
1217
1218        let mut remote_clock = VectorClock::new();
1219        remote_clock.set(&remote, 1);
1220
1221        vc.receive(&local, &remote_clock);
1222    }
1223
1224    #[test]
1225    fn causal_tracker_send_receive_protocol() {
1226        let na = node("A");
1227        let nb = node("B");
1228        let mut tracker_a = CausalTracker::new(na.clone());
1229        let mut tracker_b = CausalTracker::new(nb.clone());
1230
1231        // A does local work
1232        tracker_a.record_local_event(); // A: {A:1}
1233
1234        // A sends message to B
1235        let msg_clock = tracker_a.on_send(); // A: {A:2}
1236        assert_eq!(msg_clock.get(&na), 2);
1237
1238        // B receives message from A
1239        tracker_b.on_receive(&msg_clock); // B: merge({}, {A:2}) → {A:2}, incr → {A:2, B:1}
1240        assert_eq!(tracker_b.current_clock().get(&na), 2);
1241        assert_eq!(tracker_b.current_clock().get(&nb), 1);
1242
1243        // B does more work
1244        tracker_b.record_local_event(); // B: {A:2, B:2}
1245
1246        // B's events happen after A's send
1247        assert!(msg_clock.happens_before(tracker_b.current_clock()));
1248    }
1249
1250    #[test]
1251    fn causal_tracker_transcript_snapshot_scrubbed() {
1252        let na = node("alpha-node");
1253        let nb = node("beta-node");
1254        let mut tracker_a = CausalTracker::new(na);
1255        let mut tracker_b = CausalTracker::new(nb);
1256        let node_a = tracker_a.node().as_str().to_string();
1257        let node_b = tracker_b.node().as_str().to_string();
1258
1259        let a_local = tracker_a.record_local_event();
1260        let a_send = tracker_a.on_send();
1261        tracker_b.on_receive(&a_send);
1262        let b_after_receive = tracker_b.current_clock().clone();
1263        let b_local = tracker_b.record_local_event();
1264
1265        insta::assert_json_snapshot!(
1266            "causal_tracker_transcript_scrubbed",
1267            scrub_vclock_output(json!({
1268                "steps": [
1269                    {
1270                        "step": "a_local",
1271                        "node": node_a,
1272                        "clock": a_local.to_string(),
1273                    },
1274                    {
1275                        "step": "a_send",
1276                        "node": tracker_a.node().as_str(),
1277                        "clock": a_send.to_string(),
1278                        "order_vs_local": format!("{:?}", a_send.causal_order(&a_local)),
1279                    },
1280                    {
1281                        "step": "b_receive",
1282                        "node": node_b,
1283                        "clock": b_after_receive.to_string(),
1284                        "order_vs_send": format!("{:?}", b_after_receive.causal_order(&a_send)),
1285                    },
1286                    {
1287                        "step": "b_local",
1288                        "node": tracker_b.node().as_str(),
1289                        "clock": b_local.to_string(),
1290                        "send_happens_before": a_send.happens_before(&b_local),
1291                        "receive_happens_before": b_after_receive.happens_before(&b_local),
1292                    }
1293                ]
1294            }))
1295        );
1296    }
1297
1298    #[test]
1299    fn causal_event_ordering() {
1300        let na = node("A");
1301        let nb = node("B");
1302        let mut tracker_a = CausalTracker::new(na);
1303        let mut tracker_b = CausalTracker::new(nb);
1304
1305        let e1 = tracker_a.record("event-1");
1306        let e2 = tracker_b.record("event-2");
1307
1308        // Independent events are concurrent
1309        assert!(e1.is_concurrent_with(&e2));
1310        assert!(!e1.happens_before(&e2));
1311    }
1312
1313    #[test]
1314    fn display_formatting() {
1315        let na = node("A");
1316        let nb = node("B");
1317        let mut vc = VectorClock::new();
1318        vc.increment(&na);
1319        vc.increment(&nb);
1320        vc.increment(&nb);
1321        let display = format!("{vc}");
1322        assert!(display.contains("A=1"));
1323        assert!(display.contains("B=2"));
1324    }
1325
1326    #[test]
1327    fn partial_order_after() {
1328        let na = node("A");
1329        #[allow(clippy::many_single_char_names)]
1330        let mut a = VectorClock::new();
1331        a.increment(&na);
1332        let b = VectorClock::new();
1333        assert_eq!(a.causal_order(&b), CausalOrder::After);
1334        assert_eq!(a.partial_cmp(&b), Some(std::cmp::Ordering::Greater));
1335    }
1336
1337    #[test]
1338    fn three_node_diamond() {
1339        // Classic diamond:
1340        //   A sends to B and C independently
1341        //   B and C are concurrent
1342        //   D receives from both B and C
1343        let na = node("A");
1344        let nb = node("B");
1345        let nc = node("C");
1346        let nd = node("D");
1347
1348        let mut ta = CausalTracker::new(na);
1349        let mut tb = CausalTracker::new(nb);
1350        let mut tc = CausalTracker::new(nc);
1351        let mut td = CausalTracker::new(nd);
1352
1353        // A sends to B and C
1354        let msg_to_b = ta.on_send();
1355        let msg_to_c = ta.on_send();
1356
1357        tb.on_receive(&msg_to_b);
1358        tc.on_receive(&msg_to_c);
1359
1360        // B and C do independent work
1361        let b_clock = tb.on_send();
1362        let c_clock = tc.on_send();
1363
1364        // B and C are concurrent
1365        assert!(b_clock.is_concurrent_with(&c_clock));
1366
1367        // D receives from B then C
1368        td.on_receive(&b_clock);
1369        td.on_receive(&c_clock);
1370
1371        // D happens after both B and C
1372        assert!(b_clock.happens_before(td.current_clock()));
1373        assert!(c_clock.happens_before(td.current_clock()));
1374    }
1375
1376    // =========================================================================
1377    // Wave 55 – pure data-type trait coverage
1378    // =========================================================================
1379
1380    #[test]
1381    fn hybrid_time_debug_clone_copy_hash_ord() {
1382        use std::collections::HashSet;
1383        let ht = HybridTime::new(Time::from_nanos(1_000), 3);
1384        let dbg = format!("{ht:?}");
1385        assert!(dbg.contains("HybridTime"), "{dbg}");
1386        let copied = ht;
1387        let cloned = ht;
1388        assert_eq!(copied, cloned);
1389
1390        let earlier = HybridTime::new(Time::ZERO, 0);
1391        assert!(earlier < ht);
1392
1393        let mut set = HashSet::new();
1394        set.insert(ht);
1395        set.insert(earlier);
1396        assert_eq!(set.len(), 2);
1397        assert!(set.contains(&ht));
1398    }
1399
1400    #[test]
1401    fn logical_clock_kind_debug_clone_copy_eq() {
1402        let k = LogicalClockKind::Lamport;
1403        let dbg = format!("{k:?}");
1404        assert!(dbg.contains("Lamport"), "{dbg}");
1405        let copied = k;
1406        let cloned = k;
1407        assert_eq!(copied, cloned);
1408        assert_ne!(k, LogicalClockKind::Vector);
1409        assert_ne!(k, LogicalClockKind::Hybrid);
1410    }
1411
1412    #[test]
1413    fn logical_time_debug_clone_eq() {
1414        let lt = LogicalTime::Lamport(LamportTime::from_raw(5));
1415        let dbg = format!("{lt:?}");
1416        assert!(dbg.contains("Lamport"), "{dbg}");
1417        let cloned = lt.clone();
1418        assert_eq!(lt, cloned);
1419    }
1420
1421    #[test]
1422    fn logical_clock_mode_debug_clone() {
1423        let mode = LogicalClockMode::Lamport;
1424        let dbg = format!("{mode:?}");
1425        assert!(dbg.contains("Lamport"), "{dbg}");
1426        let cloned = mode;
1427        let dbg2 = format!("{cloned:?}");
1428        assert_eq!(dbg, dbg2);
1429    }
1430
1431    // ------------------------------------------------------------------------
1432    // Golden-artifact: canonical VectorClock serialization snapshot.
1433    //
1434    // Freezes the Debug and Display string forms of VectorClock across the
1435    // states that the rest of the distributed trace layer treats as wire
1436    // serialization: empty, single-node, multi-node after merge, after a
1437    // send/receive round-trip, and for each of the four CausalOrder verdicts.
1438    //
1439    // BTreeMap-backed entries guarantee deterministic iteration; node names
1440    // are scrubbed so the golden never captures literal identities.
1441    // ------------------------------------------------------------------------
1442    #[test]
1443    fn canonical_vector_clock_serialization_snapshot() {
1444        let na = node("alpha-node");
1445        let nb = node("beta-node");
1446
1447        // Empty.
1448        let empty = VectorClock::new();
1449
1450        // Single-node, initialized to 1.
1451        let single = VectorClock::for_node(&na);
1452
1453        // Multi-node after independent increments + merge.
1454        #[allow(clippy::many_single_char_names)]
1455        let mut a = VectorClock::new();
1456        a.increment(&na);
1457        a.increment(&na);
1458        let mut b = VectorClock::new();
1459        b.increment(&nb);
1460        let merged = a.merge(&b);
1461
1462        // Post-receive: A learns from B's clock and advances locally.
1463        let mut c = VectorClock::new();
1464        c.increment(&na);
1465        let mut d = VectorClock::new();
1466        d.increment(&nb);
1467        d.increment(&nb);
1468        c.receive(&na, &d); // merge({A:1},{B:2}) → {A:1,B:2}, incr A → {A:2,B:2}
1469
1470        // Four canonical CausalOrder pairs.
1471        //   Before:     {A:1}       vs {A:2}
1472        //   After:      {A:2}       vs {A:1}
1473        //   Equal:      {A:1,B:1}   vs {A:1,B:1}
1474        //   Concurrent: {A:1}       vs {B:1}
1475        let before_lhs = {
1476            let mut v = VectorClock::new();
1477            v.increment(&na);
1478            v
1479        };
1480        let before_rhs = {
1481            let mut v = VectorClock::new();
1482            v.increment(&na);
1483            v.increment(&na);
1484            v
1485        };
1486        let equal_lhs = {
1487            let mut v = VectorClock::new();
1488            v.increment(&na);
1489            v.increment(&nb);
1490            v
1491        };
1492        let equal_rhs = {
1493            let mut v = VectorClock::new();
1494            v.increment(&na);
1495            v.increment(&nb);
1496            v
1497        };
1498        let concurrent_lhs = {
1499            let mut v = VectorClock::new();
1500            v.increment(&na);
1501            v
1502        };
1503        let concurrent_rhs = {
1504            let mut v = VectorClock::new();
1505            v.increment(&nb);
1506            v
1507        };
1508
1509        // set(n, 0) is a documented no-op; record the resulting Display to
1510        // lock the invariant into the golden.
1511        let mut zero_set = VectorClock::new();
1512        zero_set.set(&na, 0);
1513
1514        insta::assert_json_snapshot!(
1515            "canonical_vector_clock_serialization",
1516            scrub_vclock_output(json!({
1517                "states": {
1518                    "empty":      { "display": empty.to_string(),  "debug": format!("{empty:?}"),  "node_count": empty.node_count(),  "is_zero": empty.is_zero() },
1519                    "single":     { "display": single.to_string(), "debug": format!("{single:?}"), "node_count": single.node_count(), "is_zero": single.is_zero() },
1520                    "merged":     { "display": merged.to_string(), "debug": format!("{merged:?}"), "node_count": merged.node_count(), "is_zero": merged.is_zero() },
1521                    "post_receive": { "display": c.to_string(),    "debug": format!("{c:?}"),      "node_count": c.node_count(),      "is_zero": c.is_zero() },
1522                    "set_zero_noop": { "display": zero_set.to_string(), "debug": format!("{zero_set:?}"), "is_zero": zero_set.is_zero() },
1523                },
1524                "causal_orders": [
1525                    { "case": "before",     "lhs": before_lhs.to_string(),     "rhs": before_rhs.to_string(),     "verdict": format!("{:?}", before_lhs.causal_order(&before_rhs)) },
1526                    { "case": "after",      "lhs": before_rhs.to_string(),     "rhs": before_lhs.to_string(),     "verdict": format!("{:?}", before_rhs.causal_order(&before_lhs)) },
1527                    { "case": "equal",      "lhs": equal_lhs.to_string(),      "rhs": equal_rhs.to_string(),      "verdict": format!("{:?}", equal_lhs.causal_order(&equal_rhs)) },
1528                    { "case": "concurrent", "lhs": concurrent_lhs.to_string(), "rhs": concurrent_rhs.to_string(), "verdict": format!("{:?}", concurrent_lhs.causal_order(&concurrent_rhs)) },
1529                ],
1530                "iter_determinism": {
1531                    // BTreeMap ordering guarantees keys sort lexicographically.
1532                    "merged_keys_in_order": merged.iter()
1533                        .map(|(n, v)| format!("{}={}", n.as_str(), v))
1534                        .collect::<Vec<_>>(),
1535                }
1536            }))
1537        );
1538    }
1539}