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.
T2: CRDTs GCounter/ResettableRegister + hito M1 de punta a punta
1//! `verdict` — traducción del resultado topológico a lenguaje de sistemas (§6.5).
2//!
3//! `H¹ = 0 → corre libre` vs `H¹ ≠ 0 → coordina aquí`, con localización.
4
5use std::collections::BTreeSet;
6use std::fmt;
7
8use crate::cohomology::Cohomology;
9use crate::nerve::ReplicaId;
10use crate::sheaf::Sheaf;
11
12/// Umbral para considerar no nula una componente del vector de obstrucción.
13const SUPPORT_EPS: f64 = 1e-9;
14
15/// Un nudo: una obstrucción localizada entre réplicas/aristas concretas.
16#[derive(Debug, Clone, PartialEq)]
17pub struct Knot {
18 /// Réplicas implicadas en el nudo.
19 pub replicas: Vec<ReplicaId>,
20 /// Aristas del ciclo obstruido (índices en el nervio).
21 pub edges: Vec<usize>,
22}
23
24/// El veredicto final en lenguaje de sistemas.
25#[derive(Debug, Clone, PartialEq)]
26pub enum Verdict {
27 /// `H¹ = 0`: fusión libre de coordinación.
28 RunsFree,
29 /// `H¹ ≠ 0`: hace falta coordinar, con los nudos localizados.
30 NeedsCoordination {
31 /// Los nudos detectados (uno por generador del cokernel).
32 knots: Vec<Knot>,
33 },
34}
35
36impl Verdict {
37 /// Diagnostica la configuración a partir del haz y su cohomología.
38 ///
39 /// Si `H¹ = 0` → `RunsFree`. Si no, cada vector de la base del cokernel es un
40 /// nudo independiente: las aristas con soporte no nulo lo forman, y sus
41 /// extremos son las réplicas implicadas (§7 paso 5).
42 pub fn diagnose(sheaf: &Sheaf, coho: &Cohomology) -> Verdict {
43 if coho.dim_h1 == 0 {
44 return Verdict::RunsFree;
45 }
46
47 // Offsets de cada arista dentro de C¹ (donde vive el vector de obstrucción).
48 let mut edge_offsets = Vec::with_capacity(sheaf.edge_stalks.len());
49 let mut acc = 0;
50 for &d in &sheaf.edge_stalks {
51 edge_offsets.push(acc);
52 acc += d;
53 }
54
55 let mut knots = Vec::new();
56 for basis_vec in &coho.obstruction_basis {
57 let mut edges = Vec::new();
58 let mut replicas = BTreeSet::new();
59 for (e, (&off, &d)) in edge_offsets.iter().zip(&sheaf.edge_stalks).enumerate() {
60 let touches = basis_vec[off..off + d].iter().any(|x| x.abs() > SUPPORT_EPS);
61 if touches {
62 edges.push(e);
63 let (u, v) = sheaf.edge_endpoints[e];
64 replicas.insert(u);
65 replicas.insert(v);
66 }
67 }
68 knots.push(Knot {
69 replicas: replicas.into_iter().map(ReplicaId).collect(),
70 edges,
71 });
72 }
73
74 Verdict::NeedsCoordination { knots }
75 }
76}
77
78impl fmt::Display for Verdict {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 match self {
81 Verdict::RunsFree => write!(f, "H¹ = 0 → corre libre (sin jefe)"),
82 Verdict::NeedsCoordination { knots } => {
83 write!(f, "H¹ ≠ 0 → coordina aquí")?;
84 for knot in knots {
85 let reps: Vec<usize> = knot.replicas.iter().map(|r| r.0).collect();
86 write!(f, "\n · nudo entre réplicas {reps:?} (aristas {:?})", knot.edges)?;
87 }
88 Ok(())
89 }
90 }
91 }
92}