Commit Graph

441 Commits

Author SHA1 Message Date
Sergio ad0d475a2e feat(brahman-net): capa P2P compartida — Fase 0 (extracción del swarm)
Primer paso del plan "el encuentro entre Entes no se restringe a local".
El swarm libp2p que vivía dentro de minga-p2p::network (282 LOC) sale
a una crate compartida brahman-net para que cualquier protocolo de la
familia (handshake brahman remoto en Fase 1, sync minga, futuros) reuse
una sola malla TCP+Noise+Yamux+Kad+Identify+Stream.

BrahmanNet expone:
- new() / with_keypair() para identidad efimera o persistente
- API de comandos uniforme: dial, listen, add_dht_peer,
  find_closest_peers, start_providing, find_providers
- Publica peer_id (libp2p) y control (stream::Control) — cada
  protocolo registra su StreamProtocol sin acoplarse al swarm
- Re-exporta Stream y StreamProtocol para evitar dep directa a libp2p

minga-p2p::network reduce de 282 LOC a 22: re-export del nuevo
BrahmanNet bajo el alias historico LibP2pNode (zero churn en
MingaPeer) y la const SYNC_PROTOCOL = "/minga/sync/1.0.0" especifica
del sub-protocolo de sync Minga.

Aclaracion semantica anclada por el usuario: Arje es el init (PID 1),
Brahman es el encuentro entre Entes. El nombre brahman-net refleja
que la malla pertenece al encuentro, no al runtime — Minga es un
cliente de la malla, no su dueño.

Tests: minga-p2p completo verde (58 tests, sin regresion). Behavior
identico — solo se movio codigo, ningun cambio funcional.
2026-05-09 12:29:16 +00:00
Sergio 6f993f4268 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.
2026-05-09 03:32:11 +00:00
Sergio 2ae888bc8f feat(explorer+daemon): discovery dinamico via broker + query socket
Cierra el "explorer encuentra al daemon de forma totalmente dinamica"
del meta-plan. La UI deja de hardcodear el socket admin: descubre al
daemon nouser via MatchEvent::Available del broker y le consulta sus
Monadas directo.

Pipeline end-to-end:
- Daemon publica engine Card con service_socket = $XDG_RUNTIME_DIR/
  nouser-engine.sock y flow.output = monad-list:json.
- Daemon binda Unix socket en ese path con listener blocking que
  sirve nouser_card::query::QueryRequest::ListMonads, responde
  ListMonadsResponse { engine, monads: Vec<MonadView> }.
- Explorer construye consumer Card con flow.input matched,
  brahman_sidecar::await_provider_blocking le devuelve el socket,
  y nouser_core::engine_socket::client::list_monads lo consulta.
- Cachea el socket; cualquier fallo de query lo invalida y la
  proxima iteracion re-descubre.

Wire types nuevos en nouser_card::query:
- QueryRequest::ListMonads
- ListMonadsResponse { engine: EngineInfo, monads: Vec<MonadView> }
- MonadView: proyeccion slim de MonadManifest SIN centroid ni
  members (KB que no tienen por que viajar cada poll).
- transport::default_socket_path() con env override.

Listener en nouser_core::engine_socket: spawn_listener + client
blocking con QueryError tipado. 3 tests integracion verdes.

Refactor explorer:
- Drop dep brahman-admin, add brahman-sidecar/nouser-card/nouser-core.
- State: socket cache + snapshot + socket_source informativo.
- TickOutcome enum desacopla la I/O del UI.

Trade-offs: polling 2s (no streaming — broker no empuja Data cards
hoy), re-discovery full en error (discovery es barato).

Tests: 10 (nouser-card +3 query) + 27 (nouser-core +3 engine_socket)
+ 4 (sidecar) verdes. Explorer compila clean.
2026-05-09 03:08:20 +00:00
Sergio b23ddf2980 feat(nous-real): cache de embeddings + write-through al CAS de arje
Cierra el ciclo del feedback: el modelo real (fastembed-allMiniLML6V2,
~1-50ms por archivo) era invocado ciegamente en cada re-cluster del
watcher. Ahora se cachea por sha256(bytes-vistos) + model_id, con
write-through al CAS de arje.

Pipeline en handle_file:
1. Lee primeros 8 KiB del archivo (igual que antes).
2. file_sha = ente_cas::sha256_of(buf) — hash de los bytes que el
   modelo *realmente* verá. Garantiza que un archivo creciendo mas
   alla de la ventana sin tocar la cabeza siga sirviendo cache hits.
3. Cache lookup -> HIT: respuesta en us, sin invocar fastembed.
4. MISS: ente_cas::store(&buf) (write-through, no-fatal si falla) ->
   backend.embed_one(text) -> cache.put(...).

Backend de cache: sled local en
$XDG_CACHE_HOME/brahman/nouser-nous-real-embed-cache.sled. Tree
versionado embed_cache_v1; el MODEL_ID viaja en la key, asi que
cambiar de modelo invalida el cache implicitamente. Override por env
NOUSER_NOUS_REAL_CACHE.

Encoding compacto: cada Vec<f32> se serializa como bytes little-endian
(4B por f32, sin overhead). Para 384-d son 1.5 KiB por entry. Decode
tolera bytes corruptos (longitud no-multiplo de 4 -> None, no panic).

Por que sled y no ente-cas directo: el CAS de arje es flat
sha256-keyed; la cache necesita un mapeo (file_sha, model_id) ->
embedding, no expresable como entry CAS. El write-through a CAS queda
como registro consultable + futura GC.

Mock NO se modifica — su embedding pseudo-32d es metadata-hashing puro,
sin costo. Cachearlo seria overhead.

Tests: 5 unitarios verdes (roundtrip, miss, model collision, content
collision, corrupted value). Stub mode (sin feature) sigue compilando
sin tocar cache.
2026-05-09 02:57:55 +00:00
Sergio 79d42aba28 chore(nakui): alinear nakui-core con [workspace.package] y deps compartidas
Cleanup de drift de convenciones: nakui-core era el unico crate del
monorepo que manteia version, edition y thiserror hardcoded, mientras
el resto heredaba del workspace y usaba thiserror v2. Eso significaba
que un bump global de version o edition se olvidaba sistematicamente
de nakui.

Cambios:
- [package]: version, edition, rust-version, license, authors, publish
  -> todos *.workspace = true. Agregado description (convencion).
- Deps compartidas migradas a { workspace = true }: serde, serde_json,
  thiserror (v1->v2), tokio, ulid, sha2.
- uuid migrado a { workspace = true, features = ["serde"] } — la feature
  serde no esta en el workspace dep porque nakui es el unico user;
  queda local opt-in en lugar de inflar el dep comun.
- Deps especificas de nakui (sin comparticion posible): rhai, petgraph,
  surrealdb permanecen inline con version local.

Verificacion: cargo build -p nakui-core verde tras el bump thiserror
v1->v2 — los 14+ enums de error de nakui no requirieron ajustes
(derive backwards-compat para patrones simples). cargo test -p
nakui-core --lib: 27/27 verdes.

Bonus en este commit: discovery.rs movio el import Ulid a #[cfg(test)]
porque el refactor a Card::new lo dejo unused en module-scope.
2026-05-09 02:49:41 +00:00
Sergio 4c9e4c3962 feat(card): Card::new(label) — alternativa segura a Default::default()
Cierra la trap documentada de Card::default() que devuelve id =
Ulid::nil(). Usar Card::default() viva colisionaba con cualquier otra
Card default-construida bajo el mismo id 00000...

La fix no es romper Default (sigue siendo determinista, requerido por
callers que lo usan como template para deserializacion y patterns de
busqueda) sino agregar un constructor explicito Card::new(label) que
asigna id = Ulid::new() + label provisto, manteniendo defaults seguros
en todo lo demas.

Pensado para struct-literals con override parcial:

    let card = Card {
        kind: CardKind::Data,
        payload: Payload::Embedded(json),
        ..Card::new("mi-modulo.algo")
    };

Refactor de call sites en codigo de produccion:
- brahman_sidecar::discovery::build_consumer_card
- nouser daemon::build_engine_card

Default queda con docstring expandida que apunta a Card::new para uso
"vivo". to_brahman_card en nouser-card NO se modifica porque asigna
el id estable de la Monada, no uno fresco.

Tests: 3 unitarios nuevos en brahman-card. 15 tests verdes (era 12).
2026-05-09 02:43:21 +00:00
Sergio 006640057a feat(sidecar): API reusable de discovery via broker
Promueve el patron ad-hoc discover_producer_socket que vivia inline en
'nouser attract --remote' a un modulo publico brahman_sidecar::discovery.
Cualquier consumer ahora puede preguntar al broker "quien provee este
TypeRef?" sin reimplementar el patron a mano.

API:
- build_consumer_card(label, flow_name, type_name) construye una Card
  minima (Ente, Oneshot, Virtual) con un input flow. Asigna Ulid::new()
  real (no nil), evitando colisiones en el broker.
- await_provider(card, timeout) async: conecta al init, espera
  MatchEvent::Available, devuelve producer_service_socket, manda
  Farewell. Ignora eventos Lost durante el await.
- await_provider_blocking(card, timeout) wrapper para mundos no-async
  (CLIs, std-thread loops). Crea su propio runtime current_thread.
- ConsumerError tipado: Connect{socket,source}, NoProvider{flow,type_ref,
  timeout}, Client(ClientError), Runtime(String). Adios al Box<dyn Error>.

Refactor en nouser daemon: discover_producer_socket inline (60 LOC) ->
5 LOC delegando en el helper. remote_embed ya no construye su propio
runtime.

Tests: 4 unitarios (id no-nil, id unico por llamada, formateo de Wit
TypeRef, fallback sin input). Build verde para sidecar y nouser-core.
2026-05-09 02:30:48 +00:00
Sergio 2725d6a297 feat(nouser+sidecar): watcher con debounce 150ms + re-publish al broker
Cierra los dos pendientes documentados en 487c457: el spam de eventos
duplicados de notify y la falta de propagación al broker cuando una
Mónada cambia composición.

SidecarPool ahora es idempotente respecto a Card.id: spawn rastrea un
HashMap<Ulid, AbortHandle> y aborta la sesión previa si el id ya
existía. Nuevo drop_session(id) para cerrar Mónadas que desaparecen y
live_sessions() para introspección.

Watcher reorganizado en dos threads: dispatcher filtra notify a un
canal de paths; coordinator agrupa con HashMap<PathBuf, Instant> y
dispara batch sólo cuando todos llevan ≥150ms quietos. Cada batch
re-scanea + re-clusteriza con hidratación + diffea contra prior:
removidas → drop_session, nuevas o con composición distinta → spawn
(que reemplaza la sesión previa). Re-scan global por batch es
deliberado y O(N archivos) — aceptable hasta que duela.
2026-05-09 01:37:39 +00:00
Sergio 487c457e5b feat(nouser): notify watcher — el sistema reacciona en tiempo real
El daemon monta notify::recommended_watcher recursivo sobre el dir
escaneado. Cada Create/Modify de archivo regular dispara:
embedding → filtro por centroid_model → ranking contra centroides →
log con 🧲 / · según supere DEFAULT_ATTRACTION_THRESHOLD.

  $ nouser daemon /tmp/x &
  $ vim /tmp/x/src/nuevo.rs
  [watcher] 🧲 /tmp/x/src/nuevo.rs  →  x/src  (0.7470)

  $ echo edit >> /tmp/x/docs/n1.md
  [watcher] 🧲 /tmp/x/docs/n1.md  →  x/docs  (0.8169)

Mecánica:
- DB pasa a Arc<Mutex<MonadDb>> para sharing con thread watcher.
- Watcher en thread dedicado nouser-watcher; reacciona sólo a
  Create/Modify, ignora Access/Metadata-only.
- react_to_change(path, metadata, db) computa embedding, filtra por
  centroid_model, busca best attraction.
- No re-publica al broker ni muta DB — sólo observa y narra. La
  invalidación selectiva (re-cluster + replace + diff publish) queda
  para futuro.

Limitación conocida: notify emite múltiples eventos por edición
(Create + Modify, etc.). Sin debounce el watcher reporta varias
veces. Aceptable para demo; producción conviene debounce ~100ms
por path.

Esto cierra la Fase C del plan post-reporte: el sistema "se siente"
vivo. Tocar un archivo en vim y ver inmediatamente la atracción
calculada cumple el meta-mensaje "Mónada Viva".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 01:06:31 +00:00
Sergio 65af98da13 feat(nouser): hidratación del daemon vía sled + path_hint
El daemon ya no recomputa ciegamente al arrancar. Si la DB tiene
Mónadas previas con centroid_model válido, las publica instantáneo
y el re-scan reusa sus IDs vía path_hint.

Schema:
- MonadManifest.path_hint: Option<String> — identidad estable
  derivada del origen (para by_directory, el parent dir canónico).
  Permite reusar ULID across re-scans.

Cluster:
- Nueva fn cluster::by_directory_hydrated(files, min_files, prior).
  Con prior, busca Mónada con mismo path_hint Y mismo centroid_model;
  si la encuentra, reusa id, lineage y created_at_ms.
- by_directory queda como wrapper sin hidratación (back-compat).

Daemon (cmd_daemon):
1. Open sled si NOUSER_DB_PATH existe.
2. Publica Mónadas previas con centroid_model válido (las inválidas
   se descartan con log explícito).
3. Re-scan + by_directory_hydrated(prior=&db).
4. Sólo spawnea sidecars para Mónadas con id NUEVO. Los path_hints
   existentes preservan identidad, evitando duplicados en el broker.
5. Persiste el set actualizado.

Validación:
  $ NOUSER_DB_PATH=/tmp/h.sled nouser daemon crates/core
  # arranque 1: re-scan 102 archivos → 5 mónadas (5 nuevas)
  $ NOUSER_DB_PATH=/tmp/h.sled nouser daemon crates/core
  # arranque 2: hidratadas 5 mónadas en O(1)
  #             re-scan → 5 mónadas (0 nuevas vs hidratación)

Costo del arranque 2: ~0.06s user CPU.

Tests: 7 (card) + 24 (core) verdes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 00:55:05 +00:00
Sergio 820a1a33bf feat(nouser): centroid_model — versionado de embeddings
Protege contra el bug silencioso de mezclar centroides de modelos
distintos (mock 32-d vs real 384-d), que daría scores sin sentido.

- MonadManifest.centroid_model: Option<String>. None = legacy.
- nouser_core::embed::MODEL_ID = "nouser-pseudo-32d". Cluster lo
  setea en cada Mónada que genera.
- nouser-nous-mock reusa la misma constante (use
  nouser_core::embed::MODEL_ID): produce vectores idénticos al
  cluster local, reportar el mismo ID es honesto.
- nouser-nous-real ya reportaba "real-fastembed-allMiniLML6V2-384d";
  el filter ahora lo descarta automáticamente cuando los centroides
  cacheados son del mock.
- cmd_attract:
  - Captura el model_id del embedding del target.
  - Filtra Mónadas cuyo centroid_model no matchee.
  - Reporta "embed: <source> (<model>)" y "skipped: N" cuando
    descarta.

Resultado: cambiar de mock a real vía BRAHMAN_BROKER_CONTEXT=prod
hace que attract filtre las Mónadas viejas con cero score en lugar
de fingir que las puede comparar.

Tests: 7 (card) + 24 (core) verdes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 00:24:38 +00:00
Sergio 9c371ee43e feat: profile.dev slim + dynamic binding del consumer Nous
Dos piezas del plan post-reporte, en un commit por estar acopladas
(ambas tocan cómo se construye y conecta el sistema):

profile.dev slim:
- debug = "line-tables-only" + split-debuginfo unpacked +
  codegen-units 256 en [profile.dev].
- Override [profile.dev.package.{gpui,ort,fastembed,tokenizers,image}]
  con opt-level=1, debug=false para los pesados que no debuggeamos.
- Resultado: binarios ~3× más livianos. ente-zero 125→47 MB;
  mock-nous ~50→22 MB. target/ futuro mucho más manejable.

dynamic binding (cierra priority_contexts):
- nouser-core Cargo.toml: deps directas brahman-handshake + tokio.
- cmd_attract refactor:
  - Si NOUSER_NOUS_SOCKET está set, atajo explícito (compat).
  - Si no, abre Client al brahman-init, anuncia consumer Card con
    flow.input = embed-result:json, espera 3s por MatchEvent::Available,
    usa producer_service_socket del evento.
- discover_producer_socket() es async; cmd_attract usa runtime tokio
  current_thread inline (block_on).
- embed_via(path, file) se separa como helper sync para la RPC.

Validación end-to-end:
  $ ente-zero & nouser-nous-mock &
  $ nouser attract --remote crates/core archivo.rs
    🧲  0.9058  ente-brain/src  ...
  (mock log: "embed_file path=archivo.rs" — discovery activo)

Con esto BRAHMAN_BROKER_CONTEXT=test/prod swappea el provider sin que
el consumer toque nada — la promesa de priority_contexts es real.

Bug colateral resuelto: la "flakiness" del cargo test --workspace era
disco lleno (24 GB en target/), no condición de carrera. Con
cargo clean + profile slim, tests deterministas.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 22:23:44 +00:00
Sergio 5c41ef920d feat(nouser): yahweh widget — nouser-explorer panel GPUI
Bin GPUI standalone que consulta brahman-admin cada 2s y renderea
todas las sesiones del Init como cards. Cierra el círculo visual
del ecosistema brahman.

- Crate nuevo crates/apps/nouser-explorer (deps: brahman-admin,
  brahman-card, gpui).
- Ventana 900x640 con header del estado del Init, banner de error
  cuando no conecta, y lista de cards (una por sesión).
- Cada card muestra: kind + label + lifecycle, ULID corto, summary
  (si data), keywords, lens hint, service_socket si está, y refs
  (RelationshipKind → target_label). El borde izquierdo coloreado
  diferencia ente (azul) de data (lavanda).
- cx.spawn(async move |this, cx| { ... }) corre el loop de refresh
  en el GPUI executor; query_blocking porque GPUI no tiene runtime
  tokio.
- Helper nuevo en brahman-admin: client::query_blocking(path) —
  versión sync de query(), para callers con su propio executor.

Uso:
  $ ente-zero & nouser daemon crates/core &
  $ cargo run -p nouser-explorer
  # ventana 900x640, ~6 cards en vivo, refrescando cada 2s.

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 19:54:49 +00:00
Sergio 7831c0c827 feat(nouser): persistencia sled write-through del MonadDb
MonadDb ahora soporta backend dual:
- MonadDb::new() → memoria pura (default, back-compat).
- MonadDb::open(path) → sled-backed con cache en memoria. Carga
  contenido existente al abrir; cada insert_* hace write-through
  (cache + sled).

Diseño:
- 2 trees sled: files y monads.
- Wire format: serde_json (ergonomía + inspectability con sled-cli;
  los manifests son chicos, JSON gana sobre postcard aquí).
- Reads SIEMPRE desde la cache — sled se consulta sólo al abrir.
- replace_monads() purga el tree de sled antes de escribir.

Bin nouser: nueva env var NOUSER_DB_PATH. Si está set, persiste; si
no, in-memory:

  $ NOUSER_DB_PATH=/tmp/monads.sled nouser scan crates/core
  scan: 102 archivos, 5 mónadas
  $ ls /tmp/monads.sled
  blobs  conf

Tests nuevos en db.rs:
- persistence_roundtrip — escribe, cierra, reabre, datos están.
- replace_monads_purges_persistent_tree — replace limpia tree.

24 tests en nouser-core (era 22, +2).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 19:50:37 +00:00
Sergio d7b4886164 feat(sidecar): Phase B-3 — SidecarPool consolida sidecars en un runtime
Antes: cada spawn(card) creaba un thread + tokio runtime propio.
Para módulos con muchas sesiones (nouser daemon con 50+ Mónadas)
eso es 50 threads + 50 runtimes. Ahora: un thread + un runtime
tokio current_thread que hostea N tasks de sidecar.

API nueva (aditiva, no rompe spawn/spawn_with_handle):

  let pool = SidecarPool::new()?;
  pool.spawn(card1);
  pool.spawn(card2);
  pool.spawn_conscious(card_with_wit, wit);
  pool.spawn_with_config(custom_config);
  // pool drop = todas las sesiones cierran.

run_client se hace pública para que el pool pueda enqueuar tasks
externos al runtime con handle.spawn(run_client(config)).

nouser daemon migrado al pool. Verificación con ps -L:

  $ ps -L -p $(pidof nouser)
    LWP   CMD
    28817 nouser              # main thread
    28819 brahman-sidecar     # pool thread (todas las sesiones)

Antes serían 6+ LWP (1 main + N sesiones). Ahora 2 fijos sin
importar cuántas Mónadas se publiquen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 19:47:21 +00:00
Sergio b3feaf667c feat: Crossreferencia — Card.references como grafo del fractal
Las Cards ahora declaran sus relaciones con otras Cards. El Engine
posee Mónadas; las Mónadas declaran que son poseídas por el Engine.

- brahman-card:
  - RelationshipKind { Owns, OwnedBy, Processes, ProcessedBy, Sibling }
  - CardReference { kind, target_id: Ulid, target_label: String }.
    target_label es cache para que la UI renderee sin resolver.
  - Card.references: Vec<CardReference> + espejo en WireCard.
    Conversiones From propagan.
- brahman-broker::BrokeredCard propaga references.
- brahman-status imprime "ref OwnedBy → label (id)" por sesión.
- nouser daemon: cada Mónada publicada añade OwnedBy apuntando al
  engine. Declaración unilateral — el engine no necesita conocer
  Mónada IDs de antemano.

Validación end-to-end:
  $ ente-zero & nouser daemon crates/core
  $ brahman-status
  Sessions (6):
    [ente]  brahman.nouser_engine
    [data]  brahman-handshake/src
        ref OwnedBy  →  brahman.nouser_engine  (01K...)
    [data]  ente-brain/src
        ref OwnedBy  →  brahman.nouser_engine  (01K...)
    ...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 19:44:47 +00:00
Sergio 5edc912ed8 feat: Phase D-3 + D-4 — service_socket en Card, providers coexisten
Cierra el ciclo del swap automático Nous mock↔real:

- brahman-card: Card.service_socket: Option<PathBuf> y espejo en
  WireCard. Path del data plane (distinto al Init). Cualquier
  consumer que matchee con esta Card conecta directo, sin discovery
  extra.
- brahman-broker: BrokeredCard propaga service_socket. Sin
  participación en matching — sólo metadata.
- brahman-handshake::MatchEvent: nuevo campo
  producer_service_socket. Server lo busca en BrokeredCard al emitir
  Available.
- nouser-nous::transport: provider_socket_path(provider: &str)
  devuelve nouser-nous-{provider}.sock por default. Mock y real
  coexisten en sockets distintos (Phase D-4). default_socket_path()
  conserva el comportamiento single-provider.
- Mock declara nouser-nous-mock.sock; real declara
  nouser-nous-real.sock. La Card se construye DESPUÉS del bind.
- brahman-status imprime "socket:" por sesión cuando está presente.

Validación end-to-end:
  $ ente-zero & nouser-nous-mock & nouser-nous-real &
  $ ls /run/user/1001/nouser-nous-*.sock
    nouser-nous-mock.sock
    nouser-nous-real.sock
  $ brahman-status
  Sessions (2):
    [ente]  nouser.nous_real
        socket: /run/user/1001/nouser-nous-real.sock
    [ente]  nouser.nous_mock
        socket: /run/user/1001/nouser-nous-mock.sock

Pendiente (no crítico): nouser-core attract --remote usa todavía
NOUSER_NOUS_SOCKET hardcoded. Siguiente paso: subscribirse al
MatchEvent del broker y usar producer_service_socket directo, así
BRAHMAN_BROKER_CONTEXT=test/prod swapea provider sin tocar al
consumer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 19:38:23 +00:00
Sergio 794884a90f refactor(nouser): labels de Mónada con 2 componentes del path
Resuelve la fricción visual de monorepos donde múltiples Mónadas
quedaban con label "src" (ambiguo). Nueva función label_from_path
toma los últimos hasta 2 componentes normales del path:

  $ nouser scan crates/core
    [01K..] brahman-admin/src      card=5
    [01K..] brahman-handshake/src  card=6
    [01K..] ente-brain/src         card=11
    [01K..] ente-kernel/src        card=4

Tests añadidos: label_from_root_only_one_component,
label_from_deep_path_takes_last_two. Tests existentes actualizados
con los nuevos labels (proj/src en lugar de src).

22 tests en nouser-core (era 20, +2).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 19:28:19 +00:00
Sergio 11fc95629c feat(nouser): Phase D-2 — proveedor Nous real (LLM) detrás de feature
Cierra el ciclo del módulo Nous: existe un proveedor que produce
embeddings reales con un modelo LLM, mientras que `cargo build` sin
features sigue siendo liviano (no descarga ni compila ML deps).

Crate nuevo crates/modules/nouser/nous-real con dos modos según feature:

- Sin feature (default): stub.
  cargo build -p nouser-nous-real (~10s, sin ML deps).
  Bin arranca, sidecarea a brahman-init declarando la Card,
  escucha en el socket Nous, rechaza requests con un ErrorResponse
  explicativo: "compilado sin la feature embeddings, rebuild con
  cargo build -p nouser-nous-real --features embeddings".
  cargo build --workspace SIGUE siendo limpio.

- Con --features embeddings: real.
  Pulls fastembed = "4" → ort 2.0.0-rc.9 (ONNX Runtime con binarios
  descargados por Cargo) + tokenizers 0.21 + ~30 transitive deps.
  Compila en ~50s.
  Modelo default: all-MiniLM-L6-v2 (384-d, descargado a
  ~/.cache/fastembed la primera vez).
  EmbedText: pasa el texto al modelo → vector 384-d.
  EmbedFile: lee primeros 8KiB UTF-8 lossy, embed como texto.
  Ping: devuelve model_id + embed_dim reales.

Card declara label "nouser.nous_real" + priority_contexts.prod = +1.
En contexto prod gana sobre el mock; en test el mock gana por su +1
en test. Sin contexto, empate alfabético.

Validación end-to-end con modelo real:
  $ ente-zero & nouser-nous-real &
  $ python3 socket-probe '{"kind":"embed_text","payload":{"text":"..."}}'
    model: real-fastembed-allMiniLML6V2-384d
    elapsed_ms: 8
    embed_dim: 384

Tradeoff: dim mock (32) vs real (384) son incompatibles. Cambiar
proveedor invalida centroides cacheados — documentar "limpiar DB al
swap".

Workspace state:
- cargo build --workspace limpio sin features (no ML deps pulled).
- cargo build -p nouser-nous-real --features embeddings funciona.
- 0 errores, 0 warnings en ambos modos.

Pendientes para D-3 / futuro:
- Discovery de socket: el consumer hoy usa NOUSER_NOUS_SOCKET hardcoded.
  Para que el broker elija real vs mock per-contexto, falta o un campo
  socket en el MatchEvent o un broker query "dame socket de session X".
- Coexistencia: ambos providers compiten por el mismo socket path por
  default. Parametrizarlos cuando se quiera correrlos juntos.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 19:08:27 +00:00
Sergio b3c3c00cf2 feat(nouser): Phase D — proveedor Nous mock + cliente remoto
Cierra el patrón "Nous como módulo aparte intercambiable": el contrato
del proveedor de embeddings vive en su crate, el mock determinístico
implementa ese contrato sirviéndolo por Unix socket, y nouser-core
sabe consumirlo remotamente. El switch mock↔real (futuro) será vía
priority_contexts en el broker.

Crates nuevos:

- crates/modules/nouser/nous: contrato compartido.
  - EmbedRequest { kind: { EmbedFile | EmbedText | Ping }, payload }.
  - EmbedFilePayload (path, ext, size, mtime), EmbedTextPayload.
  - EmbedResponse (embedding, model, elapsed_ms), PingResponse,
    ErrorResponse.
  - Wire: line-delimited JSON sobre Unix socket, single-shot.
  - Constants FLOW_EMBED_REQUEST, FLOW_EMBED_RESULT, FLOW_TYPE_NAME.
  - transport::default_socket_path con env NOUSER_NOUS_SOCKET.

- crates/modules/nouser/nous-mock: bin nouser-nous-mock.
  - Sidecarea a brahman-init con Card kind=Ente declarando los flows
    embed-request/embed-result + priority_contexts.test = +1.
  - Bind del socket Nous + accept loop tokio.
  - EmbedFile delega a nouser_core::embed::embed (Phase C).
  - Modelo: "mock-pseudo-32d".

Cambios:

- nouser-core: dep nueva nouser-nous. Subcomando attract --remote
  abre un UnixStream blocking, envía EmbedRequest, lee response.
  Imprime "embed: local|remote" para ver cuál ruta corrió.

Bug encontrado y corregido:
- ContextBias tenía #[serde(skip_serializing_if = ...)] en sus campos.
  Postcard NO soporta skip-condicional en formatos no self-describing:
  el serializer omitía bytes que el deserializer esperaba, rompiendo
  la wire de cualquier Card con priority_contexts poblada.
  Síntoma: "postcard decode: Hit the end of buffer" en el server,
  "early eof" en el cliente.
- Fix: removidos los skip_serializing_if de ContextBias. JSON pretty
  ahora emite {"pin_to": null, "priority_offset": 0} pero el wire
  funciona. Trade-off aceptado.
- Test wirecard_postcard_with_priority_contexts en brahman-card que
  ejercita el roundtrip postcard con biases poblados.

Validación end-to-end:
  $ ente-zero & nouser-nous-mock & nouser daemon crates/core
  $ brahman-status
  Sessions (7):
    [ente] nouser.nous_mock      flows: embed-request, embed-result
    [ente] brahman.nouser_engine
    [data] src   summary: 6 archivos en crates/core/brahman-handshake/src
    [data] graph summary: 7 archivos en crates/core/ente-zero/src/graph
    ...
  $ nouser attract --remote crates/core <archivo>.rs
    embed: remote
    🧲  0.9058  src  ...
  (mock log: embed_file path=...)

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

Próximo natural: Phase D-2 — real-nous con ONNX/Llama text-embedding.
Declara la misma Card con priority_contexts.prod = +1 y el swap es
transparente para el consumer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 18:49:25 +00:00
Sergio 77faf12e82 feat(nouser): Phase C — pseudo-embeddings + atracción por centroide
El "imán semántico" matemático del diseño Kairos, sin LLM. Cada
archivo se proyecta a un vector 32-d determinista derivado de sus
metadatos; cada Mónada calcula su centroide; archivos nuevos se
asignan por cosine similarity contra los centroides existentes.

Cambios:

- nouser-core dep nueva: blake3 (hash determinista de strings).
- crates/modules/nouser/core/src/embed.rs nuevo:
  - EMBED_DIM = 32. Vector:
    * dims 0..8:  blake3(extension)
    * dims 8..16: blake3(parent_dir)
    * dims 16..24: blake3(file_stem)
    * dims 24..28: tamaño (log + flags)
    * dims 28..32: mtime (escala día + features cíclicas)
  - Tip clave: hash bytes se centran a [-1, 1] (no [0, 1]). Sin
    centrar, dos hashes random tendrían cosine ~0.75 espurio.
    Centrados, expectativa ≈ 0 entre no-relacionados.
  - APIs: embed, cosine_similarity, centroid, cohesion,
    attraction_score, best_attraction. DEFAULT_ATTRACTION_THRESHOLD = 0.7.
- cluster::by_directory ahora computa el centroide de cada Mónada
  y lo guarda en MonadManifest.centroid. El centroide viaja al
  brahman-status vía DataFacet.centroid.
- bin nouser nuevo subcomando: attract <dir> <file>.
  - Scan del dir, embedding del archivo objetivo, ranking de afinidad
    contra Mónadas con centroide.
  - 🧲 si la mejor supera umbral, · si es mejor pero debajo.

Validación end-to-end:

  $ nouser attract crates/core crates/modules/nouser/core/src/embed.rs
    🧲  0.9058  [01K..] src  (ente-brain/src)
        0.8984  [01K..] src  (brahman-handshake/src)
        ...

  $ nouser attract crates/core crates/modules/nouser/core/Cargo.toml
        0.3427  [01K..] graph  (ente-zero/src/graph)
    (mejor score 0.3427 < umbral 0.7000 — no se 'pega')

7 tests nuevos en embed (determinismo, normalización, similitud
mismo-dir/mismo-ext, baja entre no-relacionados, centroide
unidad+coherente, attraction picks correctly, vacío skipeado).

Tests acumulados: 73. cargo check --workspace: 0 errores, 0 warnings.

Próximo: Phase D — nouser-nous, módulo aparte para LLM real.
Mock-nous determinista (basado en estos pseudo-embeddings) en
BRAHMAN_BROKER_CONTEXT=test; real-nous en prod. El switch lo hace
el broker via priority_contexts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 18:31:04 +00:00
Sergio 85886b7a3c 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>
2026-05-08 18:24:50 +00:00
Sergio b85700c538 feat: Phase B-1 — unificación ontológica de Cards (Ente ↔ Data)
La Card pasa a ser EL protocolo de presentación del ecosistema. Una
Mónada Nouser y un Ente Brahman son ambos "entidades que se presentan";
el consumidor (UI, broker, admin) discrimina por `kind` cuando importa,
pero todos hablan el mismo idioma.

brahman-card:
- CardKind { Ente (default), Data }. Backward-compat: Cards existentes
  quedan Ente.
- DataFacet { summary, keywords, centroid, member_count, dispersion,
  presentation_hint } — vista liviana para el wire. Listas grandes
  (members reales, embeddings completos) se consultan al daemon dueño
  bajo demanda.
- Card.kind y Card.data agregados. WireCard espeja, conversiones From
  propagan ambos campos.
- Default impl actualizado.

brahman-broker:
- BrokeredCard propaga kind y data desde la Card registrada. No afecta
  el matching (sigue por TypeRef + priority + pin_to); permite a
  observadores discriminar sin re-query.

nouser-card:
- Depende ahora de brahman-card.
- MonadManifest::to_brahman_card() proyecta una Mónada a Card brahman:
  - id, label, lineage directos.
  - payload Virtual, supervision Delegate, lifecycle Daemon
    (placeholder — la Mónada no se ejecuta).
  - kind = Data.
  - data = Some(DataFacet { summary, keywords, centroide,
    member_count, entropy → dispersion, presentation_hint del Lens }).
- Test nuevo projects_to_brahman_card.

brahman-status:
- Prefijo [ente] o [data] por sesión.
- Sesiones data renderean también summary, members + dispersion,
  keywords y lens hint.

Resultado: la UI ve una sola lista uniforme — no necesita saber si
mira procesos o cúmulos de datos, sólo lee el Card y se adapta por
kind. La función de presentarse es la misma para todos.

Tests: 59 (card 11, broker 15, handshake codec+tr 2 + integ 7,
card-wit 4, admin 0, nouser-card 7 +1, nouser-core 13).
cargo check --workspace: 0 errores, 0 warnings.

Próximo: Phase B-2 — bin nouser daemon que sidecarea cada Mónada como
sesión brahman, mezclándolas con los entes en brahman-status.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 18:20:51 +00:00
Sergio 7bdc26e61a feat(nouser): Phase A — mecanismo determinista de Mónadas
Primer trozo de Nouser/Kairos: explorador de Mónadas como agrupaciones
semánticas sobre el filesystem, sin tocar IA todavía. Cubre el 90% de
los casos con heurísticas puras.

Crates nuevos:

crates/modules/nouser/card:
- MonadManifest: la Tarjeta de Presentación de una Mónada. Espejo
  conceptual de brahman::Card pero para datos: id (Ulid), label,
  summary, centroid (vacío en Phase A), keywords, cardinality, entropy
  [0,1], dominant_lens (Grid|Code|Gallery|Database|Markdown|Tree),
  pins, members, timestamps, extensions (forward-compat).
- Diferencia explícita en docs: brahman::Card describe entidades
  runtime con payload/soma/supervision; MonadManifest describe una
  agrupación de datos sin proceso atrás.
- Validación: schema_version, label no vacío, entropy en rango,
  cardinality consistente con members.len().
- 6 tests (validación + JSON roundtrip).

crates/modules/nouser/core:
- scanner::scan_directory: walkdir → Vec<FileEntry> con metadatos.
  Skipea hidden por default; configurable max_depth y follow_links.
- cluster::by_directory: agrupa archivos por parent dir, mínimo 3
  para promover a Mónada (configurable). Computa keywords (top-N
  extensiones por freq + alfabético), elige Lens dominante por
  extensión más frecuente, entropía de Shannon normalizada.
- db::MonadDb: store en memoria con índices BTreeMap.
  resolve_members filtra IDs huérfanos.
- bin nouser con subcomandos scan, show, json. Env var
  NOUSER_MIN_FILES para el threshold.
- 13 tests (4 scanner + 6 cluster + 3 db).

Demo end-to-end:

  $ nouser scan crates
  scan: 255 archivos en crates, 19 mónadas (min_files=3)
    [01KR4C13] src       card=12  ent=0.00  lens=Code  keywords: rs
    [01KR4C13] tests     card=14  ent=0.00  lens=Code  keywords: rs
    [01KR4C13] fixtures  card=5   ent=0.00  lens=Grid  keywords: rhai

Pendientes (anotados en CHANGELOG, no urgentes):
- Phase B: bin nouser daemon que sidecarea a brahman-init.
- Phase C: pseudo-embeddings de metadatos + atracción por centroide.
- Phase D: módulo nouser-nous para el LLM real, swappable por
  priority_contexts (mock-nous en test, real-nous en prod).
- Polish: labels con 2-3 componentes del path.

cargo check --workspace: 0 errores, 0 warnings.
Tests acumulados: 58.

CHANGELOG.md actualizado.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 18:03:49 +00:00
Sergio bbb9a9d2f5 feat(broker): priority contexts — biases per-contexto operativo
Cierra el último pendiente de feature: el broker ahora puede operar
bajo un contexto (test/prod/foreground/secure/etc) que activa biases
declarados en las Cards.

Schema (brahman-card):
- ContextBias { pin_to: Option<String>, priority_offset: i8 }.
- Card.priority_contexts: BTreeMap<String, ContextBias>, también en
  WireCard. Las conversiones From propagan el campo.

Comportamiento (brahman-broker):
- BrokerConfig.current_context: Option<String>. Cuando es Some(ctx) y
  una Card tiene priority_contexts.get(ctx), el bias aplica:
   - Consumer-side: bias.pin_to sobreescribe Flow.pin_to estático.
   - Producer-side: bias.priority_offset se suma a la priority base
     (clamp en [Low=0, Critical=3]).
- BrokeredCard propaga priority_contexts. find_producer_for usa
  effective_priority y context_bias en lugar de comparar Priority
  directo.

Observabilidad:
- AdminConfig.current_context + StatusSnapshot.current_context.
- brahman-status imprime "Context: <nombre>" si está activo.

Wiring:
- ente-zero lee BRAHMAN_BROKER_CONTEXT del entorno y la propaga al
  broker y al admin. Sin var, biases inactivos (back-compat total).

Tests nuevos (brahman-broker, +4):
- context_priority_offset_lifts_producer_above_alphabetic_winner:
  sin contexto a-prod gana por alfabético; con context "test" b-prod
  gana por offset +1.
- context_pin_to_overrides_static_pin: static pin "real-dht", test
  override "mock-dht" → mock gana en context "test".
- unknown_context_no_op: biases declarados para "test" no aplican
  cuando broker está en "prod".
- priority_offset_clamps_to_critical: offset enorme se clampa a 3.

Validación end-to-end manual:
  $ BRAHMAN_BROKER_CONTEXT=test ente-zero &
  $ brahman-status
  Init: server=0.1.0 protocol=0.1.0 attached=true
  Context: test

Tests acumulados: 39 (card 11, broker 15, handshake codec+transport 2 +
integ 7, card-wit 4, admin 0). cargo check --workspace: 0 errores, 0
warnings.

Con esto cierran TODOS los pendientes técnicos abiertos. El único
"pendiente" que queda es el caso real para extender (priority
contexts per-deployment, scheduling biases dinámicos, etc.).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 17:46:59 +00:00
Sergio f19ca723b6 feat(card): WireCard + extensions — forward-compat sin romper postcard
Restaura el campo extensions de Card que había caído al adoptar postcard
(serde_json::Value usa secuencias/maps de longitud dinámica). La
solución es separar dos formas:

- Card (la rica): para JSON/TOML. Tiene extensions: BTreeMap<String,
  serde_json::Value> con #[serde(flatten, skip_serializing_if = is_empty)].
  Los campos desconocidos del archivo sobreviven el roundtrip.
- WireCard (la slim): para postcard. Mismo schema sin extensions y con
  genesis: Vec<WireCard> recursivo. Postcard-friendly por construcción.

Conversiones From<Card> for WireCard (descarta extensions) y
From<WireCard> for Card (extensiones quedan vacías post-wire). El
contrato es explícito: extensions son anotaciones locales que sobreviven
file I/O pero NO cruzan al Init.

brahman-handshake::Hello.card cambia de Card a WireCard. Client hace
card.into() al enviar; Server hace hello.card.into() para volver a
Card antes de validar/registrar.

Tests:
- 3 nuevos en brahman-card: extensions_preserved_in_json_roundtrip,
  wire_card_roundtrip_strips_extensions, wire_card_postcard_friendly
  (verifica que postcard::to_allocvec(&wire) NO falla — caso que
  rompía con Card.extensions populadas).
- 1 ajuste en handshake/tests/handshake.rs (struct-literal de Hello
  ahora con card: sample_card(...).into()).
- brahman-card: postcard como dev-dep.

Tests acumulados: 35 (card 11, broker 11, handshake codec+transport 2 +
integ 7, card-wit 4, admin 0). 0 errores, 0 warnings (vienen del
commit anterior 9420eae).

CHANGELOG.md actualizado con esta entrada y con el commit 9420eae
("probando" del usuario, limpieza de 17 warnings dead-code).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 17:33:15 +00:00
Sergio 9420eae0b6 probando 2026-05-08 17:27:10 +00:00
Sergio 354f992c63 feat(sidecar): WIT al sidecar — módulos conscientes vivos
Cierra el ciclo brahman-card-wit ↔ runtime: un módulo que tenga su
.wit lo parsea, lo manda en Hello, y aparece como "consciente" en el
broker y en brahman-status.

Cambios coordinados (un solo commit por la cadena de tipos):

- brahman-card::WitInterface deriva Serialize/Deserialize/Eq.
- brahman-handshake::Hello lleva wit: Option<WitInterface> (#[serde(default)]
  para tolerar Hellos antiguos en formato JSON aunque postcard exige
  presencia explícita).
- Server's register_session enruta a ResolvedCard::from_conscious cuando
  viene wit; from_agnostic cuando no.
- Client::connect queda como wrapper de connect_with(path, card,
  wit: Option<WitInterface>) — backward-compatible.
- Broker::register acepta Option<WitInterface> como tercer arg; BrokeredCard
  guarda el wit. 25 sitios de tests actualizados con `, None` (vía perl).
- brahman-sidecar::SidecarConfig.wit + helpers SidecarConfig::with_wit
  y spawn_conscious(card, wit). Log attached reporta conscious=true|false.
- brahman-status pretty-print con 🧠 + sección wit (package/world +
  imports + exports) por sesión consciente.
- Example nuevo presence-conscious: parsea protocol.wit y se presenta
  consciente.

Validación end-to-end manual:

  $ ente-zero &
  $ presence-conscious demo.conscious shared_wit/protocol.wit &
  $ brahman-status
  Sessions (1):
    01K... demo.conscious 🧠  lifecycle=Daemon
        wit: brahman:protocol@0.1.0 / module
             imports: types, handshake, lifecycle
             exports: run

Tests: 32/32 (broker 11 + card 8 + handshake codec+transport 2 + integ 7
+ admin 0 + card-wit 4). Workspace: 0 errores.

CHANGELOG.md actualizado.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 17:22:48 +00:00
Sergio f4dc019004 feat(core): brahman-card-wit — extractor opcional de contratos WIT
Crate nuevo crates/core/brahman-card-wit que parsea texto WIT con
wit-parser y devuelve un Vec<WitInterface> (de brahman-card),
listo para acoplarse a una ResolvedCard::from_conscious(card, wit).

Ámbito intencional: sólo parsing texto, no toca wasm-tools ni
wit-component. Es opt-in: brahman-card no depende de éste.

API pública:
- parse_wit(source: &str) -> Result<Vec<WitInterface>, WitError>
- parse_wit_file(path: impl AsRef<Path>) -> Result<...>

Cada WitInterface incluye: package, world, imports, exports.
Las interfaces importadas/exportadas (no sólo funciones) se
resuelven por nombre via resolve.interfaces[id].name; las
funciones inline aparecen como WorldKey::Name directo.

Example CLI: brahman-wit-info <ruta.wit> imprime los worlds.

  $ brahman-wit-info shared_wit/protocol.wit
  2 world(s):
    package: brahman:protocol@0.1.0
    world:   module
    imports: types, handshake, lifecycle
    exports: run
    ...

Tests: 4/4 (inline + archivo real + parse error + world vacío).
Workspace: 0 errores.

CHANGELOG.md actualizado con la entrada nueva y la del commit
anterior (7b589b8) que faltaba.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 17:05:17 +00:00
Sergio 7b589b863d chore: agrega CHANGELOG.md retroactivo
Registro cronológico de los 11 commits previos en la raíz del repo.
Cada entrada lista las acciones concretas tras un commit.

A partir de este punto, cada cambio sustantivo se documenta también
acá en el mismo commit que el código.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 16:57:36 +00:00
Sergio 8a83a26de0 feat(handshake): notificación push de matches del broker al cliente
El servidor empuja MatchEvent (Available | Lost) a los consumers cuando
sus inputs cambian de match — sea porque un productor llegó, porque
otro mejor lo desplazó, o porque desapareció.

Mecánica:

- Frame::MatchEvent con MatchEventKind { Available, Lost } y los datos
  del match (consumer_flow, producer_session/label/flow, ty, via, pinned).
- Server: SessionTxTable (Arc<Mutex<HashMap<SessionId, mpsc::Sender>>>)
  + LastMatches (último match conocido por consumer/input). En cada
  register/unregister, broadcast_match_diffs recomputa con el broker
  y emite SOLO los diffs respecto al estado anterior.
- Session::run_post_handshake usa tokio::select! para multiplexar
  read_frame del cliente y rx.recv() de su tx push.
- Cleanup ahora también limpia push_table y last_matches y dispara un
  broadcast (para notificar a quienes pierden el match).
- Client: VecDeque<MatchEvent> bufferea eventos que llegan mezclados
  con respuestas a Ping. API:
    - take_event() — non-blocking, drena buffer
    - await_event(timeout) — bloquea hasta evento o timeout
- ping() ahora drena MatchEvents intermedios hasta encontrar el Pong.

Capacity del canal push por sesión: 32 frames (try_send no-blocking;
si se llena, los eventos extra se descartan — se documenta como
ephemeral, el cliente puede re-consultar via brahman-status).

Test nuevo en brahman-handshake/tests/handshake.rs:
- match_event_pushed_on_producer_arrival: consumer espera, no recibe
  evento → llega productor → recibe Available → productor se va →
  recibe Lost.

Example nuevo: brahman-handshake/examples/subscriber.rs — cliente que
loguea cada MatchEvent en tiempo real. Útil para ver la dinámica del
broker. Pings cada 25s para keepalive.

Demo end-to-end verificada (4 eventos, 3 ya cubren el ciclo completo):

  T+0.3  alpha llega    → Available ← demo.alpha.out
  T+0.8  beta llega     → (sin evento: alpha gana por orden alfabético)
  T+1.3  alpha killed   → Available ← demo.beta.out (re-evaluación)
  T+1.8  beta killed    → Lost ← <none>

El broker emite diff: ningún evento cuando un nuevo productor llega
sin desplazar al ganador actual.

Tests: 28/28 (handshake integ 6→7). cargo check --workspace: 0 errores.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 15:43:41 +00:00
Sergio 70a7a0d46d feat: segundo módulo (nakui) + admin API + brahman-status
Dos cosas en una sesión, en el orden discutido:

(1) Segundo módulo brahman vivo: nakui-core
  - crates/modules/nakui/core/Cargo.toml: deps brahman-card,
    brahman-sidecar, ulid.
  - crates/modules/nakui/core/src/bin/nakui.rs: brahman_card_for_nakui()
    construye una Card como Lifecycle::Daemon, Supervision::Restart,
    flow.input "command" (json) + flow.output "report" (json). El
    cmd_run llama brahman_sidecar::spawn antes de levantar el server
    de nakui.

(2) crates/shared/brahman-sidecar (estrena crates/shared/)
  Boilerplate del sidecar extraído (DRY): el thread con tokio current
  thread runtime, conexión vía Client::connect, ping loop. Yahweh y
  nakui ahora consumen este crate. API:
  - spawn(card)                    fire-and-forget
  - spawn_with_handle(config)      con JoinHandle
  Example "presence" útil para demos: módulo dummy con label tomado
  del primer arg que se queda vivo hasta SIGTERM.

(3) crates/core/brahman-admin: observabilidad del broker
  Socket Unix paralelo en \$BRAHMAN_ADMIN_SOCKET (default
  \$XDG_RUNTIME_DIR/brahman-admin.sock). Cada conexión recibe un
  StatusSnapshot JSON line-delimited y se cierra. Compatible con nc/socat.
  - StatusSnapshot { server, protocol, init_attached, sessions, matches }
  - server::AdminServer
  - client::query(path)
  - example "brahman-status" CLI

(4) Wiring de ente-zero
  En primordial_loop, junto al handshake server, ahora también levanta
  AdminServer con misma política de degradación grácil.

(5) brahman-broker: BrokeredCard ahora incluye lifecycle. Endpoint y
  Match derivan Serialize/Deserialize. Nuevo método cards() expone
  iterador de BrokeredCard para que el admin pueda construir snapshots.

(6) brahman-card: re-export pub use ulid::* para que módulos no
  necesiten depender de ulid directamente.

(7) yahweh-shell migrado al sidecar compartido. Su brahman_client.rs
  pasa de 96 a 53 líneas: sólo declara la Card, delega el spawn.

Demo end-to-end:
  $ ente-zero &
  $ presence demo.producer &
  $ presence demo.consumer &
  $ brahman-status

  Init: server=0.1.0 protocol=0.1.0 attached=true
  Sessions (2):
    01KR42TY1J... demo.producer  lifecycle=Daemon  priority=Normal
    01KR42TY1K... demo.consumer  lifecycle=Daemon  priority=Normal
  Matches (2):
    demo.producer.in  ←  demo.consumer.out  via Exact
    demo.consumer.in  ←  demo.producer.out  via Exact

El broker matchea bidireccional por tipo. El admin lo expone.

Tests: 27/27. cargo check --workspace: 0 errores.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 15:21:49 +00:00
Sergio 595f68e252 feat(yahweh-shell): primer módulo brahman vivo
yahweh-shell se presenta al Init brahman como módulo Widget mediante un
sidecar en thread aparte. La GUI GPUI levanta normalmente; el sidecar
mantiene la sesión brahman en paralelo, desacoplado.

Cambios:

- crates/apps/yahweh-shell/Cargo.toml: deps brahman-card, brahman-handshake,
  ulid.
- crates/apps/yahweh-shell/src/brahman_client.rs: thread con tokio
  current_thread runtime que arma una Card, llama Client::connect, y
  loop de pings cada 30s. Si el Init no está disponible, loggea y
  termina — yahweh sigue funcionando standalone.
- crates/apps/yahweh-shell/src/main.rs: brahman_client::spawn() antes
  de Application::new(). El spawn no bloquea.

Card declarada por yahweh:
- label: "brahman.ui_engine"
- lifecycle: Widget
- payload: Virtual (yahweh no se inicia desde el Init, se presenta)
- supervision: Delegate
- permissions: filesystem read-write (persiste layout.json), IPC wit-v1
- flow.input: render-data (json)
- flow.output: user-intent (json)

Validación end-to-end:
  $ ente-zero &
  $ probe                          → session=...8G, init_attached=true
  $ yahweh                         → [brahman] attached: session=...Y7

Ambos clientes (probe + yahweh sidecar) se registran en el broker del
Init en sesiones distintas. yahweh es el primer módulo "real" — no un
tester — que vive como nodo del fractal mientras corre.

Tests: 27/27 verdes. cargo check --workspace: 0 errores.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 15:08:03 +00:00
Sergio df9d10cc52 feat(ente-zero): enchufa el handshake server al Init real
ente-zero (PID 1 del fractal arje) ahora levanta el server de
brahman-handshake junto al ente-bus existente, escuchando en
\$BRAHMAN_INIT_SOCKET (default \$XDG_RUNTIME_DIR/brahman-init.sock).
Es un canal paralelo dedicado a módulos brahman-conscientes que se
presentan con una Card y declaran flujos tipados.

Cambios:

- crates/core/brahman-handshake/src/transport.rs: helper nuevo con
  resolución XDG_RUNTIME_DIR → TMPDIR, override por var de entorno
  BRAHMAN_INIT_SOCKET. Test unitario para el override.
- crates/core/brahman-handshake/Cargo.toml: example "probe" + dev-dep
  anyhow. Probe sirve como herramienta de diagnóstico para conectar
  contra cualquier server vivo.
- crates/core/brahman-handshake/examples/probe.rs: cliente mínimo que
  hace Hello → Ping → Farewell e imprime el HelloAck recibido.
- crates/core/ente-zero/Cargo.toml: dependencias brahman-handshake
  + brahman-broker.
- crates/core/ente-zero/src/main.rs: en primordial_loop, tras spawn
  del ente-bus, crea Arc<Mutex<Broker>> compartido y llama
  Server::bind. Si el bind falla (FS no escribible, socket en uso),
  loggea y degrada a "modo bus-only" — la doctrina PID 1 no rompe por
  subsistemas opcionales (mismo patrón que uevents).

Validación end-to-end manual:
  $ BRAHMAN_INIT_SOCKET=/tmp/e2e.sock ./target/debug/ente-zero &
  $ BRAHMAN_INIT_SOCKET=/tmp/e2e.sock cargo run --example probe
    HelloAck: session=01KR41Q8... server=0.1.0 protocol=0.1.0 init_attached=true
    Pong: ts=1778252489714ms
    Farewell OK

Tests: 27/27 (broker 11 + card 8 + handshake codec+transport 2 + integ 6).
cargo check --workspace: 0 errores.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 15:02:29 +00:00
Sergio 07d77a335f feat(handshake): integra el broker con el ciclo de sesiones
ServerConfig acepta un Option<Arc<Mutex<Broker>>> compartido. Cuando está
presente, el servidor lo mantiene en sincronía con las sesiones:

- Tras un Hello aceptado, register_session indexa la Card en el broker
  ANTES de insertar en el SessionRegistry y de emitir HelloAck.
- Al cerrar la sesión (Farewell, EOF, o error en run_post_handshake), un
  cleanup() unificado llama unregister en el broker y remove en el
  SessionRegistry. Garantizado por refactor de Session::handle a
  do_handshake → run_post_handshake → cleanup.

Tests nuevos en handshake.rs:
- broker_registers_and_unregisters_with_session: confirma el ciclo
  register → farewell → unregister.
- broker_matches_two_live_modules: dos clientes (productor + consumidor)
  conectados; el broker resuelve find_producer_for(consumer.session, "in")
  → producer "dht". Tras farewell del productor, el match desaparece.

Fix colateral: brahman-card::TypeRef pasa de internally-tagged
(#[serde(tag = "kind")]) a externally-tagged (default). Postcard no
soporta internally-tagged en formatos no self-describing — sin este
cambio el wire de Hello con Cards que tengan flujos no codificaba.
JSON cambia de {"kind":"primitive","name":"x"} a
{"primitive":{"name":"x"}}. Documentado en el doc-comment de TypeRef.

26/26 tests verdes (broker 11 + card 8 + handshake codec 1 + integ 6).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 14:54:45 +00:00
Sergio 5091106c21 feat(core): brahman-broker — matching híbrido productor↔consumidor
Crate nuevo en crates/core/brahman-broker que indexa Cards por SessionId
y empareja flow.input de un consumidor con flow.output del productor más
adecuado.

Tres ejes de matching:

1. Estrategia (MatchStrategy):
   - Exact: igualdad estricta de TypeRef.
   - Structural: misma forma — para Wit, mismo package + name (ignora
     interface); para Primitive, mismo name.
   - ExactThenStructural (default): prefiere Exact; cae en Structural si
     no hay. Reporta cuál ganó en Match.via.

2. Override pin_to: si el consumidor declara pin_to = "label", el broker
   prefiere productores con ese label (siempre que el tipo matchee).
   Si la pista falla, cae en type-search general. Match.pinned indica
   qué camino se siguió.

3. Prioridad: empate de tipo se resuelve por Card.priority (Critical >
   High > Normal > Low). Empate de prioridad se resuelve lexicográfica-
   mente por label (estable y determinista).

API mínima:
- Broker::new(config)
- register(session, &Card) / unregister(session)
- find_producer_for(consumer_session, input_name) -> Option<Match>
- all_matches() -> Vec<Match> (introspección)

El broker es stateless w.r.t. routes: cada query se computa bajo demanda.
Sólo persiste el índice de BrokeredCard (vista mínima: label, priority,
inputs, outputs).

Cambio aditivo en brahman-card: Priority deriva PartialOrd/Ord/Hash para
ser usable como tiebreaker.

Tests: 11/11.
- exact_match_same_typeref
- structural_ignores_interface
- exact_strategy_rejects_interface_mismatch
- exact_then_structural_prefers_exact
- pin_to_overrides_type_search
- pin_to_unresolvable_falls_back_to_type_match
- priority_breaks_ties
- label_alpha_breaks_priority_ties
- unregister_removes_producer
- no_self_loops
- all_matches_lists_pairs

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 14:30:21 +00:00
Sergio 814390feec feat(core): brahman-handshake — protocolo runtime Init↔módulo
Crate nuevo en crates/core/brahman-handshake que implementa el handshake
real del shared_wit/protocol.wit como wire format Rust↔Rust sobre Unix
socket con frames length-prefixed + cuerpo postcard.

Componentes:

- src/messages.rs: Hello, HelloAck, Ping, Pong, Farewell, HandshakeError
  y Frame (enum-suma). HandshakeError ya implementa thiserror::Error y
  cruza el wire.
- src/codec.rs: write_frame / read_frame asíncronos con MAX_FRAME_BYTES
  de 4 MiB. Test interno de roundtrip.
- src/server.rs: Server::bind crea el listener en Unix socket; emite
  ResolvedCard tras validar la Card y devuelve ULID como SessionId.
  ServerConfig.init_attached se reporta en HelloAck.
- src/client.rs: Client::connect hace pre-validación local de la Card
  (fail fast), envía Hello, parsea HelloAck. ping() y farewell() expuestos.
- tests/handshake.rs: 4 tests de integración:
   * full_handshake_roundtrip — happy path con 3 pings + farewell
   * rejects_invalid_card_client_side — label vacío rechazado pre-envío
   * server_rejects_protocol_mismatch — protocol_version 999.0.0 → Error
   * ping_before_hello_rejected — Ping sin Hello previo → Rejected

Limitación conocida: postcard no serializa serde_json::Value (variantes
Array/Object con length dinámico). Se removieron por eso los campos
`extensions` (Card) y `extra` (Permissions). Forward-compat queda
cubierta por schema_version + protocol_version negotiation; si más
adelante necesitamos preservar campos JSON desconocidos, irá en un
WireCard separado o un envelope.

13/13 tests verdes (brahman-card 8 + brahman-handshake codec 1 + integ 4).
cargo check --workspace: 0 errores.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 13:57:49 +00:00
Sergio ed0e973c81 refactor(arje): migra ente-card a re-export de brahman-card
ente-card pasa a ser un crate-shim que re-exporta los tipos de
brahman-card bajo sus nombres legacy:

- EntityCard ≡ brahman_card::Card (alias)
- Capability, Payload, SomaSpec, Supervision, etc. — pub use directo

Cambios concretos:

- crates/core/brahman-card/src/lib.rs: añade impl Default for Card.
  Permite usar `..Default::default()` en struct-literals para los
  campos aditivos (permissions, lifecycle, priority, flow, extensions).
- crates/core/ente-card/src/lib.rs: reescrito como shim de re-export
  (~25 líneas). Las definiciones, validaciones y tests viven en
  brahman-card.
- crates/core/ente-card/Cargo.toml: deps reducidas a brahman-card; se
  eliminan serde/serde_json/ulid (vienen transitivos vía re-export).
- crates/core/ente-zero/src/seed.rs: 4 struct-literals de EntityCard
  ahora terminan con `..Default::default()` para cubrir los nuevos
  campos del schema híbrido.

Los 21 consumidores de ente-card (ente-zero, ente-bus, ente-brain,
ente-soma, ente-cas, los 12 *-compat, etc.) compilan sin cambios —
sus `use ente_card::EntityCard` y demás imports siguen resolviendo,
ahora a tipos de brahman-card.

cargo test -p brahman-card: 8/8.
cargo build -p ente-zero: OK.
cargo check --workspace: 0 errores.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 12:10:35 +00:00
Sergio 0feba74503 feat(core): brahman-card — Tarjeta de Presentación canónica híbrida
Crate nuevo en crates/core/brahman-card que unifica el modelo de arje y
el de brahman/core_protocol original:

De arje (ente-card):
- Identidad ULID + lineage opcional
- Capability tipado (Spawn, Journal, Endpoint, Device, Netlink, ...)
- Payload discriminado (Wasm | Native | Virtual | Legacy)
- SomaSpec (namespaces, cgroups, rlimits, cpu_affinity)
- Supervision (Restart con backoff, OneShot, Delegate)
- genesis: Vec<Card> recursivo
- Validación exhaustiva (label, self-dep, payload, rlimits, cgroup)

Aditivo brahman:
- flow: Flows con TypeRef discriminado (Primitive | Wit)
- pin_to opcional como pista para el broker
- Permissions enumerados (NetworkingPolicy, FsPolicy, IpcPolicy)
- Lifecycle ortogonal (Daemon | Oneshot | Widget)
- Priority de scheduling
- TrustLevel derivado de Permissions (no declarado)
- ResolvedCard con WitInterface opcional
- extensions: HashMap para forward-compat

Formatos: JSON canónico + TOML, auto-detectados por extensión de archivo.

Tests: 8/8 incluyendo arje_seed_format_compatible que valida que el
formato JSON existente de arje sigue parseando con defaults para los
campos nuevos.

ente-card original sigue intacto; los consumidores (ente-zero, etc.)
migrarán en una pasada posterior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 07:54:35 +00:00
Sergio 4d50bfc587 chore: absorbe nakui (ERP matemático) en modules/nakui
- crates/modules/nakui/core/: el crate nakui-core (4 bins, tests).
  Deps directas (serde, rhai, surrealdb, petgraph, sha2, uuid, tokio,
  thiserror v1) — no convertidas a workspace = true en esta pasada.
- crates/modules/nakui/modules/{inventory,sales,treasury}/: datos
  declarativos del dominio (nsmc.json, schema.k, morphisms/) que el
  crate consume — no son crates.

cargo check -p nakui-core: 0 errores.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 05:49:58 +00:00
Sergio 53dbdf0f1d chore: monorepo inicial con arje + minga + yahweh absorbidos
Workspace en 4 ejes (core/modules/apps/shared):

- core/: 24 crates de arje (Init systemd-compatible: ente-card, ente-zero,
  ente-kernel, ente-bus, ente-cas, ente-soma, ente-wasm, ente-snapshot,
  ente-brain, ente-echo, ente-policy-provider, + 12 crates *-compat)
- modules/semantic_dht/: 5 crates de minga (minga-core con AST/CAS/MST,
  minga-p2p con libp2p Kad, minga-store, minga-vfs, minga-cli)
- modules/ui_engine/: 11 crates de yahweh (libs/{core,theme,bus,providers},
  widgets/{tree,splitter,tabs,tiled,container_core,text_input})
- apps/: 5 crates de yahweh (file_explorer, database_explorer, text_viewer,
  image_viewer, yahweh-shell)
- shared_wit/protocol.wit: handshake/lifecycle inicial

Cargo.toml unificado: thiserror bumped a 2 (transparente para arje), tokio
"full", paths intra-workspace de yahweh redirigidos a su nueva ubicación.

cargo check --workspace: 0 errores, 17 warnings (dead code preexistente).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 04:45:44 +00:00