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.
This commit is contained in:
@@ -6,6 +6,45 @@ ratio/diff ver `git show <sha>`.
|
|||||||
|
|
||||||
## 2026-05-09
|
## 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
|
### feat(explorer+daemon): discovery dinámico vía broker + query socket
|
||||||
La UI deja de hardcodear el socket admin: ahora descubre al daemon
|
La UI deja de hardcodear el socket admin: ahora descubre al daemon
|
||||||
nouser vía `MatchEvent::Available` del broker brahman y le consulta
|
nouser vía `MatchEvent::Available` del broker brahman y le consulta
|
||||||
|
|||||||
Generated
-1
@@ -6386,7 +6386,6 @@ dependencies = [
|
|||||||
"brahman-sidecar",
|
"brahman-sidecar",
|
||||||
"gpui",
|
"gpui",
|
||||||
"nouser-card",
|
"nouser-card",
|
||||||
"nouser-core",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ description = "Explorador GPUI de Mónadas: panel que descubre al daemon nouser
|
|||||||
brahman-card = { path = "../../core/brahman-card" }
|
brahman-card = { path = "../../core/brahman-card" }
|
||||||
brahman-sidecar = { path = "../../shared/brahman-sidecar" }
|
brahman-sidecar = { path = "../../shared/brahman-sidecar" }
|
||||||
nouser-card = { path = "../../modules/nouser/card" }
|
nouser-card = { path = "../../modules/nouser/card" }
|
||||||
nouser-core = { path = "../../modules/nouser/core" }
|
|
||||||
gpui = { workspace = true }
|
gpui = { workspace = true }
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ use gpui::{
|
|||||||
div, prelude::*, px, rgb, App, Application, Bounds, Context, IntoElement, Render, SharedString,
|
div, prelude::*, px, rgb, App, Application, Bounds, Context, IntoElement, Render, SharedString,
|
||||||
Window, WindowBounds, WindowOptions,
|
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_card::Lens;
|
||||||
use nouser_core::engine_socket::client as query_client;
|
|
||||||
|
|
||||||
const REFRESH_INTERVAL: Duration = Duration::from_secs(2);
|
const REFRESH_INTERVAL: Duration = Duration::from_secs(2);
|
||||||
const DISCOVERY_TIMEOUT: Duration = Duration::from_secs(3);
|
const DISCOVERY_TIMEOUT: Duration = Duration::from_secs(3);
|
||||||
@@ -127,14 +127,21 @@ enum TickOutcome {
|
|||||||
QueryFailed(String),
|
QueryFailed(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resuelve el socket (cache o discovery) y consulta `ListMonads`.
|
/// Resuelve el socket (cache → broker → default path) y consulta
|
||||||
/// Pensado para correr en background: no toca GPUI, sólo I/O.
|
/// `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 {
|
fn tick(prior_socket: Option<PathBuf>) -> TickOutcome {
|
||||||
let (socket, source) = match prior_socket {
|
let (socket, source) = match prior_socket {
|
||||||
Some(p) => (p, "cache"),
|
Some(p) => (p, "cache"),
|
||||||
None => match discover() {
|
None => match resolve_socket() {
|
||||||
Ok(p) => (p, "discovery"),
|
Ok(found) => found,
|
||||||
Err(e) => return TickOutcome::DiscoveryFailed(format!("discovery: {e}")),
|
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
|
/// Discovery del daemon vía broker brahman. Construye un consumer
|
||||||
/// Card con `flow.input = monad-list:json`, espera al primer
|
/// Card con `flow.input = monad-list:json`, espera al primer
|
||||||
/// `MatchEvent::Available`, devuelve el `producer_service_socket`.
|
/// `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);
|
let card = build_consumer_card("nouser-explorer", FLOW_MONAD_LIST, FLOW_TYPE_NAME);
|
||||||
await_provider_blocking(card, DISCOVERY_TIMEOUT)
|
await_provider_blocking(card, DISCOVERY_TIMEOUT)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -126,74 +126,16 @@ fn encode_error(msg: String) -> String {
|
|||||||
serde_json::to_string(&err).unwrap_or_else(|_| "{\"error\":\"encode\"}".into())
|
serde_json::to_string(&err).unwrap_or_else(|_| "{\"error\":\"encode\"}".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cliente blocking — `client::list_monads(socket)` para que la UI no
|
// El cliente blocking vive en `nouser_card::query::client` — junto a
|
||||||
/// reimplemente el handshake JSON cada vez.
|
// los wire types — para que un consumer pueda hablar con el daemon
|
||||||
pub mod client {
|
// importando sólo `nouser-card`, sin arrastrar el peso de
|
||||||
use std::io::{BufRead, BufReader, Write};
|
// `nouser-core` (scanner / db / sled / notify / walkdir / blake3).
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::db::MonadDb;
|
use crate::db::MonadDb;
|
||||||
|
use nouser_card::query::client as query_client;
|
||||||
use nouser_card::MonadManifest;
|
use nouser_card::MonadManifest;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -225,7 +167,7 @@ mod tests {
|
|||||||
// de wait_for(socket.exists()).
|
// de wait_for(socket.exists()).
|
||||||
std::thread::sleep(Duration::from_millis(50));
|
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.id, engine_id);
|
||||||
assert_eq!(resp.engine.label, "test-engine");
|
assert_eq!(resp.engine.label, "test-engine");
|
||||||
assert_eq!(resp.engine.watching.as_deref(), Some("/tmp/x"));
|
assert_eq!(resp.engine.watching.as_deref(), Some("/tmp/x"));
|
||||||
@@ -256,7 +198,7 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
std::thread::sleep(Duration::from_millis(50));
|
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);
|
assert_eq!(resp.monads.len(), 2);
|
||||||
let labels: Vec<_> = resp.monads.iter().map(|m| m.label.as_str()).collect();
|
let labels: Vec<_> = resp.monads.iter().map(|m| m.label.as_str()).collect();
|
||||||
assert!(labels.contains(&"alpha"));
|
assert!(labels.contains(&"alpha"));
|
||||||
|
|||||||
Reference in New Issue
Block a user