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:
Sergio
2026-07-01 19:52:38 +00:00
co-authored by Claude Opus 4.8
parent 67c1eb776f
commit b666de794e
6 changed files with 458 additions and 27 deletions
+11 -7
View File
@@ -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
+70
View File
@@ -0,0 +1,70 @@
# FLIPS — de-circularización de T5T7 (T8a)
Registro de los veredictos que **cambian** al pasar del encoding de retículo de
T5T7 (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 T5T7 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.
+202
View File
@@ -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, T5T7).
**Estado:** Diseño — habilitado por la duda de circularidad al cierre de T7.
**Precondición:** T5T7 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 T5T7 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 = T5T7 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):185196 (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) GhristRiess 2022 (Tarski); Shapiro et al. 2011 (CRDTs); HellersteinAlvaro (CALM);
HansenGhrist 2019 (base lineal).
+1
View File
@@ -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
View File
@@ -0,0 +1,110 @@
//! `native` — retículo derivado del merge del CRDT (T8a, Addendum §B).
//!
//! Mata la circularidad de T5T7: 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
View File
@@ -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 T5T7 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
);
}