Skip to main content

car_sync/
fold.rs

1//! The deterministic fold: `fold(ops) → materialized state`.
2//!
3//! The proposal's convergence contract, verbatim: "each daemon **folds** the
4//! full op-set into local state deterministically. Because the fold is
5//! commutative, associative, and idempotent over the op-set (CRDT
6//! properties), two laptops writing simultaneously converge the moment they
7//! exchange ops."
8//!
9//! Fold rules per surface tier (the proposal's table):
10//! - **Grow-only** (conversations, knowledge, skills, trajectories, runs,
11//!   routing observations): union by [`crate::oplog::OpRecord::fold_key`] —
12//!   the stable entity key for logical-entity surfaces, the `op_id` for
13//!   event-stream surfaces (routing), which fold as a MULTISET: the proposal
14//!   replays "the merged **multiset** of observations", so two
15//!   byte-identical observations are two events and both survive.
16//!   Entities are immutable in this tier (a change is a new op — e.g. a
17//!   `Supersedes` fact), so on a key collision with *different* content the
18//!   earliest `(hlc, op_id)` writer wins, deterministically.
19//! - **Registry** (declagents, the file registries): LWW-register per
20//!   record keyed by id, ordered by HLC — *not per file*. Latest
21//!   `(hlc, op_id)` wins; concurrent edits to different records both
22//!   survive.
23//! - **Routing**: the fold materializes the hlc-ordered observation stream;
24//!   the EMA replay is the caller-injected [`SyncState::replay`] ("sync the
25//!   observations, not the result" — same observations + same canonical
26//!   order ⇒ bit-identical result on every device).
27//! - **Leased** (`Intent`, B5): LWW-per-run_id (monotone status) with
28//!   **per-agent epoch fencing** — a stale-epoch intent from a failed-over
29//!   lease holder loses at the fold, order-independently. See [`crate::lease`]
30//!   and the [`fold_onto`] `FoldTier::Leased` arm.
31//!
32//! Determinism discipline: all state is `BTreeMap`-backed and nothing here
33//! reads a clock — the proposal calls out "a non-determinism leak
34//! (wall-clock or HashMap iteration order sneaking into a fold)" as the bug
35//! class [`state_hash`] exists to catch.
36
37pub use crate::oplog::FoldTier;
38use crate::oplog::{canonical_json, Hlc, OpRecord};
39use car_state::crdt::{LwwMap, LwwRegister};
40use serde::{Deserialize, Serialize};
41use serde_json::Value;
42use sha2::{Digest, Sha256};
43use std::collections::BTreeMap;
44
45/// One folded entity: the winning op's payload plus the stamp/id it won with.
46#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
47pub struct FoldedRecord {
48    pub op_id: String,
49    pub hlc: Hlc,
50    pub payload: Value,
51}
52
53/// One agent's leased execution-intent ledger — the [`FoldTier::Leased`]
54/// tier's per-agent folded state (B5).
55///
56/// This slice delivers **deterministic ledger convergence plus a durable
57/// idempotency oracle** — it does NOT by itself make execution exactly-once.
58/// The exactly-once *execution* gate is B6's dispatch fence (a linearizable
59/// "am I still epoch N?" check plus the durable non-fenced idempotency read
60/// **before** the external side effect); the fold decides who wins the
61/// *ledger*, not whether the effect happens.
62///
63/// Two distinct views live here, and confusing them causes double-execution:
64///
65/// - **`committed_runs` is the idempotency oracle** — a **fence-INDEPENDENT,
66///   keep-all** map `run_id → committed record`. Once a run commits, it stays
67///   here forever (a commit is a fact; no epoch bump erases it), so
68///   [`SyncState::committed_run`] is the correct "did this run already
69///   execute?" lookup. It is carried in the checkpoint and never trimmed by
70///   retention/compaction (see [`crate::compact`]).
71/// - **`runs` is the "who holds now" view** — per-agent epoch **fencing**
72///   applies to *pending* intents (a stale zombie holder's pending is fenced),
73///   while committed/failed records are **terminal-immune** (never reverted to
74///   pending, never cleared by a fence raise). Read via [`SyncState::intent`].
75///   Do **NOT** use `runs`/`intent()` as the idempotency oracle — a pending
76///   fenced by a later epoch is absent here yet the run may have committed; ask
77///   `committed_runs` / [`SyncState::committed_run`].
78///
79/// **Fencing is per AGENT, not per run** (the proposal's spec, deliberately):
80/// a zombie's *unique* post-failover **pending** — one the new holder never
81/// re-ran — is fenced too (per-run fencing would let it through). `fencing_epoch`
82/// is the agent's max-seen lease epoch. `committed_runs` is what makes that
83/// safe for idempotency: even after prior-epoch pendings drop from `runs`, the
84/// committed fact survives keep-all.
85#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
86pub struct IntentAgent {
87    /// The agent's fencing epoch — the max lease epoch any of its intents
88    /// carried. **Pending** intents below it are fenced (terminals are immune).
89    pub fencing_epoch: u64,
90    /// run_id key (`id:<run_id>`) → the "who holds now" winner (terminal-immune;
91    /// pendings fenced to `fencing_epoch`). NOT the idempotency oracle.
92    pub runs: BTreeMap<String, FoldedRecord>,
93    /// run_id key → the committed record. **Fence-independent, keep-all** — the
94    /// durable idempotency oracle that survives epoch bumps AND compaction.
95    /// Grow-only (highest `(epoch, hlc, op_id)` committed record wins on a
96    /// collision); never cleared by fencing. `#[serde(default)]` so a state
97    /// serialized before this field parses.
98    #[serde(default)]
99    pub committed_runs: BTreeMap<String, FoldedRecord>,
100}
101
102/// The materialized read model a full op-set folds to. On-disk files
103/// (`conversations/*.jsonl`, `declagents.json`, …) are projections of this
104/// (the proposal's "files are projections" reframe); B4's checkpoint is a
105/// serialized `SyncState` at a frontier.
106#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
107pub struct SyncState {
108    /// Grow-only tier: surface tag → stable key → record (union;
109    /// first-writer-wins on a key collision).
110    pub logs: BTreeMap<String, BTreeMap<String, FoldedRecord>>,
111    /// Registry tier: surface tag → record id → LWW winner.
112    pub registries: BTreeMap<String, BTreeMap<String, FoldedRecord>>,
113    /// Leased execution-intent tier (B5): agent_id → its fenced intent
114    /// ledger. Folded from [`crate::oplog::Surface::Intent`] ops with **epoch
115    /// fencing** applied deterministically. `#[serde(default)]` so a pre-B5
116    /// serialized state still parses.
117    #[serde(default)]
118    pub intents: BTreeMap<String, IntentAgent>,
119}
120
121impl SyncState {
122    /// A grow-only surface's entries in canonical `(hlc, op_id)` order — the
123    /// deterministic total order every device agrees on (used by the routing
124    /// replay, and the order B2's transcript materialization will consume).
125    pub fn log_entries(&self, surface_tag: &str) -> Vec<&FoldedRecord> {
126        let mut entries: Vec<&FoldedRecord> = self
127            .logs
128            .get(surface_tag)
129            .map(|m| m.values().collect())
130            .unwrap_or_default();
131        entries.sort_by(|a, b| (&a.hlc, &a.op_id).cmp(&(&b.hlc, &b.op_id)));
132        entries
133    }
134
135    /// Replay an order-sensitive fold (e.g. the routing EMA) over a surface's
136    /// canonically-ordered entries: `fold(routing) =
137    /// observations.sorted_by(hlc).fold(empty_store, apply_ema)`. The apply
138    /// function is injected — execution (and the EMA itself) stays out of
139    /// this crate, like the other pure cores.
140    pub fn replay<T, F>(&self, surface_tag: &str, init: T, apply: F) -> T
141    where
142        F: FnMut(T, &FoldedRecord) -> T,
143    {
144        self.log_entries(surface_tag).into_iter().fold(init, apply)
145    }
146
147    /// The **"who holds now"** leased intent for a run (terminal-immune,
148    /// pending-fenced) — NOT the idempotency oracle. `None` when the agent has
149    /// no such run, or the run is a *pending* fenced by a later, higher-epoch
150    /// holder. A committed run is terminal-immune and stays visible here.
151    ///
152    /// **For "did this run already execute?" use [`SyncState::committed_run`]**
153    /// — `intent()` can return `None`/pending for a run that actually committed
154    /// under a prior epoch, which would cause a double-execution if trusted as
155    /// the idempotency check.
156    pub fn intent(&self, agent_id: &str, run_id: &str) -> Option<&FoldedRecord> {
157        self.intents.get(agent_id)?.runs.get(&format!("id:{run_id}"))
158    }
159
160    /// The idempotency oracle (B5): the committed record for a run, if it has
161    /// **ever** committed for this agent. **Fence-independent and keep-all** —
162    /// unaffected by epoch bumps and by compaction — so this is the correct
163    /// "did `run_id` already run?" lookup before dispatching a side effect.
164    /// `None` iff no committed intent for `(agent_id, run_id)` exists.
165    pub fn committed_run(&self, agent_id: &str, run_id: &str) -> Option<&FoldedRecord> {
166        self.intents
167            .get(agent_id)?
168            .committed_runs
169            .get(&format!("id:{run_id}"))
170    }
171
172    /// Every run_id this agent has committed (the keep-all oracle's keys, with
173    /// the `id:` prefix stripped) — for a failover executor scanning "what has
174    /// already run".
175    pub fn committed_run_ids(&self, agent_id: &str) -> Vec<&str> {
176        self.intents
177            .get(agent_id)
178            .map(|a| {
179                a.committed_runs
180                    .keys()
181                    .filter_map(|k| k.strip_prefix("id:"))
182                    .collect()
183            })
184            .unwrap_or_default()
185    }
186
187    /// The agent's current fencing epoch — the max lease epoch its intents
188    /// carry — or `None` if it has none. Pending intents below this are fenced.
189    pub fn fencing_epoch(&self, agent_id: &str) -> Option<u64> {
190        self.intents.get(agent_id).map(|a| a.fencing_epoch)
191    }
192}
193
194/// Is this folded intent record terminal (committed/failed)? Terminals are
195/// immune to fencing; only pending intents are fenced.
196fn intent_is_terminal(rec: &FoldedRecord) -> bool {
197    crate::lease::intent_status_rank(&rec.payload) == 1
198}
199
200/// Total priority for the leased tier's winner selection: terminal-flag (a
201/// terminal always outranks a pending — terminal-immunity), then `epoch`
202/// (fencing / latest-holder), then `(hlc, op_id)`. `max` under this key is
203/// order-independent.
204fn intent_priority(rec: &FoldedRecord) -> (u8, u64, &Hlc, &str) {
205    (
206        crate::lease::intent_status_rank(&rec.payload),
207        crate::lease::intent_epoch(&rec.payload),
208        &rec.hlc,
209        rec.op_id.as_str(),
210    )
211}
212
213/// Fold an op-set into its materialized state. Order-independent (per-key
214/// winner selection under a total order), idempotent (ops dedup on `op_id`
215/// first), and pure.
216///
217/// # Input contract: verify before folding untrusted input
218///
219/// `fold` does NOT verify ids or chains — that is the explicit, separate
220/// [`crate::oplog::verify_log`] pass, and any caller feeding ops from a
221/// remote/untrusted source (the B3 relay pull path) MUST run it first.
222/// `fold` stays order-independent even on invalid input (two records forging
223/// the *same* claimed `op_id` with *different* content dedup by a
224/// content-deterministic tiebreak, not arrival order), but which forged
225/// record wins is meaningless — verification is what makes the answer mean
226/// something.
227pub fn fold(ops: &[OpRecord]) -> SyncState {
228    fold_onto(&SyncState::default(), ops)
229}
230
231/// Fold additional ops **onto an already-folded base state** — the B4
232/// checkpoint-consumption primitive: a truncated device reconstructs
233/// `fold(full log)` as `fold_onto(checkpoint.state, retained tail)`.
234///
235/// Uses the same per-key winner selection as [`fold`] (grow-only earliest
236/// `(hlc, op_id)` wins; registry latest wins; event streams keyed by
237/// `op_id`), so `fold_onto(fold(prefix), suffix) == fold(prefix ∪ suffix)`
238/// exactly — the equivalence that makes compaction safe, and the invariant
239/// the lib-level B4 tests pin per surface. Re-delivering an op already in
240/// the base is idempotent (equal `(hlc, op_id)` never displaces the slot).
241///
242/// Same input contract as [`fold`]: verify (via
243/// [`crate::oplog::verify_log`] / [`crate::checkpoint::verify_anchored`])
244/// before folding untrusted input. One caveat unique to invalid input: the
245/// base keeps only `FoldedRecord`s, so a forged op colliding with a
246/// *base* record's `op_id` cannot use the full-record content tiebreak
247/// [`fold`] applies within one op-set — verification is what makes the
248/// answer mean something.
249pub fn fold_onto(base: &SyncState, ops: &[OpRecord]) -> SyncState {
250    // Idempotence: a retransmitted op (same op_id) folds once. On an id
251    // collision with DIFFERENT content (invalid input — verify_log rejects
252    // it) the tiebreak must not depend on arrival order, so the
253    // lexicographically-smaller canonical serialization wins.
254    let canonical_record = |op: &OpRecord| -> String {
255        canonical_json(&serde_json::to_value(op).expect("OpRecord serializes"))
256    };
257    let mut unique: BTreeMap<&str, &OpRecord> = BTreeMap::new();
258    for op in ops {
259        unique
260            .entry(&op.op_id)
261            .and_modify(|existing| {
262                if *existing != op && canonical_record(op) < canonical_record(existing) {
263                    *existing = op;
264                }
265            })
266            .or_insert(op);
267    }
268
269    let mut state = base.clone();
270
271    // Leased-tier pre-pass (B5): establish the FINAL per-agent `fencing_epoch`
272    // (max over the base and every new intent op) BEFORE the main loop, and
273    // evict base *pending* records that the raised fence makes stale — terminals
274    // (committed/failed) are immune and kept. Deciding pending-fencing against
275    // the final fence (not an intermediate one built up mid-loop) is what keeps
276    // the leased fold order-independent and base-composable
277    // (`fold_onto(checkpoint, tail) == fold(full)`).
278    {
279        let mut agent_max: BTreeMap<String, u64> = BTreeMap::new();
280        for op in unique.values() {
281            if op.surface.fold_tier() == FoldTier::Leased {
282                let slot = agent_max
283                    .entry(crate::lease::intent_agent(&op.payload).to_string())
284                    .or_insert(0);
285                *slot = (*slot).max(crate::lease::intent_epoch(&op.payload));
286            }
287        }
288        for (agent, max_epoch) in agent_max {
289            let entry = state.intents.entry(agent).or_default();
290            if max_epoch > entry.fencing_epoch {
291                entry.fencing_epoch = max_epoch;
292                entry.runs.retain(|_, r| intent_is_terminal(r)); // keep terminals, fence pendings
293            }
294        }
295    }
296
297    for op in unique.values() {
298        let record = FoldedRecord {
299            op_id: op.op_id.clone(),
300            hlc: op.hlc.clone(),
301            payload: op.payload.clone(),
302        };
303        match op.surface.fold_tier() {
304            FoldTier::GrowOnly => {
305                let slot = state
306                    .logs
307                    .entry(op.surface.tag())
308                    .or_default()
309                    .entry(op.fold_key());
310                slot.and_modify(|existing| {
311                    // Immutable-entity union: earliest (hlc, op_id) wins.
312                    // (Unreachable for event-stream surfaces — their fold_key
313                    // IS the op_id, so a collision is the same op.)
314                    if (&record.hlc, &record.op_id) < (&existing.hlc, &existing.op_id) {
315                        *existing = record.clone();
316                    }
317                })
318                .or_insert(record);
319            }
320            FoldTier::Registry => {
321                let slot = state
322                    .registries
323                    .entry(op.surface.tag())
324                    .or_default()
325                    .entry(op.fold_key());
326                slot.and_modify(|existing| {
327                    // LWW: latest (hlc, op_id) wins.
328                    if (&record.hlc, &record.op_id) > (&existing.hlc, &existing.op_id) {
329                        *existing = record.clone();
330                    }
331                })
332                .or_insert(record);
333            }
334            FoldTier::Leased => {
335                // `fencing_epoch` is already final for this agent (pre-pass).
336                let agent = crate::lease::intent_agent(&op.payload).to_string();
337                let epoch = crate::lease::intent_epoch(&op.payload);
338                let key = op.fold_key();
339                let is_terminal = crate::lease::intent_status_rank(&op.payload) == 1;
340                let is_committed = crate::lease::intent_is_committed(&op.payload);
341                let entry = state.intents.entry(agent).or_default();
342
343                // (A) The idempotency ORACLE: grow-only, fence-INDEPENDENT,
344                //     keep-all. A commit is a permanent fact — recorded whatever
345                //     its epoch, never cleared by the fence. Highest
346                //     `(epoch, hlc, op_id)` committed record wins a collision.
347                if is_committed {
348                    let better = entry
349                        .committed_runs
350                        .get(&key)
351                        .is_none_or(|existing| intent_priority(&record) > intent_priority(existing));
352                    if better {
353                        entry.committed_runs.insert(key.clone(), record.clone());
354                    }
355                }
356
357                // (B) The "who holds now" view: terminals are immune, pendings
358                //     are fenced to `fencing_epoch`. Eligible = terminal (always)
359                //     OR a live pending at the fence; below-fence pendings drop.
360                //     The winner is `max` under `intent_priority`, so a terminal
361                //     never reverts to a pending (terminal-immunity) and a
362                //     stale-epoch pending never wins.
363                let eligible = is_terminal || epoch == entry.fencing_epoch;
364                if eligible {
365                    let better = entry
366                        .runs
367                        .get(&key)
368                        .is_none_or(|existing| intent_priority(&record) > intent_priority(existing));
369                    if better {
370                        entry.runs.insert(key, record);
371                    }
372                }
373            }
374        }
375    }
376    state
377}
378
379/// Deterministic content hash of a folded state — the proposal's built-in
380/// divergence invariant: "Same frontier ⇒ same snapshot hash,
381/// deterministically. A mismatch is a fold bug or a non-determinism leak."
382/// B4's checkpoint hash is this value at a frontier.
383pub fn state_hash(state: &SyncState) -> String {
384    let value = serde_json::to_value(state).expect("SyncState serializes");
385    let mut hasher = Sha256::new();
386    hasher.update(canonical_json(&value).as_bytes());
387    let digest = hasher.finalize();
388    let hex: String = digest.iter().take(16).map(|b| format!("{b:02x}")).collect();
389    format!("state-{hex}")
390}
391
392/// Encode an [`Hlc`] as a single `u64` version that preserves the
393/// `(wall_ms, counter)` order — the bridge onto `car_state::crdt`'s
394/// `(version, replica)` total order. 44 bits of wall-clock milliseconds
395/// (good past year 2500) and 20 bits of counter; a counter ≥ 2^20 within one
396/// millisecond is outside the HLC's operating range (B3's clock guarantees
397/// far less) and would break the order-preservation, so it is debug-asserted.
398pub fn hlc_version(hlc: &Hlc) -> u64 {
399    debug_assert!(hlc.counter < (1 << 20), "HLC counter exceeds encoding range");
400    debug_assert!(
401        hlc.wall_ms < (1 << 44),
402        "HLC wall_ms exceeds encoding range (the << 20 would drop high bits in release)"
403    );
404    (hlc.wall_ms << 20) | (u64::from(hlc.counter) & 0xF_FFFF)
405}
406
407/// Project a folded registry surface onto the shipped
408/// [`car_state::crdt::LwwMap`], so the oplog fold composes with (and is
409/// testably equivalent to) `crdt_merge`/`crdt_export` where the domains
410/// overlap: `fold(union of ops)` ≡ `merge_maps(per-device exports)`.
411pub fn registry_as_lww(state: &SyncState, surface_tag: &str) -> LwwMap {
412    state
413        .registries
414        .get(surface_tag)
415        .map(|records| {
416            records
417                .iter()
418                .map(|(key, rec)| {
419                    (
420                        key.clone(),
421                        LwwRegister::new(
422                            rec.payload.clone(),
423                            hlc_version(&rec.hlc),
424                            rec.hlc.device_id.clone(),
425                        ),
426                    )
427                })
428                .collect()
429        })
430        .unwrap_or_default()
431}
432
433#[cfg(test)]
434mod tests {
435    use super::*;
436    use crate::lease::{Intent, IntentStatus};
437    use crate::oplog::{DeviceLog, Scope, Surface};
438    use serde_json::json;
439
440    /// Append a leased execution-intent op through a device log.
441    fn intent_op(
442        dev: &mut DeviceLog,
443        agent: &str,
444        run: &str,
445        epoch: u64,
446        status: IntentStatus,
447    ) -> OpRecord {
448        dev.append(
449            Scope::Personal,
450            Surface::Intent,
451            Intent::new(agent, run, epoch, status).payload(),
452        )
453    }
454
455    #[test]
456    fn grow_only_unions_by_stable_key() {
457        let mut a = DeviceLog::new("a");
458        let mut b = DeviceLog::new("b");
459        let ops = vec![
460            a.append(Scope::Personal, Surface::Knowledge, json!({"id": "f1", "v": 1})),
461            b.append(Scope::Personal, Surface::Knowledge, json!({"id": "f2", "v": 2})),
462        ];
463        let state = fold(&ops);
464        let knowledge = &state.logs[&Surface::Knowledge.tag()];
465        assert_eq!(knowledge.len(), 2);
466        assert_eq!(knowledge["id:f1"].payload["v"], json!(1));
467        assert_eq!(knowledge["id:f2"].payload["v"], json!(2));
468    }
469
470    #[test]
471    fn grow_only_key_collision_resolves_to_earliest_deterministically() {
472        // Two devices emit different content under one stable id — an
473        // anomaly for the immutable tier, resolved first-writer-wins.
474        let mut a = DeviceLog::new("a");
475        let mut b = DeviceLog::new("b");
476        let oa = a.append(Scope::Personal, Surface::Knowledge, json!({"id": "f", "v": "a"}));
477        b.observe(&oa.hlc); // b writes causally later
478        let ob = b.append(Scope::Personal, Surface::Knowledge, json!({"id": "f", "v": "b"}));
479        let fwd = fold(&[oa.clone(), ob.clone()]);
480        let rev = fold(&[ob, oa]);
481        assert_eq!(fwd, rev);
482        assert_eq!(fwd.logs[&Surface::Knowledge.tag()]["id:f"].payload["v"], json!("a"));
483    }
484
485    #[test]
486    fn registry_is_lww_per_record_not_per_file() {
487        let mut a = DeviceLog::new("a");
488        let mut b = DeviceLog::new("b");
489        // Concurrent edits to DIFFERENT agents both survive.
490        let oa = a.append(Scope::Personal, Surface::Declagent, json!({"id": "x", "owner": "a"}));
491        let ob = b.append(Scope::Personal, Surface::Declagent, json!({"id": "y", "owner": "b"}));
492        // Concurrent edits to the SAME agent resolve by HLC.
493        b.observe(&oa.hlc);
494        let ob2 = b.append(Scope::Personal, Surface::Declagent, json!({"id": "x", "owner": "b"}));
495        let state = fold(&[oa, ob, ob2]);
496        let reg = &state.registries[&Surface::Declagent.tag()];
497        assert_eq!(reg.len(), 2, "both records survive");
498        assert_eq!(reg["id:x"].payload["owner"], json!("b"), "later HLC wins the shared record");
499        assert_eq!(reg["id:y"].payload["owner"], json!("b"));
500    }
501
502    #[test]
503    fn registry_concurrent_tie_breaks_on_device_deterministically() {
504        // Same lamport stamp on two devices (true concurrency): the HLC's
505        // device_id component breaks the tie, both fold orders agree.
506        let mut a = DeviceLog::new("a");
507        let mut b = DeviceLog::new("b");
508        let oa = a.append(Scope::Personal, Surface::Declagent, json!({"id": "x", "owner": "a"}));
509        let ob = b.append(Scope::Personal, Surface::Declagent, json!({"id": "x", "owner": "b"}));
510        assert_eq!(oa.hlc.wall_ms, ob.hlc.wall_ms);
511        let fwd = fold(&[oa.clone(), ob.clone()]);
512        let rev = fold(&[ob, oa]);
513        assert_eq!(fwd, rev);
514        // "b" > "a" in the device tiebreak — matches crdt's replica tiebreak.
515        assert_eq!(
516            fwd.registries[&Surface::Declagent.tag()]["id:x"].payload["owner"],
517            json!("b")
518        );
519    }
520
521    #[test]
522    fn state_hash_detects_divergence_and_agrees_on_convergence() {
523        let mut a = DeviceLog::new("a");
524        let o1 = a.append(Scope::Personal, Surface::Knowledge, json!({"id": "f1"}));
525        let o2 = a.append(Scope::Personal, Surface::Knowledge, json!({"id": "f2"}));
526        let h_full = state_hash(&fold(&[o1.clone(), o2.clone()]));
527        let h_full_again = state_hash(&fold(&[o2.clone(), o1.clone()]));
528        assert_eq!(h_full, h_full_again, "same op-set → same hash");
529        let h_partial = state_hash(&fold(&[o1]));
530        assert_ne!(h_full, h_partial, "different op-set → different hash");
531        assert!(h_full.starts_with("state-"));
532    }
533
534    #[test]
535    fn hlc_version_preserves_order() {
536        let stamps = [
537            Hlc { wall_ms: 1, counter: 0, device_id: "a".into() },
538            Hlc { wall_ms: 1, counter: 1, device_id: "a".into() },
539            Hlc { wall_ms: 2, counter: 0, device_id: "a".into() },
540        ];
541        for w in stamps.windows(2) {
542            assert!(hlc_version(&w[0]) < hlc_version(&w[1]));
543        }
544    }
545
546    #[test]
547    fn registry_as_lww_matches_crdt_merge_including_export_shape() {
548        // The equivalence the proposal leans on: per-device exports merged
549        // with the shipped crdt primitives == the fold of the op union.
550        let mut a = DeviceLog::new("dev-a");
551        let mut b = DeviceLog::new("dev-b");
552        let oa1 = a.append(Scope::Personal, Surface::Registry { kind: "agents".into() }, json!({"id": "r1", "v": "a"}));
553        let oa2 = a.append(Scope::Personal, Surface::Registry { kind: "agents".into() }, json!({"id": "r2", "v": "a"}));
554        b.observe(&oa1.hlc);
555        b.observe(&oa2.hlc);
556        let ob1 = b.append(Scope::Personal, Surface::Registry { kind: "agents".into() }, json!({"id": "r1", "v": "b"}));
557
558        let tag = Surface::Registry { kind: "agents".into() }.tag();
559        let union = registry_as_lww(&fold(&[oa1.clone(), oa2.clone(), ob1.clone()]), &tag);
560        let export_a = registry_as_lww(&fold(&[oa1, oa2]), &tag);
561        let export_b = registry_as_lww(&fold(&[ob1]), &tag);
562
563        assert_eq!(car_state::crdt::merge_maps(&export_a, &export_b), union);
564        assert_eq!(car_state::crdt::merge_many(&[export_b, export_a]), union);
565        let plain = car_state::crdt::materialize(&union);
566        assert_eq!(plain["id:r1"]["v"], json!("b"));
567        assert_eq!(plain["id:r2"]["v"], json!("a"));
568    }
569
570    #[test]
571    fn log_entries_are_hlc_ordered() {
572        let mut a = DeviceLog::new("a");
573        let mut b = DeviceLog::new("b");
574        let o1 = a.append(Scope::Personal, Surface::Conversation, json!({"t": "first"}));
575        b.observe(&o1.hlc);
576        let o2 = b.append(Scope::Personal, Surface::Conversation, json!({"t": "second"}));
577        a.observe(&o2.hlc); // a's next write causally follows b's
578        let o3 = a.append(Scope::Personal, Surface::Conversation, json!({"t": "third"}));
579        // Deliver out of order; the view is canonical.
580        let state = fold(&[o3, o1, o2]);
581        let texts: Vec<&Value> = state
582            .log_entries(&Surface::Conversation.tag())
583            .iter()
584            .map(|r| &r.payload["t"])
585            .collect();
586        assert_eq!(texts, vec![&json!("first"), &json!("second"), &json!("third")]);
587    }
588
589    #[test]
590    fn routing_observations_fold_as_a_multiset() {
591        // The demonstrated kernel-review defect: "agent x succeeded" twice is
592        // TWO observations. Under content-keyed dedup the second collapsed
593        // into the first (1 entry, EMA 0.65); the proposal requires the
594        // merged MULTISET (2 entries, EMA 0.755).
595        let mut dev = DeviceLog::new("dev-a");
596        let ops = vec![
597            dev.append(Scope::Personal, Surface::Routing, json!({"sample": 1.0})),
598            dev.append(Scope::Personal, Surface::Routing, json!({"sample": 1.0})),
599        ];
600        let state = fold(&ops);
601        assert_eq!(
602            state.log_entries(&Surface::Routing.tag()).len(),
603            2,
604            "two byte-identical observations are two events"
605        );
606        let ema = |s: f64, rec: &FoldedRecord| 0.7 * s + 0.3 * rec.payload["sample"].as_f64().unwrap();
607        let value = state.replay(&Surface::Routing.tag(), 0.5_f64, ema);
608        assert!((value - 0.755).abs() < 1e-12, "EMA over both events: got {value}");
609    }
610
611    #[test]
612    fn logical_entity_surfaces_dedup_identical_content() {
613        // Content-keyed dedup is scoped to logical-ENTITY surfaces (knowledge,
614        // skills, …): the same fact emitted identically by two devices is ONE
615        // entity. (Conversation is NOT one of these — it's an event stream
616        // keyed by op_id — see the conversation module's CRIT-2 tests.)
617        let mut a = DeviceLog::new("a");
618        let mut b = DeviceLog::new("b");
619        let fact = json!({"kind": "note", "body": "the sky is blue"});
620        let oa = a.append(Scope::Personal, Surface::Knowledge, fact.clone());
621        let ob = b.append(Scope::Personal, Surface::Knowledge, fact);
622        let state = fold(&[oa, ob]);
623        assert_eq!(state.log_entries(&Surface::Knowledge.tag()).len(), 1);
624    }
625
626    #[test]
627    fn forged_colliding_op_id_dedups_order_independently() {
628        // Invalid input (verify_log rejects it), but the fold must stay
629        // order-independent: two records claiming the SAME op_id with
630        // DIFFERENT content tiebreak on content, not arrival order.
631        let mut dev = DeviceLog::new("d1");
632        let genuine = dev.append(Scope::Personal, Surface::Knowledge, json!({"id": "f", "v": 1}));
633        let mut forged = genuine.clone();
634        forged.payload = json!({"id": "f", "v": 2}); // op_id NOT recomputed
635        assert!(crate::oplog::verify_log(&[forged.clone()]).is_err());
636
637        let ab = fold(&[genuine.clone(), forged.clone()]);
638        let ba = fold(&[forged, genuine]);
639        assert_eq!(ab, ba, "colliding-id dedup must not depend on arrival order");
640        assert_eq!(state_hash(&ab), state_hash(&ba));
641    }
642
643    #[test]
644    fn fold_onto_prefix_fold_equals_full_fold() {
645        // The B4 primitive: fold(prefix) then fold_onto(., suffix) must be
646        // byte-identical to fold(prefix ∪ suffix) — for every fold rule at
647        // once, including a grow-only collision and an LWW overwrite that
648        // CROSS the split point.
649        let mut a = DeviceLog::new("a");
650        let mut b = DeviceLog::new("b");
651        let prefix = vec![
652            a.append(Scope::Personal, Surface::Knowledge, json!({"id": "f", "v": "old"})),
653            a.append(Scope::Personal, Surface::Declagent, json!({"id": "x", "owner": "a"})),
654            a.append(Scope::Personal, Surface::Routing, json!({"sample": 1.0})),
655        ];
656        for op in &prefix {
657            b.observe(&op.hlc);
658        }
659        let suffix = vec![
660            // Grow-only collision across the split: earliest wins → "old".
661            b.append(Scope::Personal, Surface::Knowledge, json!({"id": "f", "v": "new"})),
662            // LWW across the split: latest wins → owner "b".
663            b.append(Scope::Personal, Surface::Declagent, json!({"id": "x", "owner": "b"})),
664            // Event stream across the split: both observations survive.
665            b.append(Scope::Personal, Surface::Routing, json!({"sample": 0.0})),
666        ];
667        let mut full = prefix.clone();
668        full.extend(suffix.iter().cloned());
669
670        let via_base = fold_onto(&fold(&prefix), &suffix);
671        assert_eq!(via_base, fold(&full));
672        assert_eq!(state_hash(&via_base), state_hash(&fold(&full)));
673        assert_eq!(via_base.logs[&Surface::Knowledge.tag()]["id:f"].payload["v"], json!("old"));
674        assert_eq!(
675            via_base.registries[&Surface::Declagent.tag()]["id:x"].payload["owner"],
676            json!("b")
677        );
678        assert_eq!(via_base.log_entries(&Surface::Routing.tag()).len(), 2);
679
680        // Idempotent re-delivery: folding an op already in the base changes
681        // nothing.
682        assert_eq!(fold_onto(&via_base, &prefix), via_base);
683    }
684
685    #[test]
686    fn empty_fold_is_empty_and_stable() {
687        let state = fold(&[]);
688        assert_eq!(state, SyncState::default());
689        assert_eq!(state_hash(&state), state_hash(&fold(&[])));
690        assert!(state.log_entries("conversation").is_empty());
691        assert!(registry_as_lww(&state, "declagent").is_empty());
692        assert!(state.intent("milo", "R").is_none());
693        assert!(state.fencing_epoch("milo").is_none());
694    }
695
696    // ------------------------------------------------------------------
697    // B5: leased execution-intent fencing as a deterministic fold property.
698    // ------------------------------------------------------------------
699
700    #[test]
701    fn intent_fold_fences_stale_epoch_order_independently() {
702        // Failover: dev-a held epoch 1, dev-b stole epoch 2. Both fire the
703        // SAME run R — a=zombie, b=legit holder. The fold must pick epoch 2
704        // (b) in ANY delivery order and fence a's epoch-1 writes, leaving one
705        // ledger record — no double-commit.
706        let mut a = DeviceLog::new("dev-a");
707        let mut b = DeviceLog::new("dev-b");
708        let ops = vec![
709            intent_op(&mut a, "milo", "R", 1, IntentStatus::Pending),
710            intent_op(&mut a, "milo", "R", 1, IntentStatus::Committed),
711            intent_op(&mut b, "milo", "R", 2, IntentStatus::Pending),
712            intent_op(&mut b, "milo", "R", 2, IntentStatus::Committed),
713        ];
714        let b_commit = ops[3].clone();
715
716        let baseline = fold(&ops);
717        assert_eq!(baseline.fencing_epoch("milo"), Some(2));
718        assert_eq!(baseline.intents["milo"].runs.len(), 1, "single record — no double-commit");
719        let winner = baseline.intent("milo", "R").expect("R survives");
720        let decoded = Intent::from_payload(&winner.payload).unwrap();
721        assert_eq!((decoded.epoch, decoded.status), (2, IntentStatus::Committed));
722        assert_eq!(winner.op_id, b_commit.op_id, "the current holder's commit wins");
723
724        // Order-independence: several explicit permutations agree exactly.
725        for order in [
726            vec![ops[3].clone(), ops[2].clone(), ops[1].clone(), ops[0].clone()],
727            vec![ops[2].clone(), ops[0].clone(), ops[3].clone(), ops[1].clone()],
728            vec![ops[1].clone(), ops[3].clone(), ops[0].clone(), ops[2].clone()],
729        ] {
730            assert_eq!(fold(&order), baseline);
731            assert_eq!(state_hash(&fold(&order)), state_hash(&baseline));
732        }
733    }
734
735    #[test]
736    fn intent_per_agent_pending_fencing_with_committed_immunity() {
737        // Per-AGENT fencing applies to PENDINGS: dev-a (epoch 1) has an
738        // unshared PENDING run S that dev-b (epoch 2) never touched → S's
739        // pending is fenced (a stale holder's intent-to-do is silenced). But a
740        // COMMITTED run is terminal-immune — a commit is a fact, not fenced —
741        // so the zombie's unshared committed run K survives (the C1 fix: an
742        // unrelated higher-epoch run must not evict it).
743        let mut a = DeviceLog::new("dev-a");
744        let mut b = DeviceLog::new("dev-b");
745        let ops = vec![
746            intent_op(&mut a, "milo", "S", 1, IntentStatus::Pending), // unshared zombie pending
747            intent_op(&mut a, "milo", "K", 1, IntentStatus::Committed), // unshared zombie commit
748            intent_op(&mut b, "milo", "T", 2, IntentStatus::Committed), // new holder, unrelated run
749        ];
750        let state = fold(&ops);
751        assert_eq!(state.fencing_epoch("milo"), Some(2));
752        // The unshared PENDING is fenced; it never committed.
753        assert!(state.intent("milo", "S").is_none(), "unshared zombie pending is fenced");
754        assert!(state.committed_run("milo", "S").is_none(), "S never committed");
755        // The unshared COMMITTED run survives the unrelated epoch bump (C1).
756        assert!(
757            state.committed_run("milo", "K").is_some(),
758            "committed run survives an unrelated epoch bump (idempotency oracle)"
759        );
760        assert!(state.intent("milo", "K").is_some(), "committed is terminal-immune in runs too");
761        assert!(state.committed_run("milo", "T").is_some());
762        // A different agent is a different fencing group.
763        let mut c = DeviceLog::new("dev-c");
764        let mixed = {
765            let mut v = ops.clone();
766            v.push(intent_op(&mut c, "other", "U", 1, IntentStatus::Committed));
767            v
768        };
769        assert!(
770            fold(&mixed).committed_run("other", "U").is_some(),
771            "fencing does not cross agents"
772        );
773    }
774
775    #[test]
776    fn intent_fold_onto_equals_full_fold_across_an_epoch_bump() {
777        // The compaction-safety equivalence for the leased tier across an epoch
778        // bump: a checkpoint captured the agent at epoch 1 (COMMITTED run R). A
779        // later, higher-epoch tail op (run S @ 2) raises the fence — and R,
780        // being committed, is terminal-immune and SURVIVES (the C1/C3 fix; a
781        // commit is a durable fact, not evicted by an unrelated bump). The
782        // fold_onto == fold equivalence still holds exactly.
783        let mut a = DeviceLog::new("dev-a");
784        let mut b = DeviceLog::new("dev-b");
785        let prefix = vec![intent_op(&mut a, "milo", "R", 1, IntentStatus::Committed)];
786        for op in &prefix {
787            b.observe(&op.hlc);
788        }
789        let tail = vec![intent_op(&mut b, "milo", "S", 2, IntentStatus::Committed)];
790        let full = {
791            let mut v = prefix.clone();
792            v.extend(tail.iter().cloned());
793            v
794        };
795
796        let base = fold(&prefix); // the "checkpoint" state: epoch 1, R committed
797        assert_eq!(base.fencing_epoch("milo"), Some(1));
798        assert!(base.committed_run("milo", "R").is_some());
799
800        let via_base = fold_onto(&base, &tail);
801        assert_eq!(via_base, fold(&full), "fold_onto == fold across the epoch bump");
802        assert_eq!(state_hash(&via_base), state_hash(&fold(&full)));
803        assert_eq!(via_base.fencing_epoch("milo"), Some(2));
804        // R committed@1 SURVIVES the bump in both views (terminal-immune / oracle).
805        assert!(
806            via_base.committed_run("milo", "R").is_some(),
807            "committed R survives the epoch bump in the idempotency oracle"
808        );
809        assert!(via_base.intent("milo", "R").is_some(), "committed R is terminal-immune in runs");
810        assert!(via_base.committed_run("milo", "S").is_some());
811        // Idempotent re-delivery of the tail changes nothing.
812        assert_eq!(fold_onto(&via_base, &tail), via_base);
813    }
814
815    #[test]
816    fn intent_fencing_beats_a_later_hlc() {
817        // Safety is by EPOCH, not wall clock: a zombie op with a LATER hlc but
818        // a LOWER epoch still loses to the higher-epoch op — no wall-clock race.
819        let mut cloud = DeviceLog::new("cloud");
820        let mut zombie = DeviceLog::new("laptop");
821        let c = intent_op(&mut cloud, "milo", "R", 2, IntentStatus::Committed);
822        zombie.observe(&c.hlc); // the zombie's later write stamps a HIGHER hlc
823        let z = intent_op(&mut zombie, "milo", "R", 1, IntentStatus::Committed);
824        assert!(z.hlc > c.hlc, "the zombie op is later in HLC");
825
826        let state = fold(&[c.clone(), z]);
827        let winner = state.intent("milo", "R").unwrap();
828        assert_eq!(winner.op_id, c.op_id, "higher epoch wins despite lower HLC");
829        assert_eq!(state.fencing_epoch("milo"), Some(2));
830    }
831
832    #[test]
833    fn idempotent_run_under_failover_uses_the_same_deterministic_run_id() {
834        // B7 tie-in: two sites computing the same scheduled occurrence derive
835        // the SAME run_id, so a failed-over holder and a zombie collapse to ONE
836        // ledger record; epoch fencing then picks the legit (epoch-2) winner.
837        let run_id = car_proto::deterministic_run_id("milo", "3am digest", "2026-07-02T03:00");
838        assert_eq!(
839            run_id,
840            car_proto::deterministic_run_id("milo", "3am digest", "2026-07-02T03:00"),
841            "same occurrence → same run_id"
842        );
843        let mut zombie = DeviceLog::new("laptop");
844        let mut cloud = DeviceLog::new("cloud");
845        let z = intent_op(&mut zombie, "milo", &run_id, 1, IntentStatus::Committed);
846        let c = intent_op(&mut cloud, "milo", &run_id, 2, IntentStatus::Committed);
847
848        let state = fold(&[z, c.clone()]);
849        assert_eq!(state.intents["milo"].runs.len(), 1, "exactly one execution record");
850        assert_eq!(
851            state.intent("milo", &run_id).unwrap().op_id,
852            c.op_id,
853            "the epoch-2 holder's run wins; the zombie is a no-op"
854        );
855    }
856
857    #[test]
858    fn intent_fold_is_order_independent_over_every_permutation() {
859        // Brute-force the redesigned leased fold (pre-pass + terminal-immunity
860        // + committed oracle): a 5-op set mixing committed/pending across two
861        // epochs and three runs must fold IDENTICALLY in all 120 orders.
862        fn permutations<T: Clone>(items: &[T]) -> Vec<Vec<T>> {
863            fn heap<T: Clone>(k: usize, arr: &mut Vec<T>, out: &mut Vec<Vec<T>>) {
864                if k == 1 {
865                    out.push(arr.clone());
866                    return;
867                }
868                for i in 0..k {
869                    heap(k - 1, arr, out);
870                    if k.is_multiple_of(2) {
871                        arr.swap(i, k - 1);
872                    } else {
873                        arr.swap(0, k - 1);
874                    }
875                }
876            }
877            let mut arr = items.to_vec();
878            let mut out = Vec::new();
879            heap(arr.len(), &mut arr, &mut out);
880            out
881        }
882
883        let mut a = DeviceLog::new("a");
884        let mut b = DeviceLog::new("b");
885        let ops = vec![
886            intent_op(&mut a, "milo", "R", 1, IntentStatus::Committed), // terminal-immune across bump
887            intent_op(&mut a, "milo", "S", 1, IntentStatus::Pending),   // unshared pending → fenced
888            intent_op(&mut a, "milo", "T", 1, IntentStatus::Committed), // unshared committed → survives
889            intent_op(&mut b, "milo", "R", 2, IntentStatus::Pending),   // C3: must not revert R
890            intent_op(&mut b, "milo", "S", 2, IntentStatus::Committed), // S commits at the higher epoch
891        ];
892        let baseline = fold(&ops);
893        // Expected steady state.
894        assert_eq!(baseline.fencing_epoch("milo"), Some(2));
895        let mut committed = baseline.committed_run_ids("milo");
896        committed.sort();
897        assert_eq!(committed, vec!["R", "S", "T"], "the oracle keeps every committed run");
898        assert!(baseline.intent("milo", "S").map(|r| r.op_id.clone()).is_some_and(|_| {
899            Intent::from_payload(&baseline.intent("milo", "S").unwrap().payload).unwrap().status
900                == IntentStatus::Committed
901        }));
902        assert_eq!(
903            Intent::from_payload(&baseline.intent("milo", "R").unwrap().payload).unwrap().status,
904            IntentStatus::Committed,
905            "R is not reverted to pending"
906        );
907
908        for perm in permutations(&ops) {
909            assert_eq!(fold(&perm), baseline, "leased fold must be order-independent");
910            assert_eq!(state_hash(&fold(&perm)), state_hash(&baseline));
911        }
912    }
913
914    #[test]
915    fn c1_committed_run_survives_an_unrelated_higher_epoch_run() {
916        // C1 REPRO: run R commits at epoch 1; later an UNRELATED run T lands at
917        // epoch 2 for the same agent (no concurrency). The old fold cleared
918        // runs on the bump, so intent(R) → None → the idempotency check said
919        // "not run" → double-execution. FIX: the fence-INDEPENDENT
920        // committed_run oracle answers correctly regardless of the bump.
921        let mut a = DeviceLog::new("dev-a");
922        let mut b = DeviceLog::new("dev-b");
923        let r_commit = intent_op(&mut a, "milo", "R", 1, IntentStatus::Committed);
924        b.observe(&r_commit.hlc);
925        let t_pending = intent_op(&mut b, "milo", "T", 2, IntentStatus::Pending);
926
927        let state = fold(&[r_commit.clone(), t_pending]);
928        assert_eq!(state.fencing_epoch("milo"), Some(2), "the unrelated run bumped the fence");
929        // The oracle still says R committed — the correct idempotency answer.
930        assert_eq!(
931            state.committed_run("milo", "R").unwrap().op_id,
932            r_commit.op_id,
933            "committed_run(R) survives the unrelated epoch bump (C1 fixed)"
934        );
935        assert_eq!(state.committed_run_ids("milo"), vec!["R"]);
936    }
937
938    #[test]
939    fn c3_committed_then_pending_across_a_bump_stays_committed() {
940        // C3 REPRO: R commits at epoch 1; a failed-over holder writes R PENDING
941        // at epoch 2 (before checking). The old Greater arm unconditionally
942        // cleared, reverting R to pending → looked un-run → double-execute.
943        // FIX: terminal-immunity — the fold keeps committed for R in BOTH views,
944        // in any delivery order.
945        let mut orig = DeviceLog::new("orig");
946        let mut failover = DeviceLog::new("failover");
947        let committed = intent_op(&mut orig, "milo", "R", 1, IntentStatus::Committed);
948        failover.observe(&committed.hlc);
949        let late_pending = intent_op(&mut failover, "milo", "R", 2, IntentStatus::Pending);
950        assert!(late_pending.hlc > committed.hlc, "the pending is even later in HLC");
951
952        for order in [
953            vec![committed.clone(), late_pending.clone()],
954            vec![late_pending.clone(), committed.clone()],
955        ] {
956            let state = fold(&order);
957            // Oracle: committed, unconditionally.
958            assert_eq!(
959                state.committed_run("milo", "R").unwrap().op_id,
960                committed.op_id,
961                "committed stays committed across the bump (oracle)"
962            );
963            // who-holds view: terminal-immune, still committed (not reverted).
964            let decoded = Intent::from_payload(&state.intent("milo", "R").unwrap().payload).unwrap();
965            assert_eq!(decoded.status, IntentStatus::Committed, "runs view is not reverted to pending");
966        }
967    }
968}