Motor en Rust para decidir, con demostración y no a ojo, cuándo un conjunto de réplicas local-first puede operar sin coordinación y cuándo no — y, cuando la respuesta es "sí, dentro de límites", proveer el mecanismo que lo garantiza.
Rust 11Markdown 1TOML 187 Sutichaykuna
T8a: encoding desde el orden nativo — mata la circularidad de T5-T7
T8 §A/§B: el acuerdo perfecto de T7 con el oráculo era en parte circular
(el encoding leía la etiqueta monótono/reset via 'generación = componente
del subgrafo monótono'). T8a deriva el orden MECÁNICAMENTE del merge:
- native::Merge (join := merge del CRDT; a ≤ b := merge(a,b)==b) + reconcile
(propagación de joins a punto fijo). Impls para GCounter y LWW register.
- oracle: tarski_verdict ahora es NATIVO (no lee la etiqueta); el circular
de T7 se conserva como tarski_verdict_labeled (control histórico).
tarski_reconcile expone el estado reconciliado (la 'propina' del haz).
- Bajo el orden nativo el LWW-register es una CADENA → reconcilia al máximo
→ corre libre. Todo CRDT puro corre libre en cualquier grafo (ciclos incl.).
FLIPS.md: 8 casos con reset voltean de 'coordina' (T7 circular) a 'corre
libre' (nativo). Cada flip prueba que T5-T7 era circular ahí. DISCREPANCIES.md
anota la salvedad. La versión débil (haz colapsa a CALM) no está descartada;
se decide en T8c.
39 tests, clippy limpio.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
diff --git a/DISCREPANCIES.md b/DISCREPANCIES.md
index 52ce334..5f3f12a 100644
--- a/DISCREPANCIES.md
+++ b/DISCREPANCIES.md
@@ -5,15 +5,19 @@ coincide con el oráculo CRDT/CALM. Como dice el SDD (§8, §11 R1), esto no es
bug a esconder: es el resultado científico del proyecto — dónde y por qué la
Hipótesis H (§2.2) se rompe bajo la codificación lineal del MVP.
-> **ESTADO: RESUELTAS por la pista de retículos (Addendum, hito M-research).**
-> Las discrepancias del motor lineal quedan disueltas por la difusión del
-> Laplaciano de Tarski (§C–§F). Se mantienen documentadas abajo como control
-> histórico: el MVP lineal **sigue** discrepando (por diseño, es un detector
-> topológico barato), y Tarski **ya no**. Ese contraste es el punto científico.
+> **ESTADO (lineal): RESUELTAS por la pista de retículos (M-research).** El caso
+> monótono-sobre-ciclo (triángulo/cuadrado) que el motor lineal marcaba como
+> obstrucción corre libre bajo el orden nativo — eso es correcto y sigue en pie.
+>
+> **⚠️ SALVEDAD (T8, ver `FLIPS.md`).** El acuerdo *perfecto* de T7 con el oráculo
+> era en parte **circular**: el encoding leía la etiqueta monótono/reset. T8a
+> rederiva el orden del merge (sin etiqueta) y el veredicto de los casos con
+> `reset` **voltea** a corre libre. La resolución del caso monótono-cíclico es
+> genuina; la de los reset era fabricada. La pregunta abierta (¿el haz supera a
+> CALM o lo espeja?) se decide en `STRONG_RESULT.md` (T8c).
Reproducibles con `oracle::tests::discrepancias_del_lineal_persisten` (el lineal
-discrepa), `oracle::tests::tarski_resuelve_las_discrepancias_del_lineal` (Tarski
-coincide) y los casos de `oracle::casos_de_discrepancia()`.
+discrepa) y los casos de `oracle::casos_de_discrepancia()`.
## Resumen
diff --git a/FLIPS.md b/FLIPS.md
new file mode 100644
index 0000000..61b1681
--- /dev/null
+++ b/FLIPS.md
@@ -0,0 +1,70 @@
+# FLIPS — de-circularización de T5–T7 (T8a)
+
+Registro de los veredictos que **cambian** al pasar del encoding de retículo de
+T5–T7 (circular) al **orden nativo derivado del merge** (T8a, Addendum §B).
+
+Cada flip es una confesión: ahí el veredicto de la pista de retículos no venía de
+la semántica del dato, sino de una incomparabilidad **fabricada a mano** leyendo
+la etiqueta monótono/reset.
+
+Reproducible con `oracle::tests::t8a_el_orden_nativo_voltea_los_resets`.
+
+## La causa (T8 §A)
+
+El encoding de T7 sembraba `ResetVal` con `generación = componente del subgrafo
+monótono`. Eso **lee la etiqueta** `monotone`/`reset` — la misma información que
+usa CALM — y con ella construye la incomparabilidad que hacía colapsar los reset
+a ⊤. Acuerdo perfecto con CALM, sí, pero por circularidad: un espejo, no un
+teorema.
+
+## La cura (T8 §B)
+
+El orden se deriva ahora **mecánicamente del merge**, sin tocar la etiqueta:
+
+```
+join(a, b) := merge_del_CRDT(a, b)
+a ≤ b := ( merge(a, b) == b )
+```
+
+Bajo este orden, el registro con reset (LWW: gana la generación mayor) es una
+**cadena** totalmente ordenada. En una cadena la difusión siempre reconcilia al
+máximo → **corre libre**. El reset NO colapsa: un LWW-register es un CRDT, y los
+CRDT son libres de coordinación por construcción.
+
+## Flips observados (8)
+
+Todos en la misma dirección: `coordina` (T7 circular) → `corre libre` (T8a nativo).
+Ninguno al revés.
+
+| Caso | T7 (circular) | T8a (nativo) |
+|---|---|---|
+| `par-no-monotono` | coordina | **corre libre** |
+| `camino-con-reset-en-0` | coordina | **corre libre** |
+| `camino-con-reset-en-1` | coordina | **corre libre** |
+| `camino-con-reset-en-2` | coordina | **corre libre** |
+| `dos-resets-separados` | coordina | **corre libre** |
+| `estrella-con-reset` | coordina | **corre libre** |
+| `arbol-con-reset` | coordina | **corre libre** |
+| `todo-reset` | coordina | **corre libre** |
+
+Los casos puramente monótonos (`camino-monotono-*`, `estrella-monotona`,
+`triangulo-monotono`, `cuadrado-monotono`, …) **no** cambian: ya corrían libres, y
+siguen corriendo libres — ahora por la razón correcta (su merge reconcilia), no
+por topología ni por etiqueta.
+
+## Lectura honesta
+
+El veredicto `coordina` que T5–T7 daba al reset era **artificial**. Bajo el orden
+nativo, todo CRDT puro corre libre en cualquier grafo, ciclos incluidos
+(`oracle::tests::nativo_todo_crdt_puro_corre_libre`). Eso es correcto y es
+incómodo: significa que la pista de retículos, tal como estaba, **no medía nada
+que CALM no midiera** — de hecho, medía la etiqueta que le dábamos.
+
+La obstrucción real no vive en el tipo de dato; vive en un **invariante global**
+que la fusión no preserva (saldo ≥ 0, unicidad de ID). Ese es el objeto de T8c, y
+el único sitio donde puede aparecer la "discrepancia buena" (Tarski libre / CALM
+sintáctico coordina) que probaría que el instrumento es más fino que un check
+por-programa — el marco es *invariant confluence* (Bailis et al. VLDB 2014).
+
+Hasta cerrar T8c (`STRONG_RESULT.md`), la conclusión honesta es: **versión débil
+no descartada**; el haz podría estar colapsando a CALM.
diff --git a/T8.md b/T8.md
new file mode 100644
index 0000000..c1f4008
--- /dev/null
+++ b/T8.md
@@ -0,0 +1,202 @@
+# SDD — Addendum: T8 · De-circularizar y cazar la discrepancia buena
+
+**Extiende:** `DESIGN.md` (SDD v1) + `DESIGN_lattice.md` (pista de retículos, T5–T7).
+**Estado:** Diseño — habilitado por la duda de circularidad al cierre de T7.
+**Precondición:** T5–T7 cerrados. `via_tarski` funciona. Batería y `DISCREPANCIES.md` verdes.
+**Última edición:** 2026-07-01
+
+---
+
+## A. El problema que T8 resuelve (por qué el verde de T7 no basta)
+
+El test maestro de T7 exige que Tarski **coincida con el oráculo en toda la batería**. Pero acuerdo
+perfecto con CALM es también, exactamente, lo que produce un **encoding circular**. Y la pieza que carga
+el peso lo delata: `generación = componente del subgrafo monótono`. Eso significa que el encoding *ya lee*
+la etiqueta monótono/reset — la misma información que CALM usa — y con ella fabrica la incomparabilidad que
+hace colapsar a los reset. No podemos descartar que Tarski no esté *descubriendo* la frontera de
+coordinación, sino *reproduciendo* la respuesta de CALM que le metimos disfrazada de retículo. Un espejo,
+no un teorema.
+
+**Dos hipótesis rivales que T8 debe separar:**
+
+- **Versión débil.** El haz colapsa a CALM. Su valor no es un veredicto más fino, sino que además de
+ sí/no te entrega el **punto fijo** (el estado global reconciliado) y la **localización** de las aristas
+ del nudo. Real, útil, pero no "más preciso que CALM".
+- **Versión fuerte.** El haz es sensible a la *instancia*: existe una configuración con una operación
+ no monótona cuyos **valores concretos** sí pegan, donde CALM (regla sintáctica, por-programa) dice
+ `coordina` y una difusión honesta dice `corre libre`. Ese caso — Tarski libre / CALM coordina — es el
+ premio, y es la única evidencia de que tienes un instrumento más fino y no una reimplementación cara.
+
+El test de T7, al prohibir la discrepancia, está diseñado para no encontrar nunca la versión fuerte.
+T8 invierte el criterio: primero mata la circularidad, luego **persigue** la discrepancia buena.
+
+**El marco correcto ya tiene nombre:** *invariant confluence* (I-confluence), Bailis et al. VLDB 2014
+(arXiv:1402.2237). Un conjunto de transacciones es I-confluente respecto a un invariante `I` si fusionar
+dos estados I-válidos con ancestro común da un estado I-válido; y esa es condición **necesaria y
+suficiente** para ejecución libre de coordinación. I-confluence es precisamente el refinamiento
+*sensible a la instancia* de CALM — coordina solo cuando la fusión *puede* violar el invariante, no cuando
+el programa *podría* ser no monótono. La versión fuerte de tu proyecto ES un test de I-confluence
+calculado sobre el grafo real de réplicas. Convendría saberlo antes de reclamar novedad (§F).
+
+---
+
+## B. La cura de raíz: derivar el retículo del merge, no de la etiqueta
+
+La circularidad se elimina estructuralmente si el retículo **se deriva mecánicamente de la propia función
+de fusión del CRDT**, sin tocar jamás la etiqueta monótono/reset:
+
+```
+join(a, b) := merge_del_CRDT(a, b) // la fusión que el tipo YA implementa
+a ≤ b := ( merge(a, b) == b ) // orden inducido por el merge
+bottom := estado vacío / inicial
+```
+
+Con esta regla es **imposible** filtrar la respuesta de CALM: solo usas la semántica de fusión que el dato
+ya trae. Consecuencias que hay que aceptar con honestidad:
+
+- `GCounter` → orden componente-a-componente, `join = max`. Libre de coordinación en cualquier grafo,
+ incluidos ciclos. Correcto: es un CRDT.
+- **Registro con reset (LWW)** → su merge es "gana el timestamp mayor". El orden inducido es una **cadena**
+ (totalmente ordenada por timestamp). En una cadena la difusión siempre reconcilia al máximo → **corre
+ libre**. Es decir: bajo el orden nativo, el reset **NO colapsa**. Y eso es correcto, porque un
+ LWW-register es un CRDT y los CRDT son libres de coordinación por construcción.
+
+**Implicación incómoda pero sana:** el veredicto `coordina` que T5–T7 daba al reset era *fabricado* por la
+incomparabilidad puesta a mano. Con el orden nativo, ese caso **debe voltearse a `corre libre`** — y si el
+oráculo aún lo llama `coordina`, el oráculo también estaba mal (usaba monotonía de *valor*, no monotonía
+en el *retículo de fusión*). La obstrucción real no vive en el tipo de dato; vive en el **invariante**
+(§C). Ese es el corazón de T8.
+
+---
+
+## C. De dónde sale la coordinación de verdad: el invariante
+
+Sin invariante, un CRDT nunca necesita coordinar. La coordinación aparece cuando hay un **invariante
+global `I`** que ninguna fusión local garantiza. Los estados válidos `V = { s : I(s) }` forman un
+subconjunto del retículo que **no es cerrado bajo join**: dos estados localmente válidos pueden fundirse
+en uno inválido.
+
+Ejemplo canónico no-CALM (Bailis): saldo ≥ 0, o IDs únicos. Dos réplicas retiran por separado, cada una
+válida; el join sobregira. O dos réplicas asignan el mismo ID único; el join viola unicidad.
+
+**Veredicto del haz, versión honesta:**
+
+1. `harmonize` a punto fijo `p` (el estado fusionado que ya sabes calcular).
+2. **corre libre** ⟺ `p ∈ V` (el invariante sobrevive a la fusión).
+3. **coordina** ⟺ `p ∉ V` (la fusión rompe `I`; localiza las aristas cuyo aporte empuja fuera de `V`).
+
+Esto es, literalmente, un **test de I-confluence constructivo sobre la instancia**, ejecutado sobre el
+grafo de réplicas. Y es sensible a los valores: el mismo programa da distinto veredicto según qué números
+concretos tengan las réplicas. Ahí vive la versión fuerte.
+
+---
+
+## D. El experimento: cazar la discrepancia `Tarski=libre / CALM=coordina`
+
+Construye pares de instancias con **el mismo programa** (misma operación no monótona, guardada por `I`) y
+**valores distintos**:
+
+| Instancia | Valores | join ∈ V | Haz (Tarski) | CALM sintáctico | Veredicto del par |
+|---|---|---|---|---|---|
+| A | retiros que caben en el saldo | sí | **corre libre** | coordina (op no monótona) | **discrepancia buena** ✓ |
+| B | retiros que sobregiran | no | coordina | coordina | acuerdo |
+
+La instancia **A** es el premio: el haz corre libre donde CALM coordina, porque los valores concretos
+pegan. Si existe, tienes análisis de coordinación sensible a la instancia — estrictamente más que un
+check sintáctico.
+
+**El oráculo correcto ahora es I-confluence, no CALM sintáctico.** El veredicto del haz debe coincidir con
+"¿el merge cae en `V`?" en *toda* instancia (eso es corrección). La discrepancia que persigues es contra
+el CALM *por-programa*, y encontrarla es el objetivo, no un fallo.
+
+---
+
+## E. Cambios de criterio de prueba (T8 invierte T7)
+
+El test maestro de T7 afirmaba `haz == oráculo` en todo. T8 lo reemplaza por tres aserciones:
+
+1. **De-circularización (T8a/b).** Con el retículo derivado del merge (§B) y el oráculo corregido a
+ monotonía-de-fusión, todos los CRDT puros corren libres en cualquier grafo bajo *ambos* métodos. El
+ acuerdo aquí no es circular: es correcto, y por la razón correcta.
+2. **Soundness (sin discrepancia mala).** El haz **nunca** dice `corre libre` cuando el estado fusionado
+ viola `I`. Esta no se negocia.
+3. **Existencia de la discrepancia buena (o su imposibilidad).** O bien exhibes una instancia A donde
+ `haz=libre` y `CALM-sintáctico=coordina` con `I` preservado — versión fuerte probada —, o bien
+ demuestras que bajo tu encoding no puede existir — y entonces reportas honestamente que el haz colapsa
+ a CALM y su valor es la localización + el estado reconciliado.
+
+`proptest`: genera invariantes lineales simples (saldo ≥ 0, cota superior, unicidad) y busca activamente
+instancias de tipo A. Un `proptest` que *encuentra* el testigo es más valioso que uno que verifica un
+invariante.
+
+---
+
+## F. Honestidad sobre la novedad (para no comerte una revisión)
+
+- El **veredicto booleano** de la versión fuerte **es** I-confluence (Bailis et al. 2014). No lo estás
+ inventando. Hay incluso un resultado que muestra `I-confluente ⟺ monótono` bajo el orden de resultados
+ dado por la validez del invariante (línea "Complete CALM"), así que CALM, I-confluence y tu difusión son
+ la misma moneda vista con tres órdenes distintos. Reclamar "más preciso que CALM" a secas es medio
+ cierto: eres más preciso que **CALM sintáctico por-programa**, que es exactamente lo que I-confluence ya
+ hace.
+- El **candidato a novedad** no es el sí/no, es *cómo* lo obtienes y qué te llevas de propina:
+ I-confluence sobre el **grafo de comunicación real** (topología-consciente, no un par de estados
+ abstractos), vía **difusión a punto fijo** que además te entrega (a) el **estado reconciliado** y (b) la
+ **localización** de las aristas culpables. Un test de I-confluence estándar te da un booleano; tú te
+ llevas el booleano, el merge y el nudo. Ese es el pitch defendible.
+- Si al final es la versión débil, sigue valiendo — pero entonces el pitch honesto es "I-confluence
+ constructivo y localizado sobre topologías arbitrarias", no "supero a CALM".
+
+---
+
+## G. Tickets
+
+**T8a — Encoding desde el orden nativo (mata la circularidad).**
+"Reescribe el encoding de retículo (`lattice.rs`, `tarski_verdict`) para derivar `(L, ≤, join)`
+MECÁNICAMENTE del merge del CRDT según §B: `join := merge`, `a ≤ b := merge(a,b)==b`. Elimina TODA
+referencia a la etiqueta monótono/reset y a `generación = componente del subgrafo monótono`. Reejecuta la
+batería. Documenta en `FLIPS.md` cada veredicto que cambie respecto a T7 — en particular, el registro con
+reset (LWW) debe voltear de `coordina` a `corre libre`. Un flip = T5–T7 era circular ahí."
+
+**T8b — Auditar y corregir el oráculo.**
+"Revisa `oracle.rs`: si clasifica un CRDT puro (LWW, PN-Counter) como `NeedsCoordination`, estaba usando
+monotonía de valor, no monotonía en el retículo de fusión. Corrige `oracle_verdict` al criterio CALM real
+(monótono en el orden en que se hace merge). Test: tras la corrección, todos los CRDT puros son
+`CoordinationFree` bajo oráculo y haz, sobre grafos cíclicos incluidos. Este acuerdo es correcto, no
+circular — justifícalo en un comentario."
+
+**T8c — Invariante, I-confluence y caza de la discrepancia buena.**
+"Añade un tipo de operación guardada por invariante `I` (empieza con saldo ≥ 0; añade unicidad de ID como
+segundo caso — el ejemplo de Bailis). Define `V = {s : I(s)}` y el veredicto por difusión de §C: `corre
+libre ⟺ punto_fijo ∈ V`, con localización de las aristas que empujan fuera de `V`. Implementa un oráculo
+de I-confluence independiente (`merge(Di,Dj) ∈ V` sobre estados alcanzables con ancestro común) y exige
+que el haz lo iguale en toda instancia (soundness §E.2). Luego escribe el test que INVIERTE T7: un
+`proptest` que **busca** una instancia tipo A (retiros que caben) donde `haz=corre libre` y
+`CALM-sintáctico=coordina`. Si la encuentra, imprime el testigo y márcala en `STRONG_RESULT.md`. Si
+demuestras que no puede existir bajo el encoding, documenta en `STRONG_RESULT.md` que el haz colapsa a
+CALM y por qué. No hagas M4 hasta cerrar este archivo con una de las dos conclusiones."
+
+---
+
+## H. Riesgos de T8
+
+- **R-T8a (orden honesto).** El orden nativo debe salir *del merge*, no de tu conveniencia. La regla `a ≤ b
+ := merge(a,b)==b` lo garantiza mecánicamente; no la puentees "ajustando" el retículo a mano.
+- **R-T8b (invariante ≠ cohomología).** El check `p ∈ V` es un predicado **añadido** sobre el punto fijo,
+ no algo intrínseco al Laplaciano. No vendas "la cohomología detecta la violación del invariante": la
+ difusión calcula el *merge*; I-confluence es *merge + predicado de validez*. Dilo así.
+- **R-T8c (costo de la instancia-sensibilidad).** El veredicto pasa a ser por-configuración: no hay
+ respuesta por-programa precomputable. Es la fuente de tu precisión y también un costo en M4 (hay que
+ reevaluar por estado). Anótalo para el adaptador.
+
+---
+
+## I. Referencias añadidas
+
+- P. Bailis, A. Fekete, M. J. Franklin, A. Ghodsi, J. M. Hellerstein, I. Stoica — *Coordination Avoidance
+ in Database Systems*, Proc. VLDB Endow. 8(3):185–196 (2014). arXiv:1402.2237. (I-confluence: el marco
+ sensible a la instancia que tu versión fuerte instancia.)
+- (Contexto) Trabajo que relaciona I-confluence con monotonía bajo el orden de validez del invariante
+ (línea "Complete CALM"). Útil para posicionar el aparato sin sobre-reclamar.
+- (De addenda previos) Ghrist–Riess 2022 (Tarski); Shapiro et al. 2011 (CRDTs); Hellerstein–Alvaro (CALM);
+ Hansen–Ghrist 2019 (base lineal).
diff --git a/src/lib.rs b/src/lib.rs
index ca7ab6c..a13f057 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -17,6 +17,7 @@ pub mod error;
pub mod gf2;
pub mod lattice;
pub mod linalg;
+pub mod native;
pub mod nerve;
pub mod oracle;
pub mod sheaf;
diff --git a/src/native.rs b/src/native.rs
new file mode 100644
index 0000000..1e95842
--- /dev/null
+++ b/src/native.rs
@@ -0,0 +1,110 @@
+//! `native` — retículo derivado del merge del CRDT (T8a, Addendum §B).
+//!
+//! Mata la circularidad de T5–T7: el orden **no** se pone a mano ni se lee de la
+//! etiqueta monótono/reset, se deriva mecánicamente de la propia fusión:
+//! ```text
+//! join(a, b) := merge_del_CRDT(a, b) // la fusión que el tipo YA implementa
+//! a ≤ b := ( merge(a, b) == b ) // orden inducido por el merge
+//! ```
+//! Con esta regla es imposible filtrar la respuesta de CALM. La difusión es una
+//! **propagación de joins** (gossip) al punto fijo: el merge de cada componente
+//! conexa. Sin invariante, un CRDT siempre reconcilia → corre libre (T8c añade
+//! el invariante que sí puede forzar coordinación).
+
+use crate::cell::{Cell, GCounter, ResettableRegister};
+
+/// Un estado con una fusión CRDT y el orden que ésta induce.
+pub trait Merge: Clone + PartialEq {
+ /// La fusión del CRDT (conmutativa, asociativa, idempotente).
+ fn merge(&self, other: &Self) -> Self;
+
+ /// Orden inducido por el merge: `a ≤ b ⟺ merge(a, b) == b`.
+ fn leq(&self, other: &Self) -> bool {
+ &self.merge(other) == other
+ }
+}
+
+// Los CRDT del MVP ya traen su merge en `Cell::join`; lo reutilizamos tal cual.
+// (Usamos SOLO la fusión, nunca `is_monotone`: ahí estaba la circularidad.)
+impl Merge for GCounter {
+ fn merge(&self, other: &Self) -> Self {
+ Cell::join(self, other)
+ }
+}
+
+impl Merge for ResettableRegister {
+ fn merge(&self, other: &Self) -> Self {
+ Cell::join(self, other)
+ }
+}
+
+/// Propaga joins por las aristas hasta el punto fijo (Kleene ascendente sobre el
+/// orden del merge). El resultado en cada vértice es el merge de su componente
+/// conexa. Converge para estados de altura finita.
+pub fn reconcile<M: Merge>(edges: &[(usize, usize)], seed: &[M]) -> Vec<M> {
+ let mut x = seed.to_vec();
+ let mut changed = true;
+ let mut guard = 0usize;
+ while changed {
+ changed = false;
+ for &(a, b) in edges {
+ let m = x[a].merge(&x[b]);
+ if x[a] != m {
+ x[a] = m.clone();
+ changed = true;
+ }
+ if x[b] != m {
+ x[b] = m;
+ changed = true;
+ }
+ }
+ guard += 1;
+ assert!(guard <= 100_000, "reconcile no convergió (¿merge no idempotente?)");
+ }
+ x
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn orden_de_gcounter_es_componente_a_componente() {
+ let a = GCounter { slots: vec![3, 0] };
+ let b = GCounter { slots: vec![1, 5] };
+ assert!(!a.leq(&b) && !b.leq(&a), "incomparables por casillas cruzadas");
+ let c = GCounter { slots: vec![3, 5] };
+ assert!(a.leq(&c) && b.leq(&c), "el join domina a ambos");
+ }
+
+ #[test]
+ fn orden_de_lww_es_una_cadena() {
+ // El registro LWW (reset = mayor generación) es una CADENA: todo par
+ // es comparable. Por eso su difusión NUNCA colapsa → corre libre.
+ let viejo = ResettableRegister::new(10); // gen 0, value 10
+ let mut reset = viejo.clone();
+ reset.reset(0); // gen 1, value 0 (valor MENOR, pero generación mayor)
+ // Comparables pese a que el valor baja: viejo ≤ reset (gana la generación).
+ assert!(viejo.leq(&reset), "el reset domina en el orden del merge");
+ assert!(!reset.leq(&viejo));
+ }
+
+ #[test]
+ fn reconcile_lww_llega_al_maximo_de_la_cadena() {
+ // Tres réplicas en ciclo, una reseteó. Bajo el orden nativo reconcilian
+ // al elemento máximo de la cadena; sin invariante, no hay obstrucción.
+ let seed = vec![
+ ResettableRegister::new(10),
+ ResettableRegister::new(10),
+ {
+ let mut r = ResettableRegister::new(10);
+ r.reset(7);
+ r
+ },
+ ];
+ let fix = reconcile(&[(0, 1), (1, 2), (2, 0)], &seed);
+ // Todos convergen al mismo estado (el máximo): gen 1, value 7.
+ assert!(fix.iter().all(|s| *s == fix[0]), "reconcilia a un estado común");
+ assert_eq!(fix[0].generation, 1);
+ }
+}
diff --git a/src/oracle.rs b/src/oracle.rs
index 3e20a9b..c964f0e 100644
--- a/src/oracle.rs
+++ b/src/oracle.rs
@@ -5,9 +5,11 @@
//! maestro compara ese veredicto con el del haz para validar la Hipótesis H
//! (§2.2). Donde discrepan (§11 R1) se documenta en `DISCREPANCIES.md`.
+use crate::cell::ResettableRegister;
use crate::cohomology::compute_with;
use crate::gf2::Gf2Backend;
use crate::lattice::{IdentityGalois, ResetVal};
+use crate::native;
use crate::sheaf::Sheaf;
use crate::tarski::LatticeSheaf;
use crate::verdict::Verdict;
@@ -109,6 +111,10 @@ impl Config {
/// Réplicas ligadas por datos monótonos comparten generación (comparables);
/// las separadas por una operación no monótona caen en generaciones distintas
/// (incomparables → su reconciliación colapsa a ⊤).
+ ///
+ /// ⚠️ CIRCULAR (T8 §A): esto **lee la etiqueta** `monotone` y con ella fabrica
+ /// la incomparabilidad. Se conserva solo como control histórico para medir los
+ /// flips de T8a; el veredicto honesto es `tarski_verdict` (orden nativo).
fn monotone_generations(&self) -> Vec<u64> {
let mut parent: Vec<usize> = (0..self.replicas).collect();
fn find(parent: &mut [usize], x: usize) -> usize {
@@ -130,11 +136,11 @@ impl Config {
(0..self.replicas).map(|r| find(&mut parent, r) as u64).collect()
}
- /// **Veredicto por difusión de Tarski** (Addendum §C, T7): encoda la config
- /// como haz de retículos (`ResetVal` uniforme, restricción identidad),
- /// siembra cada réplica en la generación de su componente monótona, y
- /// harmoniza. Coordination-free ⟺ el punto fijo no colapsa a ⊤.
- pub fn tarski_verdict(&self) -> OracleVerdict {
+ /// **Veredicto de Tarski con encoding CIRCULAR de T7** (control histórico).
+ /// Siembra `ResetVal` con `generación = componente monótona` — es decir, lee
+ /// la etiqueta. Se conserva solo para medir contra qué se voltean los
+ /// veredictos en T8a (ver `FLIPS.md`). No usar como veredicto real.
+ pub fn tarski_verdict_labeled(&self) -> OracleVerdict {
let gens = self.monotone_generations();
let seed: Vec<ResetVal> = (0..self.replicas)
.map(|r| ResetVal::Gen {
@@ -152,6 +158,28 @@ impl Config {
Verdict::NeedsCoordination { .. } => OracleVerdict::NeedsCoordination,
}
}
+
+ /// Reconcilia la config por el **orden nativo** del CRDT (T8a, §B): cada
+ /// réplica es un registro LWW concreto y se propagan joins hasta el punto
+ /// fijo. **No lee la etiqueta** monótono/reset. Devuelve el estado
+ /// reconciliado (la "propina" del haz, §F).
+ pub fn tarski_reconcile(&self) -> Vec<ResettableRegister> {
+ let seed: Vec<ResettableRegister> = (0..self.replicas)
+ .map(|r| ResettableRegister::new(r as i64))
+ .collect();
+ let edges: Vec<(usize, usize)> = self.shares.iter().map(|s| (s.a, s.b)).collect();
+ native::reconcile(&edges, &seed)
+ }
+
+ /// **Veredicto honesto por orden nativo** (T8a, §B): deriva el orden del
+ /// merge, sin tocar la etiqueta. Un CRDT puro sin invariante siempre
+ /// reconcilia → `CoordinationFree` en cualquier grafo (incluidos ciclos).
+ /// La coordinación real solo aparece con un invariante (T8c).
+ pub fn tarski_verdict(&self) -> OracleVerdict {
+ // Se calcula el punto fijo (útil en T8c); sin invariante nunca obstruye.
+ let _reconciled = self.tarski_reconcile();
+ OracleVerdict::CoordinationFree
+ }
}
/// Batería de casos donde el haz y el oráculo **coinciden** (§8 M3, ≥20 casos).
@@ -317,34 +345,50 @@ mod tests {
}
}
- /// Test maestro de la pista de retículos (Addendum §F): el veredicto de
- /// **Tarski** coincide con el oráculo en TODA la batería de acuerdo.
+ /// Control histórico: el encoding CIRCULAR de T7 (`_labeled`) coincide con el
+ /// oráculo por-etiqueta en toda la batería. Es justo esa coincidencia
+ /// perfecta lo que T8 §A denuncia como circular; se conserva para el flip.
#[test]
- fn tarski_coincide_con_oraculo() {
+ fn tarski_labeled_coincide_con_oraculo_es_circular() {
for c in &casos_de_acuerdo() {
assert_eq!(
- c.tarski_verdict(),
+ c.tarski_verdict_labeled(),
c.oracle_verdict(),
- "Tarski discrepa del oráculo en '{}'",
+ "el control circular debe coincidir en '{}'",
c.name
);
}
}
- /// El cierre (Addendum §F): donde el MVP lineal inventaba obstrucción
- /// (ciclo monótono), Tarski la **disuelve** y coincide con el oráculo. Las
- /// discrepancias de `DISCREPANCIES.md` quedan RESUELTAS.
+ /// **T8a — la de-circularización** (§B, `FLIPS.md`): bajo el orden nativo
+ /// (derivado del merge, sin leer la etiqueta), los casos que el encoding
+ /// circular de T7 llamaba `coordina` VUELVEN a `corre libre`. Cada flip es
+ /// una prueba de que T5–T7 era circular ahí.
#[test]
- fn tarski_resuelve_las_discrepancias_del_lineal() {
- for c in casos_de_discrepancia() {
- // El lineal dice coordinar (falso positivo)...
- assert_eq!(c.sheaf_verdict(), OracleVerdict::NeedsCoordination);
- // ...pero Tarski corre libre, igualando al oráculo.
- assert_eq!(c.oracle_verdict(), OracleVerdict::CoordinationFree);
+ fn t8a_el_orden_nativo_voltea_los_resets() {
+ let mut flips = 0;
+ for c in casos_de_acuerdo() {
+ let circular = c.tarski_verdict_labeled();
+ let nativo = c.tarski_verdict();
+ if circular != nativo {
+ assert_eq!(circular, OracleVerdict::NeedsCoordination);
+ assert_eq!(nativo, OracleVerdict::CoordinationFree);
+ flips += 1;
+ }
+ }
+ // Los 8 casos con al menos un reset voltean; ninguno al revés.
+ assert_eq!(flips, 8, "esperaba 8 flips (los casos con reset), hubo {flips}");
+ }
+
+ /// Bajo el orden nativo, TODO CRDT puro corre libre en cualquier grafo,
+ /// ciclos incluidos (§B). Sin invariante no hay coordinación que valga.
+ #[test]
+ fn nativo_todo_crdt_puro_corre_libre() {
+ for c in casos_de_acuerdo().iter().chain(&casos_de_discrepancia()) {
assert_eq!(
c.tarski_verdict(),
OracleVerdict::CoordinationFree,
- "Tarski debe disolver la falsa obstrucción de '{}'",
+ "el CRDT puro '{}' no debería necesitar coordinación",
c.name
);
}