//! `oracle` — evaluador CRDT/CALM de referencia, independiente del haz (§8 M3). //! //! Para cada configuración el oráculo dice, por CALM puro (monotonicidad de las //! operaciones compartidas), si es coordination-free — sin mirar el haz. El test //! 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; /// Predicción independiente del oráculo para una configuración. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum OracleVerdict { /// El oráculo cree que la configuración es libre de coordinación. CoordinationFree, /// El oráculo cree que hace falta coordinar. NeedsCoordination, } /// Un dato compartido entre dos réplicas, con la marca CALM de si la operación /// que lo toca es monótona. #[derive(Debug, Clone, Copy)] pub struct Share { /// Primera réplica que comparte. pub a: usize, /// Segunda réplica que comparte. pub b: usize, /// ¿La operación sobre este dato es monótona? (viene de `Cell::is_monotone`). pub monotone: bool, } impl Share { /// Dato compartido monótono (se funde solo). pub fn monotona(a: usize, b: usize) -> Self { Self { a, b, monotone: true } } /// Dato compartido no monótono (reset/overwrite → posible conflicto). pub fn no_monotona(a: usize, b: usize) -> Self { Self { a, b, monotone: false } } } /// Una configuración de réplicas con sus datos compartidos. #[derive(Debug, Clone)] pub struct Config { /// Nombre del caso (para reportes). pub name: String, /// Número de réplicas. pub replicas: usize, /// Datos compartidos. pub shares: Vec, } impl Config { /// Construye una configuración. pub fn new(name: impl Into, replicas: usize, shares: Vec) -> Self { Self { name: name.into(), replicas, shares, } } /// **CALM sintáctico por-programa** (el check grueso). Coordina si aparece /// cualquier operación marcada no monótona en VALOR. Es la respuesta a batir: /// la "discrepancia buena" que persigue T8c es `haz = libre` mientras esto /// dice `coordina` (§D). No es el oráculo de corrección. pub fn calm_syntactic(&self) -> OracleVerdict { if self.shares.iter().all(|s| s.monotone) { OracleVerdict::CoordinationFree } else { OracleVerdict::NeedsCoordination } } /// **Oráculo CALM corregido** (T8b, §B/§F): la monotonía que importa es la del /// **orden de fusión**, no la del valor. Un CRDT es, por definición, monótono /// en su propio merge (join-semilattice); luego una configuración de CRDT /// puros es libre de coordinación en cualquier grafo — ciclos incluidos. /// /// La etiqueta `monotone` era monotonía de VALOR (un LWW baja de valor pero es /// un CRDT): por eso `calm_syntactic` clasificaba mal los reset. Aquí eso se /// corrige. La coordinación real solo la introduce un **invariante** (T8c); /// hasta entonces, el veredicto correcto es siempre `CoordinationFree`. Su /// acuerdo con el haz nativo NO es circular: ambos reconocen, por caminos /// independientes, que un merge de CRDT reconcilia. pub fn oracle_verdict(&self) -> OracleVerdict { // Todo `Share` está respaldado por un merge de CRDT genuino (GCounter o // registro LWW), monótono en su orden de fusión ⇒ sin coordinación. OracleVerdict::CoordinationFree } /// Codificación lineal de la configuración como haz (la codificación honesta /// de la §2.3, sin colapsar ciclos monótonos): /// - cada dato compartido → una arista de acuerdo (restricción identidad); /// - una operación no monótona añade un segundo enlace en conflicto sobre el /// mismo par → un 2-ciclo localizado (como la Corrida B de M1). pub fn to_sheaf(&self) -> Sheaf { let mut edges = Vec::new(); for s in &self.shares { edges.push((s.a, s.b)); if !s.monotone { edges.push((s.a, s.b)); // el reset impone un segundo constraint irreconciliable } } Sheaf::constant(self.replicas, &edges, 1) } /// **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"); if coho.dim_h1 == 0 { OracleVerdict::CoordinationFree } else { 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 ⊤). /// /// ⚠️ 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 { let mut parent: Vec = (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 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 = (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, } } /// 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 { let seed: Vec = (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). /// /// Régimen de acuerdo: sharing monótono acíclico (→ ambos "corre libre") y /// cualquier configuración con al menos una operación no monótona (→ ambos /// "coordina"). El test maestro verifica la coincidencia en todos. pub fn casos_de_acuerdo() -> Vec { let mut casos = Vec::new(); // --- Monótonos acíclicos → CoordinationFree --- // Caminos de longitud creciente. for k in 2..=10 { let shares = (0..k - 1).map(|i| Share::monotona(i, i + 1)).collect(); casos.push(Config::new(format!("camino-monotono-{k}"), k, shares)); } // Estrella (centro 0, hojas 1..=4). casos.push(Config::new( "estrella-monotona", 5, (1..=4).map(|h| Share::monotona(0, h)).collect(), )); // Árbol binario. casos.push(Config::new( "arbol-binario-monotono", 5, vec![ Share::monotona(0, 1), Share::monotona(0, 2), Share::monotona(1, 3), Share::monotona(1, 4), ], )); // Bosque desconexo (dos árboles). casos.push(Config::new( "bosque-monotono", 5, vec![ Share::monotona(0, 1), Share::monotona(1, 2), Share::monotona(3, 4), ], )); // Réplicas sin compartir nada (trivialmente libre). casos.push(Config::new("aislado", 3, vec![])); // --- Con operación no monótona → NeedsCoordination --- // Par único con reset. casos.push(Config::new( "par-no-monotono", 2, vec![Share::no_monotona(0, 1)], )); // Camino monótono con un reset en un tramo (localiza el nudo ahí). for reset_edge in 0..3 { let mut shares: Vec = (0..3).map(|i| Share::monotona(i, i + 1)).collect(); shares[reset_edge] = Share::no_monotona(reset_edge, reset_edge + 1); casos.push(Config::new( format!("camino-con-reset-en-{reset_edge}"), 4, shares, )); } // Dos resets independientes en partes distintas. casos.push(Config::new( "dos-resets-separados", 4, vec![Share::no_monotona(0, 1), Share::no_monotona(2, 3)], )); // Estrella monótona con una hoja reseteada. casos.push(Config::new( "estrella-con-reset", 5, vec![ Share::monotona(0, 1), Share::monotona(0, 2), Share::monotona(0, 3), Share::no_monotona(0, 4), ], )); // Árbol grande con reset en una rama. casos.push(Config::new( "arbol-con-reset", 6, vec![ Share::monotona(0, 1), Share::monotona(1, 2), Share::monotona(2, 3), Share::monotona(1, 4), Share::no_monotona(4, 5), ], )); // Todo no monótono. casos.push(Config::new( "todo-reset", 3, vec![Share::no_monotona(0, 1), Share::no_monotona(1, 2)], )); casos } /// Casos donde el haz y el oráculo **discrepan** (§11 R1): dato monótono sobre /// un grafo de compartición **cíclico**. CALM dice coordination-free (el join /// existe siempre), pero la codificación lineal ve la obstrucción topológica del /// ciclo (`H¹ = b₁ ≠ 0`). Documentados en `DISCREPANCIES.md`. pub fn casos_de_discrepancia() -> Vec { vec![ Config::new( "triangulo-monotono", 3, vec![ Share::monotona(0, 1), Share::monotona(1, 2), Share::monotona(2, 0), ], ), Config::new( "cuadrado-monotono", 4, vec![ Share::monotona(0, 1), Share::monotona(1, 2), Share::monotona(2, 3), Share::monotona(3, 0), ], ), ] } #[cfg(test)] mod tests { use super::*; /// Control histórico del MVP (§8 M3): el haz LINEAL coincide con el CALM /// **sintáctico** (grueso) en toda la batería. Ese acuerdo topológico /// ↔ sintáctico es el que T8 pone en cuarentena; se conserva como control. #[test] fn haz_lineal_coincide_con_calm_sintactico() { let casos = casos_de_acuerdo(); assert!(casos.len() >= 20, "la batería debe tener ≥20 casos, tiene {}", casos.len()); for c in &casos { assert_eq!( c.sheaf_verdict(), c.calm_syntactic(), "discrepancia inesperada en el caso '{}'", c.name ); } } /// Las discrepancias del MVP **lineal** (R1): el haz topológico dice coordinar /// donde el CALM sintáctico dice libre (ciclo monótono). Control histórico. #[test] fn discrepancias_del_lineal_persisten() { for c in casos_de_discrepancia() { assert_eq!(c.calm_syntactic(), OracleVerdict::CoordinationFree); assert_eq!( c.sheaf_verdict(), OracleVerdict::NeedsCoordination, "el ciclo monótono '{}' produce una falsa obstrucción topológica", c.name ); } } /// Control histórico: el encoding CIRCULAR de T7 (`_labeled`) coincide con el /// CALM **sintáctico** 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_labeled_coincide_con_calm_sintactico_es_circular() { for c in &casos_de_acuerdo() { assert_eq!( c.tarski_verdict_labeled(), c.calm_syntactic(), "el control circular debe coincidir en '{}'", c.name ); } } /// **T8b — el oráculo corregido**: bajo monotonía-de-fusión, todo CRDT puro es /// `CoordinationFree`, y el haz nativo coincide. El acuerdo es correcto (ambos /// reconocen que un merge de CRDT reconcilia), no circular como el de T7. #[test] fn t8b_oraculo_corregido_coincide_con_nativo() { for c in casos_de_acuerdo().iter().chain(&casos_de_discrepancia()) { assert_eq!(c.oracle_verdict(), OracleVerdict::CoordinationFree); assert_eq!( c.tarski_verdict(), c.oracle_verdict(), "oráculo corregido y haz nativo deben coincidir en '{}'", c.name ); } } /// **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 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, "el CRDT puro '{}' no debería necesitar coordinación", c.name ); } } }