# Changelog — chasqui Explorador semántico de Mónadas. Renombrado de `nouser` → `akasha` → el 2026-05-19. ### feat(nouser-explorer): integración al stack yahweh themed Iter 10. `nouser-explorer` (la app paralela a `nakui-explorer` para ver Mónadas via daemon nouser) tenía colors hardcoded idénticos al patrón previo. Aplico el mismo refactor que se hizo para `nakui-explorer` en iter 4: instala el theme global, migra chrome a slots, usa los widgets `banner_themed` / `card_themed` / `theme_switcher`. Cambios en `nouser-explorer`: - **Nuevas deps**: `yahweh-theme`, `yahweh-widget-banner`, `yahweh-widget-card`, `yahweh-widget-theme-switcher`. - **`main()`**: `Theme::install_default(cx)` antes de `cx.open_window`. - **`render`**: 4 vars `let X = rgb(...)` (chrome) → theme slots (`bg_app`/`fg_text`/`fg_muted`/`bg_panel`/`border`). - **Header**: gana flex_row + theme switcher en la derecha (mismo pattern que nakui-explorer). - **`error_banner`**: pasa de div hardcoded a `banner_themed(cx, Banner::Error, ...)` con override de padding (16/8) por convención del header. - **2 cards de Engine y Monad**: pasan de `div().flex().flex_col() .p().mb().bg(card_bg).rounded().border_l_4().border_color()...` a `card_themed(cx).border_l_4().border_color(accent)...`. - **Acentos semánticos**: `accent_engine` (cyan, las "máquinas") y `accent_data` (purple, las Mónadas) quedan locales — son señales del dominio nouser, no del chrome. Tests: workspace stack intacto. nouser-explorer no tiene tests propios (siempre fue una vista live del daemon, sin lógica testable separada). Beneficio operativo: las dos apps explorer del repo (`nakui-explorer` para event log + `nouser-explorer` para Mónadas) ahora comparten la misma paleta themed + el mismo control de switcher. Si un usuario las corre lado a lado, la consistencia visual emerge sola. ### feat(nous-real): cache de embeddings + write-through al CAS de arje Cierra el ciclo de la crítica del usuario: "Si un archivo no ha cambiado su hash en el CAS, Nouser ni siquiera debería pedirle al LLM que re-genere el embedding". El modelo real (`fastembed-allMiniLML6V2-384d`, ~1-50ms por archivo) era invocado ciegamente en cada re-cluster del watcher. Ahora se cachea por `sha256(bytes-vistos) + model_id`. Pipeline en `handle_file`: 1. Lee primeros 8 KiB (igual que antes). 2. `file_sha = ente_cas::sha256_of(buf)` — hash de los bytes que el modelo *realmente* verá (no del archivo completo). Garantiza que un archivo creciendo más allá de la ventana sin tocar la cabeza siga sirviendo cache hits. 3. Cache lookup: HIT → respuesta en ~µs. 4. MISS → `ente_cas::store(&buf)` (write-through al CAS de arje, 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, así que cambiar de modelo invalida el cache implícitamente. Override por env `NOUSER_NOUS_REAL_CACHE`. Encoding compacto: cada `Vec` se serializa como bytes little-endian (4B por f32, sin overhead). Para el modelo default (384-d) son 1.5 KiB por entry. Decode tolera bytes corruptos (longitud no-múltiplo de 4 → `None`, no panic). Por qué 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. API: - `EmbedCache::open()` → abre sled, idempotente. - `EmbedCache::open_at(dir)` para tests. - `EmbedCache::get(sha, model)` → `Option>`. - `EmbedCache::put(sha, model, &[f32])` → no-fatal en error. - `EmbedCache::len()` → contador para logs (best-effort). Mock NO se modifica — su embedding pseudo-32d es metadata-hashing puro, sin costo. Cachearlo sería overhead. Tests: 5 unitarios (`roundtrip_returns_same_vector`, `miss_returns_none`, `different_models_do_not_collide`, `different_content_different_keys`, `corrupted_value_returns_none`). Verdes con `--features embeddings`; stub mode (sin feature) sigue compilando sin tocar cache. ### feat(nouser+sidecar): watcher con debounce + re-publish al broker Cierra las dos limitaciones del watcher previo: ya no spamea N veces por una sola edición, y el broker ve los cambios estructurales en lugar de quedarse con manifests congelados al arranque. $ nouser daemon /tmp/x & $ touch /tmp/x/src/a.rs /tmp/x/src/b.rs /tmp/x/src/c.rs # daemon log (un solo batch, no 9 reacciones): [watcher] ⚙ batch: 6 path(s) coalescidos → re-scan [watcher] ✦ x/src nace (3 miembros, lens=Code) [watcher] ⌃ delta: 1 nuevas, 0 refrescadas, 0 cerradas — 3 sesiones vivas Mecánica del debounce (150ms): - `spawn_fs_watcher` arma dos threads: **dispatcher** filtra eventos notify Create/Modify/Remove a un canal de paths; **coordinator** mantiene `HashMap` y dispara batch sólo cuando todos los paths llevan ≥150ms quietos. - Un `:w` típico de vim (~5 eventos por archivo) colapsa a 1 batch. Mecánica del re-publish: - `SidecarPool` ahora trackea `HashMap` indexado por `Card.id`. Llamar `pool.spawn(card)` con un id ya presente aborta la sesión previa y abre una nueva — `spawn` se vuelve idempotente: re-publicar una Mónada cuya composición cambió refresca su sesión en el broker sin dejar zombies. - Nueva API `pool.drop_session(id)` para cerrar una sesión explícitamente cuando una Mónada desaparece (directorio quedó bajo `min_files` o se borró). - `pool.live_sessions()` para introspección/logs. - `process_change_batch` re-scanea + re-clusteriza con hidratación, diffea contra prior_monads, y para cada Mónada decide: - removida → `drop_session` - nueva → `spawn` con ✦ - composición cambió (members o centroid distintos) → `spawn` con ↻ - idéntica → no-op Trade-off aceptado: re-scan global por batch (no incremental). Es O(N archivos) por evento y para árboles típicos (<10k) cae en <100ms. Optimizar a re-cluster parcial cuando duela. Tests: workspace completo verde. ### feat(nouser): notify watcher — el sistema reacciona en tiempo real El daemon ahora monta un `notify::recommended_watcher` recursivo sobre el directorio. Cada `Create`/`Modify` de archivo regular dispara: embedding del archivo, filtro por `centroid_model`, ranking contra centroides existentes, log con marker 🧲 / · según supere el umbral de atracción. $ nouser daemon /tmp/x & # en otra terminal: $ vim /tmp/x/src/nuevo.rs # daemon log: [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>` para sharing con el thread del 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_monads + diff publish) queda como work futuro. Limitación conocida: `notify` emite múltiples eventos por una sola edición (Create + Modify, etc.). Sin debounce, el watcher reporta varias veces. Aceptable para demo; production conviene debounce ~100ms por path. Tests: 7 (card) + 24 (core) verdes, 0 errores, 0 warnings. ### 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` — identidad estable derivada del origen (para `by_directory`, el parent dir canónico). Permite reusar ULID across re-scans. Algoritmo (cluster): - Nueva fn `cluster::by_directory_hydrated(files, min_files, prior: Option<&MonadDb>)`. Cuando hay `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 las 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 que NO estaba en la hidratación inicial. Los path_hints existentes preservan identidad, evitando duplicados en el broker. 5. Persiste el set actualizado. Validación end-to-end: $ NOUSER_DB_PATH=/tmp/h.sled nouser daemon crates/core # arranque 1: DB vacía re-scan 102 archivos → 5 mónadas 1 ente + 5 mónadas vivas (5 nuevas vs hidratación) $ NOUSER_DB_PATH=/tmp/h.sled nouser daemon crates/core # arranque 2: DB poblada hidratadas 5 mónadas previas en O(1) re-scan 102 archivos → 5 mónadas 1 ente + 5 mónadas vivas (0 nuevas vs hidratación) Costo del arranque 2: ~0.06s user CPU. Antes (sin hidratación) era re-scan + cluster + spawn x N — segundos enteros para árboles grandes. Tests: 7 (card) + 24 (core) verdes. ### 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 daba scores sin sentido. - `MonadManifest.centroid_model: Option` taggea qué modelo produjo el `centroid`. `None` = legacy pre-versioning. - `nouser_core::embed::MODEL_ID = "nouser-pseudo-32d"`. El 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, así que reportar el mismo ID es honesto. - `nouser-nous-real` reporta `"real-fastembed-allMiniLML6V2-384d"` (dim distinta, semántica distinta). - `cmd_attract` ahora: - Captura el `model_id` del embedding del target (local o remote). - Filtra Mónadas cuyo `centroid_model` no matchee. - Reporta `embed: ()` y `skipped: N mónadas con centroid_model distinto` cuando descarta. Resultado operativo: 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. ## 2026-05-08 ### chore: profile.dev slim — target/ ~50% más liviano Cambios en `[profile.dev]` raíz para que builds futuras no desborden disco. Decisiones: - `debug = "line-tables-only"`: stack traces correctos, drop del resto de symbols. Sin pérdida real para nuestro flujo. - `split-debuginfo = "unpacked"`: relink más rápido, debuginfo en archivos aparte. - `codegen-units = 256`: paralelismo + builds incrementales chicas. - Override `[profile.dev.package.X]` para los pesados (gpui, ort, fastembed, tokenizers, image): `opt-level = 1`, `debug = false`. No los debuggeamos línea por línea, no necesitan info pesada. Resultado: binarios ~3× más livianos. ente-zero 125→47 MB; mock-nous ~50→22 MB. ### feat(nouser): dynamic binding — consumer descubre el provider vía broker Cierra el bucle prometido por `priority_contexts`: el cliente ya no hardcodea el socket del provider de embeddings. En su lugar: 1. Si `NOUSER_NOUS_SOCKET` está set, lo usa directo (atajo explícito). 2. Si no, abre `brahman_handshake::client::Client` al `brahman-init`, anuncia un consumer Card mínimo con `flow.input = embed-result:json`, espera 3s por el primer `MatchEvent::Available`, y usa el `producer_service_socket` que viaja en el evento. Esto activa el swap automático mock↔real: - `BRAHMAN_BROKER_CONTEXT=test`: el bias `+1 en test` del mock lo hace ganar; consumer recibe el socket del mock. - `BRAHMAN_BROKER_CONTEXT=prod`: el bias del real lo hace ganar. - Sin contexto: empate alfabético entre los presentes. Validación end-to-end: $ ente-zero & nouser-nous-mock & $ # Sin NOUSER_NOUS_SOCKET: $ nouser attract --remote crates/core archivo.rs embed: remote 🧲 0.9058 ente-brain/src ... (mock log confirma "embed_file path=...") Cambios: - `nouser-core` Cargo.toml: deps directas brahman-handshake + tokio. - `cmd_attract` resuelve el socket por discovery antes de llamar a `embed_via(&path, file)` (mini-runtime tokio current_thread inline). Bug que se descubrió en el camino: la "flakiness" reportada de `cargo test --workspace` era disco lleno (24 GB en `target/`), no condición de carrera. Con `cargo clean` + profile slim, todos los tests pasan deterministas. ### 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 900×640 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` se usa porque GPUI no provee un runtime tokio. - Nuevo helper 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 muestra ~6 cards en vivo, refrescando cada 2s. cargo check --workspace: 0 errores, 0 warnings. ### 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 en esa ruta; si no, in-memory: $ NOUSER_DB_PATH=/tmp/monads.sled nouser scan crates/core scan: 102 archivos en crates/core, 5 mónadas $ ls /tmp/monads.sled blobs conf $ NOUSER_DB_PATH=/tmp/monads.sled nouser scan crates/core # segunda corrida re-escribe la DB con el nuevo scan Tests nuevos en db.rs: - `persistence_roundtrip` — escribe, cierra, reabre, datos están. - `replace_monads_purges_persistent_tree` — replace limpia el tree. 24 tests en nouser-core (era 22, +2). ### refactor(nouser): labels de Mónada con 2 componentes del path Resuelve la fricción visual de monorepos donde múltiples Mónadas se llamaban "src". Nueva función `label_from_path` toma los últimos hasta 2 componentes normales del path y los une con `/`. $ 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. ### feat(nouser): Phase D-2 — proveedor Nous real (LLM) detrás de feature flag 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`: bin con dos modos según feature. - **Sin feature (default)**: stub. Bin compila en ~10s, arranca, sidecarea a brahman-init declarando la Card de real-nous, escucha en el socket Nous, y rechaza toda request con `ErrorResponse { error: "compilado sin la feature embeddings. Rebuild con cargo build -p nouser-nous-real --features embeddings" }`. `cargo build --workspace` sigue siendo limpio. - **Con `--features embeddings`**: pulls `fastembed = "4"`. Ese crate arrastra `ort 2.0.0-rc.9` (ONNX Runtime con binarios descargados por Cargo) + `tokenizers 0.21` + ~30 deps más. Compila en ~50s. Modelo default: `all-MiniLM-L6-v2` (384-d, descargado a `~/.cache/fastembed` la primera vez). - `EmbedText`: pasa el texto al modelo, devuelve vector 384-d. - `EmbedFile`: lee primeros 8KiB con UTF-8 lossy, embed como texto. Para binarios el resultado no es semánticamente útil — caller decide. - `Ping`: devuelve `model_id` y `embed_dim` reales. - Card de real-nous: - label `nouser.nous_real` (distinto del mock para coexistir). - `priority_contexts.prod = { priority_offset: +1 }`. En contexto prod gana sobre el mock; en `test` el mock gana por su propio `+1`. Sin contexto activo, empate alfabético entre ambos. Validación end-to-end con modelo real: $ cargo build -p nouser-nous-real --features embeddings # ~50s $ ente-zero & nouser-nous-real & $ # probe vía python al socket Unix: $ echo '{"kind":"embed_text","payload":{"text":"hello brahman"}}' \ | python3 -c "..." | head model: real-fastembed-allMiniLML6V2-384d elapsed_ms: 8 embed_dim: 384 first 5 values: [0.0034, -0.0036, 0.0078, -0.0218, -0.0162] Tradeoff conocido: las dimensiones del mock (32-d) y real (384-d) son incompatibles. Cambiar de proveedor invalida los centroides cacheados de Mónadas. Documentar como "limpiar DB al cambiar proveedor". Workspace state: - cargo build --workspace sigue limpio sin features (no ML). - cargo build -p nouser-nous-real --features embeddings funciona. - 0 errores, 0 warnings en ambos modos. Pendientes para D-3 / futuro: - Discovery de socket: hoy el consumer hardcodea NOUSER_NOUS_SOCKET. Para que el broker brahman elija real vs mock per-contexto, falta inyectar el socket del provider electo en el MatchEvent o exponer un broker query "dame el socket de la sesión X". - Coexistencia: hoy los dos providers compiten por el mismo socket path por default. Habría que parametrizarlos a sockets distintos cuando coexistan. ### 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 entre mock y real (futuro) se hará vía priority_contexts en el broker. Crates nuevos: - `crates/modules/nouser/nous`: contrato compartido. Tipos `EmbedRequest`, `RequestKind { EmbedFile, EmbedText, Ping }`, `EmbedFilePayload`, `EmbedTextPayload`, `EmbedResponse`, `PingResponse`, `ErrorResponse`. Wire format: line-delimited JSON por Unix socket, single-shot per conexión. Constants para los nombres de flow (`embed-request`/`embed-result`) y el tipo (`json`). Helper `transport::default_socket_path()` con env var `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:json`/`embed-result:json` y un `priority_contexts.test = { priority_offset: +1 }` (gana sobre cualquier real-nous en contexto test). Bind del socket Nous, accept loop, despacha por `RequestKind`. EmbedFile usa `nouser_core::embed::embed` (los pseudo-embeddings de Phase C). Modelo: `mock-pseudo-32d`. Cambios: - `nouser-core`: dep nueva `nouser-nous`. Subcomando `attract` ahora acepta `--remote` que abre un socket UnixStream blocking, envía un `EmbedRequest` y lee la response. Imprime `embed: local|remote` para que se vea cuál ruta corrió. Validación end-to-end (un solo terminal, varios procesos): $ ente-zero & $ nouser-nous-mock & $ NOUSER_MIN_FILES=5 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 embed: remote 🧲 0.9058 src ... Mock log: "embed_file path=crates/modules/nouser/core/src/embed.rs" Bug encontrado y corregido en el camino: - `ContextBias` tenía `#[serde(skip_serializing_if = ...)]` en sus campos. Postcard NO soporta skip-condicional (formato no self-describing): el serializer omitía bytes que el deserializer esperaba, rompiendo la wire de cualquier Card con `priority_contexts` poblada. - Fix: removidos los `skip_serializing_if` de `ContextBias`. JSON pretty ahora emite `{"pin_to": null, "priority_offset": 0}` en lugar de objeto vacío. Trade-off aceptado por compatibilidad de wire. - Test nuevo en brahman-card: `wirecard_postcard_with_priority_contexts` que ejercita el roundtrip completo postcard. Tests acumulados: 75 (card 12 +1 nuevo, broker 15, handshake 9, card-wit 4, admin 0, nouser-card 7, nouser-core 20, nouser-nous 2). cargo check --workspace: 0 errores, 0 warnings. Próximo natural: Phase D-2 — `real-nous` con un modelo ONNX/Llama de text-embedding. La infraestructura ya está lista: declara la misma Card con `priority_contexts.prod = { priority_offset: +1 }` y el swap es transparente para el consumer. ### 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 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`: - `EMBED_DIM = 32`. Estructura del vector: - dims 0..8: blake3(extension) → identidad de tipo - dims 8..16: blake3(parent_dir) → identidad de contenedor - dims 16..24: blake3(file_stem) → identidad léxica - dims 24..28: tamaño (log + flags) - dims 28..32: mtime (escala día + cíclicas) - **Tip clave**: bytes del hash se centran a `[-1, 1]` (no `[0, 1]`). Sin centrar, dos vectores hash random tendrían cosine ~0.75 espuria; 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 (promedio de embeddings de los miembros, L2-normalizado) y lo guarda en `MonadManifest.centroid`. El centroide viaja al brahman-status vía `DataFacet.centroid` → ahora se ven los Vec reales por cada Mónada. - bin nouser nuevo subcomando: `attract `. - Escanea el dir, embeda el archivo objetivo, ranking de afinidad contra todas las Mónadas con centroide. - Marca 🧲 si la mejor supera el umbral, `·` si es la mejor pero debajo, espacio en blanco para el resto. Validación end-to-end: $ nouser attract crates/core crates/modules/nouser/core/src/embed.rs ranking de atracción (cosine similarity): 🧲 0.9058 [01K..] src (11 archivos en crates/core/ente-brain/src) 0.8984 [01K..] src (6 archivos en crates/core/brahman-handshake/src) 0.8918 [01K..] src (5 archivos en crates/core/ente-zero/src) ... $ nouser attract crates/core crates/modules/nouser/core/Cargo.toml ranking: 0.3427 [01K..] graph (7 archivos en crates/core/ente-zero/src/graph) ... (mejor score 0.3427 < umbral 0.7000 — el archivo no se 'pega') Tests: 20 en nouser-core (era 13, +7 de embed). Total acumulado: 73 (card 11, broker 15, handshake codec+tr 2 + integ 7, card-wit 4, admin 0, nouser-card 7, nouser-core 20, ente-card 0). 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 sin tocar nada más. ### feat(nouser): Phase B-2 — daemon que publica Mónadas al Init Cierra la unificación: 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, exactamente como buscaba el diseño. Cambios: - `crates/modules/nouser/core/Cargo.toml`: deps nuevas `brahman-card` y `brahman-sidecar`. - `crates/modules/nouser/core/src/bin/nouser.rs`: subcomando `daemon `. - Spawna un sidecar para el "engine" (`brahman.nouser_engine`, kind=Ente) — el ser que produce y administra Mónadas. - Scan + cluster del dir. - Para cada Mónada, llama `monad.to_brahman_card()` y spawnea un sidecar (kind=Data). Cada Mónada es una sesión brahman propia con su ULID estable. - Park del thread principal: 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 El protocolo de presentación es uno solo: la Card. La función — anunciar identidad, exponer metadata, ser descubierto — es idéntica para procesos vivos y agrupaciones de datos. La UI lo ve como una lista uniforme. Costo conocido: cada Mónada consume un thread + tokio runtime current_thread (legacy del sidecar API). Para muchas Mónadas (>50) conviene consolidar en un único runtime con N tasks. Defer a Phase B-3. Pendientes propuestos: - **B-3**: consolidar todos los sidecars en un único runtime tokio para no spawnear N threads. - **C**: pseudo-embeddings + atracción por centroide. - **D**: módulo `nouser-nous` para LLM, swappable por priority_contexts. - **Polish**: labels con 2-3 componentes del path. - **Crossreferencia**: que un Ente pueda anunciar "estoy procesando la Mónada X" y la Mónada anuncie "Ente Y me está procesando". cargo check --workspace: 0 errores, 0 warnings. ### feat: Phase B-1 — unificación ontológica de Cards (Ente ↔ Data) La Card es **el** protocolo de presentación del ecosistema, no sólo de los procesos. 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. Cambios: - `brahman-card`: - `CardKind { Ente (default), Data }`. Conserva back-compat: Cards existentes son `Ente` por default. - `DataFacet { summary, keywords, centroid, member_count, dispersion, presentation_hint }`. Liviano para el wire — listas grandes (members, embeddings completos) se consultan al daemon dueño bajo demanda. - `Card.kind` y `Card.data: Option` agregados. WireCard espeja, conversiones `From` propagan. - Default impl actualizado. - `brahman-broker::BrokeredCard`: propaga `kind` y `data` desde la Card registrada. No afecta el matching (sigue siendo por TypeRef + priority + pin_to); permite a observadores discriminar sin re-query. - `nouser-card`: depende ahora de `brahman-card`. Nuevo método `MonadManifest::to_brahman_card()` que proyecta: - id, label, lineage → directos. - payload Virtual, supervision Delegate, lifecycle Daemon (placeholder semántico — la Mónada no se ejecuta). - kind = Data. - data = Some(DataFacet) con summary, keywords, centroide, member_count, entropy → dispersion, y un `presentation_hint` derivado del `Lens` (`Code` → `"code"`, `Gallery` → `"gallery"`, etc.). - Test nuevo: `projects_to_brahman_card`. - `brahman-status`: cada sesión muestra ahora `[ente]` o `[data]` como prefijo. Para sesiones `data`, render adicional con summary, members + dispersion, keywords y lens hint. Resultado: la UI (yahweh, brahman-status, futuro explorer) ve una sola lista uniforme. No tiene que saber si está mirando un proceso o un cúmulo de datos — sólo lee el Card y se adapta por `kind`. Tests acumulados: 59 (card 11, broker 15, handshake codec+transport 2 + integ 7, card-wit 4, admin 0, nouser-card 7, nouser-core 13). cargo check --workspace: 0 errores, 0 warnings. Próximo: **Phase B-2** — bin `nouser daemon ` que sidecarea cada Mónada como una sesión brahman, publicándola al broker. Brahman-status las verá junto a los entes. ### feat(nouser): Phase A — mecanismo determinista de Mónadas Primer trozo del módulo Nouser (Kairos): explorador de Mónadas como "imanes semánticos" sobre el filesystem. Phase A cubre el 90% de los casos sin tocar IA — sólo metadatos y heurísticas. Crates nuevos: - `crates/modules/nouser/card`: `MonadManifest` (la Tarjeta de Presentación de una Mónada — espejo conceptual de `brahman::Card` pero para datos, no para procesos runtime). Campos: id (Ulid), label, summary, centroid (vacío en Phase A), keywords, cardinality, entropy [0,1], dominant_lens, pins, members, timestamps, extensions (forward-compat). 6 tests de validación + JSON roundtrip. - `crates/modules/nouser/core`: pipeline determinista. - `scanner`: walkdir → `Vec` con metadatos (path, size, mtime, extension). Skipea hidden por default. Configurable max depth y follow_links. - `cluster::by_directory`: agrupa por parent dir, mínimo 3 archivos para promover a Mónada (configurable). Calcula keywords (top-N extensiones por frecuencia + alfabético), elige `Lens` dominante (Code/Gallery/Markdown/Database/Grid) según extensión más frecuente, computa entropía de Shannon normalizada [0,1]. - `db`: `MonadDb` en memoria con índices BTreeMap files/monads y `resolve_members(monad_id)` que filtra IDs huérfanos. Phase B traerá persistencia. - bin `nouser`: subcomandos `scan `, `show `, `json `. Env var `NOUSER_MIN_FILES` para tunear 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 ... $ nouser show crates 01KR4C Monad 01KR4C1370DVF6NMTW6SECNXAF label: src summary: 4 archivos en crates/modules/nouser/core/src (ext: rs) cardinality: 4 entropy: 0.0000 lens: Code members (4): 4132 bytes crates/modules/nouser/core/src/db.rs ... Pendientes para próximas fases (anotados, no urgentes): - **Phase B**: bin `nouser daemon` que sidecarea a brahman-init declarando flows (`scan-request:json` → `monad-update:json`). - **Phase C**: pseudo-embeddings deterministas (hash de path/ext/size a 32-d) + atracción por centroide via cosine similarity. Implementa el "imán" sin LLM. - **Phase D**: módulo `nouser-nous` aparte para el LLM real (Llama/ONNX). En `priority_contexts.test` el Init pinea a `mock-nous` (embeddings determinísticos); en `prod` a `real-nous`. - **Polish**: labels de Mónada incluir 2-3 componentes del path para desambiguar `src/` repetidos en monorepo. Workspace: 0 errores, 0 warnings. Tests acumulados: 58 (card 11, broker 15, handshake codec+transport 2 + integ 7, card-wit 4, admin 0, nouser-card 6, nouser-core 13).