gioser

tawasuyu/brahman

Rust 476TOML 190Markdown 50JSON 176602 Definiciones

feat(minga-explorer): nueva app dashboard del repo Minga sobre stack yahweh

Iter 11. Cierra integración del módulo Minga (VCS semántico P2P)
al ecosistema GUI. Antes Minga sólo tenía CLI; ahora hay un
dashboard GPUI con counts del repo en vivo.

crates/apps/minga-explorer/:
- Deps: minga-store + yahweh-theme + 3 widgets compartidos.
  Sin minga-cli (sin passphrase prompts) ni minga-core.
- PersistentRepo abierto directo (counts son lectura pública,
  sin passphrase). El DID sigue requiriendo `minga status` CLI.
- Refresh polling 2s (mismo pattern que nakui/nouser explorer).
- 3 stat cards: Nodos AST, Atestaciones, Claves MST. Cada una
  con border-l accent + label + número grande + descripción.
- Helper stat_card() factoriza la card.
- Header con título dinámico + theme switcher.
- error_banner themed.
- 2 tests sanity (missing dir errors, RepoSnapshot default).

Smoke run verificado: bootstrap completo OK, panic esperado en
open_window sin display.

Apps GUI themed: 4 (nakui-ui, nakui-explorer, nouser-explorer,
minga-explorer).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

2790b6dc8a00189b948dff7d95c5ad0a47af068c · por Sergio · (hace 5 meses) · padre 2f426b017 · ver el árbol en este commit

Delta semántico · 12

crates/apps/minga-explorer/src/main.rs

5 archivos · +384 −0

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1343cfe..2636348 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,56 @@ ratio/diff ver `git show <sha>`.
 
 ## 2026-05-10
 
+### feat(minga-explorer): nueva app dashboard del repo Minga sobre stack yahweh
+Iter 11. Cierra el último frente identificado: integración del
+módulo Minga (VCS semántico P2P) al ecosistema GUI. Antes Minga
+sólo tenía CLI (`minga init/status/ingest/listen/sync/watch`).
+Ahora hay un **dashboard GPUI** que muestra los counts del repo
+en vivo, sobre el mismo stack themed que las otras dos apps
+explorer.
+
+Crate nuevo `crates/apps/minga-explorer/`:
+- **Deps**: `minga-store` (para `PersistentRepo::open`) +
+  `yahweh-theme` + `yahweh-widget-{banner,card,theme-switcher}`.
+  Sin `minga-cli` (no necesita prompts de passphrase) ni
+  `minga-core` (counts no requieren parsear AST).
+- **Lectura sin passphrase**: el `PersistentRepo` se abre directo
+  desde `<repo>/repo` sled. Los counts (`nodes.len()`,
+  `attestations.len()`, `mst.len()`) son lectura pública. Para el
+  DID se sigue necesitando `minga status` (CLI con passphrase).
+- **Refresh por polling cada 2s**: mismo pattern que
+  `nakui-explorer`/`nouser-explorer`.
+- **3 stat cards** una por dimensión:
+  - Nodos AST (cyan) — fragments parseados del código.
+  - Atestaciones (verde) — firmas Ed25519 sobre los nodos.
+  - Claves MST (purple) — entradas del Merkle Search Tree.
+- **Helper `stat_card(cx, label, value, description, accent, ...)`**:
+  fabrica una card con border-l colored + label tenue + número
+  grande (`px(28)`) + descripción. Reutilizable.
+- **Header**: título dinámico (`Repo: <path> · reload <ms> ms`)
+  + theme switcher derecha.
+- **Error banner**: themed Banner::Error si el repo no abre.
+- 2 tests: `load_snapshot_errors_on_missing_dir` (msg claro
+  cuando el dir no existe) + sanity del `RepoSnapshot::default`.
+
+Workspace: nueva entry en `members[]`.
+
+Smoke run del binario verificado: bootstrap completo OK, panic
+esperado en open_window por falta de display.
+
+Beneficio operativo:
+- Un usuario corre `minga init` + `minga ingest archivo.rs` desde
+  CLI, después abre `minga-explorer` y ve los counts crecer en
+  vivo cuando ingiere más archivos.
+- Comparte theme switcher con `nakui-explorer` y
+  `nouser-explorer` — cualquier preset elegido se aplica
+  visualmente igual cross-app.
+- `minga` deja de ser sólo CLI; gana presencia GUI sin tocar
+  el resto del módulo.
+
+Apps GUI integradas al stack themed: **4** (nakui-ui, nakui-explorer,
+nouser-explorer, minga-explorer).
+
 ### feat(nouser-explorer): integración al stack yahweh themed
 Iter 10. `nouser-explorer` (la app paralela a `nakui-explorer`
 para ver Mónadas via daemon nouser) tenía colors hardcoded
diff --git a/Cargo.lock b/Cargo.lock
index a1f1b43..e790fce 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -6196,6 +6196,18 @@ dependencies = [
  "tree-sitter-typescript",
 ]
 
+[[package]]
+name = "minga-explorer"
+version = "0.1.0"
+dependencies = [
+ "gpui",
+ "minga-store",
+ "yahweh-theme",
+ "yahweh-widget-banner",
+ "yahweh-widget-card",
+ "yahweh-widget-theme-switcher",
+]
+
 [[package]]
 name = "minga-p2p"
 version = "0.1.0"
diff --git a/Cargo.toml b/Cargo.toml
index 4c716a5..ba5fc2c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -92,6 +92,7 @@ members = [
     "crates/apps/nouser-explorer",
     "crates/apps/nakui-explorer",
     "crates/apps/nakui-ui",
+    "crates/apps/minga-explorer",
 ]
 
 [workspace.package]
diff --git a/crates/apps/minga-explorer/Cargo.toml b/crates/apps/minga-explorer/Cargo.toml
new file mode 100644
index 0000000..995d67c
--- /dev/null
+++ b/crates/apps/minga-explorer/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "minga-explorer"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+description = "Dashboard GPUI del repo Minga: counts de nodos AST, atestaciones, claves MST, refresh por polling. Lee sled directo (sin passphrase). Standalone."
+
+[dependencies]
+minga-store = { path = "../../modules/semantic_dht/minga-store" }
+yahweh-theme = { path = "../../modules/ui_engine/libs/theme" }
+yahweh-widget-banner = { path = "../../modules/ui_engine/widgets/banner" }
+yahweh-widget-card = { path = "../../modules/ui_engine/widgets/card" }
+yahweh-widget-theme-switcher = { path = "../../modules/ui_engine/widgets/theme-switcher" }
+gpui = { workspace = true }
+
+[[bin]]
+name = "minga-explorer"
+path = "src/main.rs"
diff --git a/crates/apps/minga-explorer/src/main.rs b/crates/apps/minga-explorer/src/main.rs
new file mode 100644
index 0000000..70b09f9
--- /dev/null
+++ b/crates/apps/minga-explorer/src/main.rs
@@ -0,0 +1,303 @@
+//! `minga-explorer` — dashboard GPUI del repo Minga (VCS semántico
+//! P2P).
+//!
+//! Polling cada 2s contra `MINGA_REPO` (env, default `./.minga`),
+//! abre el `PersistentRepo` (sled, sin passphrase porque los counts
+//! son lectura pública) y muestra:
+//! - Cantidad de nodos AST almacenados.
+//! - Cantidad de atestaciones firmadas.
+//! - Cantidad de claves del MST (Merkle Search Tree).
+//!
+//! No requiere keypair descifrado — eso se queda para el CLI
+//! (`minga status`) cuando hace falta el DID. El explorer foco es
+//! observabilidad rápida.
+//!
+//! Stack visual: yahweh-theme + banner_themed + card_themed +
+//! theme_switcher. Mismo patrón que `nakui-explorer` /
+//! `nouser-explorer`.
+//!
+//! Uso:
+//! ```sh
+//! cargo run -p minga-explorer
+//! # con repo custom:
+//! MINGA_REPO=/path/to/.minga cargo run -p minga-explorer
+//! ```
+
+use std::path::PathBuf;
+use std::time::Duration;
+
+use gpui::{
+    div, prelude::*, px, App, Application, Bounds, Context, IntoElement, Render, SharedString,
+    Window, WindowBounds, WindowOptions,
+};
+use minga_store::PersistentRepo;
+use yahweh_theme::Theme;
+use yahweh_widget_banner::{banner_themed, Banner};
+use yahweh_widget_card::card_themed;
+use yahweh_widget_theme_switcher::theme_switcher;
+
+const REFRESH_INTERVAL: Duration = Duration::from_secs(2);
+const REPO_DIRNAME: &str = "repo";
+
+fn main() {
+    Application::new().run(|cx: &mut App| {
+        Theme::install_default(cx);
+        let bounds = Bounds::centered(None, gpui::size(px(800.), px(560.)), cx);
+        cx.open_window(
+            WindowOptions {
+                window_bounds: Some(WindowBounds::Windowed(bounds)),
+                titlebar: Some(gpui::TitlebarOptions {
+                    title: Some(SharedString::from("Minga — Repo")),
+                    ..Default::default()
+                }),
+                ..Default::default()
+            },
+            |_w, cx| cx.new(Explorer::new),
+        )
+        .expect("open window");
+        cx.activate(true);
+    });
+}
+
+/// Snapshot de counts del repo. Se reemplaza completo en cada
+/// refresh — los stores no diff fácilmente, y los counts son
+/// baratos (sled tracks size).
+#[derive(Clone, Default, Debug)]
+struct RepoSnapshot {
+    nodes: usize,
+    attestations: usize,
+    mst_keys: usize,
+}
+
+struct Explorer {
+    repo_path: PathBuf,
+    snapshot: Option<RepoSnapshot>,
+    error: Option<SharedString>,
+    last_load_ms: u64,
+}
+
+impl Explorer {
+    fn new(cx: &mut Context<Self>) -> Self {
+        let repo_path = std::env::var("MINGA_REPO")
+            .ok()
+            .map(PathBuf::from)
+            .unwrap_or_else(|| PathBuf::from(".minga"));
+        let path_for_loop = repo_path.clone();
+        cx.spawn(async move |this, cx| {
+            let timer = cx.background_executor().clone();
+            loop {
+                let started = std::time::Instant::now();
+                let result = load_snapshot(&path_for_loop);
+                let elapsed = started.elapsed().as_millis() as u64;
+                let _ = this.update(cx, |me, cx| {
+                    match result {
+                        Ok(snap) => {
+                            me.snapshot = Some(snap);
+                            me.error = None;
+                        }
+                        Err(e) => {
+                            me.error = Some(SharedString::from(format!(
+                                "no pude leer repo {}: {}",
+                                me.repo_path.display(),
+                                e
+                            )));
+                        }
+                    }
+                    me.last_load_ms = elapsed;
+                    cx.notify();
+                });
+                timer.timer(REFRESH_INTERVAL).await;
+            }
+        })
+        .detach();
+
+        Self {
+            repo_path,
+            snapshot: None,
+            error: None,
+            last_load_ms: 0,
+        }
+    }
+}
+
+/// Lee el repo sled `<repo_path>/repo` y devuelve los 3 counts.
+/// Falla si: el dir no existe, sled rebota al abrir, o cualquier
+/// store falla a `len()`. Ningún error es fatal — la UI muestra el
+/// banner y mantiene el último snapshot bueno.
+fn load_snapshot(repo_path: &std::path::Path) -> Result<RepoSnapshot, String> {
+    let inner = repo_path.join(REPO_DIRNAME);
+    if !inner.exists() {
+        return Err(format!(
+            "directorio del repo sled no existe: {}",
+            inner.display()
+        ));
+    }
+    let repo = PersistentRepo::open(&inner).map_err(|e| format!("open: {e}"))?;
+    Ok(RepoSnapshot {
+        nodes: repo.nodes.len(),
+        attestations: repo.attestations.len(),
+        mst_keys: repo.mst.len(),
+    })
+}
+
+impl Render for Explorer {
+    fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        let theme = Theme::global(cx).clone();
+        let bg = theme.bg_app.clone();
+        let text = theme.fg_text;
+        let text_dim = theme.fg_muted;
+        // Acentos por kind del dashboard: nodos azul, atestaciones
+        // verde, MST purple. Señales semánticas del dominio Minga.
+        let accent_nodes = gpui::rgb(0x88c0d0);
+        let accent_attestations = gpui::rgb(0xa3be8c);
+        let accent_mst = gpui::rgb(0xb48ead);
+
+        let header_text = match &self.snapshot {
+            Some(_) => format!(
+                "Repo: {}  ·  reload {} ms",
+                self.repo_path.display(),
+                self.last_load_ms
+            ),
+            None => format!("Buscando repo en {}…", self.repo_path.display()),
+        };
+
+        let header = div()
+            .flex()
+            .flex_row()
+            .items_center()
+            .px(px(16.))
+            .py(px(12.))
+            .bg(theme.bg_panel.clone())
+            .border_b_1()
+            .border_color(theme.border)
+            .text_color(text)
+            .text_size(px(14.))
+            .child(div().flex_grow().child(header_text))
+            .child(theme_switcher(cx));
+
+        let error_banner = self.error.as_ref().map(|e| {
+            banner_themed(cx, Banner::Error, e.clone())
+                .px(px(16.))
+                .py(px(8.))
+                .text_size(px(12.))
+        });
+
+        let body = match &self.snapshot {
+            None => div()
+                .px(px(16.))
+                .py(px(20.))
+                .text_color(text_dim)
+                .text_size(px(13.))
+                .child("Esperando primer refresh…"),
+            Some(snap) => div()
+                .flex()
+                .flex_col()
+                .gap(px(8.))
+                .px(px(16.))
+                .py(px(16.))
+                .child(stat_card(
+                    cx,
+                    "Nodos AST",
+                    snap.nodes,
+                    "fragments parseados del código",
+                    accent_nodes,
+                    text,
+                    text_dim,
+                ))
+                .child(stat_card(
+                    cx,
+                    "Atestaciones",
+                    snap.attestations,
+                    "firmas Ed25519 sobre los nodos",
+                    accent_attestations,
+                    text,
+                    text_dim,
+                ))
+                .child(stat_card(
+                    cx,
+                    "Claves MST",
+                    snap.mst_keys,
+                    "entradas del Merkle Search Tree",
+                    accent_mst,
+                    text,
+                    text_dim,
+                )),
+        };
+
+        div()
+            .flex()
+            .flex_col()
+            .size_full()
+            .bg(bg)
+            .child(header)
+            .when_some(error_banner, |d, b| d.child(b))
+            .child(body)
+    }
+}
+
+/// Card visual para una estadística del dashboard. Border-l por
+/// kind, label arriba + número grande + descripción abajo.
+fn stat_card(
+    cx: &mut Context<Explorer>,
+    label: &str,
+    value: usize,
+    description: &str,
+    accent: gpui::Rgba,
+    text: gpui::Hsla,
+    text_dim: gpui::Hsla,
+) -> impl IntoElement {
+    card_themed(cx)
+        .border_l_4()
+        .border_color(accent)
+        .child(
+            div()
+                .text_color(accent)
+                .text_size(px(11.))
+                .child(SharedString::from(label.to_string())),
+        )
+        .child(
+            div()
+                .text_color(text)
+                .text_size(px(28.))
+                .child(SharedString::from(value.to_string())),
+        )
+        .child(
+            div()
+                .text_color(text_dim)
+                .text_size(px(11.))
+                .child(SharedString::from(description.to_string())),
+        )
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    /// Sanity: load_snapshot rebota si el dir no existe (mensaje
+    /// claro). Es el path típico para "no inicializaste el repo".
+    #[test]
+    fn load_snapshot_errors_on_missing_dir() {
+        let p = std::env::temp_dir().join(format!(
+            "minga-explorer-missing-{}-{}",
+            std::process::id(),
+            std::time::SystemTime::now()
+                .duration_since(std::time::UNIX_EPOCH)
+                .map(|d| d.as_nanos())
+                .unwrap_or(0)
+        ));
+        // p NO existe.
+        let err = load_snapshot(&p).unwrap_err();
+        assert!(
+            err.contains("no existe"),
+            "msg debe explicar el missing: {err}"
+        );
+    }
+
+    #[test]
+    fn snapshot_default_is_zeros() {
+        let s = RepoSnapshot::default();
+        assert_eq!(s.nodes, 0);
+        assert_eq!(s.attestations, 0);
+        assert_eq!(s.mst_keys, 0);
+    }
+}