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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
67c1eb776f
commit
b666de794e
+11
-7
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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;
|
||||
|
||||
+110
@@ -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);
|
||||
}
|
||||
}
|
||||
+64
-20
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user