tawasuyu

tawasuyu/hifas

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 Definitions

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>

b666de794e8bd2a30e43a46f9c19249964c82836 · by Sergio · (3 months ago) · parent 67c1eb776 · browse the tree at this commit

Semantic delta · 18

src/lib.rs

6 files · +458 −27

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
             );
         }