tawasuyu

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 614Markdown 274TOML 149RON 310606 Definiciones

⎇ main shared/grafo/src/lib.rs Rust · 262 líneas · 8.9 KB Historia Quién tocó qué crudo

chore: refresco desde el monorepo — 27 crates nuevos (anim, lottie, svg, mesh, image, layer, test, wire-view, widgets rag-sidebar/router/lazy-list/rive-button) y sync de los 95 existentes 93253cc22 · Sergio ·

1// =============================================================================
2//  grafo-nav — el contrato del DAG navegable, una sola vez (PLAN-GRAFO F1)
3// -----------------------------------------------------------------------------
4//  Un DAG cualquiera del repo (Mónadas, commits, celdas, mensajes) se vuelve
5//  navegable y gestionable implementando [`GrafoNavegable`]: dice sus nodos, sus
6//  aristas dirigidas `(padre → hijo)`, y qué [`Verbo`]s permite para la
7//  selección. Todo lo estructural — raíces, cabezas, cono, bifurcación — se
8//  deriva solo, puro, de las aristas.
9//
10//  El cockpit compartido (`llimphi-widget-navigator::cockpit_grafo_view`) toma
11//  cualquier `impl GrafoNavegable` y lo pinta con pan/zoom/selección y la paleta
12//  de verbos. Esta caja NO conoce la UI: sólo topología y vocabulario.
13//
14//  Convención de dirección de arista: `(from, to)` = `ancestro → descendiente`
15//  (padre → hijo). Así una **raíz** no tiene aristas entrantes (commit inicial)
16//  y una **cabeza** no tiene salientes (tip / mensaje sin respuesta) — coincide
17//  con `minga::GrafoCommits` y con el layout por rango del nodegraph.
18// =============================================================================
19
20#![no_std]
21#![forbid(unsafe_code)]
22
23extern crate alloc;
24
25use alloc::string::String;
26use alloc::vec::Vec;
27
28/// Identificador opaco de un nodo. Lo asigna el dominio (un índice a su propio
29/// mapa `id → CommitHash | MensajeId | MonadId`); el visor no lo interpreta.
30pub type NodoId = u64;
31
32/// Estado **de dominio** de un nodo — lo que la topología no puede deducir sola
33/// (a diferencia de raíz/cabeza, que sí se derivan de las aristas). Elige tinte
34/// y matiza los verbos.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum Rol {
37    /// Nodo interior corriente.
38    Normal,
39    /// El nodo "activo": HEAD actual, celda en foco, último leído.
40    Activo,
41    /// Marcado como desactualizado por el dominio (hija stale de pluma, celda
42    /// con dependencia cambiada) — candidato a *restablecer*.
43    Stale,
44    /// Inalcanzable / colgante — candidato a *cercenar*.
45    Huerfano,
46}
47
48impl Default for Rol {
49    fn default() -> Self {
50        Rol::Normal
51    }
52}
53
54/// Un nodo del grafo: id opaco, etiqueta legible y su rol de dominio.
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct Nodo {
57    pub id: NodoId,
58    pub etiqueta: String,
59    pub rol: Rol,
60}
61
62impl Nodo {
63    pub fn new(id: NodoId, etiqueta: impl Into<String>, rol: Rol) -> Self {
64        Self { id, etiqueta: etiqueta.into(), rol }
65    }
66}
67
68/// Los cuatro verbos compartidos. El mismo vocabulario para todos los dominios;
69/// cada uno los implementa a su manera (minga: checkout/gc/fusionar; ayni: merge
70/// de cabezas; notebook: run_from / borrar celda).
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum Verbo {
73    /// Enfocar / ir al nodo (navegación pura, siempre disponible).
74    Saltar,
75    /// Mover el puntero/HEAD a este nodo (reset, checkout, re-ejecutar cono).
76    Restablecer,
77    /// Podar este nodo y su cono descendente (gc, borrar celda + dependientes).
78    Cercenar,
79    /// Unir cabezas divergentes (fusionar commits, merge de mensajes/CRDT).
80    Conciliar,
81}
82
83impl Verbo {
84    /// Rótulo para el menú contextual del cockpit.
85    pub fn etiqueta(self) -> &'static str {
86        match self {
87            Verbo::Saltar => "Saltar",
88            Verbo::Restablecer => "Restablecer",
89            Verbo::Cercenar => "Cercenar",
90            Verbo::Conciliar => "Conciliar",
91        }
92    }
93
94    /// Los cuatro, en orden canónico (de menos a más destructivo).
95    pub const TODOS: [Verbo; 4] = [
96        Verbo::Saltar,
97        Verbo::Restablecer,
98        Verbo::Cercenar,
99        Verbo::Conciliar,
100    ];
101}
102
103/// El contrato. Un dominio lo implementa sobre su modelo ya existente; las
104/// derivadas estructurales vienen gratis.
105pub trait GrafoNavegable {
106    /// Los nodos a mostrar.
107    fn nodos(&self) -> Vec<Nodo>;
108
109    /// Aristas dirigidas `(ancestro, descendiente)`.
110    fn aristas(&self) -> Vec<(NodoId, NodoId)>;
111
112    /// Verbos permitidos para la selección (o para el grafo entero si `None`).
113    /// El dominio decide la política: p.ej. `Conciliar` sólo si hay bifurcación,
114    /// `Restablecer` sólo sobre un nodo histórico, `Cercenar` sólo lo inalcanzable.
115    fn acciones(&self, seleccion: Option<NodoId>) -> Vec<Verbo>;
116
117    // ── derivadas puras (default; el dominio rara vez las sobreescribe) ──────
118
119    /// Raíces: nodos sin aristas entrantes (ancestros de todo).
120    fn raices(&self) -> Vec<NodoId> {
121        let edges = self.aristas();
122        self.nodos()
123            .into_iter()
124            .map(|n| n.id)
125            .filter(|id| !edges.iter().any(|(_, t)| t == id))
126            .collect()
127    }
128
129    /// Cabezas: nodos sin aristas salientes (los tips / lo más nuevo).
130    fn cabezas(&self) -> Vec<NodoId> {
131        let edges = self.aristas();
132        self.nodos()
133            .into_iter()
134            .map(|n| n.id)
135            .filter(|id| !edges.iter().any(|(f, _)| f == id))
136            .collect()
137    }
138
139    /// `true` si el grafo **bifurcó** (más de una cabeza) — condición para que
140    /// *conciliar* tenga sentido.
141    fn bifurcado(&self) -> bool {
142        self.cabezas().len() > 1
143    }
144
145    /// Cono **ascendente** de `id`: sus ancestros, incluido `id`.
146    fn cono_ascendente(&self, id: NodoId) -> Vec<NodoId> {
147        cono(&self.aristas(), id, Sentido::Arriba)
148    }
149
150    /// Cono **descendente** de `id`: sus descendientes, incluido `id` (lo que se
151    /// arrastra al *cercenar*).
152    fn cono_descendente(&self, id: NodoId) -> Vec<NodoId> {
153        cono(&self.aristas(), id, Sentido::Abajo)
154    }
155}
156
157#[derive(Clone, Copy)]
158enum Sentido {
159    Arriba,
160    Abajo,
161}
162
163/// BFS del cono en un sentido, con dedup por `Vec::contains` (sin `std`; apto
164/// para los grafos modestos que maneja un cockpit interactivo).
165fn cono(edges: &[(NodoId, NodoId)], id: NodoId, sentido: Sentido) -> Vec<NodoId> {
166    let mut visto: Vec<NodoId> = Vec::new();
167    visto.push(id);
168    let mut i = 0;
169    while i < visto.len() {
170        let cur = visto[i];
171        i += 1;
172        for &(f, t) in edges {
173            let vecino = match sentido {
174                Sentido::Arriba if t == cur => Some(f),
175                Sentido::Abajo if f == cur => Some(t),
176                _ => None,
177            };
178            if let Some(v) = vecino {
179                if !visto.contains(&v) {
180                    visto.push(v);
181                }
182            }
183        }
184    }
185    visto.sort_unstable();
186    visto
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192    use alloc::vec;
193
194    /// Grafo sintético en diamante con una segunda cabeza suelta:
195    ///   1 → 2, 1 → 3, 2 → 4, 3 → 4   (diamante),   1 → 5   (cabeza extra)
196    /// Raíz: 1.  Cabezas: 4 y 5  ⇒ bifurcado.
197    struct Diamante;
198    impl GrafoNavegable for Diamante {
199        fn nodos(&self) -> Vec<Nodo> {
200            vec![
201                Nodo::new(1, "raíz", Rol::Normal),
202                Nodo::new(2, "A", Rol::Normal),
203                Nodo::new(3, "B", Rol::Normal),
204                Nodo::new(4, "merge", Rol::Activo),
205                Nodo::new(5, "suelto", Rol::Huerfano),
206            ]
207        }
208        fn aristas(&self) -> Vec<(NodoId, NodoId)> {
209            vec![(1, 2), (1, 3), (2, 4), (3, 4), (1, 5)]
210        }
211        fn acciones(&self, seleccion: Option<NodoId>) -> Vec<Verbo> {
212            let mut v = vec![Verbo::Saltar];
213            if seleccion.is_some() {
214                v.push(Verbo::Cercenar);
215            }
216            if self.bifurcado() {
217                v.push(Verbo::Conciliar);
218            }
219            v
220        }
221    }
222
223    #[test]
224    fn raices_y_cabezas_derivadas() {
225        let g = Diamante;
226        assert_eq!(g.raices(), vec![1]);
227        let mut cab = g.cabezas();
228        cab.sort_unstable();
229        assert_eq!(cab, vec![4, 5]);
230        assert!(g.bifurcado(), "dos cabezas ⇒ bifurcado");
231    }
232
233    #[test]
234    fn conos_ascendente_y_descendente() {
235        let g = Diamante;
236        assert_eq!(g.cono_ascendente(4), vec![1, 2, 3, 4]);
237        assert_eq!(g.cono_descendente(1), vec![1, 2, 3, 4, 5]);
238        // una hoja sólo se contiene a sí misma hacia abajo.
239        assert_eq!(g.cono_descendente(4), vec![4]);
240        // una raíz sólo se contiene a sí misma hacia arriba.
241        assert_eq!(g.cono_ascendente(1), vec![1]);
242    }
243
244    #[test]
245    fn acciones_dependen_de_la_seleccion() {
246        let g = Diamante;
247        // Sin selección: no ofrece cercenar.
248        let sin = g.acciones(None);
249        assert!(sin.contains(&Verbo::Saltar));
250        assert!(!sin.contains(&Verbo::Cercenar));
251        assert!(sin.contains(&Verbo::Conciliar)); // bifurcado
252        // Con selección: aparece cercenar.
253        assert!(g.acciones(Some(4)).contains(&Verbo::Cercenar));
254    }
255
256    #[test]
257    fn verbo_etiquetas_y_orden() {
258        assert_eq!(Verbo::TODOS.len(), 4);
259        assert_eq!(Verbo::Saltar.etiqueta(), "Saltar");
260        assert_eq!(Verbo::Conciliar.etiqueta(), "Conciliar");
261    }
262}