Skip to main content

dreamwell_engine/
chronoshift.rs

1// Chronoshift — deterministic timeline replay, checkpoint, and fork primitives.
2//
3// Provides:
4//   - CheckpointBuilder:  snapshot creation from event streams and state
5//   - ReplayEngine:       deterministic event replay with chain validation
6//   - ForkSpec:           timeline fork specification
7//   - TimelineDiff:       structural comparison between divergent timelines
8//   - ProofBundle:        cryptographic proof of a timeline segment (Merkle tree)
9//   - StateSnapshot:      serializable simulation state at a tick
10//   - CanonEventSnapshot: lightweight event record for replay
11//
12// Hashing strategy (per CRYPTOGRAPHY.md):
13//   - FNV-1a (64-bit): fast deterministic digests for event chains and tick hashes
14//   - BLAKE3 (256-bit): cryptographic hashes for state snapshots and Merkle trees
15
16use serde::{Deserialize, Serialize};
17
18// Note: fnv1a_64 import removed — all hashing in chronoshift now uses BLAKE3.
19
20// =============================================================================
21// Error Types
22// =============================================================================
23
24/// Errors produced by Chronobreak operations.
25#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
26pub enum ChronoshiftError {
27    /// Referenced checkpoint does not exist for the given timeline and tick.
28    CheckpointNotFound { timeline: String, tick: u64 },
29    /// Digest chain is broken at the specified event.
30    DigestChainBroken {
31        tick: u64,
32        seq: u64,
33        expected: String,
34        actual: String,
35    },
36    /// Replay produced a different hash than the original, indicating
37    /// non-determinism or state corruption.
38    DeterminismViolation {
39        tick: u64,
40        source_hash: String,
41        replay_hash: String,
42    },
43    /// Maximum number of concurrent forks has been reached.
44    ForkLimitExceeded { max: u32, current: u32 },
45    /// Replay buffer would exceed the maximum event cap.
46    ReplayOverflow { max: usize, attempted: usize },
47    /// Event tick is less than the previous event's tick (out of order).
48    TickOrderViolation { prev_tick: u64, curr_tick: u64 },
49    /// The requested replay range is invalid (from > to, or zero-length).
50    ReplayRangeInvalid { from: u64, to: u64 },
51    /// Referenced timeline does not exist.
52    TimelineNotFound { timeline: String },
53    /// Timeline has expired and can no longer be read or replayed.
54    TimelineExpired { timeline: String, expired_at: u64 },
55    /// Fork would exceed the compute budget cap.
56    ComputeCapExceeded { cap_pct: u32 },
57}
58
59impl core::fmt::Display for ChronoshiftError {
60    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
61        match self {
62            Self::CheckpointNotFound { timeline, tick } => {
63                write!(f, "checkpoint_not_found: timeline={timeline}, tick={tick}")
64            }
65            Self::DigestChainBroken {
66                tick,
67                seq,
68                expected,
69                actual,
70            } => {
71                write!(
72                    f,
73                    "digest_chain_broken: tick={tick}, seq={seq}, expected={expected}, actual={actual}"
74                )
75            }
76            Self::DeterminismViolation {
77                tick,
78                source_hash,
79                replay_hash,
80            } => {
81                write!(
82                    f,
83                    "determinism_violation: tick={tick}, source={source_hash}, replay={replay_hash}"
84                )
85            }
86            Self::ForkLimitExceeded { max, current } => {
87                write!(f, "fork_limit_exceeded: max={max}, current={current}")
88            }
89            Self::ReplayOverflow { max, attempted } => {
90                write!(f, "replay_overflow: max={max}, attempted={attempted}")
91            }
92            Self::TickOrderViolation { prev_tick, curr_tick } => {
93                write!(f, "tick_order_violation: prev_tick={prev_tick}, curr_tick={curr_tick}")
94            }
95            Self::ReplayRangeInvalid { from, to } => {
96                write!(f, "replay_range_invalid: from={from}, to={to}")
97            }
98            Self::TimelineNotFound { timeline } => {
99                write!(f, "timeline_not_found: {timeline}")
100            }
101            Self::TimelineExpired { timeline, expired_at } => {
102                write!(f, "timeline_expired: {timeline}, expired_at={expired_at}")
103            }
104            Self::ComputeCapExceeded { cap_pct } => {
105                write!(f, "compute_cap_exceeded: cap_pct={cap_pct}")
106            }
107        }
108    }
109}
110
111// =============================================================================
112// Data Snapshots
113// =============================================================================
114
115/// Lightweight event record for replay. Contains only the fields needed
116/// to reconstruct the digest chain and replay deterministically.
117#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
118pub struct CanonEventSnapshot {
119    pub event_id: String,
120    pub timeline_id: String,
121    pub event_type: String,
122    pub scope_key: String,
123    pub actor_id: String,
124    pub target_id: String,
125    pub payload: String,
126    pub tick: u64,
127    pub seq: u64,
128    pub digest: String,
129    pub prev_event_id: String,
130}
131
132/// Serializable snapshot of simulation state at a specific tick.
133#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
134pub struct StateSnapshot {
135    pub timeline_id: String,
136    pub tick: u64,
137    pub entity_count: u64,
138    pub event_count: u64,
139    /// Head of each scope's digest chain: (scope_key, latest_digest).
140    pub scope_chain_heads: Vec<(String, String)>,
141    pub state_hash: String,
142}
143
144/// Immutable checkpoint record produced by `CheckpointBuilder::build`.
145#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
146pub struct CheckpointSnapshot {
147    pub timeline_id: String,
148    pub tick: u64,
149    pub event_hash: String,
150    pub state_hash: String,
151}
152
153// =============================================================================
154// Fork Specification
155// =============================================================================
156
157/// Visibility level for forked timelines.
158#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
159pub enum ForkVisibilityLevel {
160    Private,
161    Shared,
162    Public,
163}
164
165/// Specification for creating a new timeline fork from a parent.
166#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
167pub struct ForkSpec {
168    pub parent_timeline: String,
169    pub fork_tick: u64,
170    pub name: String,
171    pub visibility: ForkVisibilityLevel,
172    /// Maximum compute budget as a percentage (0..=100).
173    pub compute_cap_pct: u32,
174    /// Time-to-live in ticks. The fork expires at `fork_tick + ttl_ticks`.
175    pub ttl_ticks: u64,
176}
177
178// =============================================================================
179// CheckpointBuilder
180// =============================================================================
181
182/// Constructs checkpoint snapshots from event streams and state.
183pub struct CheckpointBuilder {
184    timeline_id: String,
185    tick: u64,
186}
187
188impl CheckpointBuilder {
189    /// Create a builder targeting a specific timeline and tick.
190    pub fn new(timeline_id: &str, tick: u64) -> Self {
191        Self {
192            timeline_id: timeline_id.to_string(),
193            tick,
194        }
195    }
196
197    /// Compute the BLAKE3 chain hash over all events up to this checkpoint.
198    ///
199    /// Events are hashed in order by feeding each event's digest.
200    /// This produces the same result regardless of how many times it is
201    /// computed, as long as the input events are in canonical order.
202    pub fn compute_event_hash(events: &[CanonEventSnapshot]) -> String {
203        let mut hasher = blake3::Hasher::new_derive_key("dreamwell.checkpoint.events.v1");
204        for event in events {
205            hasher.update(event.digest.as_bytes());
206            hasher.update(b"|");
207        }
208        hasher.finalize().to_hex().to_string()
209    }
210
211    /// Compute BLAKE3 hash of the serialized state snapshot.
212    pub fn compute_state_hash(state: &StateSnapshot) -> Result<String, String> {
213        let serialized = serde_json::to_string(state).map_err(|e| format!("state_hash_serialization_failed:{}", e))?;
214        Ok(blake3::hash(serialized.as_bytes()).to_hex().to_string())
215    }
216
217    /// Build a complete checkpoint snapshot.
218    pub fn build(&self, events: &[CanonEventSnapshot], state: &StateSnapshot) -> Result<CheckpointSnapshot, String> {
219        let event_hash = Self::compute_event_hash(events);
220        let state_hash = Self::compute_state_hash(state)?;
221        Ok(CheckpointSnapshot {
222            timeline_id: self.timeline_id.clone(),
223            tick: self.tick,
224            event_hash,
225            state_hash,
226        })
227    }
228}
229
230// =============================================================================
231// ReplayEngine
232// =============================================================================
233
234/// Replays canon events from a source timeline onto a target timeline.
235///
236/// Events are validated for digest chain integrity before replay.
237/// The engine supports tick-by-tick and range-based replay, and can
238/// produce a determinism hash for comparison with the original.
239/// Maximum number of events a ReplayEngine buffer will accept.
240pub const MAX_REPLAY_EVENTS: usize = 1_000_000;
241
242pub struct ReplayEngine {
243    source_timeline: String,
244    target_timeline: String,
245    from_tick: u64,
246    to_tick: u64,
247    events: Vec<CanonEventSnapshot>,
248}
249
250impl ReplayEngine {
251    /// Create a replay engine for the given source/target and tick range.
252    pub fn new(source: &str, target: &str, from_tick: u64, to_tick: u64) -> Self {
253        Self {
254            source_timeline: source.to_string(),
255            target_timeline: target.to_string(),
256            from_tick,
257            to_tick,
258            events: Vec::new(),
259        }
260    }
261
262    /// Return a reference to the source timeline ID.
263    pub fn source_timeline(&self) -> &str {
264        &self.source_timeline
265    }
266
267    /// Return a reference to the target timeline ID.
268    pub fn target_timeline(&self) -> &str {
269        &self.target_timeline
270    }
271
272    /// Return the start tick (inclusive).
273    pub fn from_tick(&self) -> u64 {
274        self.from_tick
275    }
276
277    /// Return the end tick (inclusive).
278    pub fn to_tick(&self) -> u64 {
279        self.to_tick
280    }
281
282    /// Return a reference to all loaded events.
283    pub fn events(&self) -> &[CanonEventSnapshot] {
284        &self.events
285    }
286
287    /// Append an event to the replay buffer.
288    ///
289    /// Returns `Err(ReplayOverflow)` if the buffer has reached `MAX_REPLAY_EVENTS`.
290    pub fn add_event(&mut self, event: CanonEventSnapshot) -> Result<(), ChronoshiftError> {
291        if self.events.len() >= MAX_REPLAY_EVENTS {
292            return Err(ChronoshiftError::ReplayOverflow {
293                max: MAX_REPLAY_EVENTS,
294                attempted: self.events.len() + 1,
295            });
296        }
297        self.events.push(event);
298        Ok(())
299    }
300
301    /// Validate the digest chain of all loaded events.
302    ///
303    /// For each event after the first, recompute the expected digest from
304    /// the event fields and compare it to the stored digest. The first
305    /// event's digest is trusted as the chain root.
306    ///
307    /// Returns `Ok(())` if the chain is intact, or `Err` at the first
308    /// broken link.
309    pub fn validate_chain(&self) -> Result<(), ChronoshiftError> {
310        if self.events.is_empty() {
311            return Ok(());
312        }
313
314        for i in 1..self.events.len() {
315            let prev = &self.events[i - 1];
316            let curr = &self.events[i];
317
318            // Tick ordering: current tick must not be less than previous tick.
319            if curr.tick < prev.tick {
320                return Err(ChronoshiftError::TickOrderViolation {
321                    prev_tick: prev.tick,
322                    curr_tick: curr.tick,
323                });
324            }
325
326            // The current event must reference the previous event.
327            if curr.prev_event_id != prev.event_id {
328                return Err(ChronoshiftError::DigestChainBroken {
329                    tick: curr.tick,
330                    seq: curr.seq,
331                    expected: prev.event_id.clone(),
332                    actual: curr.prev_event_id.clone(),
333                });
334            }
335
336            // Recompute the digest and verify.
337            let recomputed = recompute_digest(curr);
338            if recomputed != curr.digest {
339                return Err(ChronoshiftError::DigestChainBroken {
340                    tick: curr.tick,
341                    seq: curr.seq,
342                    expected: recomputed,
343                    actual: curr.digest.clone(),
344                });
345            }
346        }
347
348        Ok(())
349    }
350
351    /// Get all events for a specific tick, in sequence order.
352    pub fn replay_tick(&self, tick: u64) -> Vec<CanonEventSnapshot> {
353        let mut tick_events: Vec<CanonEventSnapshot> = self.events.iter().filter(|e| e.tick == tick).cloned().collect();
354        tick_events.sort_by_key(|e| e.seq);
355        tick_events
356    }
357
358    /// Get all events in a tick range [from, to] inclusive, in order.
359    pub fn replay_range(&self, from: u64, to: u64) -> Vec<CanonEventSnapshot> {
360        if from > to {
361            return Vec::new(); // inverted range returns empty — caller must validate
362        }
363        let mut range_events: Vec<CanonEventSnapshot> = self
364            .events
365            .iter()
366            .filter(|e| e.tick >= from && e.tick <= to)
367            .cloned()
368            .collect();
369        range_events.sort_by(|a, b| a.tick.cmp(&b.tick).then(a.seq.cmp(&b.seq)));
370        range_events
371    }
372
373    /// Compute a BLAKE3 hash over all replayed events for determinism
374    /// comparison. The hash covers event_id, event_type, tick, seq,
375    /// and digest of every event in canonical order.
376    pub fn compute_determinism_hash(&self) -> String {
377        let mut sorted = self.events.clone();
378        sorted.sort_by(|a, b| a.tick.cmp(&b.tick).then(a.seq.cmp(&b.seq)));
379
380        let mut hasher = blake3::Hasher::new();
381        for event in &sorted {
382            hasher.update(event.event_id.as_bytes());
383            hasher.update(event.event_type.as_bytes());
384            hasher.update(&event.tick.to_le_bytes());
385            hasher.update(&event.seq.to_le_bytes());
386            hasher.update(event.digest.as_bytes());
387        }
388        hasher.finalize().to_hex().to_string()
389    }
390}
391
392// =============================================================================
393// Timeline Diff
394// =============================================================================
395
396/// A single event mismatch between two timelines at the same (tick, seq).
397#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
398pub struct EventMismatch {
399    pub tick: u64,
400    pub seq: u64,
401    pub source_digest: String,
402    pub target_digest: String,
403    pub source_event_type: String,
404    pub target_event_type: String,
405}
406
407/// Structural diff result between two timelines over a tick range.
408#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
409pub struct TimelineDiff {
410    pub source: String,
411    pub target: String,
412    pub from_tick: u64,
413    pub to_tick: u64,
414    pub source_event_count: u64,
415    pub target_event_count: u64,
416    /// The first tick at which the timelines diverge, if any.
417    pub divergence_tick: Option<u64>,
418    /// All events where the digests differ at the same (tick, seq).
419    pub mismatched_events: Vec<EventMismatch>,
420    /// True if all events in the overlap produced identical digests.
421    pub determinism_verified: bool,
422}
423
424// =============================================================================
425// ProofBundle
426// =============================================================================
427
428/// Cryptographic proof of a timeline segment.
429///
430/// Binds a contiguous sequence of canon events to a Merkle root,
431/// allowing offline verification of segment integrity without
432/// replaying the full chain.
433#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
434pub struct ProofBundle {
435    pub proof_id: String,
436    pub timeline_id: String,
437    pub from_tick: u64,
438    pub to_tick: u64,
439    pub event_count: u64,
440    pub root_event_id: String,
441    pub final_event_id: String,
442    pub root_digest: String,
443    pub final_digest: String,
444    pub state_hash_start: String,
445    pub state_hash_end: String,
446    pub merkle_root: String,
447    pub created_at_tick: u64,
448}
449
450impl ProofBundle {
451    /// Compute the BLAKE3 Merkle root over event digests.
452    pub fn compute_merkle_root(events: &[CanonEventSnapshot]) -> String {
453        let leaves: Vec<String> = events.iter().map(|e| e.digest.clone()).collect();
454        compute_merkle_tree(&leaves)
455    }
456
457    /// Verify the digest chain of the given events.
458    ///
459    /// Delegates to the same chain-validation logic used by `ReplayEngine`.
460    pub fn verify_chain(events: &[CanonEventSnapshot]) -> Result<(), ChronoshiftError> {
461        if events.is_empty() {
462            return Ok(());
463        }
464
465        for i in 1..events.len() {
466            let prev = &events[i - 1];
467            let curr = &events[i];
468
469            if curr.prev_event_id != prev.event_id {
470                return Err(ChronoshiftError::DigestChainBroken {
471                    tick: curr.tick,
472                    seq: curr.seq,
473                    expected: prev.event_id.clone(),
474                    actual: curr.prev_event_id.clone(),
475                });
476            }
477
478            let recomputed = recompute_digest(curr);
479            if recomputed != curr.digest {
480                return Err(ChronoshiftError::DigestChainBroken {
481                    tick: curr.tick,
482                    seq: curr.seq,
483                    expected: recomputed,
484                    actual: curr.digest.clone(),
485                });
486            }
487        }
488
489        Ok(())
490    }
491}
492
493// =============================================================================
494// Merkle Tree
495// =============================================================================
496
497/// Compute a binary Merkle tree root from leaf hashes using BLAKE3.
498///
499/// Leaves are hashed as-is (they are already hex-encoded digests).
500/// Internal nodes are `BLAKE3(left || right)`. If the number of leaves
501/// at any level is odd, the last leaf is promoted unpaired.
502///
503/// Returns the 64-character hex root hash. An empty input returns a
504/// BLAKE3 hash of the empty string (well-defined zero value).
505pub fn compute_merkle_tree(leaves: &[String]) -> String {
506    if leaves.is_empty() {
507        return blake3::hash(b"").to_hex().to_string();
508    }
509
510    // Hash each leaf to produce a uniform-length layer.
511    let mut layer: Vec<blake3::Hash> = leaves.iter().map(|leaf| blake3::hash(leaf.as_bytes())).collect();
512
513    while layer.len() > 1 {
514        let mut next_layer = Vec::with_capacity(layer.len().div_ceil(2));
515
516        let mut i = 0;
517        while i + 1 < layer.len() {
518            let mut hasher = blake3::Hasher::new();
519            hasher.update(layer[i].as_bytes());
520            hasher.update(layer[i + 1].as_bytes());
521            next_layer.push(hasher.finalize());
522            i += 2;
523        }
524
525        // Odd leaf: hash with itself per RFC 6962 style instead of promoting unpaired.
526        if i < layer.len() {
527            let mut hasher = blake3::Hasher::new();
528            hasher.update(layer[i].as_bytes());
529            hasher.update(layer[i].as_bytes());
530            next_layer.push(hasher.finalize());
531        }
532
533        layer = next_layer;
534    }
535
536    layer[0].to_hex().to_string()
537}
538
539// =============================================================================
540// Helper Functions
541// =============================================================================
542
543/// Recompute an event's BLAKE3 digest from its fields.
544///
545/// Delegates to `canon::compute_event_digest` to ensure a single source of truth
546/// for the digest format.
547fn recompute_digest(event: &CanonEventSnapshot) -> String {
548    crate::canon::compute_event_digest(
549        &event.event_id,
550        &event.event_type,
551        event.tick,
552        event.seq,
553        &event.timeline_id,
554        &event.scope_key,
555        &event.actor_id,
556        &event.prev_event_id,
557    )
558}
559
560/// Compare two event streams and produce a structural diff.
561///
562/// Both streams are filtered to the `[from_tick, to_tick]` range.
563/// Events are matched by (tick, seq) pairs; mismatches are recorded
564/// when digests differ. The divergence tick is the first tick
565/// containing a mismatch.
566pub fn diff_timelines(
567    source: &[CanonEventSnapshot],
568    target: &[CanonEventSnapshot],
569    from_tick: u64,
570    to_tick: u64,
571) -> TimelineDiff {
572    let source_filtered: Vec<&CanonEventSnapshot> = source
573        .iter()
574        .filter(|e| e.tick >= from_tick && e.tick <= to_tick)
575        .collect();
576    let target_filtered: Vec<&CanonEventSnapshot> = target
577        .iter()
578        .filter(|e| e.tick >= from_tick && e.tick <= to_tick)
579        .collect();
580
581    let source_timeline = source_filtered
582        .first()
583        .map(|e| e.timeline_id.clone())
584        .unwrap_or_default();
585    let target_timeline = target_filtered
586        .first()
587        .map(|e| e.timeline_id.clone())
588        .unwrap_or_default();
589
590    // Build lookup: (tick, seq) -> event for the target.
591    let mut target_map: std::collections::HashMap<(u64, u64), &CanonEventSnapshot> = std::collections::HashMap::new();
592    for event in &target_filtered {
593        target_map.insert((event.tick, event.seq), event);
594    }
595
596    let mut mismatched_events = Vec::new();
597    let mut divergence_tick: Option<u64> = None;
598
599    for src_event in &source_filtered {
600        let key = (src_event.tick, src_event.seq);
601        if let Some(tgt_event) = target_map.get(&key) {
602            if src_event.digest != tgt_event.digest {
603                mismatched_events.push(EventMismatch {
604                    tick: src_event.tick,
605                    seq: src_event.seq,
606                    source_digest: src_event.digest.clone(),
607                    target_digest: tgt_event.digest.clone(),
608                    source_event_type: src_event.event_type.clone(),
609                    target_event_type: tgt_event.event_type.clone(),
610                });
611                if divergence_tick.is_none() || src_event.tick < divergence_tick.unwrap() {
612                    divergence_tick = Some(src_event.tick);
613                }
614            }
615        } else {
616            // Source event has no counterpart in target — divergence.
617            mismatched_events.push(EventMismatch {
618                tick: src_event.tick,
619                seq: src_event.seq,
620                source_digest: src_event.digest.clone(),
621                target_digest: String::new(),
622                source_event_type: src_event.event_type.clone(),
623                target_event_type: String::new(),
624            });
625            if divergence_tick.is_none() || src_event.tick < divergence_tick.unwrap() {
626                divergence_tick = Some(src_event.tick);
627            }
628        }
629    }
630
631    // Check for target events not present in source.
632    let mut source_map: std::collections::HashMap<(u64, u64), &CanonEventSnapshot> = std::collections::HashMap::new();
633    for event in &source_filtered {
634        source_map.insert((event.tick, event.seq), event);
635    }
636    for tgt_event in &target_filtered {
637        let key = (tgt_event.tick, tgt_event.seq);
638        if !source_map.contains_key(&key) {
639            mismatched_events.push(EventMismatch {
640                tick: tgt_event.tick,
641                seq: tgt_event.seq,
642                source_digest: String::new(),
643                target_digest: tgt_event.digest.clone(),
644                source_event_type: String::new(),
645                target_event_type: tgt_event.event_type.clone(),
646            });
647            if divergence_tick.is_none() || tgt_event.tick < divergence_tick.unwrap() {
648                divergence_tick = Some(tgt_event.tick);
649            }
650        }
651    }
652
653    // Sort mismatches by (tick, seq) for deterministic output.
654    mismatched_events.sort_by(|a, b| a.tick.cmp(&b.tick).then(a.seq.cmp(&b.seq)));
655
656    let determinism_verified = mismatched_events.is_empty();
657
658    TimelineDiff {
659        source: source_timeline,
660        target: target_timeline,
661        from_tick,
662        to_tick,
663        source_event_count: source_filtered.len() as u64,
664        target_event_count: target_filtered.len() as u64,
665        divergence_tick,
666        mismatched_events,
667        determinism_verified,
668    }
669}
670
671/// Check whether two event streams are deterministically equivalent.
672///
673/// Returns `true` if both streams have the same length and every
674/// event at the same index has an identical digest.
675pub fn verify_determinism(original: &[CanonEventSnapshot], replay: &[CanonEventSnapshot]) -> bool {
676    if original.len() != replay.len() {
677        return false;
678    }
679    original.iter().zip(replay.iter()).all(|(o, r)| o.digest == r.digest)
680}
681
682/// Compute the BLAKE3 hash of all events at a specific tick.
683///
684/// Events are filtered to the given tick, sorted by seq, and their
685/// digests are fed into a BLAKE3 hasher. Returns a 64-character hex string.
686pub fn compute_tick_hash(events: &[CanonEventSnapshot], tick: u64) -> String {
687    let mut tick_events: Vec<&CanonEventSnapshot> = events.iter().filter(|e| e.tick == tick).collect();
688    tick_events.sort_by_key(|e| e.seq);
689
690    let mut hasher = blake3::Hasher::new_derive_key("dreamwell.chronoshift.tick.v1");
691    for event in &tick_events {
692        hasher.update(event.digest.as_bytes());
693        hasher.update(b"|");
694    }
695
696    hasher.finalize().to_hex().to_string()
697}
698
699// =============================================================================
700// Tests
701// =============================================================================
702
703#[cfg(test)]
704mod tests {
705    use super::*;
706
707    fn make_event(
708        id: &str,
709        timeline: &str,
710        event_type: &str,
711        tick: u64,
712        seq: u64,
713        prev_id: &str,
714    ) -> CanonEventSnapshot {
715        let scope_key = format!("tl:{}/lvl:world/id:test", timeline);
716        let digest =
717            crate::canon::compute_event_digest(id, event_type, tick, seq, timeline, &scope_key, "actor_1", prev_id);
718        CanonEventSnapshot {
719            event_id: id.to_string(),
720            timeline_id: timeline.to_string(),
721            event_type: event_type.to_string(),
722            scope_key,
723            actor_id: "actor_1".to_string(),
724            target_id: "target_1".to_string(),
725            payload: "{}".to_string(),
726            tick,
727            seq,
728            digest,
729            prev_event_id: prev_id.to_string(),
730        }
731    }
732
733    fn make_chain(timeline: &str, count: usize) -> Vec<CanonEventSnapshot> {
734        let mut events = Vec::with_capacity(count);
735        let mut prev_id = String::new();
736        for i in 0..count {
737            let id = format!("evt:{timeline}:{}:{:06}", i / 3, i % 3);
738            let tick = (i / 3) as u64;
739            let seq = (i % 3) as u64;
740            let event = make_event(&id, timeline, "test_event", tick, seq, &prev_id);
741            prev_id = id;
742            events.push(event);
743        }
744        events
745    }
746
747    #[test]
748    fn test_checkpoint_builder_event_hash_deterministic() {
749        let events = make_chain("tl_sacred", 5);
750        let hash_a = CheckpointBuilder::compute_event_hash(&events);
751        let hash_b = CheckpointBuilder::compute_event_hash(&events);
752        assert_eq!(hash_a, hash_b);
753        assert_eq!(hash_a.len(), 64);
754    }
755
756    #[test]
757    fn test_checkpoint_builder_state_hash_uses_blake3() {
758        let state = StateSnapshot {
759            timeline_id: "tl_sacred".to_string(),
760            tick: 10,
761            entity_count: 100,
762            event_count: 50,
763            scope_chain_heads: vec![("scope_a".to_string(), "abc123".to_string())],
764            state_hash: String::new(),
765        };
766        let hash = CheckpointBuilder::compute_state_hash(&state).unwrap();
767        // BLAKE3 hex is 64 characters.
768        assert_eq!(hash.len(), 64);
769    }
770
771    #[test]
772    fn test_checkpoint_builder_build() {
773        let events = make_chain("tl_sacred", 3);
774        let state = StateSnapshot {
775            timeline_id: "tl_sacred".to_string(),
776            tick: 1,
777            entity_count: 10,
778            event_count: 3,
779            scope_chain_heads: vec![],
780            state_hash: String::new(),
781        };
782        let builder = CheckpointBuilder::new("tl_sacred", 1);
783        let cp = builder.build(&events, &state).unwrap();
784        assert_eq!(cp.timeline_id, "tl_sacred");
785        assert_eq!(cp.tick, 1);
786        assert!(!cp.event_hash.is_empty());
787        assert!(!cp.state_hash.is_empty());
788    }
789
790    #[test]
791    fn test_replay_engine_validate_chain_ok() {
792        let events = make_chain("tl_a", 6);
793        let mut engine = ReplayEngine::new("tl_a", "tl_b", 0, 2);
794        for e in events {
795            engine.add_event(e).unwrap();
796        }
797        assert!(engine.validate_chain().is_ok());
798    }
799
800    #[test]
801    fn test_replay_engine_validate_chain_broken() {
802        let mut events = make_chain("tl_a", 4);
803        // Break the chain by altering the second event's prev pointer.
804        events[2].prev_event_id = "wrong_id".to_string();
805        let mut engine = ReplayEngine::new("tl_a", "tl_b", 0, 1);
806        for e in events {
807            engine.add_event(e).unwrap();
808        }
809        let result = engine.validate_chain();
810        assert!(result.is_err());
811        match result.unwrap_err() {
812            ChronoshiftError::DigestChainBroken { tick, .. } => {
813                assert_eq!(tick, 0);
814            }
815            other => panic!("unexpected error: {other:?}"),
816        }
817    }
818
819    #[test]
820    fn test_replay_engine_replay_tick() {
821        let events = make_chain("tl_a", 9);
822        let mut engine = ReplayEngine::new("tl_a", "tl_b", 0, 3);
823        for e in events {
824            engine.add_event(e).unwrap();
825        }
826        let tick_1 = engine.replay_tick(1);
827        assert_eq!(tick_1.len(), 3);
828        for e in &tick_1 {
829            assert_eq!(e.tick, 1);
830        }
831        // Verify sorted by seq.
832        assert!(tick_1[0].seq <= tick_1[1].seq);
833        assert!(tick_1[1].seq <= tick_1[2].seq);
834    }
835
836    #[test]
837    fn test_replay_engine_replay_range() {
838        let events = make_chain("tl_a", 9);
839        let mut engine = ReplayEngine::new("tl_a", "tl_b", 0, 3);
840        for e in events {
841            engine.add_event(e).unwrap();
842        }
843        let range = engine.replay_range(0, 1);
844        assert_eq!(range.len(), 6);
845        for e in &range {
846            assert!(e.tick <= 1);
847        }
848    }
849
850    #[test]
851    fn test_replay_engine_determinism_hash() {
852        let events = make_chain("tl_a", 5);
853        let mut engine = ReplayEngine::new("tl_a", "tl_b", 0, 2);
854        for e in events.clone() {
855            engine.add_event(e).unwrap();
856        }
857        let hash_a = engine.compute_determinism_hash();
858        let hash_b = engine.compute_determinism_hash();
859        assert_eq!(hash_a, hash_b);
860        assert_eq!(hash_a.len(), 64); // BLAKE3
861    }
862
863    #[test]
864    fn test_replay_engine_accessors() {
865        let engine = ReplayEngine::new("src", "tgt", 10, 20);
866        assert_eq!(engine.source_timeline(), "src");
867        assert_eq!(engine.target_timeline(), "tgt");
868        assert_eq!(engine.from_tick(), 10);
869        assert_eq!(engine.to_tick(), 20);
870        assert!(engine.events().is_empty());
871    }
872
873    #[test]
874    fn test_diff_timelines_identical() {
875        let events = make_chain("tl_a", 6);
876        let diff = diff_timelines(&events, &events, 0, 2);
877        assert!(diff.determinism_verified);
878        assert!(diff.divergence_tick.is_none());
879        assert!(diff.mismatched_events.is_empty());
880        assert_eq!(diff.source_event_count, 6);
881        assert_eq!(diff.target_event_count, 6);
882    }
883
884    #[test]
885    fn test_diff_timelines_divergent() {
886        let source = make_chain("tl_a", 6);
887        let mut target = make_chain("tl_a", 6);
888        // Alter an event's digest in the target at index 3 (tick 1).
889        target[3].digest = "0000000000000000".to_string();
890        let diff = diff_timelines(&source, &target, 0, 2);
891        assert!(!diff.determinism_verified);
892        assert_eq!(diff.divergence_tick, Some(1));
893        assert!(!diff.mismatched_events.is_empty());
894    }
895
896    #[test]
897    fn test_diff_timelines_different_lengths() {
898        let source = make_chain("tl_a", 6);
899        let target = make_chain("tl_b", 3);
900        let diff = diff_timelines(&source, &target, 0, 2);
901        assert!(!diff.determinism_verified);
902        assert_eq!(diff.source_event_count, 6);
903        assert_eq!(diff.target_event_count, 3);
904    }
905
906    #[test]
907    fn test_verify_determinism_true() {
908        let events = make_chain("tl_a", 5);
909        assert!(verify_determinism(&events, &events));
910    }
911
912    #[test]
913    fn test_verify_determinism_false_different_digests() {
914        let original = make_chain("tl_a", 3);
915        let mut replay = original.clone();
916        replay[1].digest = "ffffffffffffffff".to_string();
917        assert!(!verify_determinism(&original, &replay));
918    }
919
920    #[test]
921    fn test_verify_determinism_false_different_lengths() {
922        let original = make_chain("tl_a", 5);
923        let replay = make_chain("tl_a", 3);
924        assert!(!verify_determinism(&original, &replay));
925    }
926
927    #[test]
928    fn test_compute_tick_hash_deterministic() {
929        let events = make_chain("tl_a", 9);
930        let hash_a = compute_tick_hash(&events, 1);
931        let hash_b = compute_tick_hash(&events, 1);
932        assert_eq!(hash_a, hash_b);
933        assert_eq!(hash_a.len(), 64);
934    }
935
936    #[test]
937    fn test_compute_tick_hash_different_ticks() {
938        let events = make_chain("tl_a", 9);
939        let hash_0 = compute_tick_hash(&events, 0);
940        let hash_1 = compute_tick_hash(&events, 1);
941        assert_ne!(hash_0, hash_1);
942    }
943
944    #[test]
945    fn test_compute_tick_hash_empty_tick() {
946        let events = make_chain("tl_a", 3);
947        let hash = compute_tick_hash(&events, 999);
948        // Should still produce a valid 64-char BLAKE3 hex digest.
949        assert_eq!(hash.len(), 64);
950    }
951
952    #[test]
953    fn test_merkle_tree_single_leaf() {
954        let leaves = vec!["abc123".to_string()];
955        let root = compute_merkle_tree(&leaves);
956        let expected = blake3::hash(b"abc123").to_hex().to_string();
957        assert_eq!(root, expected);
958    }
959
960    #[test]
961    fn test_merkle_tree_two_leaves() {
962        let leaves = vec!["aaa".to_string(), "bbb".to_string()];
963        let root = compute_merkle_tree(&leaves);
964
965        let left = blake3::hash(b"aaa");
966        let right = blake3::hash(b"bbb");
967        let mut hasher = blake3::Hasher::new();
968        hasher.update(left.as_bytes());
969        hasher.update(right.as_bytes());
970        let expected = hasher.finalize().to_hex().to_string();
971        assert_eq!(root, expected);
972    }
973
974    #[test]
975    fn test_merkle_tree_odd_leaves() {
976        let leaves = vec!["a".to_string(), "b".to_string(), "c".to_string()];
977        let root = compute_merkle_tree(&leaves);
978        assert_eq!(root.len(), 64);
979    }
980
981    #[test]
982    fn test_merkle_tree_empty() {
983        let root = compute_merkle_tree(&[]);
984        let expected = blake3::hash(b"").to_hex().to_string();
985        assert_eq!(root, expected);
986    }
987
988    #[test]
989    fn test_merkle_tree_deterministic() {
990        let leaves: Vec<String> = (0..8).map(|i| format!("leaf_{i}")).collect();
991        let root_a = compute_merkle_tree(&leaves);
992        let root_b = compute_merkle_tree(&leaves);
993        assert_eq!(root_a, root_b);
994    }
995
996    #[test]
997    fn test_proof_bundle_compute_merkle_root() {
998        let events = make_chain("tl_a", 4);
999        let root = ProofBundle::compute_merkle_root(&events);
1000        assert_eq!(root.len(), 64);
1001    }
1002
1003    #[test]
1004    fn test_proof_bundle_verify_chain_ok() {
1005        let events = make_chain("tl_a", 5);
1006        assert!(ProofBundle::verify_chain(&events).is_ok());
1007    }
1008
1009    #[test]
1010    fn test_proof_bundle_verify_chain_broken() {
1011        let mut events = make_chain("tl_a", 4);
1012        events[1].digest = "0000000000000000".to_string();
1013        assert!(ProofBundle::verify_chain(&events).is_err());
1014    }
1015
1016    #[test]
1017    fn test_fork_spec_roundtrip() {
1018        let spec = ForkSpec {
1019            parent_timeline: "tl_sacred".to_string(),
1020            fork_tick: 100,
1021            name: "what_if_war".to_string(),
1022            visibility: ForkVisibilityLevel::Shared,
1023            compute_cap_pct: 25,
1024            ttl_ticks: 500,
1025        };
1026        let json = serde_json::to_string(&spec).unwrap();
1027        let deserialized: ForkSpec = serde_json::from_str(&json).unwrap();
1028        assert_eq!(spec, deserialized);
1029    }
1030
1031    #[test]
1032    fn test_chronoshift_error_display() {
1033        let err = ChronoshiftError::CheckpointNotFound {
1034            timeline: "tl_a".to_string(),
1035            tick: 42,
1036        };
1037        let msg = format!("{err}");
1038        assert!(msg.contains("checkpoint_not_found"));
1039        assert!(msg.contains("tl_a"));
1040        assert!(msg.contains("42"));
1041    }
1042
1043    #[test]
1044    fn test_state_snapshot_serialization() {
1045        let state = StateSnapshot {
1046            timeline_id: "tl_sacred".to_string(),
1047            tick: 50,
1048            entity_count: 1000,
1049            event_count: 500,
1050            scope_chain_heads: vec![
1051                ("world:ayora".to_string(), "abc123".to_string()),
1052                ("realm:verdant".to_string(), "def456".to_string()),
1053            ],
1054            state_hash: "deadbeef".to_string(),
1055        };
1056        let json = serde_json::to_string(&state).unwrap();
1057        let deserialized: StateSnapshot = serde_json::from_str(&json).unwrap();
1058        assert_eq!(state, deserialized);
1059    }
1060
1061    #[test]
1062    fn test_replay_engine_empty_chain_validates() {
1063        let engine = ReplayEngine::new("a", "b", 0, 0);
1064        assert!(engine.validate_chain().is_ok());
1065    }
1066
1067    #[test]
1068    fn test_replay_engine_single_event_validates() {
1069        let events = make_chain("tl_a", 1);
1070        let mut engine = ReplayEngine::new("tl_a", "tl_b", 0, 0);
1071        engine.add_event(events[0].clone()).unwrap();
1072        assert!(engine.validate_chain().is_ok());
1073    }
1074
1075    #[test]
1076    fn test_replay_engine_overflow() {
1077        let mut engine = ReplayEngine::new("a", "b", 0, 0);
1078        // Fill to the cap — we can't actually push 1M events in a test, so
1079        // directly set the events vec length and test the guard.
1080        engine.events = Vec::with_capacity(0);
1081        engine
1082            .events
1083            .resize(MAX_REPLAY_EVENTS, make_event("e", "t", "x", 0, 0, ""));
1084        let result = engine.add_event(make_event("overflow", "t", "x", 1, 0, "e"));
1085        assert!(result.is_err());
1086        match result.unwrap_err() {
1087            ChronoshiftError::ReplayOverflow { max, .. } => {
1088                assert_eq!(max, MAX_REPLAY_EVENTS);
1089            }
1090            other => panic!("unexpected error: {other:?}"),
1091        }
1092    }
1093
1094    #[test]
1095    fn test_validate_chain_tick_order_violation() {
1096        // Create two events where the second has a lower tick than the first.
1097        let e0 = make_event("evt:tl_a:10:000000", "tl_a", "test_event", 10, 0, "");
1098        let e1 = make_event("evt:tl_a:5:000000", "tl_a", "test_event", 5, 0, "evt:tl_a:10:000000");
1099        let mut engine = ReplayEngine::new("tl_a", "tl_b", 0, 20);
1100        engine.add_event(e0).unwrap();
1101        engine.add_event(e1).unwrap();
1102        let result = engine.validate_chain();
1103        assert!(result.is_err());
1104        match result.unwrap_err() {
1105            ChronoshiftError::TickOrderViolation { prev_tick, curr_tick } => {
1106                assert_eq!(prev_tick, 10);
1107                assert_eq!(curr_tick, 5);
1108            }
1109            other => panic!("unexpected error: {other:?}"),
1110        }
1111    }
1112}