Commit Graph

72 Commits

Author SHA1 Message Date
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 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 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 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