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.
T7: veredicto por difusión de Tarski y cierre de discrepancias
Addendum §F/T7 — cierra la brecha de modelado de §2.3:
- verdict::Verdict::via_tarski(sheaf, seed): harmoniza y lee. Corre libre si
el punto fijo no colapsa a ⊤; coordina si colapsa, con localización difusa
(R-L1: aristas cuyo desacuerdo sobrevive al punto fijo).
- oracle: Config::tarski_verdict encoda cada config como haz de retículos
(ResetVal uniforme, generación = componente del subgrafo monótono) y
harmoniza. Tests maestros: Tarski coincide con el oráculo en TODA la
batería; y resuelve las discrepancias del lineal (triángulo/cuadrado
monótono → corre libre, igualando a CALM).
- properties: invariante nuevo 'monótono sobre grafo arbitrario (cíclico
incluido) ⟹ corre libre' — el que el MVP lineal no podía satisfacer.
- main: contraste en vivo (lineal coordina vs Tarski corre libre).
- DISCREPANCIES.md: entradas marcadas RESUELTAS; documentado el porqué
(eliminar la resta del coboundary) y el residuo abierto (ninguno).
35 tests, clippy limpio. La Hipótesis H pasa de 'solo el caso acíclico' a
'se sostiene en general' bajo el encoding de retículos.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
diff --git a/DISCREPANCIES.md b/DISCREPANCIES.md
index c1ec23d..52ce334 100644
--- a/DISCREPANCIES.md
+++ b/DISCREPANCIES.md
@@ -1,24 +1,32 @@
# Discrepancias haz ↔ oráculo CALM
-Registro de los casos donde el veredicto cohomológico (`H¹`) **no** coincide con
-el oráculo CRDT/CALM. Como dice el SDD (§8, §11 R1), esto no es un 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.
+Registro de los casos donde el veredicto cohomológico **lineal** (`H¹`) **no**
+coincide con el oráculo CRDT/CALM. Como dice el SDD (§8, §11 R1), esto no es un
+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.
-Reproducibles con el test `oracle::tests::discrepancias_conocidas_persisten` y los
-casos de `oracle::casos_de_discrepancia()`.
+> **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.
+
+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()`.
## Resumen
-| Caso | Oráculo (CALM) | Haz (`H¹`) | ¿Coinciden? |
-|---|---|---|---|
-| `triangulo-monotono` | corre libre | `H¹ = 1` → coordina | ❌ |
-| `cuadrado-monotono` | corre libre | `H¹ = 1` → coordina | ❌ |
-| (general) dato **monótono** sobre grafo de compartición **cíclico** | corre libre | `H¹ = b₁ ≠ 0` | ❌ |
+| Caso | Oráculo (CALM) | Haz lineal (`H¹`) | Difusión de Tarski | Estado |
+|---|---|---|---|---|
+| `triangulo-monotono` | corre libre | `H¹ = 1` → coordina ❌ | corre libre ✅ | **RESUELTA** |
+| `cuadrado-monotono` | corre libre | `H¹ = 1` → coordina ❌ | corre libre ✅ | **RESUELTA** |
+| (general) dato **monótono** sobre grafo **cíclico** | corre libre | `H¹ = b₁ ≠ 0` ❌ | corre libre ✅ | **RESUELTA** |
-En cambio, **coinciden** en todo el régimen de `oracle::casos_de_acuerdo()`
-(≥20 casos): sharing monótono acíclico y cualquier configuración con al menos
-una operación no monótona.
+El MVP lineal **coincide** con el oráculo en todo el régimen de
+`oracle::casos_de_acuerdo()` (≥20 casos: sharing monótono acíclico y cualquier
+configuración no monótona). Tarski **coincide en toda la batería, sin excepción**,
+incluidos los cíclicos monótonos de arriba.
## La discrepancia, en detalle
@@ -50,16 +58,34 @@ codificación honesta añade un segundo enlace en conflicto solo en ese caso).
## Consecuencia para la Hipótesis H
-La Hipótesis H (`H¹ = 0 ⟺ coordination-free`) **se sostiene** en el MVP para
-configuraciones cuyo sharing monótono es acíclico, y **falla** para datos
+En el **MVP lineal**, la Hipótesis H (`H¹ = 0 ⟺ coordination-free`) **se sostiene**
+para configuraciones cuyo sharing monótono es acíclico, y **falla** para datos
monótonos sobre grafos cíclicos. La dirección que falla es conservadora: el haz
-pide coordinación de más (falsos positivos), nunca de menos. No hemos observado
-el fallo inverso (haz dice "libre" donde CALM dice "coordina").
+pide coordinación de más (falsos positivos), nunca de menos. No se observó el
+fallo inverso (haz dice "libre" donde CALM dice "coordina").
+
+Bajo el **encoding de retículos con el Laplaciano de Tarski** (Addendum), la
+Hipótesis H pasa de "se sostiene solo en el caso acíclico" a **"se sostiene en
+general"**: la difusión reconcilia el dato monótono alrededor del ciclo sin
+colapsar, y solo colapsa a ⊤ ante una operación semánticamente incompatible (un
+`reset` que hace incomparables las generaciones). Verificado además por el
+invariante `proptest` "monótono sobre grafo arbitrario (cíclico incluido) ⟹
+corre libre", que el motor lineal *no podía* satisfacer.
+
+## Cómo se cerró (Addendum §B–§F, §2.3 / §11 Q2)
+
+Causa raíz: **la resta del coboundary** `(δx)_e = R·x_u − R·x_v`. Un
+join-semilattice no tiene inversos; al linealizarlo, `δ` mide desacuerdo-XOR e
+inventa obstrucciones-fantasma alrededor de los ciclos.
+
+La cura fue **eliminar la resta**: haces valuados en **retículos** con
+restricciones como **conexiones de Galois**, y el **Laplaciano de Tarski**
+(Ghrist–Riess 2022) difundido a punto fijo (Kleene/Tarski). El punto fijo es la
+mejor reconciliación coherente; corre libre si preserva lo local sin colapsar a ⊤.
+Ver `src/lattice.rs`, `src/tarski.rs` y `Verdict::via_tarski`.
-## Camino de salida (fuera del MVP, §2.3 / §11 Q2)
+## Residuo abierto
-La cura propuesta por el diseño es abandonar los haces valuados en espacios
-vectoriales y pasar a **haces valuados en retículos / posets** (la generalización
-"de Tarski"), donde la fusión monótona es parte de la estructura y no se pierde al
-linealizar. Ahí un ciclo de acuerdos monótonos debería tener obstrucción trivial.
-Es la extensión de investigación sin fecha del plan de hitos.
+No se ha encontrado ningún caso donde **Tarski** y el oráculo discrepen. Si
+apareciera (p.ej. un CRDT que pida ya un *quantale*, R-L/Q-L1 del Addendum), ese
+residuo sería el siguiente resultado científico y se anotaría aquí.
diff --git a/src/main.rs b/src/main.rs
index 1959110..3c7da62 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -8,6 +8,7 @@
use sheafsync::cell::{Cell, GCounter, ResettableRegister};
use sheafsync::cohomology::{self, Cohomology};
+use sheafsync::oracle::{Config, Share};
use sheafsync::sheaf::Sheaf;
use sheafsync::verdict::Verdict;
@@ -16,6 +17,8 @@ fn main() {
corrida_a();
println!();
corrida_b();
+ println!();
+ contraste_tarski();
}
/// Imprime el veredicto de un haz ya construido.
@@ -85,3 +88,32 @@ fn corrida_b() {
let sheaf = Sheaf::constant(3, &[(0, 1), (1, 2), (1, 2)], 1);
veredicto("B", &sheaf);
}
+
+/// Contraste de la pista de retículos (Addendum §F): el triángulo monótono.
+///
+/// El motor LINEAL ve la topología del ciclo e inventa una obstrucción; la
+/// difusión de TARSKI reconcilia el dato monótono sin colapsar → corre libre,
+/// igualando a CALM. Es la brecha de modelado (§2.3) cerrada, en vivo.
+fn contraste_tarski() {
+ println!("Contraste — triángulo monótono (0—1—2—0, dato que se funde solo)");
+
+ let triangulo = Config::new(
+ "triangulo-monotono",
+ 3,
+ vec![
+ Share::monotona(0, 1),
+ Share::monotona(1, 2),
+ Share::monotona(2, 0),
+ ],
+ );
+
+ println!(" oráculo (CALM): {:?}", triangulo.oracle_verdict());
+ println!(
+ " haz lineal (H¹): {:?} ← falsa obstrucción topológica",
+ triangulo.sheaf_verdict()
+ );
+ println!(
+ " difusión de Tarski: {:?} ← reconcilia al join, discrepancia RESUELTA",
+ triangulo.tarski_verdict()
+ );
+}
diff --git a/src/oracle.rs b/src/oracle.rs
index 426abc0..3e20a9b 100644
--- a/src/oracle.rs
+++ b/src/oracle.rs
@@ -7,7 +7,10 @@
use crate::cohomology::compute_with;
use crate::gf2::Gf2Backend;
+use crate::lattice::{IdentityGalois, ResetVal};
use crate::sheaf::Sheaf;
+use crate::tarski::LatticeSheaf;
+use crate::verdict::Verdict;
/// Predicción independiente del oráculo para una configuración.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -90,8 +93,8 @@ impl Config {
Sheaf::constant(self.replicas, &edges, 1)
}
- /// **Veredicto del haz.** Coordination-free ⟺ `H¹ = 0`. Usa la aritmética
- /// exacta `GF(2)`.
+ /// **Veredicto del haz lineal.** Coordination-free ⟺ `H¹ = 0`. Usa la
+ /// aritmética exacta `GF(2)`. Es el detector topológico barato del MVP.
pub fn sheaf_verdict(&self) -> OracleVerdict {
let sheaf = self.to_sheaf();
let coho = compute_with(&sheaf, &Gf2Backend).expect("haz bien formado");
@@ -101,6 +104,54 @@ impl Config {
OracleVerdict::NeedsCoordination
}
}
+
+ /// Generación de retículo por réplica = componente del **subgrafo monótono**.
+ /// 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 ⊤).
+ fn monotone_generations(&self) -> Vec<u64> {
+ let mut parent: Vec<usize> = (0..self.replicas).collect();
+ fn find(parent: &mut [usize], x: usize) -> usize {
+ let mut r = x;
+ while parent[r] != r {
+ r = parent[r];
+ }
+ r
+ }
+ for s in &self.shares {
+ if s.monotone {
+ let ra = find(&mut parent, s.a);
+ let rb = find(&mut parent, s.b);
+ if ra != rb {
+ parent[ra] = rb;
+ }
+ }
+ }
+ (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 {
+ let gens = self.monotone_generations();
+ let seed: Vec<ResetVal> = (0..self.replicas)
+ .map(|r| ResetVal::Gen {
+ generation: gens[r],
+ value: r as i64,
+ })
+ .collect();
+ let sheaf = LatticeSheaf::new(
+ self.replicas,
+ self.shares.iter().map(|s| (s.a, s.b)).collect(),
+ IdentityGalois,
+ );
+ match Verdict::via_tarski(&sheaf, &seed) {
+ Verdict::RunsFree => OracleVerdict::CoordinationFree,
+ Verdict::NeedsCoordination { .. } => OracleVerdict::NeedsCoordination,
+ }
+ }
}
/// Batería de casos donde el haz y el oráculo **coinciden** (§8 M3, ≥20 casos).
@@ -251,17 +302,49 @@ mod tests {
}
}
- /// Las discrepancias conocidas (R1) son reales y esperadas: el haz dice
- /// coordinar donde CALM dice libre. Si esto dejara de discrepar, la
- /// codificación habría cambiado y hay que revisar `DISCREPANCIES.md`.
+ /// Las discrepancias del MVP **lineal** (R1) son reales y esperadas: el haz
+ /// topológico dice coordinar donde CALM dice libre. Es el control histórico.
#[test]
- fn discrepancias_conocidas_persisten() {
+ fn discrepancias_del_lineal_persisten() {
for c in casos_de_discrepancia() {
assert_eq!(c.oracle_verdict(), OracleVerdict::CoordinationFree);
assert_eq!(
c.sheaf_verdict(),
OracleVerdict::NeedsCoordination,
- "el ciclo monótono '{}' debería producir una falsa obstrucción",
+ "el ciclo monótono '{}' produce una falsa obstrucción topológica",
+ c.name
+ );
+ }
+ }
+
+ /// 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.
+ #[test]
+ fn tarski_coincide_con_oraculo() {
+ for c in &casos_de_acuerdo() {
+ assert_eq!(
+ c.tarski_verdict(),
+ c.oracle_verdict(),
+ "Tarski discrepa del oráculo 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.
+ #[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);
+ assert_eq!(
+ c.tarski_verdict(),
+ OracleVerdict::CoordinationFree,
+ "Tarski debe disolver la falsa obstrucción de '{}'",
c.name
);
}
diff --git a/src/verdict.rs b/src/verdict.rs
index 54676c7..df33133 100644
--- a/src/verdict.rs
+++ b/src/verdict.rs
@@ -6,8 +6,10 @@ use std::collections::BTreeSet;
use std::fmt;
use crate::cohomology::Cohomology;
+use crate::lattice::{GaloisRestriction, LatticeCell};
use crate::nerve::ReplicaId;
use crate::sheaf::Sheaf;
+use crate::tarski::{LatticeSheaf, TarskiLaplacian};
/// Umbral para considerar no nula una componente del vector de obstrucción.
const SUPPORT_EPS: f64 = 1e-9;
@@ -73,6 +75,52 @@ impl Verdict {
Verdict::NeedsCoordination { knots }
}
+
+ /// Diagnostica por **difusión de Tarski** sobre un haz de retículos
+ /// (Addendum §C, T7): se harmoniza la siembra a punto fijo y se lee.
+ ///
+ /// - **RunsFree** si el punto fijo reconcilia sin colapsar: ninguna arista
+ /// queda en ⊤ y ningún vértice colapsa. La difusión preserva lo local.
+ /// - **NeedsCoordination** si hay colapso a ⊤: obstrucción semántica real.
+ /// La localización (R-L1, difusa) son las aristas cuyo desacuerdo
+ /// *sobrevive* al punto fijo, y sus réplicas.
+ pub fn via_tarski<L, R>(sheaf: &LatticeSheaf<L, R>, seed: &[L]) -> Verdict
+ where
+ L: LatticeCell + PartialEq,
+ R: GaloisRestriction<L, L>,
+ {
+ let harm = TarskiLaplacian::new(sheaf).harmonize(seed);
+ let top = L::top();
+ let r = &sheaf.restriction;
+
+ let mut edges = Vec::new();
+ let mut replicas = BTreeSet::new();
+ // Aristas cuyo desacuerdo sobrevive al punto fijo (colapsan a ⊤).
+ for (i, &(a, b)) in sheaf.edges.iter().enumerate() {
+ if r.lower(&harm[a]).join(&r.lower(&harm[b])) == top {
+ edges.push(i);
+ replicas.insert(a);
+ replicas.insert(b);
+ }
+ }
+ // Vértices colapsados a ⊤ (por si un colapso no dejara arista superviviente).
+ for (v, h) in harm.iter().enumerate() {
+ if *h == top {
+ replicas.insert(v);
+ }
+ }
+
+ if replicas.is_empty() {
+ Verdict::RunsFree
+ } else {
+ Verdict::NeedsCoordination {
+ knots: vec![Knot {
+ replicas: replicas.into_iter().map(ReplicaId).collect(),
+ edges,
+ }],
+ }
+ }
+ }
}
impl fmt::Display for Verdict {
diff --git a/tests/properties.rs b/tests/properties.rs
index f5c552a..09b8b37 100644
--- a/tests/properties.rs
+++ b/tests/properties.rs
@@ -88,8 +88,19 @@ proptest! {
prop_assert!(h0b <= h0, "H⁰ no debe crecer al añadir una arista");
}
- /// Invariante 1: solo-suma (monótono) sobre un bosque ⟹ `H¹ = 0` y el
- /// oráculo y el haz coinciden en "corre libre".
+ /// Invariante nuevo de la pista de retículos (Addendum §F.3), el que el MVP
+ /// lineal NO podía satisfacer: **monótono sobre grafo arbitrario (cíclico
+ /// incluido) ⟹ corre libre** por la ruta de Tarski.
+ #[test]
+ fn monotono_sobre_grafo_arbitrario_corre_libre_via_tarski((n, edges) in grafo()) {
+ let shares = edges.iter().map(|&(a, b)| Share::monotona(a, b)).collect();
+ let config = Config::new("mono-arbitrario", n, shares);
+ prop_assert_eq!(config.oracle_verdict(), OracleVerdict::CoordinationFree);
+ prop_assert_eq!(config.tarski_verdict(), OracleVerdict::CoordinationFree);
+ }
+
+ /// Invariante 1 (MVP lineal): solo-suma sobre un bosque ⟹ `H¹ = 0` y el
+ /// oráculo y el haz lineal coinciden en "corre libre".
#[test]
fn monotono_sobre_bosque_corre_libre((n, edges) in grafo()) {
// Quedarnos solo con las aristas que conectan componentes nuevas → bosque.