Skip to main content

formualizer_eval/engine/graph/editor/
change_log.rs

1//! Standalone change logging infrastructure for tracking graph mutations
2//!
3//! This module provides:
4//! - ChangeLog: Audit trail of all graph changes
5//! - ChangeEvent: Granular representation of individual changes
6//! - ChangeLogger: Trait for pluggable logging strategies
7
8use crate::SheetId;
9use crate::engine::addr::GridAddr;
10use crate::engine::named_range::{NameScope, NamedDefinition};
11use crate::engine::row_visibility::RowVisibilitySource;
12use crate::engine::vertex::VertexId;
13use crate::reference::CellRef;
14use formualizer_common::LiteralValue;
15use formualizer_parse::parser::ASTNode;
16
17#[derive(Debug, Clone, PartialEq)]
18pub struct SpillSnapshot {
19    /// Declared target cells (row-major rectangle) owned by this spill anchor.
20    pub target_cells: Vec<CellRef>,
21    /// Row-major rectangular values corresponding to the target rectangle.
22    pub values: Vec<Vec<LiteralValue>>,
23}
24
25/// Per-event metadata attached by the caller.
26///
27/// This is intentionally lightweight (Strings) to avoid leaking application types
28/// into the engine layer.
29#[derive(Debug, Clone, PartialEq, Eq, Default)]
30pub struct ChangeEventMeta {
31    pub actor_id: Option<String>,
32    pub correlation_id: Option<String>,
33    pub reason: Option<String>,
34}
35
36/// Represents a single change to the dependency graph
37#[derive(Debug, Clone, PartialEq)]
38pub enum ChangeEvent {
39    // Simple events
40    SetValue {
41        addr: CellRef,
42        old_value: Option<LiteralValue>,
43        old_formula: Option<ASTNode>,
44        new: LiteralValue,
45    },
46    SetFormula {
47        addr: CellRef,
48        old_value: Option<LiteralValue>,
49        old_formula: Option<ASTNode>,
50        new: ASTNode,
51    },
52    SetRowVisibility {
53        sheet_id: SheetId,
54        row0: u32,
55        source: RowVisibilitySource,
56        old_hidden: bool,
57        new_hidden: bool,
58    },
59    /// Vertex creation snapshot (for undo). Minimal for now.
60    AddVertex {
61        id: VertexId,
62        coord: GridAddr,
63        sheet_id: SheetId,
64        value: Option<LiteralValue>,
65        formula: Option<ASTNode>,
66        kind: Option<crate::engine::vertex::VertexKind>,
67        flags: Option<u8>,
68    },
69    RemoveVertex {
70        id: VertexId,
71        // Need to capture more for rollback!
72        old_value: Option<LiteralValue>,
73        old_formula: Option<ASTNode>,
74        old_dependencies: Vec<VertexId>, // outgoing
75        old_dependents: Vec<VertexId>,   // incoming
76        coord: Option<GridAddr>,
77        sheet_id: Option<SheetId>,
78        kind: Option<crate::engine::vertex::VertexKind>,
79        flags: Option<u8>,
80    },
81
82    // Compound operation markers
83    CompoundStart {
84        description: String, // e.g., "InsertRows(sheet=0, before=5, count=2)"
85        depth: usize,
86    },
87    CompoundEnd {
88        depth: usize,
89    },
90
91    // Granular events for compound operations
92    VertexMoved {
93        id: VertexId,
94        sheet_id: SheetId,
95        old_coord: GridAddr,
96        new_coord: GridAddr,
97    },
98    FormulaAdjusted {
99        id: VertexId,
100        /// Cell address for replay. May be None for non-cell formula vertices.
101        addr: Option<CellRef>,
102        old_ast: ASTNode,
103        new_ast: ASTNode,
104    },
105    NamedRangeAdjusted {
106        name: String,
107        scope: NameScope,
108        old_definition: NamedDefinition,
109        new_definition: NamedDefinition,
110    },
111    EdgeAdded {
112        from: VertexId,
113        to: VertexId,
114    },
115    EdgeRemoved {
116        from: VertexId,
117        to: VertexId,
118    },
119
120    // Named range operations
121    DefineName {
122        name: String,
123        scope: NameScope,
124        definition: NamedDefinition,
125    },
126    UpdateName {
127        name: String,
128        scope: NameScope,
129        old_definition: NamedDefinition,
130        new_definition: NamedDefinition,
131    },
132    DeleteName {
133        name: String,
134        scope: NameScope,
135        old_definition: Option<NamedDefinition>,
136    },
137
138    // Spill region changes (dynamic arrays)
139    SpillCommitted {
140        anchor: VertexId,
141        old: Option<SpillSnapshot>,
142        new: SpillSnapshot,
143    },
144    SpillCleared {
145        anchor: VertexId,
146        old: SpillSnapshot,
147    },
148    /// Workbook-level per-cell staged formula delta used to keep deferred edits
149    /// undoable.
150    ///
151    /// Replaces the former `StagedFormulaStateChanged` full before/after snapshot
152    /// pair (which made interactive `set_formula` O(N) per edit and O(N^2) in
153    /// changelog memory — see #126). Each edit records only the affected cell's
154    /// staged text transition, so a sequence of N edits costs O(N) total.
155    ///
156    /// - `old`: the staged formula text for the cell before the edit, if any.
157    /// - `new`: the staged formula text for the cell after the edit, if any.
158    ///
159    /// Undo restores `old` (re-stage if `Some`, clear if `None`); redo applies
160    /// `new` (re-stage if `Some`, clear if `None`).
161    StagedFormulaCellChanged {
162        sheet: String,
163        row: u32,
164        col: u32,
165        old: Option<String>,
166        new: Option<String>,
167    },
168}
169
170/// The `FormulaAdjusted` events of a run of consecutive family members
171/// that a structural edit rewrote as one (Program 2, contract decision
172/// 20.5): members `0..len` are vertices `first + i` at rows `row0 + i` of
173/// column `col` (after the edit); each member's old and new formula is the
174/// first member's relocated down by `i` rows. Logs keep the record and
175/// expand it into the per-member events only when those are read.
176#[derive(Debug, Clone, PartialEq)]
177pub(crate) struct FormulaRunAdjusted {
178    pub(crate) first: VertexId,
179    pub(crate) len: u32,
180    pub(crate) sheet_id: SheetId,
181    pub(crate) col: u32,
182    pub(crate) row0: u32,
183    pub(crate) old_first: ASTNode,
184    pub(crate) new_first: ASTNode,
185}
186
187impl FormulaRunAdjusted {
188    #[inline]
189    pub(crate) fn len(&self) -> usize {
190        self.len as usize
191    }
192
193    /// Member `i`'s event, exactly as the per-cell editor logs it.
194    pub(crate) fn event(&self, i: u32) -> ChangeEvent {
195        let relocate = |ast: &ASTNode| {
196            if i == 0 {
197                return ast.clone();
198            }
199            // Every member between the run's (checked) first and last
200            // relocates: references are affine in the member row.
201            crate::engine::template::relocate::instantiate_member_ast(ast, i64::from(i), 0)
202                .expect("a run member's formula relocates")
203        };
204        ChangeEvent::FormulaAdjusted {
205            id: VertexId(self.first.0 + i),
206            addr: Some(CellRef::new(
207                self.sheet_id,
208                crate::reference::Coord::new(self.row0 + i, self.col, true, true),
209            )),
210            old_ast: relocate(&self.old_first),
211            new_ast: relocate(&self.new_first),
212        }
213    }
214
215    /// Every member's event, in vertex order.
216    pub(crate) fn events(&self) -> impl Iterator<Item = ChangeEvent> + '_ {
217        (0..self.len).map(|i| self.event(i))
218    }
219}
220
221/// A run record held by a log: it stands for `run.len` events placed
222/// before retained event `pos`, with consecutive sequence numbers from
223/// `seq0`, one group and one meta.
224#[derive(Debug)]
225struct LazyEntry {
226    pos: usize,
227    seq0: u64,
228    group: Option<u64>,
229    meta: ChangeEventMeta,
230    run: std::sync::Arc<FormulaRunAdjusted>,
231}
232
233/// The retained events of a log. `events`/`metas`/`seqs`/`groups` are the
234/// canonical (parallel) vectors; `lazy` holds run records not yet expanded
235/// into them, in log order, each positioned before `events[pos]`. Every
236/// record's position is at or after the previous record's, and the events
237/// before the first record are expanded history.
238#[derive(Debug, Default)]
239struct Retained {
240    events: Vec<ChangeEvent>,
241    metas: Vec<ChangeEventMeta>,
242    seqs: Vec<u64>,
243    groups: Vec<Option<u64>>,
244    lazy: Vec<LazyEntry>,
245}
246
247impl Retained {
248    /// Expand every pending run record in place. Only the events from the
249    /// first record's position on are touched (moved, never cloned), so
250    /// history expanded by an earlier call costs nothing again. Returns the
251    /// number of events written (expanded plus moved), for the work gate.
252    fn expand(&mut self) -> usize {
253        let Some(first) = self.lazy.first() else {
254            return 0;
255        };
256        let start = first.pos;
257        let tail_events = self.events.split_off(start);
258        let tail_metas = self.metas.split_off(start);
259        let tail_seqs = self.seqs.split_off(start);
260        let tail_groups = self.groups.split_off(start);
261        let tail_len = tail_events.len();
262        let added: usize = self.lazy.iter().map(|e| e.run.len()).sum();
263        self.events.reserve(tail_len + added);
264        self.metas.reserve(tail_len + added);
265        self.seqs.reserve(tail_len + added);
266        self.groups.reserve(tail_len + added);
267        let mut tail = tail_events
268            .into_iter()
269            .zip(tail_metas)
270            .zip(tail_seqs.into_iter().zip(tail_groups));
271        let mut p = start;
272        for entry in std::mem::take(&mut self.lazy) {
273            while p < entry.pos {
274                let ((event, meta), (seq, group)) = tail.next().expect("record position in range");
275                self.events.push(event);
276                self.metas.push(meta);
277                self.seqs.push(seq);
278                self.groups.push(group);
279                p += 1;
280            }
281            for (i, event) in entry.run.events().enumerate() {
282                self.events.push(event);
283                self.metas.push(entry.meta.clone());
284                self.seqs.push(entry.seq0 + i as u64);
285                self.groups.push(entry.group);
286            }
287        }
288        for ((event, meta), (seq, group)) in tail {
289            self.events.push(event);
290            self.metas.push(meta);
291            self.seqs.push(seq);
292            self.groups.push(group);
293        }
294        tail_len + added
295    }
296}
297
298/// [`Retained`] behind a cell that lets a shared read expand pending run
299/// records in place, once, so readers borrow one contiguous expanded log
300/// and every run record is expanded exactly once over the log's life.
301///
302/// Invariant: `expanded` is initialized exactly when `inner.lazy` is
303/// empty. Shared access to `inner` goes only through [`Self::get`], which
304/// runs the expansion inside `expanded.get_or_init` and hands out
305/// references only after it completes; every other access takes `&mut
306/// self`, and a record is added (resetting `expanded`) only through
307/// `&mut self` as well.
308struct RetainedCell {
309    inner: std::cell::UnsafeCell<Retained>,
310    expanded: std::sync::OnceLock<()>,
311    /// Retained events, pending records included.
312    len: usize,
313    /// Pending run records (meaningful while `expanded` is uninitialized).
314    pending_records: usize,
315    /// Events written by expansion (expanded and moved) over the log's
316    /// life: the work gate of the changelog scaling test.
317    #[cfg(test)]
318    work: std::sync::atomic::AtomicUsize,
319}
320
321// SAFETY: `inner` is mutated through a shared reference only inside
322// `expanded.get_or_init`, which runs the closure at most once per
323// initialization, blocks concurrent callers until it finishes and
324// publishes its writes (happens-before) to them. No shared reference into
325// `inner` can exist while it runs: `get` returns only after initialization,
326// and `expanded` is reset only through `&mut self`. This is the same
327// contract as `Mutex<Retained>` / `OnceLock<Retained>`, hence the
328// `Send + Sync` requirement on the contents (asserted below).
329unsafe impl Sync for RetainedCell {}
330
331const _: () = {
332    const fn assert_send_sync<T: Send + Sync>() {}
333    assert_send_sync::<Retained>();
334};
335
336impl Default for RetainedCell {
337    fn default() -> Self {
338        Self {
339            inner: std::cell::UnsafeCell::new(Retained::default()),
340            expanded: std::sync::OnceLock::from(()),
341            len: 0,
342            pending_records: 0,
343            #[cfg(test)]
344            work: std::sync::atomic::AtomicUsize::new(0),
345        }
346    }
347}
348
349impl std::fmt::Debug for RetainedCell {
350    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
351        f.debug_struct("RetainedCell")
352            .field("len", &self.len)
353            .field("expanded", &self.expanded.get().is_some())
354            .finish()
355    }
356}
357
358impl RetainedCell {
359    #[inline]
360    fn note_work(&self, _work: usize) {
361        #[cfg(test)]
362        self.work
363            .fetch_add(_work, std::sync::atomic::Ordering::Relaxed);
364    }
365
366    /// The expanded log (pending records are expanded first, once).
367    fn get(&self) -> &Retained {
368        self.expanded.get_or_init(|| {
369            // SAFETY: see the `Sync` impl: this closure is the only access
370            // to `inner` while it runs.
371            let inner = unsafe { &mut *self.inner.get() };
372            self.note_work(inner.expand());
373        });
374        // SAFETY: initialized: no mutation through `&self` until `&mut self`.
375        unsafe { &*self.inner.get() }
376    }
377
378    /// Expand pending records in place and return the expanded vectors.
379    fn settle(&mut self) -> &mut Retained {
380        if self.expanded.get().is_none() {
381            let work = self.inner.get_mut().expand();
382            self.note_work(work);
383            self.expanded = std::sync::OnceLock::from(());
384            self.pending_records = 0;
385        }
386        self.inner.get_mut()
387    }
388
389    /// Plain events so far (the position of a record appended now).
390    fn plain_len(&mut self) -> usize {
391        self.inner.get_mut().events.len()
392    }
393
394    fn push(&mut self, event: ChangeEvent, meta: ChangeEventMeta, seq: u64, group: Option<u64>) {
395        let r = self.inner.get_mut();
396        r.events.push(event);
397        r.metas.push(meta);
398        r.seqs.push(seq);
399        r.groups.push(group);
400        self.len += 1;
401    }
402
403    fn push_run(&mut self, entry: LazyEntry) {
404        self.len += entry.run.len();
405        self.inner.get_mut().lazy.push(entry);
406        self.pending_records += 1;
407        self.expanded = std::sync::OnceLock::new();
408    }
409
410    fn drain_front(&mut self, n: usize) {
411        let r = self.settle();
412        r.events.drain(0..n);
413        r.metas.drain(0..n);
414        r.seqs.drain(0..n);
415        r.groups.drain(0..n);
416        self.len = r.events.len();
417    }
418
419    fn truncate(&mut self, len: usize) {
420        let r = self.settle();
421        r.events.truncate(len);
422        r.metas.truncate(len);
423        r.seqs.truncate(len);
424        r.groups.truncate(len);
425        self.len = r.events.len();
426    }
427
428    fn split_off(&mut self, index: usize) -> Vec<ChangeEvent> {
429        let r = self.settle();
430        let events = r.events.split_off(index);
431        let _ = r.metas.split_off(index);
432        let _ = r.seqs.split_off(index);
433        let _ = r.groups.split_off(index);
434        self.len = r.events.len();
435        events
436    }
437
438    fn clear(&mut self) {
439        *self = Self {
440            #[cfg(test)]
441            work: std::sync::atomic::AtomicUsize::new(
442                self.work.load(std::sync::atomic::Ordering::Relaxed),
443            ),
444            ..Self::default()
445        };
446    }
447}
448
449/// Audit trail for tracking all changes to the dependency graph
450#[derive(Debug, Default)]
451pub struct ChangeLog {
452    enabled: bool,
453    /// Optional cap on retained events; when exceeded, oldest events are evicted (FIFO).
454    max_changelog_events: Option<usize>,
455    /// Track compound operations for atomic rollback
456    compound_depth: usize,
457    next_seq: u64,
458    /// Stack of active group ids for nested compounds
459    group_stack: Vec<u64>,
460    next_group_id: u64,
461
462    current_meta: ChangeEventMeta,
463
464    /// Retained events with their metadata, monotonic sequence numbers and
465    /// optional group (compound) ids. Run records (Program 2) stay
466    /// unexpanded until the log is read or indexed by a mutation; each
467    /// record is expanded in place exactly once (see [`RetainedCell`]).
468    retained: RetainedCell,
469}
470
471/// Complete, operation-local mutation capture used by `Engine` correctness paths.
472///
473/// Unlike `ChangeLog`, this sink is always enabled and never evicts. It is crate-private so
474/// audit retention remains a property of `ChangeLog`, not of graph mutation.
475#[derive(Debug)]
476pub(crate) struct MutationCapture {
477    events: Vec<ChangeEvent>,
478    /// Run records (Program 2): `(pos, run)` stands for the run's events
479    /// placed before `events[pos]`, in order.
480    lazy: Vec<(usize, std::sync::Arc<FormulaRunAdjusted>)>,
481    compound_depth: usize,
482    current_meta: ChangeEventMeta,
483}
484
485impl MutationCapture {
486    pub(crate) fn new(current_meta: ChangeEventMeta) -> Self {
487        Self {
488            events: Vec::new(),
489            lazy: Vec::new(),
490            compound_depth: 0,
491            current_meta,
492        }
493    }
494
495    /// Number of plain (non-run) events: a position marker for
496    /// [`Self::events`].
497    pub(crate) fn len(&self) -> usize {
498        self.events.len()
499    }
500
501    /// The plain events; run records are not in this slice (see
502    /// [`Self::expanded_events_from`]). Every consumer of a forward edit's
503    /// events ignores `FormulaAdjusted` except invalidation, which also
504    /// asks [`Self::lazy_len`].
505    pub(crate) fn events(&self) -> &[ChangeEvent] {
506        &self.events
507    }
508
509    /// Number of run records (a position marker for records).
510    pub(crate) fn lazy_len(&self) -> usize {
511        self.lazy.len()
512    }
513
514    /// Record a run's `FormulaAdjusted` events without expanding them.
515    pub(crate) fn record_lazy(&mut self, run: FormulaRunAdjusted) {
516        if run.len == 0 {
517            return;
518        }
519        self.lazy
520            .push((self.events.len(), std::sync::Arc::new(run)));
521    }
522
523    /// Every event from plain position `start` (and run record
524    /// `lazy_start`) on, run records expanded in place.
525    pub(crate) fn expanded_events_from(&self, start: usize, lazy_start: usize) -> Vec<ChangeEvent> {
526        let lazy = &self.lazy[lazy_start.min(self.lazy.len())..];
527        let mut out = Vec::with_capacity(
528            self.events.len().saturating_sub(start)
529                + lazy.iter().map(|(_, r)| r.len()).sum::<usize>(),
530        );
531        let mut k = 0;
532        for p in start..=self.events.len() {
533            while k < lazy.len() && lazy[k].0 <= p {
534                out.extend(lazy[k].1.events());
535                k += 1;
536            }
537            if let Some(e) = self.events.get(p) {
538                out.push(e.clone());
539            }
540        }
541        out
542    }
543
544    pub(crate) fn close_compounds(&mut self) {
545        while self.compound_depth > 0 {
546            self.end_compound();
547        }
548    }
549
550    fn push(&mut self, event: ChangeEvent) {
551        self.events.push(event);
552    }
553}
554
555/// Backward replay helper: the description of the compound whose end
556/// marker is event `end` (`event(i)` reads the events in forward order):
557/// the start marker that closes it, counting nesting (depth fields are not
558/// unique: a redo re-logs a group's markers inside its own compound).
559pub(crate) fn compound_start_description<'a>(
560    end: usize,
561    event: impl Fn(usize) -> &'a ChangeEvent,
562) -> Option<&'a str> {
563    if !matches!(event(end), ChangeEvent::CompoundEnd { .. }) {
564        return None;
565    }
566    let mut open = 1usize;
567    for i in (0..end).rev() {
568        match event(i) {
569            ChangeEvent::CompoundEnd { .. } => open += 1,
570            ChangeEvent::CompoundStart { description, .. } => {
571                open -= 1;
572                if open == 0 {
573                    return Some(description.as_str());
574                }
575            }
576            _ => {}
577        }
578    }
579    None
580}
581
582impl ChangeLogger for MutationCapture {
583    fn record(&mut self, event: ChangeEvent) {
584        self.push(event);
585    }
586
587    fn set_enabled(&mut self, _: bool) {}
588
589    fn begin_compound(&mut self, description: String) {
590        self.compound_depth += 1;
591        self.push(ChangeEvent::CompoundStart {
592            description,
593            depth: self.compound_depth,
594        });
595    }
596
597    fn end_compound(&mut self) {
598        if self.compound_depth == 0 {
599            return;
600        }
601        self.push(ChangeEvent::CompoundEnd {
602            depth: self.compound_depth,
603        });
604        self.compound_depth -= 1;
605    }
606}
607
608impl ChangeLog {
609    pub fn new() -> Self {
610        Self {
611            enabled: true,
612            max_changelog_events: None,
613            compound_depth: 0,
614            next_seq: 0,
615            group_stack: Vec::new(),
616            next_group_id: 1,
617            current_meta: ChangeEventMeta::default(),
618            retained: RetainedCell::default(),
619        }
620    }
621
622    pub fn with_max_changelog_events(max: usize) -> Self {
623        let mut out = Self::new();
624        out.max_changelog_events = Some(max);
625        out
626    }
627
628    pub fn set_max_changelog_events(&mut self, max: Option<usize>) {
629        self.max_changelog_events = max;
630        self.enforce_cap();
631    }
632
633    fn enforce_cap(&mut self) {
634        let Some(max) = self.max_changelog_events else {
635            return;
636        };
637        if max == 0 {
638            self.retained.clear();
639            return;
640        }
641        if self.len() <= max {
642            return;
643        }
644        let drop_n = self.len() - max;
645        self.retained.drain_front(drop_n);
646    }
647
648    fn replay_lazy(
649        &mut self,
650        run: std::sync::Arc<FormulaRunAdjusted>,
651        meta: &ChangeEventMeta,
652        retain: bool,
653    ) {
654        if !self.enabled {
655            return;
656        }
657        let seq0 = self.next_seq;
658        self.next_seq += run.len as u64;
659        if retain {
660            let entry = LazyEntry {
661                pos: self.retained.plain_len(),
662                seq0,
663                group: self.group_stack.last().copied(),
664                meta: meta.clone(),
665                run,
666            };
667            self.retained.push_run(entry);
668        }
669    }
670
671    fn replay_record(&mut self, event: ChangeEvent, meta: &ChangeEventMeta, retain: bool) {
672        if !self.enabled {
673            return;
674        }
675        let seq = self.next_seq;
676        self.next_seq += 1;
677        if retain {
678            let group = self.group_stack.last().copied();
679            self.retained.push(event, meta.clone(), seq, group);
680        }
681    }
682
683    fn replay_begin_compound(&mut self, description: String, meta: &ChangeEventMeta, retain: bool) {
684        self.compound_depth += 1;
685        if self.compound_depth == 1 {
686            let gid = self.next_group_id;
687            self.next_group_id += 1;
688            self.group_stack.push(gid);
689        } else if let Some(&gid) = self.group_stack.last() {
690            self.group_stack.push(gid);
691        }
692        self.replay_record(
693            ChangeEvent::CompoundStart {
694                description,
695                depth: self.compound_depth,
696            },
697            meta,
698            retain,
699        );
700    }
701
702    fn replay_end_compound(&mut self, meta: &ChangeEventMeta, retain: bool) {
703        if self.compound_depth == 0 {
704            return;
705        }
706        self.replay_record(
707            ChangeEvent::CompoundEnd {
708                depth: self.compound_depth,
709            },
710            meta,
711            retain,
712        );
713        self.compound_depth -= 1;
714        self.group_stack.pop();
715    }
716
717    fn replay_capture(&mut self, capture: MutationCapture, retain: bool) {
718        let mut lazy = capture.lazy.into_iter().peekable();
719        for (p, event) in capture.events.into_iter().enumerate() {
720            while let Some((_, run)) = lazy.next_if(|(pos, _)| *pos <= p) {
721                self.replay_lazy(run, &capture.current_meta, retain);
722            }
723            match event {
724                ChangeEvent::CompoundStart { description, .. } => {
725                    self.replay_begin_compound(description, &capture.current_meta, retain);
726                }
727                ChangeEvent::CompoundEnd { .. } => {
728                    self.replay_end_compound(&capture.current_meta, retain);
729                }
730                event => self.replay_record(event, &capture.current_meta, retain),
731            }
732        }
733        for (_, run) in lazy {
734            self.replay_lazy(run, &capture.current_meta, retain);
735        }
736        if retain {
737            self.enforce_cap();
738        }
739    }
740
741    pub(crate) fn current_meta(&self) -> ChangeEventMeta {
742        self.current_meta.clone()
743    }
744
745    pub(crate) fn publish_capture(&mut self, capture: MutationCapture) {
746        self.replay_capture(capture, true);
747    }
748
749    pub(crate) fn discard_capture(&mut self, capture: MutationCapture) {
750        self.replay_capture(capture, false);
751    }
752
753    pub fn record(&mut self, event: ChangeEvent) {
754        if self.enabled {
755            let seq = self.next_seq;
756            self.next_seq += 1;
757            let current_group = self.group_stack.last().copied();
758            self.retained
759                .push(event, self.current_meta.clone(), seq, current_group);
760            self.enforce_cap();
761        }
762    }
763
764    /// Record an event with explicit metadata (used for replay/redo).
765    pub fn record_with_meta(&mut self, event: ChangeEvent, meta: ChangeEventMeta) {
766        if self.enabled {
767            let seq = self.next_seq;
768            self.next_seq += 1;
769            let current_group = self.group_stack.last().copied();
770            self.retained.push(event, meta, seq, current_group);
771            self.enforce_cap();
772        }
773    }
774
775    /// Begin a compound operation (multiple changes from single action)
776    pub fn begin_compound(&mut self, description: String) {
777        self.compound_depth += 1;
778        if self.compound_depth == 1 {
779            // allocate new group id
780            let gid = self.next_group_id;
781            self.next_group_id += 1;
782            self.group_stack.push(gid);
783        } else {
784            // nested: reuse top id
785            if let Some(&gid) = self.group_stack.last() {
786                self.group_stack.push(gid);
787            }
788        }
789        if self.enabled {
790            self.record(ChangeEvent::CompoundStart {
791                description,
792                depth: self.compound_depth,
793            });
794        }
795    }
796
797    /// End a compound operation
798    pub fn end_compound(&mut self) {
799        if self.compound_depth > 0 {
800            if self.enabled {
801                self.record(ChangeEvent::CompoundEnd {
802                    depth: self.compound_depth,
803                });
804            }
805            self.compound_depth -= 1;
806            self.group_stack.pop();
807        }
808    }
809
810    /// Run records not yet expanded in place (tests: laziness).
811    #[cfg(test)]
812    pub(crate) fn unexpanded_run_records(&self) -> usize {
813        if self.retained.expanded.get().is_some() {
814            0
815        } else {
816            self.retained.pending_records
817        }
818    }
819
820    /// Events written into the retained vectors by run-record expansion
821    /// (expanded members plus plain events moved behind them) over the
822    /// log's life (tests: the work-scaling gate).
823    #[cfg(test)]
824    pub(crate) fn expansion_work(&self) -> usize {
825        self.retained
826            .work
827            .load(std::sync::atomic::Ordering::Relaxed)
828    }
829
830    pub fn events(&self) -> &[ChangeEvent] {
831        &self.retained.get().events
832    }
833
834    pub fn event_meta(&self, index: usize) -> Option<&ChangeEventMeta> {
835        self.retained.get().metas.get(index)
836    }
837
838    pub fn set_actor_id(&mut self, actor_id: Option<String>) {
839        self.current_meta.actor_id = actor_id;
840    }
841
842    pub fn set_correlation_id(&mut self, correlation_id: Option<String>) {
843        self.current_meta.correlation_id = correlation_id;
844    }
845
846    pub fn set_reason(&mut self, reason: Option<String>) {
847        self.current_meta.reason = reason;
848    }
849
850    /// Truncate log (and metadata) to len
851    pub fn truncate(&mut self, len: usize) {
852        self.retained.truncate(len);
853    }
854
855    pub fn clear(&mut self) {
856        self.retained.clear();
857        self.compound_depth = 0;
858        self.group_stack.clear();
859    }
860
861    pub fn len(&self) -> usize {
862        self.retained.len
863    }
864
865    pub fn is_empty(&self) -> bool {
866        self.len() == 0
867    }
868
869    /// Extract events from index to end
870    pub fn take_from(&mut self, index: usize) -> Vec<ChangeEvent> {
871        self.retained.split_off(index)
872    }
873
874    /// Temporarily disable logging (for rollback operations)
875    pub fn set_enabled(&mut self, enabled: bool) {
876        self.enabled = enabled;
877    }
878
879    /// Get current compound depth (for testing)
880    pub fn compound_depth(&self) -> usize {
881        self.compound_depth
882    }
883
884    /// Return (sequence_number, group_id) metadata for event index
885    pub fn meta(&self, index: usize) -> Option<(u64, Option<u64>)> {
886        let r = self.retained.get();
887        r.seqs.get(index).copied().zip(r.groups.get(index).copied())
888    }
889
890    /// Collect indices belonging to the last (innermost) complete group. Fallback: last single event.
891    pub fn last_group_indices(&self) -> Vec<usize> {
892        let groups = &self.retained.get().groups;
893        if let Some(&last_gid) = groups.iter().rev().flatten().next() {
894            let idxs: Vec<usize> = groups
895                .iter()
896                .enumerate()
897                .filter_map(|(i, g)| if *g == Some(last_gid) { Some(i) } else { None })
898                .collect();
899            if !idxs.is_empty() {
900                return idxs;
901            }
902        }
903        self.len().checked_sub(1).into_iter().collect()
904    }
905}
906
907/// Trait for pluggable logging strategies
908pub trait ChangeLogger {
909    fn record(&mut self, event: ChangeEvent);
910    fn set_enabled(&mut self, enabled: bool);
911    fn begin_compound(&mut self, description: String);
912    fn end_compound(&mut self);
913}
914
915impl ChangeLogger for ChangeLog {
916    fn record(&mut self, event: ChangeEvent) {
917        ChangeLog::record(self, event);
918    }
919
920    fn set_enabled(&mut self, enabled: bool) {
921        self.enabled = enabled;
922    }
923
924    fn begin_compound(&mut self, description: String) {
925        ChangeLog::begin_compound(self, description);
926    }
927
928    fn end_compound(&mut self) {
929        ChangeLog::end_compound(self);
930    }
931}
932
933/// Null logger for when change tracking not needed
934pub struct NullChangeLogger;
935
936impl ChangeLogger for NullChangeLogger {
937    fn record(&mut self, _: ChangeEvent) {}
938    fn set_enabled(&mut self, _: bool) {}
939    fn begin_compound(&mut self, _: String) {}
940    fn end_compound(&mut self) {}
941}