gioser

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 24Markdown 15TOML 1358 Sutichaykuna

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>

67c1eb776f64dd43ab986286053d01f4fa8e7780 · ruwaq Sergio · (3 killa ñawpaq) · tayta 3819f9d88 · kay commitpi sach'ata qhaway

Semántico sayk'uy · 10

src/main.rs

src/verdict.rs

5 qillqakuna · +233 −33

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.