gioser

tawasuyu/brahman

Rust 405TOML 156Markdown 41JSON 175449 Definiciones

refactor(explorer+card): independencia jerarquica enforced

Cierra el unico debt estructural detectado en el audit de
independencia: nouser-explorer ya no arrastra nouser-core (que
aportaba notify/walkdir/sled/blake3 al grafo de compilacion de una
UI que solo habla JSON contra un socket).

- Cliente movido: engine_socket::client::list_monads (~60 LOC, std +
  serde_json puros) emigra de nouser_core::engine_socket a
  nouser_card::query::client. Vive donde viven los wire types,
  consistente con el principio "un consumer importa el contrato, no
  el runtime del productor".
- Drop dep: nouser-explorer deja de depender de nouser-core.
  Verificado con cargo tree: notify, sled, blake3 desaparecen del
  grafo del binario.
- Fallback "falla hacia la simplicidad": nueva resolve_socket() en
  el explorer intenta primero broker discovery; si el broker no
  responde / no hay init vivo, fallback directo al
  default_socket_path. El explorer queda funcional contra un daemon
  huerfano (standalone sin init) — completa "consciente cuando hay
  ecosistema, soberano cuando esta solo".
- socket_source gana tercer estado "default-path" para visibilidad.

Audit estructural confirmo que el resto del ecosistema ya respeta
el principio. Brahman es pegamento opcional, no chasis obligatorio
— y ahora el grafo de Cargo lo enforcea, no solo la convencion.

Tests: 4 + 10 + 27 verdes. Cliente movido ejercitado end-to-end
por los 3 tests integracion de engine_socket.

6f993f426820d746a531232d71486e32af033f8b · por Sergio · (hace 5 meses) · padre 2ae888bc8 · ver el árbol en este commit

Delta semántico · 12

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

crates/modules/nouser/card/src/query.rs

crates/modules/nouser/core/src/engine_socket.rs

6 archivos · +155 −75

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3661db1..9f74919 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,45 @@ ratio/diff ver `git show <sha>`.
 
 ## 2026-05-09
 
+### refactor(explorer+card): independencia jerárquica enforced — cliente con los wire types + fallback al default path
+Cierra el único debt estructural detectado en el audit de
+independencia: `nouser-explorer` ya no arrastra `nouser-core`
+(que aportaba `notify`/`walkdir`/`sled`/`blake3` al grafo de
+compilación de una UI que sólo habla JSON contra un socket).
+
+Cambios:
+- **Cliente movido**: `engine_socket::client::list_monads` (~60 LOC,
+  std + serde_json puros) emigra de `nouser_core::engine_socket` a
+  `nouser_card::query::client`. Vive donde viven los wire types,
+  consistente con el principio "un consumer importa el contrato,
+  no el runtime del productor".
+- **Drop dep**: `nouser-explorer` deja de dependener de
+  `nouser-core`. Verificado con `cargo tree`: `notify`, `sled`,
+  `blake3` desaparecen del grafo del binario. (`walkdir` sigue
+  pero llega vía `gpui_util` → `rust-embed`, fuera de nuestro
+  control y pre-existente.)
+- **Fallback "falla hacia la simplicidad"**: nueva función
+  `resolve_socket()` en el explorer intenta primero broker
+  discovery; si el broker no responde / no hay init vivo,
+  fallback directo a `nouser_card::query::transport::default_socket_path()`.
+  El explorer queda funcional contra un daemon "huérfano"
+  (corriendo standalone sin init) — completa la cadena
+  "consciente cuando hay ecosistema, soberano cuando está solo".
+- `socket_source` en el header gana un tercer estado
+  `"default-path"` para que el usuario vea por dónde se conectó.
+
+Audit estructural confirmó que el resto del ecosistema ya
+respeta el principio: todos los `yahweh-*` viewers, `minga-cli`,
+`minga-core`, `nouser-card`, `nouser-nous`, los providers
+`nouser-nous-{mock,real}` y `nakui-core` corren standalone con
+soft-fail hacia infra brahman cuando está ausente. Brahman es
+"pegamento opcional, no chasis obligatorio" — y ahora el grafo
+de Cargo lo enforcea, no sólo la convención.
+
+Tests: 4 (sidecar) + 10 (nouser-card) + 27 (nouser-core) verdes.
+El cliente movido se ejercita end-to-end por los 3 tests integración
+de `engine_socket` (importa ahora `nouser_card::query::client`).
+
 ### feat(explorer+daemon): discovery dinámico vía broker + query socket
 La UI deja de hardcodear el socket admin: ahora descubre al daemon
 nouser vía `MatchEvent::Available` del broker brahman y le consulta
diff --git a/Cargo.lock b/Cargo.lock
index 51beaa6..4f1aef7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -6386,7 +6386,6 @@ dependencies = [
  "brahman-sidecar",
  "gpui",
  "nouser-card",
- "nouser-core",
 ]
 
 [[package]]
diff --git a/crates/apps/nouser-explorer/Cargo.toml b/crates/apps/nouser-explorer/Cargo.toml
index a050959..a3befc9 100644
--- a/crates/apps/nouser-explorer/Cargo.toml
+++ b/crates/apps/nouser-explorer/Cargo.toml
@@ -9,7 +9,6 @@ description = "Explorador GPUI de Mónadas: panel que descubre al daemon nouser
 brahman-card = { path = "../../core/brahman-card" }
 brahman-sidecar = { path = "../../shared/brahman-sidecar" }
 nouser-card = { path = "../../modules/nouser/card" }
-nouser-core = { path = "../../modules/nouser/core" }
 gpui = { workspace = true }
 
 [[bin]]
diff --git a/crates/apps/nouser-explorer/src/main.rs b/crates/apps/nouser-explorer/src/main.rs
index ef05d3b..16b0021 100644
--- a/crates/apps/nouser-explorer/src/main.rs
+++ b/crates/apps/nouser-explorer/src/main.rs
@@ -26,9 +26,9 @@ use gpui::{
     div, prelude::*, px, rgb, App, Application, Bounds, Context, IntoElement, Render, SharedString,
     Window, WindowBounds, WindowOptions,
 };
-use nouser_card::query::{ListMonadsResponse, FLOW_MONAD_LIST, FLOW_TYPE_NAME};
+use nouser_card::query::client as query_client;
+use nouser_card::query::{transport, ListMonadsResponse, FLOW_MONAD_LIST, FLOW_TYPE_NAME};
 use nouser_card::Lens;
-use nouser_core::engine_socket::client as query_client;
 
 const REFRESH_INTERVAL: Duration = Duration::from_secs(2);
 const DISCOVERY_TIMEOUT: Duration = Duration::from_secs(3);
@@ -127,14 +127,21 @@ enum TickOutcome {
     QueryFailed(String),
 }
 
-/// Resuelve el socket (cache o discovery) y consulta `ListMonads`.
-/// Pensado para correr en background: no toca GPUI, sólo I/O.
+/// Resuelve el socket (cache → broker → default path) y consulta
+/// `ListMonads`. Pensado para correr en background: no toca GPUI,
+/// sólo I/O.
+///
+/// **Falla hacia la simplicidad**: si el broker brahman no está vivo
+/// (init caído / no instalado), intentamos directo el path canónico
+/// del daemon vía `transport::default_socket_path()`. El explorer
+/// sigue funcionando contra un daemon "huérfano" que no se publicó
+/// al broker — útil para correr la UI sin todo el stack.
 fn tick(prior_socket: Option<PathBuf>) -> TickOutcome {
     let (socket, source) = match prior_socket {
         Some(p) => (p, "cache"),
-        None => match discover() {
-            Ok(p) => (p, "discovery"),
-            Err(e) => return TickOutcome::DiscoveryFailed(format!("discovery: {e}")),
+        None => match resolve_socket() {
+            Ok(found) => found,
+            Err(e) => return TickOutcome::DiscoveryFailed(e),
         },
     };
 
@@ -151,10 +158,35 @@ fn tick(prior_socket: Option<PathBuf>) -> TickOutcome {
     }
 }
 
+/// Resuelve el socket del daemon en dos pasos:
+/// 1. **Broker**: consumer Card + `await_provider_blocking`. Path
+///    "consciente" (ecosistema brahman activo).
+/// 2. **Default path**: si el broker no responde, probamos
+///    `transport::default_socket_path()` directo. Path "soberano"
+///    (daemon corriendo solo, sin init).
+///
+/// Falla únicamente si ninguno responde.
+fn resolve_socket() -> Result<(PathBuf, &'static str), String> {
+    match discover_via_broker() {
+        Ok(p) => Ok((p, "broker")),
+        Err(broker_err) => {
+            let fallback = transport::default_socket_path();
+            if fallback.exists() {
+                Ok((fallback, "default-path"))
+            } else {
+                Err(format!(
+                    "broker: {broker_err}; fallback {} no existe",
+                    fallback.display()
+                ))
+            }
+        }
+    }
+}
+
 /// Discovery del daemon vía broker brahman. Construye un consumer
 /// Card con `flow.input = monad-list:json`, espera al primer
 /// `MatchEvent::Available`, devuelve el `producer_service_socket`.
-fn discover() -> Result<PathBuf, ConsumerError> {
+fn discover_via_broker() -> Result<PathBuf, ConsumerError> {
     let card = build_consumer_card("nouser-explorer", FLOW_MONAD_LIST, FLOW_TYPE_NAME);
     await_provider_blocking(card, DISCOVERY_TIMEOUT)
 }
diff --git a/crates/modules/nouser/card/src/query.rs b/crates/modules/nouser/card/src/query.rs
index a0eb61a..3d0c432 100644
--- a/crates/modules/nouser/card/src/query.rs
+++ b/crates/modules/nouser/card/src/query.rs
@@ -151,6 +151,75 @@ pub mod transport {
     }
 }
 
+// =====================================================================
+// Cliente blocking — vive con los wire types para que un consumer
+// (UI, CLI, otro módulo) pueda hablar con el daemon importando sólo
+// `nouser-card`, sin arrastrar `nouser-core` (notify/walkdir/sled/blake3).
+// =====================================================================
+
+/// Cliente síncrono para el query socket del daemon. Sólo Unix (el
+/// resto del ecosistema brahman es Unix-only de facto).
+#[cfg(unix)]
+pub mod client {
+    use std::io::{BufRead, BufReader, Write};
+    use std::os::unix::net::UnixStream;
+    use std::path::Path;
+    use std::time::Duration;
+
+    use super::{ErrorResponse, ListMonadsResponse, QueryRequest};
+
+    #[derive(Debug, thiserror::Error)]
+    pub enum QueryError {
+        #[error("conectar a {path}: {source}")]
+        Connect {
+            path: std::path::PathBuf,
+            #[source]
+            source: std::io::Error,
+        },
+        #[error("I/O: {0}")]
+        Io(#[from] std::io::Error),
+        #[error("serializacion: {0}")]
+        Serde(#[from] serde_json::Error),
+        #[error("daemon: {0}")]
+        Daemon(String),
+        #[error("response vacía del daemon")]
+        Empty,
+    }
+
+    /// Envía `ListMonads` al daemon en `socket` y devuelve la response.
+    /// `timeout` se aplica tanto al read como al write del stream.
+    pub fn list_monads(
+        socket: &Path,
+        timeout: Duration,
+    ) -> Result<ListMonadsResponse, QueryError> {
+        let mut stream = UnixStream::connect(socket).map_err(|e| QueryError::Connect {
+            path: socket.to_path_buf(),
+            source: e,
+        })?;
+        stream.set_read_timeout(Some(timeout))?;
+        stream.set_write_timeout(Some(timeout))?;
+
+        let req = QueryRequest::ListMonads;
+        let line = serde_json::to_string(&req)?;
+        stream.write_all(line.as_bytes())?;
+        stream.write_all(b"\n")?;
+        stream.flush()?;
+
+        let mut reader = BufReader::new(stream);
+        let mut response = String::new();
+        let n = reader.read_line(&mut response)?;
+        if n == 0 {
+            return Err(QueryError::Empty);
+        }
+
+        if let Ok(resp) = serde_json::from_str::<ListMonadsResponse>(response.trim()) {
+            return Ok(resp);
+        }
+        let err: ErrorResponse = serde_json::from_str(response.trim())?;
+        Err(QueryError::Daemon(err.error))
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/crates/modules/nouser/core/src/engine_socket.rs b/crates/modules/nouser/core/src/engine_socket.rs
index 326515e..495639e 100644
--- a/crates/modules/nouser/core/src/engine_socket.rs
+++ b/crates/modules/nouser/core/src/engine_socket.rs
@@ -126,74 +126,16 @@ fn encode_error(msg: String) -> String {
     serde_json::to_string(&err).unwrap_or_else(|_| "{\"error\":\"encode\"}".into())
 }
 
-/// Cliente blocking — `client::list_monads(socket)` para que la UI no
-/// reimplemente el handshake JSON cada vez.
-pub mod client {
-    use std::io::{BufRead, BufReader, Write};
-    use std::os::unix::net::UnixStream;
-    use std::path::Path;
-    use std::time::Duration;
-
-    use nouser_card::query::{ErrorResponse, ListMonadsResponse, QueryRequest};
-
-    #[derive(Debug, thiserror::Error)]
-    pub enum QueryError {
-        #[error("conectar a {path}: {source}")]
-        Connect {
-            path: std::path::PathBuf,
-            #[source]
-            source: std::io::Error,
-        },
-        #[error("I/O: {0}")]
-        Io(#[from] std::io::Error),
-        #[error("serializacion: {0}")]
-        Serde(#[from] serde_json::Error),
-        #[error("daemon: {0}")]
-        Daemon(String),
-        #[error("timeout esperando response")]
-        Timeout,
-        #[error("response vacía del daemon")]
-        Empty,
-    }
-
-    /// Envía `ListMonads` y devuelve la response. Timeout aplicado
-    /// tanto al connect como al read.
-    pub fn list_monads(
-        socket: &Path,
-        timeout: Duration,
-    ) -> Result<ListMonadsResponse, QueryError> {
-        let mut stream = UnixStream::connect(socket).map_err(|e| QueryError::Connect {
-            path: socket.to_path_buf(),
-            source: e,
-        })?;
-        stream.set_read_timeout(Some(timeout))?;
-        stream.set_write_timeout(Some(timeout))?;
-
-        let req = QueryRequest::ListMonads;
-        let line = serde_json::to_string(&req)?;
-        stream.write_all(line.as_bytes())?;
-        stream.write_all(b"\n")?;
-        stream.flush()?;
-
-        let mut reader = BufReader::new(stream);
-        let mut response = String::new();
-        let n = reader.read_line(&mut response)?;
-        if n == 0 {
-            return Err(QueryError::Empty);
-        }
-
-        if let Ok(resp) = serde_json::from_str::<ListMonadsResponse>(response.trim()) {
-            return Ok(resp);
-        }
-        let err: ErrorResponse = serde_json::from_str(response.trim())?;
-        Err(QueryError::Daemon(err.error))
-    }
-}
+// El cliente blocking vive en `nouser_card::query::client` — junto a
+// los wire types — para que un consumer pueda hablar con el daemon
+// importando sólo `nouser-card`, sin arrastrar el peso de
+// `nouser-core` (scanner / db / sled / notify / walkdir / blake3).
 
 #[cfg(test)]
 mod tests {
     use super::*;
     use crate::db::MonadDb;
+    use nouser_card::query::client as query_client;
     use nouser_card::MonadManifest;
     use std::time::Duration;
 
@@ -225,7 +167,7 @@ mod tests {
         // de wait_for(socket.exists()).
         std::thread::sleep(Duration::from_millis(50));
 
-        let resp = client::list_monads(&socket, Duration::from_secs(2)).unwrap();
+        let resp = query_client::list_monads(&socket, Duration::from_secs(2)).unwrap();
         assert_eq!(resp.engine.id, engine_id);
         assert_eq!(resp.engine.label, "test-engine");
         assert_eq!(resp.engine.watching.as_deref(), Some("/tmp/x"));
@@ -256,7 +198,7 @@ mod tests {
         .unwrap();
         std::thread::sleep(Duration::from_millis(50));
 
-        let resp = client::list_monads(&socket, Duration::from_secs(2)).unwrap();
+        let resp = query_client::list_monads(&socket, Duration::from_secs(2)).unwrap();
         assert_eq!(resp.monads.len(), 2);
         let labels: Vec<_> = resp.monads.iter().map(|m| m.label.as_str()).collect();
         assert!(labels.contains(&"alpha"));