feat(nouser): Phase B-2 — daemon que publica Mónadas al Init brahman

Cierra la unificación ontológica de B-1: el nouser daemon se
sidecarea como Ente y publica cada Mónada como su propia sesión Data.
Un solo brahman-status muestra procesos y datos en la misma lista.

Cambios:

- nouser-core gana deps brahman-card + brahman-sidecar.
- bin nouser nuevo subcomando: daemon <dir>.
  1. Spawna sidecar para el engine (brahman.nouser_engine, kind=Ente):
     el "ser" que produce y administra Mónadas.
  2. Scan + cluster del directorio.
  3. Para cada Mónada, monad.to_brahman_card() + sidecar (kind=Data).
     Cada Mónada es una sesión brahman propia, con su ULID estable.
  4. Park del main thread; los sidecars siguen pingueando.

Validación end-to-end:

  $ ente-zero &
  $ NOUSER_MIN_FILES=5 nouser daemon crates/core &
  $ brahman-status

  Sessions (6):
    [ente]  ... brahman.nouser_engine  lifecycle=Daemon
    [data]  ... src    summary: 5 archivos en crates/core/brahman-admin/src
                       members: 5 (dispersion=0.00)  lens hint: code
    [data]  ... src    summary: 11 archivos en crates/core/ente-brain/src
    [data]  ... graph  summary: 7 archivos en crates/core/ente-zero/src/graph
    ...

La función de presentarse es la misma para procesos y datos. UI ve
una lista uniforme y discrimina por `kind` cuando le importa.

Costo conocido: cada Mónada consume thread + tokio runtime
current_thread (legacy del sidecar API). Para escalar a >50 Mónadas
conviene consolidar en un único runtime con N tasks. Defer a B-3.

Pendientes propuestos (en CHANGELOG):
- B-3: consolidar sidecars en un solo runtime.
- C: pseudo-embeddings + atracción por centroide.
- D: módulo nouser-nous para LLM, swappable por priority_contexts.
- Polish: labels con 2-3 componentes de path.
- Crossreferencia: Ente anuncia "procesando Mónada X", Mónada anuncia
  "siendo procesada por Ente Y".

cargo check --workspace: 0 errores, 0 warnings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sergio
2026-05-08 18:24:50 +00:00
parent b85700c538
commit 85886b7a3c
4 changed files with 123 additions and 1 deletions
+2
View File
@@ -10,6 +10,8 @@ description = "Nouser — explorador de Mónadas: scanner, clustering determinis
[dependencies]
nouser-card = { path = "../card" }
brahman-card = { path = "../../../core/brahman-card" }
brahman-sidecar = { path = "../../../shared/brahman-sidecar" }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
+65 -1
View File
@@ -34,6 +34,7 @@ fn main() -> ExitCode {
"scan" => cmd_scan(rest),
"show" => cmd_show(rest),
"json" => cmd_json(rest),
"daemon" => cmd_daemon(rest),
"--help" | "-h" | "help" => {
print_usage(&prog);
return ExitCode::SUCCESS;
@@ -61,9 +62,11 @@ fn print_usage(prog: &str) {
eprintln!(" scan <dir> recorre un directorio y lista las Mónadas detectadas");
eprintln!(" show <dir> <prefix> scan + detalle de la Mónada cuyo ID empieza con <prefix>");
eprintln!(" json <dir> scan + dump JSON de todos los manifests");
eprintln!(" daemon <dir> scan + sidecarea cada Mónada al Init brahman");
eprintln!();
eprintln!("env:");
eprintln!(" NOUSER_MIN_FILES mínimo de archivos por Mónada (default: 3)");
eprintln!(" NOUSER_MIN_FILES mínimo de archivos por Mónada (default: 3)");
eprintln!(" BRAHMAN_INIT_SOCKET socket del Init (heredado de brahman-handshake)");
}
type Cmd = Result<(), Box<dyn std::error::Error>>;
@@ -155,3 +158,64 @@ fn cmd_json(args: &[String]) -> Cmd {
println!("{}", serde_json::to_string_pretty(&manifests)?);
Ok(())
}
fn cmd_daemon(args: &[String]) -> Cmd {
let dir = require_dir(args)?;
// 1. El propio engine se presenta como Ente.
let engine_card = build_engine_card();
eprintln!(
"nouser daemon: publicando engine '{}' (kind=Ente)",
engine_card.label
);
brahman_sidecar::spawn(engine_card);
// 2. Scan y cluster.
let (db, n_files) = run_scan(&dir)?;
eprintln!(
"nouser daemon: {} archivos en {}, {} mónadas detectadas",
n_files,
dir.display(),
db.monad_count()
);
// 3. Cada Mónada se presenta como Card de tipo Data.
let mut handles = Vec::with_capacity(db.monad_count());
for monad in db.monads() {
let card = monad.to_brahman_card();
match brahman_sidecar::spawn_with_handle(brahman_sidecar::SidecarConfig::new(card)) {
Ok(h) => handles.push(h),
Err(e) => eprintln!(
"nouser daemon: falló sidecar para mónada '{}': {e}",
monad.label
),
}
}
eprintln!(
"nouser daemon: {} sidecars activos (1 ente + {} data). Ctrl-C para terminar.",
handles.len() + 1,
handles.len()
);
// 4. Park: el proceso principal queda dormido, los sidecars siguen
// pingueando en sus threads.
std::thread::park();
Ok(())
}
/// Card del propio engine (kind=Ente). Es el "ser" que produce y
/// administra Mónadas; aparece en brahman-status junto a sus Mónadas.
fn build_engine_card() -> brahman_card::Card {
use brahman_card::{ulid::Ulid, Card, CardKind, Lifecycle, Payload, Priority, Supervision};
Card {
schema_version: brahman_card::CARD_SCHEMA_VERSION,
id: Ulid::new(),
label: "brahman.nouser_engine".into(),
payload: Payload::Virtual,
supervision: Supervision::Delegate,
lifecycle: Lifecycle::Daemon,
priority: Priority::Normal,
kind: CardKind::Ente,
..Default::default()
}
}