refactor(monorepo): reorganización lógica + renames + SDDs + split CHANGELOG
Reorganización física de crates/: - core/ (mezclaba 6 propósitos) se divide en protocol/, init/, runtime/, compat/ - shared/ (3 crates) se redistribuye en protocol/ e init/ - lapaloma (sub-módulo de ui_engine) se promueve a modules/pineal/ Renames de proyectos: - shipote → shuma (runtime de sandboxes) - nouser → akasha (explorador de Mónadas) - yahweh → nahual (motor GPUI, antes ui_engine/) - lapaloma → pineal (data-viz agnóstica) Fraccionamiento UI → core agnóstico: - vista-core (DeckState + snap, 175 LOC, 5 tests verdes) - barra-core (Task + render_html + sanitize, 90 LOC, 5 tests verdes) - vista-web y barra-web ahora son thin DOM bindings Documentación nueva: - 16 SDDs por subdirectorio (≤80 LOC c/u): protocol/init/runtime/compat + 10 módulos + apps/ - docs/STATUS.md con cifras reales por proyecto - docs/ROADMAP.md con plan a finalización (6 hitos, ~6-8 semanas) - CHANGELOG.md particionado en docs/changelog/<proyecto>.md (7 buckets) Automatización: - scripts/reorg.py — script idempotente que: git mv directorios, renombra package names, recomputa path = refs, reescribe imports rust, actualiza workspace Cargo.toml. Soporta --dry-run. - scripts/split-changelog.py — particiona CHANGELOG por componente. Validación: - cargo check --workspace pasa (124 crates + 2 nuevos cores). - 10 tests adicionales (5 en vista-core + 5 en barra-core) verdes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "brahman-admin"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
publish.workspace = true
|
||||
description = "Brahman — admin API: snapshot del estado del broker (sesiones + matches) por Unix socket, formato JSON."
|
||||
|
||||
[dependencies]
|
||||
brahman-broker = { path = "../brahman-broker" }
|
||||
brahman-card = { path = "../brahman-card" }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
||||
ulid = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
|
||||
[[example]]
|
||||
name = "brahman-status"
|
||||
path = "examples/brahman-status.rs"
|
||||
@@ -0,0 +1,98 @@
|
||||
//! `brahman-status` — CLI para inspeccionar el estado del Init.
|
||||
//!
|
||||
//! Conecta al socket admin (default `$XDG_RUNTIME_DIR/brahman-admin.sock`,
|
||||
//! override con `$BRAHMAN_ADMIN_SOCKET`), recibe el snapshot, y lo imprime.
|
||||
|
||||
use brahman_admin::{client, transport};
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let path = transport::default_socket_path();
|
||||
let snap = client::query(&path).await?;
|
||||
|
||||
println!(
|
||||
"Init: server={} protocol={} attached={}",
|
||||
snap.server_version, snap.protocol_version, snap.init_attached
|
||||
);
|
||||
if let Some(ctx) = &snap.current_context {
|
||||
println!("Context: {}", ctx);
|
||||
}
|
||||
println!();
|
||||
println!("Sessions ({}):", snap.sessions.len());
|
||||
if snap.sessions.is_empty() {
|
||||
println!(" (ninguna)");
|
||||
} else {
|
||||
for s in &snap.sessions {
|
||||
let conscious_marker = if s.wit.is_some() { " 🧠" } else { "" };
|
||||
let kind_marker = match s.kind {
|
||||
brahman_card::CardKind::Ente => "ente",
|
||||
brahman_card::CardKind::Data => "data",
|
||||
};
|
||||
println!(
|
||||
" [{}] {} {}{} lifecycle={:?} priority={:?}",
|
||||
kind_marker, s.session, s.label, conscious_marker, s.lifecycle, s.priority
|
||||
);
|
||||
if let Some(sock) = &s.service_socket {
|
||||
println!(" socket: {}", sock.display());
|
||||
}
|
||||
for r in &s.references {
|
||||
println!(
|
||||
" ref {:?} → {} ({})",
|
||||
r.kind, r.target_label, r.target_id
|
||||
);
|
||||
}
|
||||
if let Some(data) = &s.data {
|
||||
if !data.summary.is_empty() {
|
||||
println!(" summary: {}", data.summary);
|
||||
}
|
||||
if data.member_count > 0 {
|
||||
println!(
|
||||
" members: {} (dispersion={:.2})",
|
||||
data.member_count, data.dispersion
|
||||
);
|
||||
}
|
||||
if !data.keywords.is_empty() {
|
||||
println!(" keywords: {}", data.keywords.join(", "));
|
||||
}
|
||||
if !data.presentation_hint.is_empty() {
|
||||
println!(" lens hint: {}", data.presentation_hint);
|
||||
}
|
||||
}
|
||||
if let Some(wit) = &s.wit {
|
||||
println!(" wit: {} / {}", wit.package, wit.world);
|
||||
if !wit.imports.is_empty() {
|
||||
println!(" imports: {}", wit.imports.join(", "));
|
||||
}
|
||||
if !wit.exports.is_empty() {
|
||||
println!(" exports: {}", wit.exports.join(", "));
|
||||
}
|
||||
}
|
||||
for f in &s.inputs {
|
||||
println!(" in {}: {:?}", f.name, f.ty);
|
||||
}
|
||||
for f in &s.outputs {
|
||||
println!(" out {}: {:?}", f.name, f.ty);
|
||||
}
|
||||
}
|
||||
}
|
||||
println!();
|
||||
println!("Matches ({}):", snap.matches.len());
|
||||
if snap.matches.is_empty() {
|
||||
println!(" (ninguno)");
|
||||
} else {
|
||||
for m in &snap.matches {
|
||||
let pin_marker = if m.pinned { "📌" } else { " " };
|
||||
println!(
|
||||
" {} {}.{} ← {}.{} via {:?}",
|
||||
pin_marker,
|
||||
m.consumer_label,
|
||||
m.consumer.flow_name,
|
||||
m.producer_label,
|
||||
m.producer.flow_name,
|
||||
m.via
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
//! Cliente admin: lee un `StatusSnapshot` desde un socket admin.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use thiserror::Error;
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
use tokio::net::UnixStream;
|
||||
|
||||
use crate::snapshot::StatusSnapshot;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum AdminError {
|
||||
#[error("E/S: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("respuesta vacía")]
|
||||
Empty,
|
||||
#[error("JSON inválido: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
/// Conecta al socket admin, lee la línea JSON y deserializa.
|
||||
pub async fn query(path: impl AsRef<Path>) -> Result<StatusSnapshot, AdminError> {
|
||||
let stream = UnixStream::connect(path).await?;
|
||||
let mut reader = BufReader::new(stream);
|
||||
let mut line = String::new();
|
||||
let n = reader.read_line(&mut line).await?;
|
||||
if n == 0 {
|
||||
return Err(AdminError::Empty);
|
||||
}
|
||||
let snapshot = serde_json::from_str(&line)?;
|
||||
Ok(snapshot)
|
||||
}
|
||||
|
||||
/// Variante sync de [`query`] para callers que no tienen runtime tokio
|
||||
/// (típicamente: GUIs con su propio executor, como GPUI).
|
||||
pub fn query_blocking(path: impl AsRef<Path>) -> Result<StatusSnapshot, AdminError> {
|
||||
use std::io::{BufRead, BufReader as StdBufReader};
|
||||
use std::os::unix::net::UnixStream as StdUnixStream;
|
||||
let stream = StdUnixStream::connect(path)?;
|
||||
let mut reader = StdBufReader::new(stream);
|
||||
let mut line = String::new();
|
||||
let n = reader.read_line(&mut line)?;
|
||||
if n == 0 {
|
||||
return Err(AdminError::Empty);
|
||||
}
|
||||
let snapshot = serde_json::from_str(&line)?;
|
||||
Ok(snapshot)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//! `brahman-admin` — observabilidad del broker.
|
||||
//!
|
||||
//! Expone un Unix socket separado (no se mezcla con el handshake) en el
|
||||
//! que cada conexión recibe un `StatusSnapshot` JSON y se cierra. Es
|
||||
//! single-shot por conexión: pensado para herramientas como
|
||||
//! `brahman-status`, dashboards y health-checks.
|
||||
//!
|
||||
//! Wire format: una línea JSON por conexión, terminada en `\n`. Esto
|
||||
//! hace trivial inspeccionar con `nc` o `socat` además del cliente
|
||||
//! tipado de este crate.
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![warn(rust_2018_idioms)]
|
||||
|
||||
pub mod client;
|
||||
pub mod server;
|
||||
pub mod snapshot;
|
||||
pub mod transport;
|
||||
|
||||
pub use snapshot::StatusSnapshot;
|
||||
|
||||
/// Versión del crate de admin.
|
||||
pub const ADMIN_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
@@ -0,0 +1,110 @@
|
||||
//! Servidor admin: emite un `StatusSnapshot` JSON por conexión y cierra.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use brahman_broker::Broker;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::net::{UnixListener, UnixStream};
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::snapshot::StatusSnapshot;
|
||||
|
||||
/// Configuración del servidor admin.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct AdminConfig {
|
||||
/// `true` si el Init está atado al servidor que aloja este admin.
|
||||
pub init_attached: bool,
|
||||
/// Contexto operativo del broker, espejado en el snapshot.
|
||||
pub current_context: Option<String>,
|
||||
}
|
||||
|
||||
/// Servidor admin escuchando en un Unix socket.
|
||||
pub struct AdminServer {
|
||||
listener: UnixListener,
|
||||
socket_path: PathBuf,
|
||||
broker: Arc<Mutex<Broker>>,
|
||||
config: AdminConfig,
|
||||
}
|
||||
|
||||
impl AdminServer {
|
||||
/// Crea el listener. Si `path` existe, lo elimina (asume socket stale).
|
||||
pub fn bind(
|
||||
path: impl Into<PathBuf>,
|
||||
broker: Arc<Mutex<Broker>>,
|
||||
config: AdminConfig,
|
||||
) -> std::io::Result<Self> {
|
||||
let socket_path = path.into();
|
||||
if socket_path.exists() {
|
||||
std::fs::remove_file(&socket_path)?;
|
||||
}
|
||||
if let Some(parent) = socket_path.parent() {
|
||||
if !parent.as_os_str().is_empty() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
}
|
||||
let listener = UnixListener::bind(&socket_path)?;
|
||||
Ok(Self {
|
||||
listener,
|
||||
socket_path,
|
||||
broker,
|
||||
config,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn socket_path(&self) -> &Path {
|
||||
&self.socket_path
|
||||
}
|
||||
|
||||
/// Loop de aceptación: cada conexión recibe un snapshot y se cierra.
|
||||
pub async fn run(self) -> std::io::Result<()> {
|
||||
loop {
|
||||
let (stream, _addr) = self.listener.accept().await?;
|
||||
let broker = self.broker.clone();
|
||||
let config = self.config.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = handle_conn(stream, broker, config).await {
|
||||
warn!(error = %e, "admin conn falló");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for AdminServer {
|
||||
fn drop(&mut self) {
|
||||
if let Err(e) = std::fs::remove_file(&self.socket_path) {
|
||||
if e.kind() != std::io::ErrorKind::NotFound {
|
||||
warn!(path = %self.socket_path.display(), error = %e, "no se pudo limpiar admin socket");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_conn(
|
||||
mut stream: UnixStream,
|
||||
broker: Arc<Mutex<Broker>>,
|
||||
config: AdminConfig,
|
||||
) -> std::io::Result<()> {
|
||||
let snapshot = build_snapshot(&broker, &config).await;
|
||||
let mut json = serde_json::to_string(&snapshot)?;
|
||||
json.push('\n');
|
||||
stream.write_all(json.as_bytes()).await?;
|
||||
stream.shutdown().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn build_snapshot(broker: &Arc<Mutex<Broker>>, config: &AdminConfig) -> StatusSnapshot {
|
||||
let b = broker.lock().await;
|
||||
let sessions: Vec<_> = b.cards().cloned().collect();
|
||||
let matches = b.all_matches();
|
||||
StatusSnapshot {
|
||||
server_version: crate::ADMIN_VERSION.to_string(),
|
||||
protocol_version: brahman_card::PROTOCOL_VERSION.to_string(),
|
||||
init_attached: config.init_attached,
|
||||
current_context: config.current_context.clone(),
|
||||
sessions,
|
||||
matches,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
//! Tipos del snapshot que el admin server emite.
|
||||
|
||||
use brahman_broker::{BrokeredCard, Match};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Snapshot completo del estado del Init en un instante.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StatusSnapshot {
|
||||
/// Versión del crate del Init que respondió.
|
||||
pub server_version: String,
|
||||
/// Versión del protocolo brahman.
|
||||
pub protocol_version: String,
|
||||
/// `true` si el Init está atado al servidor.
|
||||
pub init_attached: bool,
|
||||
/// Contexto operativo activo del broker (p. ej. `"test"`, `"prod"`).
|
||||
/// `None` si no hay contexto configurado — los biases per-contexto
|
||||
/// declarados en las Cards quedan inactivos.
|
||||
#[serde(default)]
|
||||
pub current_context: Option<String>,
|
||||
/// Cards actualmente registradas (sesiones vivas).
|
||||
pub sessions: Vec<BrokeredCard>,
|
||||
/// Matches consumer↔producer derivados del set actual.
|
||||
pub matches: Vec<Match>,
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
//! Convenciones de transporte para el socket admin.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Variable de entorno que sobreescribe la ruta del socket admin.
|
||||
pub const SOCKET_ENV: &str = "BRAHMAN_ADMIN_SOCKET";
|
||||
|
||||
/// Nombre del socket admin dentro del runtime dir.
|
||||
pub const SOCKET_NAME: &str = "brahman-admin.sock";
|
||||
|
||||
/// Ruta canónica al socket admin del Init.
|
||||
pub fn default_socket_path() -> PathBuf {
|
||||
if let Ok(p) = std::env::var(SOCKET_ENV) {
|
||||
return PathBuf::from(p);
|
||||
}
|
||||
let base = std::env::var_os("XDG_RUNTIME_DIR")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(std::env::temp_dir);
|
||||
base.join(SOCKET_NAME)
|
||||
}
|
||||
Reference in New Issue
Block a user