gioser

tawasuyu/brahman

Rust 323TOML 122JSON 14Markdown 114446 Definitions

⎇ 79d42aba2825390bcce1ee77120b7829edbbeb2a CHANGELOG.md Markdown · 1121 lines · 51.2 KB History raw

chore(nakui): alinear nakui-core con [workspace.package] y deps compartidas 79d42aba2 · Sergio ·

1# Changelog
2
3Registro cronológico de cambios sustantivos en el monorepo Brahman. Cada
4entrada lista las acciones concretas tras un commit; para detalles de
5ratio/diff ver `git show <sha>`.
6
7## 2026-05-09
8
9### chore(nakui): alinear `nakui-core` con `[workspace.package]` y deps compartidas
10Cleanup de drift de convenciones: `nakui-core` era el único crate del
11monorepo que mantenía `version = "0.1.0"` / `edition = "2021"` /
12`thiserror = "1"` hardcoded, mientras el resto heredaba del workspace
13y usaba `thiserror = "2"`. Eso significaba que un bump global de versión
14o de edition se olvidaba sistemáticamente de nakui.
15
16Cambios:
17- `[package]`: `version`, `edition`, `rust-version`, `license`, `authors`,
18  `publish` → todos `*.workspace = true`. Agregado `description` (cumple
19  convención del resto de crates).
20- Deps compartidas migradas a `{ workspace = true }`: serde, serde_json,
21  thiserror (v1→v2), tokio, ulid, sha2.
22- `uuid` migrado a `{ workspace = true, features = ["serde"] }` — la
23  feature `serde` no está en el workspace dep porque nakui es el único
24  user; queda local opt-in en lugar de inflar el dep común.
25- Deps específicas de nakui (sin compartición posible): rhai, petgraph,
26  surrealdb permanecen inline con versión local.
27
28Verificación: `cargo build -p nakui-core` verde tras el bump de
29`thiserror` v1→v2 — el `#[derive(Error)]` de los 14+ enums de error
30en nakui no requirió ajustes (la API de derive es backwards-compatible
31para los patrones simples). `cargo test -p nakui-core --lib`: 27/27
32verdes, sin regresión.
33
34### feat(card): `Card::new(label)` — alternativa segura a `Default::default()`
35Cierra la trap documentada de `Card::default()` que devuelve `id =
36Ulid::nil()`. Usar `Card::default()` "viva" colisionaba con cualquier
37otra Card default-construida bajo el mismo id `00000000…`. La fix no
38es romper `Default` (sigue siendo determinista, requerido por callers
39que lo usan como template para deserialización), sino agregar un
40constructor explícito:
41
42  let card = Card {
43      kind: CardKind::Data,
44      payload: Payload::Embedded(json),
45      ..Card::new("mi-modulo.algo")
46  };
47
48`Card::new(label)` asigna `id = Ulid::new()` (único) + `label`
49provisto, dejando el resto en defaults seguros (Virtual / OneShot /
50Ente). Pensado para usarse en struct-literals con override parcial,
51igual sintaxis que el patrón viejo pero sin la trap.
52
53Refactor de call sites:
54- `brahman_sidecar::discovery::build_consumer_card` → `..Card::new(label)`
55- `nouser daemon::build_engine_card` → `..Card::new("brahman.nouser_engine")`
56
57`Default` se mantiene tal cual con docstring expandida que advierte
58explícitamente sobre el uso "vivo" y apunta a `Card::new`. Tests
59existentes y el patrón `nouser_card::MonadManifest::to_brahman_card`
60(que asigna el id estable de la Mónada, no uno fresco) NO se
61modifican — `Default` sigue siendo correcto cuando el caller
62sobreescribe `id` explícitamente.
63
64Tests: 3 unitarios nuevos en brahman-card (`new_assigns_real_ulid_and_label`,
65`new_yields_distinct_ids_per_call`, `default_keeps_nil_id_for_struct_update_pattern`).
6615 tests verdes (era 12).
67
68### feat(sidecar): API reusable de discovery vía broker
69Promueve el patrón ad-hoc `discover_producer_socket` (que vivía
70inline en `nouser attract --remote`) a un módulo público
71`brahman_sidecar::discovery`. Cualquier consumer puede ahora
72preguntar al broker "¿quién provee este TypeRef?" con dos llamadas:
73
74  // Construir un consumer Card mínimo (Ente, Oneshot, Virtual)
75  let card = brahman_sidecar::build_consumer_card(
76      "mi-cli",
77      "embed-result",   // flow.input.name
78      "json",            // TypeRef::Primitive { name }
79  );
80
81  // Bloqueante (CLIs, std-thread loops):
82  let socket: PathBuf = brahman_sidecar::await_provider_blocking(
83      card, Duration::from_secs(3),
84  )?;
85  // O async (módulos con runtime tokio propio):
86  let socket = brahman_sidecar::await_provider(card, timeout).await?;
87
88API:
89- `build_consumer_card(label, flow_name, type_name) -> Card`
90  abstrae la verbosidad del struct-literal repetido en cada caller.
91  Genera un `id: Ulid::new()` real (no nil → seguro contra
92  colisiones en el broker).
93- `await_provider(card, timeout) -> Result<PathBuf, ConsumerError>`
94  conecta al init, espera `MatchEvent::Available`, devuelve
95  `producer_service_socket`, manda Farewell. Ignora eventos
96  `Lost` durante el await (no aplican al arranque).
97- `await_provider_blocking(card, timeout)` arma su propio
98  runtime `current_thread` para mundos no-async.
99- `ConsumerError` con variantes tipadas: `Connect { socket, source }`,
100  `NoProvider { flow, type_ref, timeout }`, `Client(ClientError)`,
101  `Runtime(String)`. Adiós al `Box<dyn Error>` de antes.
102
103Refactor en `nouser daemon`:
104- `discover_producer_socket` (60 LOC inline en `bin/nouser.rs`) → 5
105  líneas que delegan en el helper.
106- `remote_embed` ya no construye su propio runtime tokio.
107
108Próximo consumer natural: `nouser-explorer`. Hoy renderea
109`StatusSnapshot` vía socket admin (introspección pura). El día que
110quiera **interactuar** con un Ente — p. ej., disparar un re-embed
111desde la UI — usa este helper para resolver el socket del provider
112sin hardcodear paths.
113
114Nota sobre identidad: este commit fuerza `Ulid::new()` para los
115consumer Cards generados, evitando la trampa documentada del
116`Card::default()` que devuelve `Ulid::nil()`. La fijación global de
117`Default` queda como cleanup separado (requiere auditar que ningún
118caller dependa del determinismo de `nil`).
119
120Tests: 4 unitarios nuevos en `discovery::tests` (id no-nil, id
121único por llamada, formateo de TypeRef::Wit, fallback sin input).
122Workspace verde.
123
124### feat(nouser+sidecar): watcher con debounce + re-publish al broker
125Cierra las dos limitaciones del watcher previo: ya no spamea N veces por
126una sola edición, y el broker ve los cambios estructurales en lugar de
127quedarse con manifests congelados al arranque.
128
129  $ nouser daemon /tmp/x &
130  $ touch /tmp/x/src/a.rs /tmp/x/src/b.rs /tmp/x/src/c.rs
131  # daemon log (un solo batch, no 9 reacciones):
132  [watcher] ⚙ batch: 6 path(s) coalescidos → re-scan
133  [watcher] ✦ x/src nace (3 miembros, lens=Code)
134  [watcher] ⌃ delta: 1 nuevas, 0 refrescadas, 0 cerradas — 3 sesiones vivas
135
136Mecánica del debounce (150ms):
137- `spawn_fs_watcher` arma dos threads: **dispatcher** filtra eventos
138  notify Create/Modify/Remove a un canal de paths; **coordinator**
139  mantiene `HashMap<PathBuf, Instant>` y dispara batch sólo cuando
140  todos los paths llevan ≥150ms quietos.
141- Un `:w` típico de vim (~5 eventos por archivo) colapsa a 1 batch.
142
143Mecánica del re-publish:
144- `SidecarPool` ahora trackea `HashMap<Ulid, AbortHandle>` indexado
145  por `Card.id`. Llamar `pool.spawn(card)` con un id ya presente
146  aborta la sesión previa y abre una nueva — `spawn` se vuelve
147  idempotente: re-publicar una Mónada cuya composición cambió
148  refresca su sesión en el broker sin dejar zombies.
149- Nueva API `pool.drop_session(id)` para cerrar una sesión
150  explícitamente cuando una Mónada desaparece (directorio quedó
151  bajo `min_files` o se borró).
152- `pool.live_sessions()` para introspección/logs.
153- `process_change_batch` re-scanea + re-clusteriza con hidratación,
154  diffea contra prior_monads, y para cada Mónada decide:
155  - removida → `drop_session`
156  - nueva → `spawn` con ✦
157  - composición cambió (members o centroid distintos) → `spawn` con ↻
158  - idéntica → no-op
159
160Trade-off aceptado: re-scan global por batch (no incremental). Es
161O(N archivos) por evento y para árboles típicos (<10k) cae en
162<100ms. Optimizar a re-cluster parcial cuando duela.
163
164Tests: workspace completo verde.
165
166### feat(nouser): notify watcher — el sistema reacciona en tiempo real
167El daemon ahora monta un `notify::recommended_watcher` recursivo
168sobre el directorio. Cada `Create`/`Modify` de archivo regular
169dispara: embedding del archivo, filtro por `centroid_model`, ranking
170contra centroides existentes, log con marker 🧲 / · según supere
171el umbral de atracción.
172
173  $ nouser daemon /tmp/x &
174  # en otra terminal:
175  $ vim /tmp/x/src/nuevo.rs
176  # daemon log:
177  [watcher] 🧲 /tmp/x/src/nuevo.rs  →  x/src  (0.7470)
178
179  $ echo "edit" >> /tmp/x/docs/n1.md
180  [watcher] 🧲 /tmp/x/docs/n1.md  →  x/docs  (0.8169)
181
182Mecánica:
183- DB pasa a `Arc<Mutex<MonadDb>>` para sharing con el thread del
184  watcher.
185- Watcher en thread dedicado (`nouser-watcher`); reacciona sólo a
186  Create/Modify, ignora Access/Metadata-only.
187- `react_to_change(path, metadata, db)` computa embedding,
188  filtra por `centroid_model`, busca best attraction.
189- No re-publica al broker ni muta DB — sólo observa y narra. La
190  invalidación selectiva (re-cluster + replace_monads + diff
191  publish) queda como work futuro.
192
193Limitación conocida: `notify` emite múltiples eventos por una sola
194edición (Create + Modify, etc.). Sin debounce, el watcher reporta
195varias veces. Aceptable para demo; production conviene debounce
196~100ms por path.
197
198Tests: 7 (card) + 24 (core) verdes, 0 errores, 0 warnings.
199
200### feat(nouser): hidratación del daemon vía sled + path_hint
201El daemon ya no recomputa ciegamente al arrancar. Si la DB tiene
202Mónadas previas con `centroid_model` válido, las publica instantáneo
203y el re-scan reusa sus IDs vía `path_hint`.
204
205Schema:
206- `MonadManifest.path_hint: Option<String>` — identidad estable
207  derivada del origen (para `by_directory`, el parent dir
208  canónico). Permite reusar ULID across re-scans.
209
210Algoritmo (cluster):
211- Nueva fn `cluster::by_directory_hydrated(files, min_files,
212  prior: Option<&MonadDb>)`. Cuando hay `prior`, busca Mónada con
213  mismo `path_hint` Y mismo `centroid_model`; si la encuentra,
214  reusa `id`, `lineage` y `created_at_ms`.
215- `by_directory` queda como wrapper sin hidratación (back-compat).
216
217Daemon (cmd_daemon):
2181. Open sled si NOUSER_DB_PATH existe.
2192. Publica las Mónadas previas con `centroid_model` válido (las
220  inválidas se descartan con log explícito).
2213. Re-scan + `by_directory_hydrated(prior=&db)`.
2224. Sólo spawnea sidecars para Mónadas con id que NO estaba en la
223  hidratación inicial. Los path_hints existentes preservan identidad,
224  evitando duplicados en el broker.
2255. Persiste el set actualizado.
226
227Validación end-to-end:
228
229  $ NOUSER_DB_PATH=/tmp/h.sled nouser daemon crates/core
230  # arranque 1: DB vacía
231    re-scan 102 archivos → 5 mónadas
232    1 ente + 5 mónadas vivas (5 nuevas vs hidratación)
233
234  $ NOUSER_DB_PATH=/tmp/h.sled nouser daemon crates/core
235  # arranque 2: DB poblada
236    hidratadas 5 mónadas previas en O(1)
237    re-scan 102 archivos → 5 mónadas
238    1 ente + 5 mónadas vivas (0 nuevas vs hidratación)
239
240Costo del arranque 2: ~0.06s user CPU. Antes (sin hidratación) era
241re-scan + cluster + spawn x N — segundos enteros para árboles grandes.
242
243Tests: 7 (card) + 24 (core) verdes.
244
245### feat(nouser): centroid_model — versionado de embeddings
246Protege contra el bug silencioso de mezclar centroides de modelos
247distintos (mock 32-d vs real 384-d), que daba scores sin sentido.
248
249- `MonadManifest.centroid_model: Option<String>` taggea qué modelo
250  produjo el `centroid`. `None` = legacy pre-versioning.
251- `nouser_core::embed::MODEL_ID = "nouser-pseudo-32d"`. El cluster lo
252  setea en cada Mónada que genera.
253- `nouser-nous-mock` reusa la misma constante (`use
254  nouser_core::embed::MODEL_ID`); produce vectores idénticos al
255  cluster local, así que reportar el mismo ID es honesto.
256- `nouser-nous-real` reporta `"real-fastembed-allMiniLML6V2-384d"`
257  (dim distinta, semántica distinta).
258- `cmd_attract` ahora:
259  - Captura el `model_id` del embedding del target (local o remote).
260  - Filtra Mónadas cuyo `centroid_model` no matchee.
261  - Reporta `embed: <source> (<model>)` y `skipped: N mónadas con
262    centroid_model distinto` cuando descarta.
263
264Resultado operativo: cambiar de mock a real (vía
265`BRAHMAN_BROKER_CONTEXT=prod`) hace que `attract` filtre las Mónadas
266viejas con cero score en lugar de fingir que las puede comparar.
267
268## 2026-05-08
269
270### chore: profile.dev slim — target/ ~50% más liviano
271Cambios en `[profile.dev]` raíz para que builds futuras no desborden
272disco. Decisiones:
273- `debug = "line-tables-only"`: stack traces correctos, drop del resto
274  de symbols. Sin pérdida real para nuestro flujo.
275- `split-debuginfo = "unpacked"`: relink más rápido, debuginfo en
276  archivos aparte.
277- `codegen-units = 256`: paralelismo + builds incrementales chicas.
278- Override `[profile.dev.package.X]` para los pesados (gpui, ort,
279  fastembed, tokenizers, image): `opt-level = 1`, `debug = false`.
280  No los debuggeamos línea por línea, no necesitan info pesada.
281
282Resultado: binarios ~3× más livianos. ente-zero 125→47 MB; mock-nous
283~50→22 MB.
284
285### feat(nouser): dynamic binding — consumer descubre el provider vía broker
286Cierra el bucle prometido por `priority_contexts`: el cliente ya no
287hardcodea el socket del provider de embeddings. En su lugar:
288
2891. Si `NOUSER_NOUS_SOCKET` está set, lo usa directo (atajo explícito).
2902. Si no, abre `brahman_handshake::client::Client` al `brahman-init`,
291   anuncia un consumer Card mínimo con `flow.input = embed-result:json`,
292   espera 3s por el primer `MatchEvent::Available`, y usa el
293   `producer_service_socket` que viaja en el evento.
294
295Esto activa el swap automático mock↔real:
296- `BRAHMAN_BROKER_CONTEXT=test`: el bias `+1 en test` del mock lo hace
297  ganar; consumer recibe el socket del mock.
298- `BRAHMAN_BROKER_CONTEXT=prod`: el bias del real lo hace ganar.
299- Sin contexto: empate alfabético entre los presentes.
300
301Validación end-to-end:
302
303  $ ente-zero & nouser-nous-mock &
304  $ # Sin NOUSER_NOUS_SOCKET:
305  $ nouser attract --remote crates/core archivo.rs
306    embed: remote
307    🧲  0.9058  ente-brain/src  ...
308  (mock log confirma "embed_file path=...")
309
310Cambios:
311- `nouser-core` Cargo.toml: deps directas brahman-handshake + tokio.
312- `cmd_attract` resuelve el socket por discovery antes de llamar a
313  `embed_via(&path, file)` (mini-runtime tokio current_thread inline).
314
315Bug que se descubrió en el camino: la "flakiness" reportada de
316`cargo test --workspace` era disco lleno (24 GB en `target/`), no
317condición de carrera. Con `cargo clean` + profile slim, todos los
318tests pasan deterministas.
319
320### feat(nouser): yahweh widget — `nouser-explorer` panel GPUI
321Bin GPUI standalone que consulta `brahman-admin` cada 2s y renderea
322todas las sesiones del Init como cards. Cierra el círculo visual del
323ecosistema brahman.
324
325- Crate nuevo `crates/apps/nouser-explorer` (deps: brahman-admin,
326  brahman-card, gpui).
327- Ventana 900×640 con header del estado del Init, banner de error
328  cuando no conecta, y lista de cards (una por sesión).
329- Cada card muestra: kind + label + lifecycle, ULID corto, summary
330  (si data), keywords, lens hint, service_socket si está, y refs
331  (RelationshipKind → target_label). El borde izquierdo coloreado
332  diferencia ente (azul) de data (lavanda).
333- `cx.spawn(async move |this, cx| { … })` corre el loop de refresh
334  en el GPUI executor; `query_blocking` se usa porque GPUI no provee
335  un runtime tokio.
336- Nuevo helper en brahman-admin: `client::query_blocking(path)` —
337  versión sync de `query()`, para callers con su propio executor.
338
339Uso:
340
341  $ ente-zero & nouser daemon crates/core &
342  $ cargo run -p nouser-explorer
343  # ventana muestra ~6 cards en vivo, refrescando cada 2s.
344
345cargo check --workspace: 0 errores, 0 warnings.
346
347### feat(nouser): persistencia sled write-through del MonadDb
348`MonadDb` ahora soporta backend dual:
349
350- `MonadDb::new()` → memoria pura (default, back-compat).
351- `MonadDb::open(path)` → sled-backed con cache en memoria. Carga
352  contenido existente al abrir; cada `insert_*` hace write-through
353  (cache + sled).
354
355Diseño:
356- 2 trees sled: `files` y `monads`.
357- Wire format: serde_json (ergonomía + inspectability con sled-cli;
358  los manifests son chicos, JSON gana sobre postcard aquí).
359- Reads SIEMPRE desde la cache — sled se consulta sólo al abrir.
360- `replace_monads()` purga el tree de sled antes de escribir.
361
362Bin nouser: nueva env var `NOUSER_DB_PATH`. Si está set, persiste
363en esa ruta; si no, in-memory:
364
365  $ NOUSER_DB_PATH=/tmp/monads.sled nouser scan crates/core
366  scan: 102 archivos en crates/core, 5 mónadas
367  $ ls /tmp/monads.sled
368  blobs  conf
369  $ NOUSER_DB_PATH=/tmp/monads.sled nouser scan crates/core
370  # segunda corrida re-escribe la DB con el nuevo scan
371
372Tests nuevos en db.rs:
373- `persistence_roundtrip` — escribe, cierra, reabre, datos están.
374- `replace_monads_purges_persistent_tree` — replace limpia el tree.
375
37624 tests en nouser-core (era 22, +2).
377
378### feat(sidecar): Phase B-3 — SidecarPool consolida en un runtime
379Antes: cada `spawn(card)` creaba un thread + tokio runtime propio.
380Para módulos que publican muchas sesiones (nouser daemon con 50+
381Mónadas) eso es 50 threads + 50 runtimes. Ahora: **un thread + un
382runtime tokio current_thread** que hostea N tasks de sidecar.
383
384API nueva (aditiva, no rompe `spawn`/`spawn_with_handle`):
385
386  let pool = SidecarPool::new()?;
387  pool.spawn(card1);
388  pool.spawn(card2);
389  pool.spawn_conscious(card_wit, wit);
390  pool.spawn_with_config(SidecarConfig::new(c).with_wit(w));
391  // pool drop = todas las sesiones cierran.
392
393`run_client` se hace pública para que el pool pueda enqueuar tasks
394externos al runtime con `handle.spawn(run_client(config))`.
395
396`nouser daemon` migrado al pool. Verificación con `ps -L`:
397
398  $ ps -L -p $(pidof nouser)
399    LWP   CMD
400    28817 nouser              # main thread
401    28819 brahman-sidecar     # pool thread (todas las sesiones)
402
403Antes serían 6+ LWP (1 main + N sesiones); ahora 2 fijos sin importar
404cuántas Mónadas se publiquen.
405
406### feat: Crossreferencia — Card.references como grafo del fractal
407Las Cards ahora declaran sus relaciones con otras Cards. El Engine
408posee Mónadas; las Mónadas declaran que son poseídas por el Engine.
409La UI puede cruzar el grafo sin discovery especial.
410
411- `brahman-card`:
412  - `RelationshipKind { Owns, OwnedBy, Processes, ProcessedBy, Sibling }`.
413  - `CardReference { kind, target_id, target_label }` — `target_label`
414    es cache del label en el momento de declarar (la UI puede pintar
415    sin resolver).
416  - `Card.references: Vec<CardReference>` y espejo en `WireCard`.
417    Conversiones `From` propagan.
418- `brahman-broker::BrokeredCard` propaga `references`.
419- `brahman-status` imprime cada referencia: `ref OwnedBy → label (id)`.
420- **nouser daemon**: cada Mónada que publica añade
421  `RelationshipKind::OwnedBy` apuntando al engine. La declaración es
422  unilateral; el engine no necesita conocer las IDs de antemano.
423
424Validación end-to-end:
425
426  $ ente-zero & nouser daemon crates/core
427  $ brahman-status
428  Sessions (6):
429    [ente]  ... brahman.nouser_engine
430    [data]  ... brahman-handshake/src
431        ref OwnedBy  →  brahman.nouser_engine  (01K...)
432        summary: 6 archivos...
433    [data]  ... ente-brain/src
434        ref OwnedBy  →  brahman.nouser_engine  (01K...)
435    ...
436
437### feat: Phase D-3 + D-4 — service_socket en Card, providers coexisten
438Cierra el ciclo del swap automático de Nous (mock↔real):
439
440- **Schema** (`brahman-card`): `Card.service_socket: Option<PathBuf>` y
441  espejo en `WireCard`. Conversiones `From` propagan. Es el path del
442  **data plane** (distinto del socket del Init); cualquier consumer
443  que matchee con esta Card puede conectar directo sin discovery
444  adicional.
445- **Broker** (`brahman-broker`): `BrokeredCard` propaga
446  `service_socket` desde la Card. Sin participación en el matching —
447  sólo metadata para los observadores.
448- **MatchEvent** (`brahman-handshake`): nuevo campo
449  `producer_service_socket: Option<PathBuf>`. Cuando el server emite
450  `Available`, busca la `BrokeredCard` del productor en el broker y
451  copia su `service_socket`. El consumer recibe la ruta completa para
452  conectar.
453- **Transport** (`nouser-nous`): `provider_socket_path(provider: &str)`
454  devuelve `nouser-nous-{provider}.sock` por default — mock y real
455  coexisten en sockets distintos (Phase D-4). `default_socket_path()`
456  conserva el comportamiento single-provider.
457- **Providers**: mock declara `service_socket =
458  /run/user/X/nouser-nous-mock.sock`; real declara
459  `nouser-nous-real.sock`. La Card se construye DESPUÉS del bind para
460  que el path declarado sea el real.
461- **Status**: `brahman-status` imprime `socket:` por sesión cuando
462  está presente.
463
464Validación end-to-end:
465
466  $ ente-zero & nouser-nous-mock & nouser-nous-real &
467  $ ls /run/user/1001/nouser-nous-*.sock
468    nouser-nous-mock.sock
469    nouser-nous-real.sock
470
471  $ brahman-status
472  Sessions (2):
473    [ente]  ... nouser.nous_real
474        socket:     /run/user/1001/nouser-nous-real.sock
475        in  embed-request: Primitive { name: "json" }
476        out embed-result: Primitive { name: "json" }
477    [ente]  ... nouser.nous_mock
478        socket:     /run/user/1001/nouser-nous-mock.sock
479        in  embed-request, out embed-result
480
481Pendientes para futuro (no críticos):
482- nouser-core attract --remote todavía usa NOUSER_NOUS_SOCKET hardcoded
483  o `default_socket_path()`. El siguiente paso es subscribirse al
484  MatchEvent del broker y usar `producer_service_socket` directo —
485  con eso `BRAHMAN_BROKER_CONTEXT=test/prod` swapea provider sin
486  tocar al consumer.
487
488### refactor(nouser): labels de Mónada con 2 componentes del path
489Resuelve la fricción visual de monorepos donde múltiples Mónadas se
490llamaban "src". Nueva función `label_from_path` toma los últimos hasta
4912 componentes normales del path y los une con `/`.
492
493  $ nouser scan crates/core
494    [01K..] brahman-admin/src       card=5
495    [01K..] brahman-handshake/src   card=6
496    [01K..] ente-brain/src          card=11
497    [01K..] ente-kernel/src         card=4
498    ...
499
500Tests añadidos: `label_from_root_only_one_component`,
501`label_from_deep_path_takes_last_two`. Tests existentes actualizados
502con los nuevos labels.
503
504### feat(nouser): Phase D-2 — proveedor Nous real (LLM) detrás de feature flag
505Cierra el ciclo del módulo Nous: existe un proveedor que produce
506embeddings reales con un modelo LLM, mientras que `cargo build` sin
507features sigue siendo liviano (no descarga ni compila ML deps).
508
509Crate nuevo:
510
511- `crates/modules/nouser/nous-real`: bin con dos modos según feature.
512  - **Sin feature (default)**: stub. Bin compila en ~10s, arranca,
513    sidecarea a brahman-init declarando la Card de real-nous, escucha
514    en el socket Nous, y rechaza toda request con `ErrorResponse {
515    error: "compilado sin la feature embeddings. Rebuild con
516    cargo build -p nouser-nous-real --features embeddings" }`.
517    `cargo build --workspace` sigue siendo limpio.
518  - **Con `--features embeddings`**: pulls `fastembed = "4"`. Ese crate
519    arrastra `ort 2.0.0-rc.9` (ONNX Runtime con binarios descargados
520    por Cargo) + `tokenizers 0.21` + ~30 deps más. Compila en ~50s.
521    Modelo default: `all-MiniLM-L6-v2` (384-d, descargado a
522    `~/.cache/fastembed` la primera vez).
523  - `EmbedText`: pasa el texto al modelo, devuelve vector 384-d.
524  - `EmbedFile`: lee primeros 8KiB con UTF-8 lossy, embed como texto.
525    Para binarios el resultado no es semánticamente útil — caller
526    decide.
527  - `Ping`: devuelve `model_id` y `embed_dim` reales.
528
529- Card de real-nous:
530  - label `nouser.nous_real` (distinto del mock para coexistir).
531  - `priority_contexts.prod = { priority_offset: +1 }`. En contexto
532    prod gana sobre el mock; en `test` el mock gana por su propio
533    `+1`. Sin contexto activo, empate alfabético entre ambos.
534
535Validación end-to-end con modelo real:
536
537  $ cargo build -p nouser-nous-real --features embeddings   # ~50s
538  $ ente-zero & nouser-nous-real &
539  $ # probe vía python al socket Unix:
540  $ echo '{"kind":"embed_text","payload":{"text":"hello brahman"}}' \
541    | python3 -c "..." | head
542    model: real-fastembed-allMiniLML6V2-384d
543    elapsed_ms: 8
544    embed_dim: 384
545    first 5 values: [0.0034, -0.0036, 0.0078, -0.0218, -0.0162]
546
547Tradeoff conocido: las dimensiones del mock (32-d) y real (384-d) son
548incompatibles. Cambiar de proveedor invalida los centroides cacheados
549de Mónadas. Documentar como "limpiar DB al cambiar proveedor".
550
551Workspace state:
552- cargo build --workspace sigue limpio sin features (no ML).
553- cargo build -p nouser-nous-real --features embeddings funciona.
554- 0 errores, 0 warnings en ambos modos.
555
556Pendientes para D-3 / futuro:
557- Discovery de socket: hoy el consumer hardcodea NOUSER_NOUS_SOCKET.
558  Para que el broker brahman elija real vs mock per-contexto, falta
559  inyectar el socket del provider electo en el MatchEvent o exponer
560  un broker query "dame el socket de la sesión X".
561- Coexistencia: hoy los dos providers compiten por el mismo socket
562  path por default. Habría que parametrizarlos a sockets distintos
563  cuando coexistan.
564
565### feat(nouser): Phase D — proveedor Nous mock + cliente remoto
566Cierra el patrón "Nous como módulo aparte intercambiable": el contrato
567del proveedor de embeddings vive en su crate, el mock determinístico
568implementa ese contrato sirviéndolo por Unix socket, y `nouser-core`
569sabe consumirlo remotamente. El switch entre mock y real (futuro) se
570hará vía priority_contexts en el broker.
571
572Crates nuevos:
573
574- `crates/modules/nouser/nous`: contrato compartido. Tipos
575  `EmbedRequest`, `RequestKind { EmbedFile, EmbedText, Ping }`,
576  `EmbedFilePayload`, `EmbedTextPayload`, `EmbedResponse`,
577  `PingResponse`, `ErrorResponse`. Wire format: line-delimited JSON
578  por Unix socket, single-shot per conexión. Constants para los nombres
579  de flow (`embed-request`/`embed-result`) y el tipo (`json`). Helper
580  `transport::default_socket_path()` con env var
581  `NOUSER_NOUS_SOCKET`.
582- `crates/modules/nouser/nous-mock`: bin `nouser-nous-mock`. Sidecarea
583  a brahman-init con Card kind=Ente declarando los flows
584  `embed-request:json`/`embed-result:json` y un
585  `priority_contexts.test = { priority_offset: +1 }` (gana sobre
586  cualquier real-nous en contexto test). Bind del socket Nous, accept
587  loop, despacha por `RequestKind`. EmbedFile usa
588  `nouser_core::embed::embed` (los pseudo-embeddings de Phase C).
589  Modelo: `mock-pseudo-32d`.
590
591Cambios:
592
593- `nouser-core`: dep nueva `nouser-nous`. Subcomando `attract` ahora
594  acepta `--remote` que abre un socket UnixStream blocking, envía un
595  `EmbedRequest` y lee la response. Imprime `embed: local|remote`
596  para que se vea cuál ruta corrió.
597
598Validación end-to-end (un solo terminal, varios procesos):
599
600  $ ente-zero &
601  $ nouser-nous-mock &
602  $ NOUSER_MIN_FILES=5 nouser daemon crates/core &
603  $ brahman-status
604
605  Sessions (7):
606    [ente] nouser.nous_mock      flows: embed-request, embed-result
607    [ente] brahman.nouser_engine
608    [data] src   summary: 6 archivos en crates/core/brahman-handshake/src
609    [data] graph summary: 7 archivos en crates/core/ente-zero/src/graph
610    ...
611
612  $ nouser attract --remote crates/core <archivo.rs>
613    embed: remote
614    🧲  0.9058  src  ...
615
616  Mock log: "embed_file path=crates/modules/nouser/core/src/embed.rs"
617
618Bug encontrado y corregido en el camino:
619- `ContextBias` tenía `#[serde(skip_serializing_if = ...)]` en sus
620  campos. Postcard NO soporta skip-condicional (formato no
621  self-describing): el serializer omitía bytes que el deserializer
622  esperaba, rompiendo la wire de cualquier Card con
623  `priority_contexts` poblada.
624- Fix: removidos los `skip_serializing_if` de `ContextBias`. JSON
625  pretty ahora emite `{"pin_to": null, "priority_offset": 0}` en lugar
626  de objeto vacío. Trade-off aceptado por compatibilidad de wire.
627- Test nuevo en brahman-card: `wirecard_postcard_with_priority_contexts`
628  que ejercita el roundtrip completo postcard.
629
630Tests acumulados: 75 (card 12 +1 nuevo, broker 15, handshake 9,
631card-wit 4, admin 0, nouser-card 7, nouser-core 20, nouser-nous 2).
632cargo check --workspace: 0 errores, 0 warnings.
633
634Próximo natural: Phase D-2 — `real-nous` con un modelo ONNX/Llama de
635text-embedding. La infraestructura ya está lista: declara la misma
636Card con `priority_contexts.prod = { priority_offset: +1 }` y el
637swap es transparente para el consumer.
638
639### feat(nouser): Phase C — pseudo-embeddings + atracción por centroide
640El "imán semántico" matemático del diseño Kairos, sin LLM. Cada
641archivo se proyecta a un vector 32-d derivado de sus metadatos; cada
642Mónada calcula su centroide; archivos nuevos se asignan por cosine
643similarity contra los centroides existentes.
644
645Cambios:
646
647- nouser-core dep nueva: `blake3` (hash determinista de strings).
648- `crates/modules/nouser/core/src/embed.rs`:
649  - `EMBED_DIM = 32`. Estructura del vector:
650    - dims  0..8:  blake3(extension)     → identidad de tipo
651    - dims  8..16: blake3(parent_dir)    → identidad de contenedor
652    - dims 16..24: blake3(file_stem)     → identidad léxica
653    - dims 24..28: tamaño (log + flags)
654    - dims 28..32: mtime (escala día + cíclicas)
655  - **Tip clave**: bytes del hash se centran a `[-1, 1]` (no `[0, 1]`).
656    Sin centrar, dos vectores hash random tendrían cosine ~0.75
657    espuria; centrados, expectativa ≈ 0 entre no-relacionados.
658  - APIs: `embed`, `cosine_similarity`, `centroid`, `cohesion`,
659    `attraction_score`, `best_attraction`. `DEFAULT_ATTRACTION_THRESHOLD = 0.7`.
660- `cluster::by_directory` ahora computa el centroide de cada Mónada
661  (promedio de embeddings de los miembros, L2-normalizado) y lo guarda
662  en `MonadManifest.centroid`. El centroide viaja al brahman-status vía
663  `DataFacet.centroid` → ahora se ven los Vec<f32> reales por cada Mónada.
664- bin nouser nuevo subcomando: `attract <dir> <file>`.
665  - Escanea el dir, embeda el archivo objetivo, ranking de afinidad
666    contra todas las Mónadas con centroide.
667  - Marca 🧲 si la mejor supera el umbral, `·` si es la mejor pero
668    debajo, espacio en blanco para el resto.
669
670Validación end-to-end:
671
672  $ nouser attract crates/core crates/modules/nouser/core/src/embed.rs
673  ranking de atracción (cosine similarity):
674    🧲  0.9058  [01K..] src  (11 archivos en crates/core/ente-brain/src)
675        0.8984  [01K..] src  (6 archivos en crates/core/brahman-handshake/src)
676        0.8918  [01K..] src  (5 archivos en crates/core/ente-zero/src)
677        ...
678
679  $ nouser attract crates/core crates/modules/nouser/core/Cargo.toml
680  ranking:
681        0.3427  [01K..] graph  (7 archivos en crates/core/ente-zero/src/graph)
682        ...
683    (mejor score 0.3427 < umbral 0.7000 — el archivo no se 'pega')
684
685Tests: 20 en nouser-core (era 13, +7 de embed). Total acumulado: 73
686(card 11, broker 15, handshake codec+tr 2 + integ 7, card-wit 4,
687admin 0, nouser-card 7, nouser-core 20, ente-card 0).
688cargo check --workspace: 0 errores, 0 warnings.
689
690Próximo: **Phase D** — `nouser-nous`, módulo aparte para LLM real.
691Mock-nous determinista (basado en estos pseudo-embeddings) en
692`BRAHMAN_BROKER_CONTEXT=test`; real-nous en `prod`. El switch lo hace
693el broker via priority_contexts sin tocar nada más.
694
695### feat(nouser): Phase B-2 — daemon que publica Mónadas al Init
696Cierra la unificación: el `nouser daemon` se sidecarea como Ente y
697publica cada Mónada como su propia sesión Data. Un solo
698`brahman-status` muestra procesos y datos en la misma lista, exactamente
699como buscaba el diseño.
700
701Cambios:
702
703- `crates/modules/nouser/core/Cargo.toml`: deps nuevas `brahman-card`
704  y `brahman-sidecar`.
705- `crates/modules/nouser/core/src/bin/nouser.rs`: subcomando
706  `daemon <dir>`.
707  - Spawna un sidecar para el "engine" (`brahman.nouser_engine`,
708    kind=Ente) — el ser que produce y administra Mónadas.
709  - Scan + cluster del dir.
710  - Para cada Mónada, llama `monad.to_brahman_card()` y spawnea un
711    sidecar (kind=Data). Cada Mónada es una sesión brahman propia
712    con su ULID estable.
713  - Park del thread principal: los sidecars siguen pingueando.
714
715Validación end-to-end:
716
717  $ ente-zero &
718  $ NOUSER_MIN_FILES=5 nouser daemon crates/core &
719  $ brahman-status
720
721  Sessions (6):
722    [ente]  ... brahman.nouser_engine  lifecycle=Daemon
723    [data]  ... src    summary: 5 archivos en crates/core/brahman-admin/src
724                       members: 5 (dispersion=0.00)
725                       lens hint: code
726    [data]  ... src    summary: 11 archivos en crates/core/ente-brain/src
727                       ...
728    [data]  ... graph  summary: 7 archivos en crates/core/ente-zero/src/graph
729
730El protocolo de presentación es uno solo: la Card. La función — anunciar
731identidad, exponer metadata, ser descubierto — es idéntica para procesos
732vivos y agrupaciones de datos. La UI lo ve como una lista uniforme.
733
734Costo conocido: cada Mónada consume un thread + tokio runtime
735current_thread (legacy del sidecar API). Para muchas Mónadas (>50)
736conviene consolidar en un único runtime con N tasks. Defer a Phase B-3.
737
738Pendientes propuestos:
739- **B-3**: consolidar todos los sidecars en un único runtime tokio
740  para no spawnear N threads.
741- **C**: pseudo-embeddings + atracción por centroide.
742- **D**: módulo `nouser-nous` para LLM, swappable por priority_contexts.
743- **Polish**: labels con 2-3 componentes del path.
744- **Crossreferencia**: que un Ente pueda anunciar "estoy procesando la
745  Mónada X" y la Mónada anuncie "Ente Y me está procesando".
746
747cargo check --workspace: 0 errores, 0 warnings.
748
749### feat: Phase B-1 — unificación ontológica de Cards (Ente ↔ Data)
750La Card es **el** protocolo de presentación del ecosistema, no sólo de
751los procesos. Una Mónada Nouser y un Ente Brahman son ambos "entidades
752que se presentan"; el consumidor (UI, broker, admin) discrimina por
753`kind` cuando importa, pero todos hablan el mismo idioma.
754
755Cambios:
756
757- `brahman-card`:
758  - `CardKind { Ente (default), Data }`. Conserva back-compat:
759    Cards existentes son `Ente` por default.
760  - `DataFacet { summary, keywords, centroid, member_count, dispersion,
761    presentation_hint }`. Liviano para el wire — listas grandes
762    (members, embeddings completos) se consultan al daemon dueño bajo
763    demanda.
764  - `Card.kind` y `Card.data: Option<DataFacet>` agregados. WireCard
765    espeja, conversiones `From` propagan.
766  - Default impl actualizado.
767
768- `brahman-broker::BrokeredCard`: propaga `kind` y `data` desde la Card
769  registrada. No afecta el matching (sigue siendo por TypeRef +
770  priority + pin_to); permite a observadores discriminar sin re-query.
771
772- `nouser-card`: depende ahora de `brahman-card`. Nuevo método
773  `MonadManifest::to_brahman_card()` que proyecta:
774  - id, label, lineage → directos.
775  - payload Virtual, supervision Delegate, lifecycle Daemon (placeholder
776    semántico — la Mónada no se ejecuta).
777  - kind = Data.
778  - data = Some(DataFacet) con summary, keywords, centroide,
779    member_count, entropy → dispersion, y un `presentation_hint` derivado
780    del `Lens` (`Code` → `"code"`, `Gallery` → `"gallery"`, etc.).
781  - Test nuevo: `projects_to_brahman_card`.
782
783- `brahman-status`: cada sesión muestra ahora `[ente]` o `[data]` como
784  prefijo. Para sesiones `data`, render adicional con summary, members
785  + dispersion, keywords y lens hint.
786
787Resultado: la UI (yahweh, brahman-status, futuro explorer) ve una sola
788lista uniforme. No tiene que saber si está mirando un proceso o un
789cúmulo de datos — sólo lee el Card y se adapta por `kind`.
790
791Tests acumulados: 59 (card 11, broker 15, handshake codec+transport 2 +
792integ 7, card-wit 4, admin 0, nouser-card 7, nouser-core 13).
793cargo check --workspace: 0 errores, 0 warnings.
794
795Próximo: **Phase B-2** — bin `nouser daemon <dir>` que sidecarea cada
796Mónada como una sesión brahman, publicándola al broker. Brahman-status
797las verá junto a los entes.
798
799### feat(nouser): Phase A — mecanismo determinista de Mónadas
800Primer trozo del módulo Nouser (Kairos): explorador de Mónadas como
801"imanes semánticos" sobre el filesystem. Phase A cubre el 90% de los
802casos sin tocar IA — sólo metadatos y heurísticas.
803
804Crates nuevos:
805
806- `crates/modules/nouser/card`: `MonadManifest` (la Tarjeta de
807  Presentación de una Mónada — espejo conceptual de `brahman::Card`
808  pero para datos, no para procesos runtime). Campos: id (Ulid),
809  label, summary, centroid (vacío en Phase A), keywords, cardinality,
810  entropy [0,1], dominant_lens, pins, members, timestamps,
811  extensions (forward-compat). 6 tests de validación + JSON roundtrip.
812- `crates/modules/nouser/core`: pipeline determinista.
813  - `scanner`: walkdir → `Vec<FileEntry>` con metadatos (path, size,
814    mtime, extension). Skipea hidden por default. Configurable max
815    depth y follow_links.
816  - `cluster::by_directory`: agrupa por parent dir, mínimo 3 archivos
817    para promover a Mónada (configurable). Calcula keywords (top-N
818    extensiones por frecuencia + alfabético), elige `Lens` dominante
819    (Code/Gallery/Markdown/Database/Grid) según extensión más
820    frecuente, computa entropía de Shannon normalizada [0,1].
821  - `db`: `MonadDb` en memoria con índices BTreeMap files/monads y
822    `resolve_members(monad_id)` que filtra IDs huérfanos. Phase B
823    traerá persistencia.
824  - bin `nouser`: subcomandos `scan <dir>`, `show <dir> <prefix>`,
825    `json <dir>`. Env var `NOUSER_MIN_FILES` para tunear el threshold.
826  - 13 tests (4 scanner + 6 cluster + 3 db).
827
828Demo end-to-end:
829
830  $ nouser scan crates
831  scan: 255 archivos en crates, 19 mónadas (min_files=3)
832    [01KR4C13]  src                             card=12  ent=0.00  lens=Code
833             keywords: rs
834    [01KR4C13]  tests                           card=14  ent=0.00  lens=Code
835             keywords: rs
836    [01KR4C13]  fixtures                        card=5   ent=0.00  lens=Grid
837             keywords: rhai
838    ...
839
840  $ nouser show crates 01KR4C
841  Monad 01KR4C1370DVF6NMTW6SECNXAF
842    label:       src
843    summary:     4 archivos en crates/modules/nouser/core/src (ext: rs)
844    cardinality: 4
845    entropy:     0.0000
846    lens:        Code
847    members (4):
848            4132 bytes  crates/modules/nouser/core/src/db.rs
849            ...
850
851Pendientes para próximas fases (anotados, no urgentes):
852- **Phase B**: bin `nouser daemon` que sidecarea a brahman-init
853  declarando flows (`scan-request:json` → `monad-update:json`).
854- **Phase C**: pseudo-embeddings deterministas (hash de path/ext/size
855  a 32-d) + atracción por centroide via cosine similarity. Implementa
856  el "imán" sin LLM.
857- **Phase D**: módulo `nouser-nous` aparte para el LLM real
858  (Llama/ONNX). En `priority_contexts.test` el Init pinea a
859  `mock-nous` (embeddings determinísticos); en `prod` a `real-nous`.
860- **Polish**: labels de Mónada incluir 2-3 componentes del path para
861  desambiguar `src/` repetidos en monorepo.
862
863Workspace: 0 errores, 0 warnings. Tests acumulados: 58
864(card 11, broker 15, handshake codec+transport 2 + integ 7,
865card-wit 4, admin 0, nouser-card 6, nouser-core 13).
866
867### feat(broker): priority contexts — biases per-contexto operativo
868- `brahman-card::ContextBias { pin_to: Option<String>, priority_offset: i8 }`
869  declara un override per-contexto.
870- `Card.priority_contexts: BTreeMap<String, ContextBias>` y mismo en
871  `WireCard` (cruza el wire). Las conversiones `From` lo propagan.
872- `BrokerConfig.current_context: Option<String>`. Cuando el broker corre
873  bajo un contexto y una Card declara biases para ese nombre, se aplican:
874  - Como **consumidor**: `pin_to` sobreescribe el `Flow.pin_to` estático.
875  - Como **productor**: `priority_offset` se suma a la priority base
876    (clamp en `[Low=0, Critical=3]`) para el ranking.
877- `BrokeredCard` propaga `priority_contexts`. `find_producer_for` usa
878  `effective_priority(card)` y `effective_pin(card, input)` antes de
879  los tiebreaks.
880- `brahman-admin::AdminConfig.current_context` + `StatusSnapshot.current_context`
881  espejan el contexto activo. `brahman-status` lo imprime como
882  `Context: <nombre>` justo debajo de `Init: ...`.
883- `ente-zero` lee `BRAHMAN_BROKER_CONTEXT` env var y la propaga al
884  broker y al admin. Sin var, biases per-contexto inactivos.
885- 4 tests nuevos en brahman-broker:
886  `context_priority_offset_lifts_producer_above_alphabetic_winner`,
887  `context_pin_to_overrides_static_pin`, `unknown_context_no_op`,
888  `priority_offset_clamps_to_critical`.
889- Validación end-to-end: `BRAHMAN_BROKER_CONTEXT=test ente-zero` →
890  `brahman-status` muestra `Context: test`.
891
892### feat(card): WireCard + extensions — forward-compat sin romper postcard
893- `Card.extensions: BTreeMap<String, serde_json::Value>` restaurado con
894  `#[serde(flatten, default, skip_serializing_if = is_empty)]`. Los
895  campos JSON/TOML desconocidos sobreviven el roundtrip de archivos.
896- Nuevo `WireCard`: proyección postcard-friendly (sin `extensions`,
897  `genesis: Vec<WireCard>` recursivo). Conversiones `From<Card>` y
898  `From<WireCard>` con descarte/recreación de extensions.
899- `brahman-handshake::Hello.card` pasa de `Card` a `WireCard`. Client
900  hace `card.into()` antes de enviar; Server hace `hello.card.into()`
901  para volver a Card antes de validar/registrar.
902- 3 tests nuevos en brahman-card:
903  `extensions_preserved_in_json_roundtrip`,
904  `wire_card_roundtrip_strips_extensions`,
905  `wire_card_postcard_friendly` (postcard encode/decode efectivo).
906- brahman-card gana `postcard` como dev-dep para el último test.
907- Contrato documentado: extensions = anotaciones locales que NO cruzan
908  al Init; sólo viven en archivos.
909
910### `9420eae` chore: limpia warnings dead-code en arje (commit del usuario)
911- `ente-zero/src/events.rs`: `#![allow(dead_code)]` a nivel módulo —
912  es vocabulario de eventos con variantes/campos reservados para flujos
913  no cableados aún (CapabilityRequested, ShutdownReason::Signal,
914  CapabilityGrant::{Granted, Denied, QuotaExceeded}, ExitStatus
915  fields).
916- `ente-zero/src/graph/mod.rs`: comentado el re-export ahora innecesario
917  de `SHUTDOWN_GRACE`. `DEFAULT_GRANT_TTL` con `#[allow(dead_code)]`
918  + nota "reservado para capability granting".
919- `ente-zero/src/graph/capabilities.rs`: `renew_grant` con
920  `#[allow(dead_code)]` (capability renewal pendiente).
921- `ente-kernel/src/surface.rs`: drop de `use anyhow::Context` (no se
922  usaba).
923- `ente-hostnamed-compat/src/main.rs`: drop de `Connection` (no se
924  usaba).
925- `ente-polkit-compat/src/main.rs`: `PolicyDecision.source` con
926  `#[allow(dead_code)]` (sólo aparece en `Debug` para logging).
927- `cargo check --workspace`: 17 warnings → 0.
928
929### feat(sidecar): WIT al sidecar — módulos conscientes vivos
930- `brahman-card::WitInterface` deriva `Serialize`, `Deserialize`,
931  `PartialEq`, `Eq` para cruzar el wire postcard.
932- `brahman-handshake::Hello` lleva `wit: Option<WitInterface>`. Server
933  usa `ResolvedCard::from_conscious` cuando viene presente, `from_agnostic`
934  cuando no.
935- `brahman-handshake::Client::connect` queda como wrapper agnóstico de
936  `connect_with(path, card, wit: Option<WitInterface>)`.
937- `brahman-broker::Broker::register` ahora toma `Option<WitInterface>`
938  como tercer arg. `BrokeredCard` guarda el wit. 25 sitios de tests
939  actualizados con `, None`.
940- `brahman-sidecar::SidecarConfig` con campo `wit`. Helpers nuevos:
941  `SidecarConfig::new(card).with_wit(wit)` y `spawn_conscious(card, wit)`.
942  El log `attached` reporta `conscious=true|false`.
943- `brahman-status` muestra marker 🧠 + sección `wit:` (package/world,
944  imports, exports) por sesión consciente.
945- Example nuevo `crates/shared/brahman-sidecar/examples/presence-conscious.rs`:
946  toma label + path .wit (default `shared_wit/protocol.wit`), parsea
947  con brahman-card-wit, spawna sidecar consciente.
948- Validado end-to-end:
949  ```
950  $ presence-conscious demo.conscious shared_wit/protocol.wit &
951  $ brahman-status
952  Sessions (1):
953    01K... demo.conscious 🧠  lifecycle=Daemon
954        wit: brahman:protocol@0.1.0 / module
955             imports: types, handshake, lifecycle
956             exports: run
957  ```
958
959### feat(core): brahman-card-wit — extractor opcional de contratos WIT
960- Crate nuevo `crates/core/brahman-card-wit` con `wit-parser = "0.230"`.
961- API: `parse_wit(source)` y `parse_wit_file(path)` devuelven
962  `Vec<WitInterface>` (uno por `world` declarado).
963- Interfaces importadas/exportadas (no sólo funciones) se resuelven
964  por nombre via `resolve.interfaces[id].name`.
965- Example `crates/core/brahman-card-wit/examples/brahman-wit-info.rs`
966  CLI: `brahman-wit-info shared_wit/protocol.wit` → lista paquete,
967  worlds, imports y exports.
968- 4 tests: inline, archivo real (`shared_wit/protocol.wit`), parse
969  error, world vacío.
970- Validado contra `protocol.wit`: detecta worlds `module` y
971  `admin-host` con sus imports/exports correctos.
972
973### `7b589b8` chore: agrega CHANGELOG.md retroactivo
974- `CHANGELOG.md` en la raíz con los 11 commits previos documentados
975  acción por acción. A partir de este punto, cada cambio sustantivo
976  actualiza también este archivo en el mismo commit.
977
978### `8a83a26` feat(handshake): notificación push de matches
979- Frame `MatchEvent { kind: Available | Lost, ... }` añadido al protocolo.
980- `Session::run_post_handshake` usa `tokio::select!` para multiplexar
981  reads del cliente y un canal `mpsc` push del server.
982- Server: `SessionTxTable` (Arc<Mutex<HashMap<SessionId, Sender<Frame>>>>)
983  y `LastMatches` para diff por sesión. `broadcast_match_diffs` corre
984  tras cada `register` y `unregister`, emite sólo los cambios.
985- Capacity del canal push: 32 (ephemeral, `try_send` non-blocking).
986- Client: `VecDeque<MatchEvent>` interno, `take_event()` (non-blocking)
987  y `await_event(timeout)`. `ping()` ahora drena MatchEvents intermedios
988  hasta encontrar el Pong.
989- Example `crates/core/brahman-handshake/examples/subscriber.rs`.
990- Test `match_event_pushed_on_producer_arrival` (handshake integ 6→7).
991
992### `70a7a0d` feat: segundo módulo (nakui) + admin API + brahman-status
993- Crate nuevo `crates/shared/brahman-sidecar` (DRY del thread + tokio +
994  ping loop). API: `spawn(card)` / `spawn_with_handle(config)`.
995- `nakui` cmd_run llama `brahman_sidecar::spawn` antes de `run_server`.
996  Card: lifecycle Daemon, supervision Restart, flow `command` (json) /
997  `report` (json).
998- Crate nuevo `crates/core/brahman-admin` con `StatusSnapshot` JSON
999  line-delim, `AdminServer` y `client::query`.
1000- ente-zero levanta también el AdminServer en `primordial_loop`.
1001- Example `crates/shared/brahman-sidecar/examples/presence.rs`
1002  (módulo dummy long-lived parametrizable por label).
1003- Example `crates/core/brahman-admin/examples/brahman-status.rs`
1004  (CLI que pretty-printa el snapshot).
1005- `brahman-broker`: `BrokeredCard` ahora incluye `lifecycle`. `Endpoint`
1006  y `Match` derivan `Serialize`/`Deserialize`. Nuevo `Broker::cards()`
1007  iterador.
1008- `brahman-card`: `pub use ::ulid` para que módulos no dependan de ulid.
1009- yahweh-shell migrado al sidecar compartido (96→53 LOC).
1010
1011### `595f68e` feat(yahweh-shell): primer módulo brahman vivo
1012- yahweh-shell spawnea sidecar antes de `Application::new()`.
1013- Card declarada: label `brahman.ui_engine`, lifecycle Widget,
1014  supervision Delegate, payload Virtual, flow input `render-data`
1015  (json) / output `user-intent` (json).
1016- Sidecar en thread aparte con tokio current_thread runtime,
1017  desacoplado del runtime GPUI.
1018
1019### `df9d10c` feat(ente-zero): enchufa el handshake server al Init real
1020- ente-zero levanta `brahman_handshake::server::Server::bind` en
1021  `primordial_loop` después del ente-bus, con degradación grácil
1022  si bind falla (mismo patrón que uevents).
1023- Nuevo módulo `brahman-handshake/src/transport.rs`: helper
1024  `default_socket_path()` con resolución `BRAHMAN_INIT_SOCKET` →
1025  `XDG_RUNTIME_DIR` → `TMPDIR`.
1026- Example `crates/core/brahman-handshake/examples/probe.rs`.
1027- Validación end-to-end manual: probe contra ente-zero vivo
1028  imprime `HelloAck: session=... init_attached=true`.
1029
1030### `07d77a3` feat(handshake): integra el broker con el ciclo de sesiones
1031- `ServerConfig` acepta `Option<Arc<Mutex<Broker>>>`.
1032- `register_session` indexa la Card en el broker y la `SessionRegistry`
1033  antes de emitir HelloAck.
1034- `Session::handle` refactor a `do_handshake → run_post_handshake →
1035  cleanup` con cleanup unificado (broker + sessions).
1036- Tests integ nuevos: `broker_registers_and_unregisters_with_session`
1037  y `broker_matches_two_live_modules`.
1038- Fix colateral: `brahman-card::TypeRef` pasa de internally-tagged
1039  (`#[serde(tag = "kind")]`) a externally-tagged. Postcard no soporta
1040  internally-tagged en formatos no self-describing. JSON cambia de
1041  `{"kind":"primitive","name":"x"}` a `{"primitive":{"name":"x"}}`.
1042
1043### `5091106` feat(core): brahman-broker — matching híbrido
1044- Crate nuevo `crates/core/brahman-broker`.
1045- 3 estrategias de matching: `Exact`, `Structural`, `ExactThenStructural`
1046  (default). Devuelven `Match::via` con la estrategia que ganó.
1047- Override `pin_to`: el consumer pide un productor por label; si la
1048  pista no resuelve, cae en type-search.
1049- Tiebreak por `Card.priority` desc, luego `label` asc (estable y
1050  determinista).
1051- API: `register`, `unregister`, `find_producer_for`, `all_matches`,
1052  `cards`, `sessions`, `len`, `is_empty`.
1053- 11 tests (matching, pin_to, priority, no-self-loops, all-matches).
1054
1055### `814390f` feat(core): brahman-handshake — protocolo runtime
1056- Crate nuevo `crates/core/brahman-handshake` con server y client
1057  Rust↔Rust sobre Unix socket.
1058- Frames length-prefixed (4 bytes LE) + cuerpo postcard.
1059- Mensajes: `Hello`, `HelloAck`, `Ping`, `Pong`, `Farewell`, `Error`.
1060- `MAX_FRAME_BYTES = 4 MiB` para evitar reservas absurdas.
1061- Tradeoff: drop `extensions`/`extra` de Card por incompat
1062  postcard ↔ `serde_json::Value`. Forward-compat queda en
1063  `schema_version` + `protocol_version` negotiation.
1064- 4 tests integ + 1 unit en codec.
1065
1066### `ed0e973` refactor(arje): migra ente-card a re-export de brahman-card
1067- `ente-card/src/lib.rs` reescrito como crate-shim de re-export
1068  (327 LOC → 25 LOC).
1069- `EntityCard` ≡ `brahman_card::Card` por type alias.
1070- `ente-card/Cargo.toml`: deps reducidas a `brahman-card`.
1071- `Card` impl `Default` (Ulid::nil(), label vacío) para que
1072  `..Default::default()` funcione en struct-literals.
1073- 4 sitios en `ente-zero/src/seed.rs` actualizados con
1074  `..Default::default()` para los campos aditivos.
1075- Los 21 consumidores arje compilan sin tocar fuente.
1076
1077### `0feba74` feat(core): brahman-card — Tarjeta canónica híbrida
1078- Crate nuevo `crates/core/brahman-card`.
1079- Hereda de arje: `id: Ulid`, `lineage`, `Capability` tipado,
1080  `Payload::{Wasm, Native, Virtual, Legacy}`, `SomaSpec`
1081  (namespaces, cgroups, rlimits, cpu_affinity), `Supervision`
1082  (Restart con backoff, OneShot, Delegate), `genesis` recursivo.
1083- Aditivo brahman: `Permissions` enumerados (`NetworkingPolicy`,
1084  `FsPolicy`, `IpcPolicy`), `Lifecycle` ortogonal a Supervision,
1085  `Priority` de scheduling, `Flows` con `TypeRef` discriminado
1086  (Primitive | Wit), `pin_to` opcional.
1087- `TrustLevel` derivado de `Permissions` (no declarado).
1088- `ResolvedCard { card, wit: Option<WitInterface>, trust }`.
1089- Soporta JSON (canónico) + TOML (auto-detectado por extensión).
1090- 8 tests incluido `arje_seed_format_compatible` que valida que
1091  el JSON de arje sigue parseando con defaults para los aditivos.
1092
1093### `4d50bfc` chore: absorbe nakui (ERP matemático) en modules/nakui
1094- `~/nakui` → `crates/modules/nakui/{core,modules}`.
1095- `core/`: el crate `nakui-core` con 4 bins (nakui, demo,
1096  inventory_demo, sales_demo) y tests.
1097- `modules/{inventory,sales,treasury}/`: data declarativa
1098  (`nsmc.json`, `schema.k`, `morphisms/`) que el crate consume.
1099  No son crates Cargo.
1100- Deps directas (no `workspace = true`): thiserror v1, surrealdb,
1101  rhai, petgraph. No conflicto con el resto del workspace.
1102
1103### `53dbdf0` chore: monorepo inicial con arje + minga + yahweh absorbidos
1104- 45 crates absorbidos en 4 ejes:
1105  - `crates/core/`: 24 crates de arje (Init systemd-compatible:
1106    `ente-card`, `ente-zero`, `ente-kernel`, `ente-bus`, `ente-cas`,
1107    `ente-soma`, `ente-wasm`, `ente-snapshot`, `ente-brain`,
1108    `ente-echo`, `ente-policy-provider`, + 12 `*-compat`).
1109  - `crates/modules/semantic_dht/`: 5 crates de minga (`minga-core`
1110    con AST/CAS/MST, `minga-p2p` con libp2p Kad, `minga-store`,
1111    `minga-vfs`, `minga-cli`).
1112  - `crates/modules/ui_engine/`: 11 crates de yahweh (libs/{core,
1113    theme, bus, providers}, widgets/{tree, splitter, tabs, tiled,
1114    container_core, text_input}).
1115  - `crates/apps/`: 5 crates de yahweh (file_explorer,
1116    database_explorer, text_viewer, image_viewer, yahweh-shell).
1117- `shared_wit/protocol.wit` con handshake/lifecycle inicial.
1118- `Cargo.toml` unificado: thiserror bumped a 2 (transparente para
1119  arje), tokio "full", paths intra-workspace de yahweh redirigidos.
1120- `cargo check --workspace`: 0 errores (sólo dead-code warnings
1121  preexistentes en ente-zero).

CHANGELOG.md

Changelog

Registro cronológico de cambios sustantivos en el monorepo Brahman. Cada entrada lista las acciones concretas tras un commit; para detalles de ratio/diff ver git show <sha>.

2026-05-09

chore(nakui): alinear nakui-core con [workspace.package] y deps compartidas

Cleanup de drift de convenciones: nakui-core era el único crate del monorepo que mantenía version = "0.1.0" / edition = "2021" / thiserror = "1" hardcoded, mientras el resto heredaba del workspace y usaba thiserror = "2". Eso significaba que un bump global de versión o de edition se olvidaba sistemáticamente de nakui.

Cambios:

Verificación: cargo build -p nakui-core verde tras el bump de thiserror v1→v2 — el #[derive(Error)] de los 14+ enums de error en nakui no requirió ajustes (la API de derive es backwards-compatible para los patrones simples). cargo test -p nakui-core --lib: 27/27 verdes, sin regresión.

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 00000000…. La fix no es romper Default (sigue siendo determinista, requerido por callers que lo usan como template para deserialización), sino agregar un constructor explícito:

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

Card::new(label) asigna id = Ulid::new() (único) + label provisto, dejando el resto en defaults seguros (Virtual / OneShot / Ente). Pensado para usarse en struct-literals con override parcial, igual sintaxis que el patrón viejo pero sin la trap.

Refactor de call sites:

Default se mantiene tal cual con docstring expandida que advierte explícitamente sobre el uso "vivo" y apunta a Card::new. Tests existentes y el patrón nouser_card::MonadManifest::to_brahman_card (que asigna el id estable de la Mónada, no uno fresco) NO se modifican — Default sigue siendo correcto cuando el caller sobreescribe id explícitamente.

Tests: 3 unitarios nuevos en brahman-card (new_assigns_real_ulid_and_label, new_yields_distinct_ids_per_call, default_keeps_nil_id_for_struct_update_pattern). 15 tests verdes (era 12).

feat(sidecar): API reusable de discovery vía broker

Promueve el patrón ad-hoc discover_producer_socket (que vivía inline en nouser attract --remote) a un módulo público brahman_sidecar::discovery. Cualquier consumer puede ahora preguntar al broker "¿quién provee este TypeRef?" con dos llamadas:

// Construir un consumer Card mínimo (Ente, Oneshot, Virtual) let card = brahman_sidecar::build_consumer_card( "mi-cli", "embed-result", // flow.input.name "json", // TypeRef::Primitive { name } );

// Bloqueante (CLIs, std-thread loops): let socket: PathBuf = brahman_sidecar::await_provider_blocking( card, Duration::from_secs(3), )?; // O async (módulos con runtime tokio propio): let socket = brahman_sidecar::await_provider(card, timeout).await?;

API:

Refactor en nouser daemon:

Próximo consumer natural: nouser-explorer. Hoy renderea StatusSnapshot vía socket admin (introspección pura). El día que quiera interactuar con un Ente — p. ej., disparar un re-embed desde la UI — usa este helper para resolver el socket del provider sin hardcodear paths.

Nota sobre identidad: este commit fuerza Ulid::new() para los consumer Cards generados, evitando la trampa documentada del Card::default() que devuelve Ulid::nil(). La fijación global de Default queda como cleanup separado (requiere auditar que ningún caller dependa del determinismo de nil).

Tests: 4 unitarios nuevos en discovery::tests (id no-nil, id único por llamada, formateo de TypeRef::Wit, fallback sin input). Workspace verde.

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):

Mecánica del re-publish:

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:

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:

Algoritmo (cluster):

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.

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:

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:

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:

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.

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:

Diseño:

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:

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

feat(sidecar): Phase B-3 — SidecarPool consolida en un runtime

Antes: cada spawn(card) creaba un thread + tokio runtime propio. Para módulos que publican 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_wit, wit); pool.spawn_with_config(SidecarConfig::new(c).with_wit(w)); // 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.

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. La UI puede cruzar el grafo sin discovery especial.

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...) summary: 6 archivos... [data] ... ente-brain/src ref OwnedBy → brahman.nouser_engine (01K...) ...

feat: Phase D-3 + D-4 — service_socket en Card, providers coexisten

Cierra el ciclo del swap automático de Nous (mock↔real):

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 in embed-request: Primitive { name: "json" } out embed-result: Primitive { name: "json" } [ente] ... nouser.nous_mock socket: /run/user/1001/nouser-nous-mock.sock in embed-request, out embed-result

Pendientes para futuro (no críticos):

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:

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:

Pendientes para D-3 / futuro:

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:

Cambios:

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 <archivo.rs> embed: remote 🧲 0.9058 src ...

Mock log: "embed_file path=crates/modules/nouser/core/src/embed.rs"

Bug encontrado y corregido en el camino:

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:

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:

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:

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:

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 <dir> 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:

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):

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).

feat(broker): priority contexts — biases per-contexto operativo

feat(card): WireCard + extensions — forward-compat sin romper postcard

9420eae chore: limpia warnings dead-code en arje (commit del usuario)

feat(sidecar): WIT al sidecar — módulos conscientes vivos

feat(core): brahman-card-wit — extractor opcional de contratos WIT

7b589b8 chore: agrega CHANGELOG.md retroactivo

8a83a26 feat(handshake): notificación push de matches

70a7a0d feat: segundo módulo (nakui) + admin API + brahman-status

595f68e feat(yahweh-shell): primer módulo brahman vivo

df9d10c feat(ente-zero): enchufa el handshake server al Init real

07d77a3 feat(handshake): integra el broker con el ciclo de sesiones

5091106 feat(core): brahman-broker — matching híbrido

814390f feat(core): brahman-handshake — protocolo runtime

ed0e973 refactor(arje): migra ente-card a re-export de brahman-card

0feba74 feat(core): brahman-card — Tarjeta canónica híbrida

4d50bfc chore: absorbe nakui (ERP matemático) en modules/nakui

53dbdf0 chore: monorepo inicial con arje + minga + yahweh absorbidos