tawasuyu

tawasuyu/nakui

> Excel-style reactive engine, on solid principles — exact Decimal, topological cascade, WAL, time-travel — in Rust, with a Llimphi UI.

Rust 82Markdown 14JSON 12TOML 101471 Sutichaykuna

⎇ 7041e0499f8449ae8d7bba2a65695d82cef05a9e 01_yachay/nakui/nakui-sheet/src/workbook.rs Rust · 949 siq'ikuna · 35.4 KB Ñawpa willay Pi imata llamkarqan ch'uya

feat: nakui standalone — front-door ERP/Hoja/Grafo sobre Llimphi 7041e0499 · Sergio ·

1//! `Workbook` — `Sheet` con WAL persistente e invariantes.
2//!
3//! Cada `set_cell` ejecutado por el usuario se aplica sobre un
4//! candidato (`Sheet::clone`), se validan todos los invariantes
5//! declarados, y solo si todos pasan el cambio se promueve. Si algún
6//! invariante falla, el workbook queda EXACTAMENTE como estaba —
7//! "atomicidad de hoja", el principio del que se hablaba en el plan
8//! inicial.
9//!
10//! Esta capa es donde Nakui se diferencia del Excel tradicional:
11//! puedes declarar "el balance de caja nunca puede ser negativo" o
12//! "SUM(D:D) = K1" como reglas, y el motor las hace cumplir contra
13//! cada edición. No hay "fórmula rota y nadie se entera".
14//!
15//! El WAL aquí es local (Vec + JSONL). La integración con
16//! `nakui-core::event_log` (canonical, drift-detected, replay vía
17//! morfismos) es el siguiente bloque y vive como un trait que
18//! implementa este `Vec` y, en producción, el log durable.
19
20use crate::cell::{CellRange, CellRef};
21use crate::formula::{self, CellResolver, FormulaExpr};
22use crate::sheet::{SetError, SetReport, Sheet};
23use crate::sink::{EventSink, MemorySink, SinkError};
24use crate::value::{CellFormat, SheetValue};
25use serde::{Deserialize, Serialize};
26use std::io::{self, BufRead, Write};
27use thiserror::Error;
28
29#[derive(Debug, Error)]
30pub enum WorkbookError {
31    #[error(transparent)]
32    Set(#[from] SetError),
33    #[error("invariant `{name}` violated; edit reverted")]
34    InvariantViolated { name: String, value: SheetValue },
35    #[error("invariant parse error: {0}")]
36    InvariantParse(#[from] formula::ParseError),
37    #[error("io: {0}")]
38    Io(#[from] io::Error),
39    #[error("event log decode error at line {line}: {reason}")]
40    LogDecode { line: usize, reason: String },
41    #[error("event log refers to sequence numbers out of order")]
42    LogOutOfOrder,
43    #[error("sink error: {0}")]
44    Sink(#[from] SinkError),
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
48#[serde(tag = "op", rename_all = "snake_case")]
49pub enum SheetEvent {
50    SetCell { cell: CellRef, raw: String },
51    ClearCell { cell: CellRef },
52    /// Fill desde una celda fuente a un rango destino. Se registra
53    /// como un solo evento (no como N SetCell) para que el replay
54    /// sea idéntico al gesto del usuario y el WAL ocupe menos.
55    Fill { src: CellRef, dest: CellRange },
56    /// Restauración atómica de varias celdas a un estado anterior.
57    /// Emitido por `undo` y por cualquier flujo que necesite revertir
58    /// un batch arbitrario. `None` en el raw significa "borrar esta
59    /// celda" (lo que hacía `ClearCell` para un solo elemento).
60    ///
61    /// Esto NO viola la inmutabilidad del WAL: Restore es un evento
62    /// más al final del log, no una mutación del pasado. El estado
63    /// del workbook tras un undo+redo es indistinguible del que
64    /// habría producido la edición original — pero el WAL conserva
65    /// el rastro completo de la operación.
66    Restore { cells: Vec<(CellRef, Option<String>)> },
67    /// Cambia el formato de display de una celda. NO toca el valor
68    /// (sigue siendo el mismo Decimal/Text/Bool), solo cómo se pinta.
69    SetFormat { cell: CellRef, format: CellFormat },
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
73pub struct RecordedEvent {
74    pub seq: u64,
75    /// Milisegundos desde Unix epoch. Sirve para time-travel por
76    /// reloj, pero el orden canónico es `seq` — un sistema que
77    /// rebobina el reloj no rompe el replay.
78    pub timestamp_ms: u128,
79    pub event: SheetEvent,
80}
81
82#[derive(Debug, Clone)]
83struct Invariant {
84    name: String,
85    expr: FormulaExpr,
86}
87
88pub struct Workbook {
89    sheet: Sheet,
90    /// Sink de eventos — la capa que decide si vive en RAM, en
91    /// disco, o en `nakui-core::event_log`. Default: [`MemorySink`].
92    sink: Box<dyn EventSink>,
93    /// Cache de los eventos para que `events()` siga devolviendo
94    /// `&[...]` sin tocar el sink (que sí podría hacer I/O).
95    events_cache: Vec<RecordedEvent>,
96    invariants: Vec<Invariant>,
97    /// Cursor virtual: cuántos eventos del cache están aplicados al
98    /// `sheet` actual. En operación normal `applied == events_cache.
99    /// len()`. Cuando hay un `undo` activo, `applied <
100    /// events_cache.len()`; los eventos por delante quedan para
101    /// `redo`. Una edición nueva cuando `applied < len()` trunca
102    /// los eventos por delante del sink (la historia alternativa
103    /// se pierde, semántica clásica de editor).
104    applied: usize,
105}
106
107impl std::fmt::Debug for Workbook {
108    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109        f.debug_struct("Workbook")
110            .field("sheet", &self.sheet)
111            .field("events", &self.events_cache.len())
112            .field("invariants", &self.invariants.len())
113            .finish()
114    }
115}
116
117impl Default for Workbook {
118    fn default() -> Self {
119        Self {
120            sheet: Sheet::default(),
121            sink: Box::new(MemorySink::new()),
122            events_cache: Vec::new(),
123            invariants: Vec::new(),
124            applied: 0,
125        }
126    }
127}
128
129impl Workbook {
130    pub fn new() -> Self {
131        Self::default()
132    }
133
134    /// Construye un workbook con un sink custom (file, custom, etc).
135    /// Si el sink trae eventos previos (FileSink leyendo un archivo
136    /// existente), se reaplican al sheet en orden para reconstruir
137    /// el estado.
138    pub fn with_sink(sink: Box<dyn EventSink>) -> Result<Self, WorkbookError> {
139        let mut sheet = Sheet::default();
140        let existing = sink.events();
141        for ev in &existing {
142            apply_to_sheet(&mut sheet, &ev.event)?;
143        }
144        let applied = existing.len();
145        Ok(Self {
146            sheet,
147            sink,
148            events_cache: existing,
149            invariants: Vec::new(),
150            applied,
151        })
152    }
153
154    pub fn sheet(&self) -> &Sheet {
155        &self.sheet
156    }
157
158    pub fn events(&self) -> &[RecordedEvent] {
159        &self.events_cache
160    }
161
162    pub fn value(&self, cr: CellRef) -> SheetValue {
163        self.sheet.value(cr)
164    }
165
166    pub fn raw(&self, cr: CellRef) -> Option<&str> {
167        self.sheet.raw(cr)
168    }
169
170    /// Declara un invariante que debe evaluar a `TRUE` tras cada
171    /// edición. La fórmula se compila una vez; el name es para
172    /// mensajes de error.
173    pub fn add_invariant(&mut self, name: &str, formula: &str) -> Result<(), WorkbookError> {
174        let expr = formula::compile(formula.strip_prefix('=').unwrap_or(formula))?;
175        self.invariants.push(Invariant {
176            name: name.to_string(),
177            expr,
178        });
179        Ok(())
180    }
181
182    /// Aplica un `set_cell` con validación atómica de invariantes.
183    /// Si cualquier invariante falla en el estado resultante, el
184    /// workbook queda intacto y se devuelve el error.
185    pub fn set_cell(&mut self, cr: CellRef, raw: &str) -> Result<SetReport, WorkbookError> {
186        let event = if raw.is_empty() {
187            SheetEvent::ClearCell { cell: cr }
188        } else {
189            SheetEvent::SetCell {
190                cell: cr,
191                raw: raw.to_string(),
192            }
193        };
194        self.apply_user_event(event)
195    }
196
197    pub fn clear_cell(&mut self, cr: CellRef) -> Result<SetReport, WorkbookError> {
198        self.apply_user_event(SheetEvent::ClearCell { cell: cr })
199    }
200
201    /// Replica la fórmula de `src` al rango `dest`, ajustando refs
202    /// relativas y respetando `$` (igual que el fill-handle de
203    /// Excel). El rango destino puede incluir o no a `src`; si lo
204    /// incluye, `src` se preserva intacto. Si una ref shifted se
205    /// sale de la hoja queda como `#REF!` en esa celda específica.
206    /// Atómico vs. invariantes: si tras el fill alguno se viola, se
207    /// revierte todo.
208    pub fn fill(&mut self, src: CellRef, dest: CellRange) -> Result<SetReport, WorkbookError> {
209        self.apply_user_event(SheetEvent::Fill { src, dest })
210    }
211
212    /// Copia `src` a `dest` con shift (igual que `fill` sobre un
213    /// rango de una sola celda).
214    pub fn copy_cell(&mut self, src: CellRef, dest: CellRef) -> Result<SetReport, WorkbookError> {
215        self.fill(src, CellRange::new(dest, dest))
216    }
217
218    /// Aplica un formato de display a una celda. Si tras el cambio
219    /// algún invariante se viola (extraño — los invariantes leen
220    /// valores, no formatos, pero podría haber edge cases), se
221    /// revierte.
222    pub fn set_format(
223        &mut self,
224        cell: CellRef,
225        format: CellFormat,
226    ) -> Result<SetReport, WorkbookError> {
227        self.apply_user_event(SheetEvent::SetFormat { cell, format })
228    }
229
230    /// Formato actual de la celda (`General` por defecto).
231    pub fn format(&self, cr: CellRef) -> CellFormat {
232        self.sheet.format(cr)
233    }
234
235    /// Display de la celda respetando su formato — lo que la UI
236    /// debe pintar. Para celdas sin formato custom es equivalente a
237    /// `value(cell).to_display_string()`.
238    pub fn formatted(&self, cr: CellRef) -> String {
239        self.sheet
240            .value(cr)
241            .to_formatted_string(&self.sheet.format(cr))
242    }
243
244    /// Deshace la última edición. Estrategia: cursor virtual.
245    /// El WAL no se modifica; sólo movemos `applied` hacia atrás y
246    /// reconstruimos el sheet desde el snapshot. Una edición nueva
247    /// estando en estado `applied < len()` trunca el "futuro
248    /// alternativo" tanto del cache como del sink.
249    pub fn undo(&mut self) -> Result<Option<SetReport>, WorkbookError> {
250        if self.applied == 0 {
251            return Ok(None);
252        }
253        let new_applied = self.applied - 1;
254        let new_sheet = self.snapshot_up_to(new_applied)?;
255        self.sheet = new_sheet;
256        self.applied = new_applied;
257        // Reporte vacío: no comparamos celda por celda. La UI
258        // sabe que tras undo todo cambió y va a repintar de
259        // todos modos.
260        Ok(Some(SetReport::default()))
261    }
262
263    /// Rehace el último undo, avanzando el cursor virtual una
264    /// posición y reaplicando el evento al sheet. Devuelve `None`
265    /// si no hay nada por rehacer.
266    pub fn redo(&mut self) -> Result<Option<SetReport>, WorkbookError> {
267        if self.applied >= self.events_cache.len() {
268            return Ok(None);
269        }
270        let event = self.events_cache[self.applied].event.clone();
271        let mut candidate = self.sheet.clone();
272        let report = apply_to_sheet(&mut candidate, &event)?;
273        Self::check_invariants(&self.invariants, &candidate)?;
274        self.sheet = candidate;
275        self.applied += 1;
276        Ok(Some(report))
277    }
278
279    pub fn can_undo(&self) -> bool {
280        self.applied > 0
281    }
282
283    pub fn can_redo(&self) -> bool {
284        self.applied < self.events_cache.len()
285    }
286
287    /// Cuántos eventos del cache están actualmente aplicados al
288    /// sheet. En estado normal == `events().len()`. Tras un undo
289    /// queda por debajo, dejando eventos disponibles para `redo`.
290    pub fn applied_count(&self) -> usize {
291        self.applied
292    }
293
294    /// Reconstruye un `Sheet` aplicando los primeros `n` eventos del
295    /// cache. Igual que `snapshot_at` pero sin acceso público — lo
296    /// usa `undo` para volver al estado anterior.
297    fn snapshot_up_to(&self, n: usize) -> Result<Sheet, WorkbookError> {
298        let mut s = Sheet::new();
299        for ev in self.events_cache.iter().take(n) {
300            apply_to_sheet(&mut s, &ev.event)?;
301        }
302        Ok(s)
303    }
304
305    /// Recalcula explícitamente las celdas volátiles (`TODAY`,
306    /// `NOW`, `RAND`, etc.). No se registra como evento en el WAL
307    /// — un refresh manual no cambia la historia editable, solo
308    /// "despierta" lo que es función del tiempo. Si tras el recalc
309    /// algún invariante se viola, se revierte y se devuelve el
310    /// error (igual que set_cell).
311    pub fn refresh_volatiles(&mut self) -> Result<SetReport, WorkbookError> {
312        let mut candidate = self.sheet.clone();
313        let report = candidate.recompute_volatiles();
314        Self::check_invariants(&self.invariants, &candidate)?;
315        self.sheet = candidate;
316        Ok(report)
317    }
318
319    pub fn volatile_count(&self) -> usize {
320        self.sheet.volatile_count()
321    }
322
323    fn apply_user_event(&mut self, event: SheetEvent) -> Result<SetReport, WorkbookError> {
324        // Si hay redos pendientes (applied < len), los descartamos:
325        // el "futuro alternativo" se pierde al editar. Truncamos
326        // tanto el cache local como el sink (filesystem/memory).
327        if self.applied < self.events_cache.len() {
328            let truncate_from_seq = self
329                .events_cache
330                .get(self.applied)
331                .map(|e| e.seq)
332                .unwrap_or(0);
333            self.sink.truncate_from(truncate_from_seq)?;
334            self.events_cache.truncate(self.applied);
335        }
336
337        let mut candidate = self.sheet.clone();
338        let report = apply_to_sheet(&mut candidate, &event)?;
339        Self::check_invariants(&self.invariants, &candidate)?;
340        self.sheet = candidate;
341        let timestamp_ms = now_ms();
342        let seq = self.sink.record(event.clone(), timestamp_ms)?;
343        self.events_cache.push(RecordedEvent {
344            seq,
345            timestamp_ms,
346            event,
347        });
348        self.applied = self.events_cache.len();
349        Ok(report)
350    }
351
352    fn check_invariants(invariants: &[Invariant], sheet: &Sheet) -> Result<(), WorkbookError> {
353        let resolver = SheetResolver { sheet };
354        for inv in invariants {
355            let value = formula::eval_formula(&inv.expr, &resolver);
356            let ok = matches!(value, SheetValue::Bool(true));
357            if !ok {
358                return Err(WorkbookError::InvariantViolated {
359                    name: inv.name.clone(),
360                    value,
361                });
362            }
363        }
364        Ok(())
365    }
366
367    /// Serializa los eventos como JSONL — una línea por evento. El
368    /// formato es estable: misma versión de Nakui produce el mismo
369    /// bytes-for-bytes, lo cual es lo que permite verificar drift.
370    pub fn write_log<W: Write>(&self, mut w: W) -> Result<(), WorkbookError> {
371        for ev in &self.events_cache {
372            serde_json::to_writer(&mut w, ev).map_err(|e| {
373                WorkbookError::LogDecode {
374                    line: ev.seq as usize,
375                    reason: e.to_string(),
376                }
377            })?;
378            w.write_all(b"\n")?;
379        }
380        Ok(())
381    }
382
383    /// Reconstruye un workbook desde un log JSONL. Reaplica cada
384    /// evento en orden de `seq` (debe ser estrictamente creciente
385    /// desde 0). No reaplica invariantes — el log es la fuente de
386    /// verdad de lo que ocurrió, y si fuera inconsistente lo
387    /// detectaríamos al evaluar.
388    pub fn from_log<R: BufRead>(r: R) -> Result<Self, WorkbookError> {
389        let sink = MemorySink::from_reader(r)?;
390        Self::with_sink(Box::new(sink))
391    }
392
393    /// Time-travel: reconstruye la hoja como estaba después de
394    /// procesar los primeros `n` eventos (`n=0` → hoja vacía;
395    /// `n=events.len()` → hoja actual). El workbook actual no se
396    /// modifica — devolvemos un `Sheet` snapshot.
397    pub fn snapshot_at(&self, n: usize) -> Result<Sheet, WorkbookError> {
398        let mut s = Sheet::new();
399        for ev in self.events_cache.iter().take(n) {
400            apply_to_sheet(&mut s, &ev.event)?;
401        }
402        Ok(s)
403    }
404}
405
406/// Aplica un `SheetEvent` directamente a un `Sheet`. Reusada por
407/// `apply_user_event` (sobre el candidato) y por el replay del log.
408fn apply_to_sheet(sheet: &mut Sheet, event: &SheetEvent) -> Result<SetReport, SetError> {
409    match event {
410        SheetEvent::SetCell { cell, raw } => sheet.set_cell(*cell, raw),
411        SheetEvent::ClearCell { cell } => Ok(sheet.clear_cell(*cell)),
412        SheetEvent::Fill { src, dest } => apply_fill(sheet, *src, *dest),
413        SheetEvent::Restore { cells } => apply_restore(sheet, cells),
414        SheetEvent::SetFormat { cell, format } => {
415            sheet.set_format(*cell, format.clone());
416            // SetFormat no dispara cascada de cómputo (es metadata),
417            // pero igual reportamos la celda como "tocada" para que la
418            // UI sepa que tiene que repintar.
419            let mut rep = SetReport::default();
420            rep.changed
421                .push((*cell, sheet.value(*cell), sheet.value(*cell)));
422            Ok(rep)
423        }
424    }
425}
426
427/// Aplica un batch de restauración. Acumula los SetReport individuales
428/// para que el caller vea TODAS las celdas que cambiaron.
429fn apply_restore(
430    sheet: &mut Sheet,
431    cells: &[(CellRef, Option<String>)],
432) -> Result<SetReport, SetError> {
433    let mut combined = SetReport::default();
434    for (cr, raw) in cells {
435        let rep = match raw {
436            Some(s) => sheet.set_cell(*cr, s)?,
437            None => sheet.clear_cell(*cr),
438        };
439        combined.changed.extend(rep.changed);
440    }
441    Ok(combined)
442}
443
444/// Implementación del fill: lee la celda fuente, shifta su expr por
445/// cada celda destino, persiste el resultado. Se incluye `src` en
446/// `dest` solo si dest lo incluye; si no, el src queda intacto.
447///
448/// Atomicidad: aplicamos uno a uno con `set_cell_expr`. Si una de las
449/// celdas destino cierra un ciclo (caso raro pero posible si la
450/// fórmula se auto-referencia tras shiftar), la celda específica
451/// queda con su valor anterior — las demás siguen aplicándose. La
452/// transacción más amplia (vs. invariantes) la maneja `Workbook`
453/// arriba con candidate-swap.
454fn apply_fill(sheet: &mut Sheet, src: CellRef, dest: CellRange) -> Result<SetReport, SetError> {
455    let src_state = match sheet.cells_get(src) {
456        Some(s) => s,
457        None => {
458            // Sin fuente no hay qué replicar; reporte vacío.
459            return Ok(SetReport::default());
460        }
461    };
462    let src_expr = src_state.expr.clone();
463    let src_raw = src_state.raw.clone();
464    let mut combined = SetReport::default();
465    for target in dest.iter() {
466        if target == src {
467            continue;
468        }
469        let drow = target.row as i32 - src.row as i32;
470        let dcol = target.col as i32 - src.col as i32;
471        let shifted = formula::shift(&src_expr, drow, dcol);
472        let new_raw = build_raw(&src_raw, &shifted);
473        match sheet.set_cell_expr(target, shifted, new_raw) {
474            Ok(rep) => combined.changed.extend(rep.changed),
475            Err(SetError::Cycle(_)) => {
476                // El shift creó un ciclo en esta celda (raro). La
477                // saltamos y seguimos — no rompemos el fill entero.
478            }
479            Err(SetError::Parse(_)) => unreachable!("expr ya parseada"),
480        }
481    }
482    Ok(combined)
483}
484
485/// Reconstruye el raw a partir del expr shifted. Mantiene el prefijo
486/// `=` solo si el raw original lo tenía (literales no llevan `=`).
487fn build_raw(orig_raw: &str, expr: &FormulaExpr) -> String {
488    let rendered = formula::render(expr);
489    if orig_raw.starts_with('=') {
490        format!("={rendered}")
491    } else {
492        rendered
493    }
494}
495
496struct SheetResolver<'a> {
497    sheet: &'a Sheet,
498}
499
500impl<'a> CellResolver for SheetResolver<'a> {
501    fn resolve(&self, cell: CellRef) -> SheetValue {
502        self.sheet.value(cell)
503    }
504}
505
506fn now_ms() -> u128 {
507    std::time::SystemTime::now()
508        .duration_since(std::time::UNIX_EPOCH)
509        .map(|d| d.as_millis())
510        .unwrap_or(0)
511}
512
513#[cfg(test)]
514mod tests {
515    use super::*;
516    use crate::cell::CellRef;
517    use crate::sink::FileSink;
518    use crate::value::SheetError;
519    use rust_decimal::Decimal;
520    use std::io::Cursor;
521    use std::str::FromStr;
522
523    fn cr(s: &str) -> CellRef {
524        s.parse().unwrap()
525    }
526    fn dec(s: &str) -> Decimal {
527        Decimal::from_str(s).unwrap()
528    }
529
530    #[test]
531    fn events_record_in_order() {
532        let mut wb = Workbook::new();
533        wb.set_cell(cr("A1"), "1").unwrap();
534        wb.set_cell(cr("B1"), "=A1+10").unwrap();
535        wb.set_cell(cr("A1"), "5").unwrap();
536        assert_eq!(wb.events().len(), 3);
537        for (i, ev) in wb.events().iter().enumerate() {
538            assert_eq!(ev.seq, i as u64);
539        }
540    }
541
542    #[test]
543    fn replay_reconstructs_state() {
544        let mut wb = Workbook::new();
545        wb.set_cell(cr("A1"), "10").unwrap();
546        wb.set_cell(cr("B1"), "=A1*3").unwrap();
547        wb.set_cell(cr("A1"), "7").unwrap();
548
549        let mut buf = Vec::new();
550        wb.write_log(&mut buf).unwrap();
551        let wb2 = Workbook::from_log(Cursor::new(buf)).unwrap();
552        assert_eq!(wb2.value(cr("A1")), SheetValue::Number(dec("7")));
553        assert_eq!(wb2.value(cr("B1")), SheetValue::Number(dec("21")));
554    }
555
556    #[test]
557    fn snapshot_at_walks_history() {
558        let mut wb = Workbook::new();
559        wb.set_cell(cr("A1"), "1").unwrap(); // seq 0
560        wb.set_cell(cr("A1"), "2").unwrap(); // seq 1
561        wb.set_cell(cr("A1"), "3").unwrap(); // seq 2
562
563        assert_eq!(wb.snapshot_at(0).unwrap().value(cr("A1")), SheetValue::Empty);
564        assert_eq!(
565            wb.snapshot_at(1).unwrap().value(cr("A1")),
566            SheetValue::Number(dec("1"))
567        );
568        assert_eq!(
569            wb.snapshot_at(2).unwrap().value(cr("A1")),
570            SheetValue::Number(dec("2"))
571        );
572        assert_eq!(
573            wb.snapshot_at(3).unwrap().value(cr("A1")),
574            SheetValue::Number(dec("3"))
575        );
576    }
577
578    #[test]
579    fn invariant_blocks_violating_edit() {
580        let mut wb = Workbook::new();
581        wb.set_cell(cr("A1"), "100").unwrap();
582        // Regla: saldo (A1) jamás negativo.
583        wb.add_invariant("saldo_no_negativo", "=A1>=0").unwrap();
584        // Edición OK: A1 = 50.
585        wb.set_cell(cr("A1"), "50").unwrap();
586        assert_eq!(wb.value(cr("A1")), SheetValue::Number(dec("50")));
587        // Edición prohibida: A1 = -10. Debe rechazarse.
588        let err = wb.set_cell(cr("A1"), "-10").unwrap_err();
589        assert!(matches!(err, WorkbookError::InvariantViolated { .. }));
590        // El workbook quedó en el estado anterior intacto.
591        assert_eq!(wb.value(cr("A1")), SheetValue::Number(dec("50")));
592        // Y el evento NO se registró en el log.
593        assert_eq!(wb.events().len(), 2);
594    }
595
596    #[test]
597    fn invariant_evaluates_downstream_sum() {
598        let mut wb = Workbook::new();
599        wb.set_cell(cr("A1"), "10").unwrap();
600        wb.set_cell(cr("A2"), "20").unwrap();
601        wb.set_cell(cr("A3"), "30").unwrap();
602        wb.set_cell(cr("B1"), "=SUM(A1:A3)").unwrap();
603        // Regla: el total nunca > 100.
604        wb.add_invariant("tope_total", "=B1<=100").unwrap();
605        // Permitido: total 70.
606        wb.set_cell(cr("A3"), "40").unwrap();
607        assert_eq!(wb.value(cr("B1")), SheetValue::Number(dec("70")));
608        // Prohibido: total 130.
609        assert!(wb.set_cell(cr("A2"), "80").is_err());
610        // El total sigue siendo 70.
611        assert_eq!(wb.value(cr("B1")), SheetValue::Number(dec("70")));
612    }
613
614    #[test]
615    fn cycle_error_propagates_through_workbook() {
616        let mut wb = Workbook::new();
617        wb.set_cell(cr("A1"), "=B1+1").unwrap();
618        let err = wb.set_cell(cr("B1"), "=A1+1").unwrap_err();
619        assert!(matches!(err, WorkbookError::Set(SetError::Cycle(_))));
620    }
621
622    #[test]
623    fn fill_replicates_formula_shifting_refs() {
624        let mut wb = Workbook::new();
625        // Columna A con cantidades.
626        wb.set_cell(cr("A1"), "10").unwrap();
627        wb.set_cell(cr("A2"), "20").unwrap();
628        wb.set_cell(cr("A3"), "30").unwrap();
629        // B1 = A1 * 2. Fill hasta B3.
630        wb.set_cell(cr("B1"), "=A1*2").unwrap();
631        wb.fill(cr("B1"), "B1:B3".parse().unwrap()).unwrap();
632        assert_eq!(wb.value(cr("B1")), SheetValue::Number(dec("20")));
633        assert_eq!(wb.value(cr("B2")), SheetValue::Number(dec("40")));
634        assert_eq!(wb.value(cr("B3")), SheetValue::Number(dec("60")));
635        // El raw de B2 debe reflejar el shift.
636        assert_eq!(wb.raw(cr("B2")), Some("=A2*2"));
637    }
638
639    #[test]
640    fn fill_respects_dollar_anchors() {
641        let mut wb = Workbook::new();
642        wb.set_cell(cr("A1"), "10").unwrap();
643        wb.set_cell(cr("A2"), "20").unwrap();
644        wb.set_cell(cr("A3"), "30").unwrap();
645        wb.set_cell(cr("C1"), "100").unwrap(); // factor anclado
646        // B1 = A1 * $C$1
647        wb.set_cell(cr("B1"), "=A1*$C$1").unwrap();
648        wb.fill(cr("B1"), "B1:B3".parse().unwrap()).unwrap();
649        assert_eq!(wb.value(cr("B1")), SheetValue::Number(dec("1000")));
650        assert_eq!(wb.value(cr("B2")), SheetValue::Number(dec("2000")));
651        // Verifico que $C$1 no se shifteó.
652        assert_eq!(wb.raw(cr("B3")), Some("=A3*$C$1"));
653    }
654
655    #[test]
656    fn fill_out_of_sheet_produces_ref_error() {
657        let mut wb = Workbook::new();
658        wb.set_cell(cr("B2"), "=A2*2").unwrap();
659        // Fill hacia A2 (drow=0, dcol=-1) → A2 referenciaría col -1 → #REF!
660        wb.fill(cr("B2"), "A2:A2".parse().unwrap()).unwrap();
661        assert_eq!(wb.value(cr("A2")), SheetValue::Error(SheetError::Ref));
662    }
663
664    #[test]
665    fn fill_preserves_src_when_dest_includes_it() {
666        let mut wb = Workbook::new();
667        wb.set_cell(cr("A1"), "5").unwrap();
668        wb.set_cell(cr("B1"), "=A1*10").unwrap();
669        // Fill B1:B3 con B1 dentro del rango: B1 no debe modificarse.
670        let before_raw = wb.raw(cr("B1")).unwrap().to_string();
671        wb.fill(cr("B1"), "B1:B3".parse().unwrap()).unwrap();
672        assert_eq!(wb.raw(cr("B1")).unwrap(), before_raw);
673    }
674
675    #[test]
676    fn copy_cell_is_fill_of_singleton() {
677        let mut wb = Workbook::new();
678        wb.set_cell(cr("A1"), "7").unwrap();
679        wb.set_cell(cr("A2"), "11").unwrap();
680        wb.set_cell(cr("B1"), "=A1+1").unwrap();
681        wb.copy_cell(cr("B1"), cr("B2")).unwrap();
682        assert_eq!(wb.value(cr("B2")), SheetValue::Number(dec("12")));
683        assert_eq!(wb.raw(cr("B2")), Some("=A2+1"));
684    }
685
686    #[test]
687    fn volatile_count_tracks_today_cells() {
688        let mut wb = Workbook::new();
689        wb.set_cell(cr("A1"), "=TODAY()").unwrap();
690        wb.set_cell(cr("A2"), "=RAND()").unwrap();
691        wb.set_cell(cr("A3"), "=A1+1").unwrap(); // no es volátil ella misma
692        assert_eq!(wb.volatile_count(), 2);
693        // Reescribir A1 como literal saca la celda del set volátil.
694        wb.set_cell(cr("A1"), "42").unwrap();
695        assert_eq!(wb.volatile_count(), 1);
696    }
697
698    #[test]
699    fn refresh_volatiles_updates_rand_value() {
700        let mut wb = Workbook::new();
701        wb.set_cell(cr("A1"), "=RAND()").unwrap();
702        let v1 = wb.value(cr("A1"));
703        wb.refresh_volatiles().unwrap();
704        let v2 = wb.value(cr("A1"));
705        // Con PRNG y nanos del reloj, prácticamente seguro que
706        // cambia. Si por mala suerte coincide en un test único,
707        // sigue siendo un Number — el test no se vuelve flaky por
708        // valor, sino por shape.
709        match (v1, v2) {
710            (SheetValue::Number(_), SheetValue::Number(_)) => {}
711            other => panic!("rand no devolvió Number: {other:?}"),
712        }
713    }
714
715    #[test]
716    fn editing_unrelated_cell_recomputes_volatiles() {
717        let mut wb = Workbook::new();
718        wb.set_cell(cr("A1"), "=TODAY()").unwrap();
719        let initial = wb.value(cr("A1"));
720        // Editar B1 (sin dependencia con A1) debe re-evaluar A1.
721        // No comprobamos cambio de valor (el día casi nunca cambia
722        // entre dos llamadas), pero sí que A1 figure en el report.
723        let report = wb.set_cell(cr("B1"), "999").unwrap();
724        let touched: std::collections::HashSet<_> =
725            report.changed.iter().map(|(c, _, _)| *c).collect();
726        // B1 sí cambió de seguro. A1 puede no aparecer si el TODAY no
727        // cambió de valor — eso significa que recompute_volatiles ya
728        // se llamó pero el delta fue cero. Esa es la semántica que
729        // queremos.
730        assert!(touched.contains(&cr("B1")));
731        // Si quería A1 en el report siempre, tendría que cambiar la
732        // semántica de SetReport. Lo que sí garantizo: el valor
733        // sigue siendo un Number (no se quedó Empty por accidente).
734        match wb.value(cr("A1")) {
735            SheetValue::Number(_) => {}
736            other => panic!("A1 perdió su valor: {other:?}"),
737        }
738        let _ = initial;
739    }
740
741    #[test]
742    fn now_includes_subsecond_fraction() {
743        let mut wb = Workbook::new();
744        wb.set_cell(cr("A1"), "=NOW()").unwrap();
745        match wb.value(cr("A1")) {
746            SheetValue::Number(n) => {
747                // El test corre años 2026+ → serial > 20000.
748                assert!(n > dec("20000"));
749            }
750            other => panic!("NOW() no fue Number: {other:?}"),
751        }
752    }
753
754    #[test]
755    fn randbetween_in_range() {
756        let mut wb = Workbook::new();
757        wb.set_cell(cr("A1"), "=RANDBETWEEN(1, 6)").unwrap();
758        for _ in 0..20 {
759            wb.refresh_volatiles().unwrap();
760            match wb.value(cr("A1")) {
761                SheetValue::Number(n) => {
762                    assert!(n >= dec("1") && n <= dec("6"), "out of range: {n}");
763                    assert_eq!(n.fract(), Decimal::ZERO);
764                }
765                other => panic!("RANDBETWEEN no devolvió Number: {other:?}"),
766            }
767        }
768    }
769
770    #[test]
771    fn workbook_with_file_sink_round_trip() {
772        // Sesión 1: edito unas celdas y dejo el archivo cerrado.
773        let mut p = std::env::temp_dir();
774        let pid = std::process::id();
775        let nanos = std::time::SystemTime::now()
776            .duration_since(std::time::UNIX_EPOCH)
777            .map(|d| d.as_nanos())
778            .unwrap_or(0);
779        p.push(format!("nakui-wb-roundtrip-{pid}-{nanos}.jsonl"));
780
781        {
782            let sink = Box::new(FileSink::open(&p).unwrap());
783            let mut wb = Workbook::with_sink(sink).unwrap();
784            wb.set_cell(cr("A1"), "10").unwrap();
785            wb.set_cell(cr("B1"), "=A1*5").unwrap();
786            assert_eq!(wb.value(cr("B1")), SheetValue::Number(dec("50")));
787        }
788
789        // Sesión 2: vuelvo a abrir el mismo archivo y el estado
790        // debe reaparecer intacto.
791        {
792            let sink = Box::new(FileSink::open(&p).unwrap());
793            let wb = Workbook::with_sink(sink).unwrap();
794            assert_eq!(wb.value(cr("A1")), SheetValue::Number(dec("10")));
795            assert_eq!(wb.value(cr("B1")), SheetValue::Number(dec("50")));
796            assert_eq!(wb.events().len(), 2);
797        }
798        let _ = std::fs::remove_file(&p);
799    }
800
801    #[test]
802    fn undo_reverts_last_edit() {
803        let mut wb = Workbook::new();
804        wb.set_cell(cr("A1"), "10").unwrap();
805        wb.set_cell(cr("A1"), "20").unwrap();
806        assert_eq!(wb.value(cr("A1")), SheetValue::Number(dec("20")));
807        assert!(wb.can_undo());
808        wb.undo().unwrap();
809        assert_eq!(wb.value(cr("A1")), SheetValue::Number(dec("10")));
810        wb.undo().unwrap();
811        assert_eq!(wb.value(cr("A1")), SheetValue::Empty);
812    }
813
814    #[test]
815    fn undo_propagates_to_downstream() {
816        let mut wb = Workbook::new();
817        wb.set_cell(cr("A1"), "10").unwrap();
818        wb.set_cell(cr("B1"), "=A1*5").unwrap();
819        wb.set_cell(cr("A1"), "100").unwrap();
820        // B1 = 500.
821        assert_eq!(wb.value(cr("B1")), SheetValue::Number(dec("500")));
822        wb.undo().unwrap();
823        // A1 vuelve a 10, B1 cascada → 50.
824        assert_eq!(wb.value(cr("A1")), SheetValue::Number(dec("10")));
825        assert_eq!(wb.value(cr("B1")), SheetValue::Number(dec("50")));
826    }
827
828    #[test]
829    fn redo_replays_after_undo() {
830        let mut wb = Workbook::new();
831        wb.set_cell(cr("A1"), "10").unwrap();
832        wb.set_cell(cr("A1"), "20").unwrap();
833        wb.undo().unwrap();
834        assert_eq!(wb.value(cr("A1")), SheetValue::Number(dec("10")));
835        assert!(wb.can_redo());
836        wb.redo().unwrap();
837        assert_eq!(wb.value(cr("A1")), SheetValue::Number(dec("20")));
838    }
839
840    #[test]
841    fn editing_clears_redo_stack() {
842        let mut wb = Workbook::new();
843        wb.set_cell(cr("A1"), "10").unwrap();
844        wb.set_cell(cr("A1"), "20").unwrap();
845        wb.undo().unwrap();
846        assert!(wb.can_redo());
847        // Edición nueva tras undo → redo se pierde.
848        wb.set_cell(cr("A1"), "999").unwrap();
849        assert!(!wb.can_redo());
850    }
851
852    #[test]
853    fn undo_on_empty_workbook_is_noop() {
854        let mut wb = Workbook::new();
855        assert!(!wb.can_undo());
856        assert!(wb.undo().unwrap().is_none());
857    }
858
859    #[test]
860    fn undo_of_fill_restores_all_destination_cells() {
861        let mut wb = Workbook::new();
862        wb.set_cell(cr("A1"), "1").unwrap();
863        wb.set_cell(cr("A2"), "2").unwrap();
864        wb.set_cell(cr("A3"), "3").unwrap();
865        // B2 y B3 ya tienen contenido previo distinto.
866        wb.set_cell(cr("B2"), "OLD2").unwrap();
867        wb.set_cell(cr("B3"), "OLD3").unwrap();
868        wb.set_cell(cr("B1"), "=A1*10").unwrap();
869        // Fill: B1 ya está; B2 y B3 reciben =A2*10 y =A3*10.
870        wb.fill(cr("B1"), "B1:B3".parse().unwrap()).unwrap();
871        assert_eq!(wb.value(cr("B2")), SheetValue::Number(dec("20")));
872        assert_eq!(wb.value(cr("B3")), SheetValue::Number(dec("30")));
873        // Undo: B2 y B3 vuelven a OLD2 y OLD3.
874        wb.undo().unwrap();
875        assert_eq!(wb.value(cr("B2")), SheetValue::Text("OLD2".into()));
876        assert_eq!(wb.value(cr("B3")), SheetValue::Text("OLD3".into()));
877        // B1 NO se toca (no estaba en el write-set del Fill,
878        // excluida por ser la fuente).
879        assert_eq!(wb.value(cr("B1")), SheetValue::Number(dec("10")));
880    }
881
882    #[test]
883    fn set_format_changes_display_not_value() {
884        let mut wb = Workbook::new();
885        wb.set_cell(cr("A1"), "1234.5").unwrap();
886        // Sin formato: usa el natural.
887        assert_eq!(wb.formatted(cr("A1")), "1234.5");
888        wb.set_format(
889            cr("A1"),
890            CellFormat::Currency {
891                symbol: "$".into(),
892                decimals: 2,
893            },
894        )
895        .unwrap();
896        // El valor sigue siendo el mismo Decimal:
897        assert_eq!(wb.value(cr("A1")), SheetValue::Number(dec("1234.5")));
898        // ...pero el display cambió:
899        assert_eq!(wb.formatted(cr("A1")), "$1,234.50");
900    }
901
902    #[test]
903    fn format_persists_through_edits() {
904        let mut wb = Workbook::new();
905        wb.set_cell(cr("A1"), "10").unwrap();
906        wb.set_format(cr("A1"), CellFormat::Percent { decimals: 0 })
907            .unwrap();
908        // Reescribimos el valor. El formato debe sobrevivir.
909        wb.set_cell(cr("A1"), "0.25").unwrap();
910        assert_eq!(wb.format(cr("A1")), CellFormat::Percent { decimals: 0 });
911        assert_eq!(wb.formatted(cr("A1")), "25%");
912    }
913
914    #[test]
915    fn undo_undo_redo_redo_round_trip() {
916        let mut wb = Workbook::new();
917        wb.set_cell(cr("A1"), "1").unwrap();
918        wb.set_cell(cr("A1"), "2").unwrap();
919        wb.set_cell(cr("A1"), "3").unwrap();
920        wb.undo().unwrap();
921        wb.undo().unwrap();
922        assert_eq!(wb.value(cr("A1")), SheetValue::Number(dec("1")));
923        wb.redo().unwrap();
924        wb.redo().unwrap();
925        assert_eq!(wb.value(cr("A1")), SheetValue::Number(dec("3")));
926    }
927
928    #[test]
929    fn out_of_order_log_rejected() {
930        let mut wb = Workbook::new();
931        wb.set_cell(cr("A1"), "1").unwrap();
932        wb.set_cell(cr("A1"), "2").unwrap();
933        // Manipulación maliciosa del log: invertimos los eventos.
934        let mut wb2_events = wb.events().to_vec();
935        wb2_events.reverse();
936        let mut buf = Vec::new();
937        for ev in &wb2_events {
938            serde_json::to_writer(&mut buf, ev).unwrap();
939            buf.push(b'\n');
940        }
941        let err = Workbook::from_log(Cursor::new(buf)).unwrap_err();
942        // Tras refactorizar a EventSink, el out-of-order lo detecta
943        // MemorySink::from_reader → WorkbookError::Sink(Skew{..}).
944        assert!(
945            matches!(err, WorkbookError::Sink(SinkError::Skew { .. })),
946            "got: {err:?}"
947        );
948    }
949}