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:
[package]: version, edition, rust-version, license, authors,
publish → todos *.workspace = true. Agregado description (cumple
convención del resto de crates).
- Deps compartidas migradas a
{ workspace = true }: serde, serde_json,
thiserror (v1→v2), tokio, ulid, sha2.
uuid migrado a { workspace = true, features = ["serde"] } — la
feature serde no está en el workspace dep porque nakui es el único
user; queda local opt-in en lugar de inflar el dep común.
- Deps específicas de nakui (sin compartición posible): rhai, petgraph,
surrealdb permanecen inline con versión local.
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:
brahman_sidecar::discovery::build_consumer_card → ..Card::new(label)
nouser daemon::build_engine_card → ..Card::new("brahman.nouser_engine")
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:
build_consumer_card(label, flow_name, type_name) -> Card
abstrae la verbosidad del struct-literal repetido en cada caller.
Genera un id: Ulid::new() real (no nil → seguro contra
colisiones en el broker).
await_provider(card, timeout) -> Result<PathBuf, ConsumerError>
conecta al init, espera MatchEvent::Available, devuelve
producer_service_socket, manda Farewell. Ignora eventos
Lost durante el await (no aplican al arranque).
await_provider_blocking(card, timeout) arma su propio
runtime current_thread para mundos no-async.
ConsumerError con variantes tipadas: Connect { socket, source },
NoProvider { flow, type_ref, timeout }, Client(ClientError),
Runtime(String). Adiós al Box<dyn Error> de antes.
Refactor en nouser daemon:
discover_producer_socket (60 LOC inline en bin/nouser.rs) → 5
líneas que delegan en el helper.
remote_embed ya no construye su propio runtime tokio.
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):
spawn_fs_watcher arma dos threads: dispatcher filtra eventos
notify Create/Modify/Remove a un canal de paths; coordinator
mantiene HashMap<PathBuf, Instant> 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<Ulid, AbortHandle> 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<Mutex<MonadDb>> 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<String> — 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):
- Open sled si NOUSER_DB_PATH existe.
- Publica las Mónadas previas con
centroid_model válido (las
inválidas se descartan con log explícito).
- Re-scan +
by_directory_hydrated(prior=&db).
- 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.
- 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<String> 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: <source> (<model>) 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:
- Si
NOUSER_NOUS_SOCKET está set, lo usa directo (atajo explícito).
- 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).
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.
brahman-card:
RelationshipKind { Owns, OwnedBy, Processes, ProcessedBy, Sibling }.
CardReference { kind, target_id, target_label } — target_label
es cache del label en el momento de declarar (la UI puede pintar
sin resolver).
Card.references: Vec<CardReference> y espejo en WireCard.
Conversiones From propagan.
brahman-broker::BrokeredCard propaga references.
brahman-status imprime cada referencia: ref OwnedBy → label (id).
- nouser daemon: cada Mónada que publica añade
RelationshipKind::OwnedBy apuntando al engine. La declaración es
unilateral; el engine no necesita conocer las IDs de antemano.
Validación end-to-end:
$ ente-zero & nouser daemon crates/core
$ brahman-status
Sessions (6):
[ente] ... brahman.nouser_engine
[data] ... brahman-handshake/src
ref OwnedBy → brahman.nouser_engine (01K...)
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):
- Schema (
brahman-card): Card.service_socket: Option<PathBuf> y
espejo en WireCard. Conversiones From propagan. Es el path del
data plane (distinto del socket del Init); cualquier consumer
que matchee con esta Card puede conectar directo sin discovery
adicional.
- Broker (
brahman-broker): BrokeredCard propaga
service_socket desde la Card. Sin participación en el matching —
sólo metadata para los observadores.
- MatchEvent (
brahman-handshake): nuevo campo
producer_service_socket: Option<PathBuf>. Cuando el server emite
Available, busca la BrokeredCard del productor en el broker y
copia su service_socket. El consumer recibe la ruta completa para
conectar.
- Transport (
nouser-nous): provider_socket_path(provider: &str)
devuelve nouser-nous-{provider}.sock por default — mock y real
coexisten en sockets distintos (Phase D-4). default_socket_path()
conserva el comportamiento single-provider.
- Providers: mock declara
service_socket = /run/user/X/nouser-nous-mock.sock; real declara
nouser-nous-real.sock. La Card se construye DESPUÉS del bind para
que el path declarado sea el real.
- Status:
brahman-status imprime socket: por sesión cuando
está presente.
Validación end-to-end:
$ ente-zero & nouser-nous-mock & nouser-nous-real &
$ ls /run/user/1001/nouser-nous-*.sock
nouser-nous-mock.sock
nouser-nous-real.sock
$ brahman-status
Sessions (2):
[ente] ... nouser.nous_real
socket: /run/user/1001/nouser-nous-real.sock
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):
- nouser-core attract --remote todavía usa NOUSER_NOUS_SOCKET hardcoded
o
default_socket_path(). El siguiente paso es subscribirse al
MatchEvent del broker y usar producer_service_socket directo —
con eso BRAHMAN_BROKER_CONTEXT=test/prod swapea provider sin
tocar al consumer.
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:
- 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 <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:
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<f32> reales por cada Mónada.
- bin nouser nuevo subcomando:
attract <dir> <file>.
- 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 <dir>.
- 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<DataFacet> 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 <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:
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<FileEntry> 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 <dir>, show <dir> <prefix>,
json <dir>. 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).
feat(broker): priority contexts — biases per-contexto operativo
brahman-card::ContextBias { pin_to: Option<String>, priority_offset: i8 }
declara un override per-contexto.
Card.priority_contexts: BTreeMap<String, ContextBias> y mismo en
WireCard (cruza el wire). Las conversiones From lo propagan.
BrokerConfig.current_context: Option<String>. Cuando el broker corre
bajo un contexto y una Card declara biases para ese nombre, se aplican:
- Como consumidor:
pin_to sobreescribe el Flow.pin_to estático.
- Como productor:
priority_offset se suma a la priority base
(clamp en [Low=0, Critical=3]) para el ranking.
BrokeredCard propaga priority_contexts. find_producer_for usa
effective_priority(card) y effective_pin(card, input) antes de
los tiebreaks.
brahman-admin::AdminConfig.current_context + StatusSnapshot.current_context
espejan el contexto activo. brahman-status lo imprime como
Context: <nombre> justo debajo de Init: ....
ente-zero lee BRAHMAN_BROKER_CONTEXT env var y la propaga al
broker y al admin. Sin var, biases per-contexto inactivos.
- 4 tests nuevos en brahman-broker:
context_priority_offset_lifts_producer_above_alphabetic_winner,
context_pin_to_overrides_static_pin, unknown_context_no_op,
priority_offset_clamps_to_critical.
- Validación end-to-end:
BRAHMAN_BROKER_CONTEXT=test ente-zero →
brahman-status muestra Context: test.
feat(card): WireCard + extensions — forward-compat sin romper postcard
Card.extensions: BTreeMap<String, serde_json::Value> restaurado con
#[serde(flatten, default, skip_serializing_if = is_empty)]. Los
campos JSON/TOML desconocidos sobreviven el roundtrip de archivos.
- Nuevo
WireCard: proyección postcard-friendly (sin extensions,
genesis: Vec<WireCard> recursivo). Conversiones From<Card> y
From<WireCard> con descarte/recreación de extensions.
brahman-handshake::Hello.card pasa de Card a WireCard. Client
hace card.into() antes de enviar; Server hace hello.card.into()
para volver a Card antes de validar/registrar.
- 3 tests nuevos en brahman-card:
extensions_preserved_in_json_roundtrip,
wire_card_roundtrip_strips_extensions,
wire_card_postcard_friendly (postcard encode/decode efectivo).
- brahman-card gana
postcard como dev-dep para el último test.
- Contrato documentado: extensions = anotaciones locales que NO cruzan
al Init; sólo viven en archivos.
9420eae chore: limpia warnings dead-code en arje (commit del usuario)
ente-zero/src/events.rs: #![allow(dead_code)] a nivel módulo —
es vocabulario de eventos con variantes/campos reservados para flujos
no cableados aún (CapabilityRequested, ShutdownReason::Signal,
CapabilityGrant::{Granted, Denied, QuotaExceeded}, ExitStatus
fields).
ente-zero/src/graph/mod.rs: comentado el re-export ahora innecesario
de SHUTDOWN_GRACE. DEFAULT_GRANT_TTL con #[allow(dead_code)]
- nota "reservado para capability granting".
ente-zero/src/graph/capabilities.rs: renew_grant con
#[allow(dead_code)] (capability renewal pendiente).
ente-kernel/src/surface.rs: drop de use anyhow::Context (no se
usaba).
ente-hostnamed-compat/src/main.rs: drop de Connection (no se
usaba).
ente-polkit-compat/src/main.rs: PolicyDecision.source con
#[allow(dead_code)] (sólo aparece en Debug para logging).
cargo check --workspace: 17 warnings → 0.
feat(sidecar): WIT al sidecar — módulos conscientes vivos
brahman-card::WitInterface deriva Serialize, Deserialize,
PartialEq, Eq para cruzar el wire postcard.
brahman-handshake::Hello lleva wit: Option<WitInterface>. Server
usa ResolvedCard::from_conscious cuando viene presente, from_agnostic
cuando no.
brahman-handshake::Client::connect queda como wrapper agnóstico de
connect_with(path, card, wit: Option<WitInterface>).
brahman-broker::Broker::register ahora toma Option<WitInterface>
como tercer arg. BrokeredCard guarda el wit. 25 sitios de tests
actualizados con , None.
brahman-sidecar::SidecarConfig con campo wit. Helpers nuevos:
SidecarConfig::new(card).with_wit(wit) y spawn_conscious(card, wit).
El log attached reporta conscious=true|false.
brahman-status muestra marker 🧠 + sección wit: (package/world,
imports, exports) por sesión consciente.
- Example nuevo
crates/shared/brahman-sidecar/examples/presence-conscious.rs:
toma label + path .wit (default shared_wit/protocol.wit), parsea
con brahman-card-wit, spawna sidecar consciente.
- Validado end-to-end:
$ presence-conscious demo.conscious shared_wit/protocol.wit &
$ brahman-status
Sessions (1):
01K... demo.conscious 🧠 lifecycle=Daemon
wit: brahman:protocol@0.1.0 / module
imports: types, handshake, lifecycle
exports: run
feat(core): brahman-card-wit — extractor opcional de contratos WIT
- Crate nuevo
crates/core/brahman-card-wit con wit-parser = "0.230".
- API:
parse_wit(source) y parse_wit_file(path) devuelven
Vec<WitInterface> (uno por world declarado).
- Interfaces importadas/exportadas (no sólo funciones) se resuelven
por nombre via
resolve.interfaces[id].name.
- Example
crates/core/brahman-card-wit/examples/brahman-wit-info.rs
CLI: brahman-wit-info shared_wit/protocol.wit → lista paquete,
worlds, imports y exports.
- 4 tests: inline, archivo real (
shared_wit/protocol.wit), parse
error, world vacío.
- Validado contra
protocol.wit: detecta worlds module y
admin-host con sus imports/exports correctos.
7b589b8 chore: agrega CHANGELOG.md retroactivo
CHANGELOG.md en la raíz con los 11 commits previos documentados
acción por acción. A partir de este punto, cada cambio sustantivo
actualiza también este archivo en el mismo commit.
8a83a26 feat(handshake): notificación push de matches
- Frame
MatchEvent { kind: Available | Lost, ... } añadido al protocolo.
Session::run_post_handshake usa tokio::select! para multiplexar
reads del cliente y un canal mpsc push del server.
- Server:
SessionTxTable (Arc<Mutex<HashMap<SessionId, Sender<Frame>>>>)
y LastMatches para diff por sesión. broadcast_match_diffs corre
tras cada register y unregister, emite sólo los cambios.
- Capacity del canal push: 32 (ephemeral,
try_send non-blocking).
- Client:
VecDeque<MatchEvent> interno, take_event() (non-blocking)
y await_event(timeout). ping() ahora drena MatchEvents intermedios
hasta encontrar el Pong.
- Example
crates/core/brahman-handshake/examples/subscriber.rs.
- Test
match_event_pushed_on_producer_arrival (handshake integ 6→7).
70a7a0d feat: segundo módulo (nakui) + admin API + brahman-status
- Crate nuevo
crates/shared/brahman-sidecar (DRY del thread + tokio +
ping loop). API: spawn(card) / spawn_with_handle(config).
nakui cmd_run llama brahman_sidecar::spawn antes de run_server.
Card: lifecycle Daemon, supervision Restart, flow command (json) /
report (json).
- Crate nuevo
crates/core/brahman-admin con StatusSnapshot JSON
line-delim, AdminServer y client::query.
- ente-zero levanta también el AdminServer en
primordial_loop.
- Example
crates/shared/brahman-sidecar/examples/presence.rs
(módulo dummy long-lived parametrizable por label).
- Example
crates/core/brahman-admin/examples/brahman-status.rs
(CLI que pretty-printa el snapshot).
brahman-broker: BrokeredCard ahora incluye lifecycle. Endpoint
y Match derivan Serialize/Deserialize. Nuevo Broker::cards()
iterador.
brahman-card: pub use ::ulid para que módulos no dependan de ulid.
- yahweh-shell migrado al sidecar compartido (96→53 LOC).
595f68e feat(yahweh-shell): primer módulo brahman vivo
- yahweh-shell spawnea sidecar antes de
Application::new().
- Card declarada: label
brahman.ui_engine, lifecycle Widget,
supervision Delegate, payload Virtual, flow input render-data
(json) / output user-intent (json).
- Sidecar en thread aparte con tokio current_thread runtime,
desacoplado del runtime GPUI.
df9d10c feat(ente-zero): enchufa el handshake server al Init real
- ente-zero levanta
brahman_handshake::server::Server::bind en
primordial_loop después del ente-bus, con degradación grácil
si bind falla (mismo patrón que uevents).
- Nuevo módulo
brahman-handshake/src/transport.rs: helper
default_socket_path() con resolución BRAHMAN_INIT_SOCKET →
XDG_RUNTIME_DIR → TMPDIR.
- Example
crates/core/brahman-handshake/examples/probe.rs.
- Validación end-to-end manual: probe contra ente-zero vivo
imprime
HelloAck: session=... init_attached=true.
07d77a3 feat(handshake): integra el broker con el ciclo de sesiones
ServerConfig acepta Option<Arc<Mutex<Broker>>>.
register_session indexa la Card en el broker y la SessionRegistry
antes de emitir HelloAck.
Session::handle refactor a do_handshake → run_post_handshake → cleanup con cleanup unificado (broker + sessions).
- Tests integ nuevos:
broker_registers_and_unregisters_with_session
y broker_matches_two_live_modules.
- Fix colateral:
brahman-card::TypeRef pasa de internally-tagged
(#[serde(tag = "kind")]) a externally-tagged. Postcard no soporta
internally-tagged en formatos no self-describing. JSON cambia de
{"kind":"primitive","name":"x"} a {"primitive":{"name":"x"}}.
5091106 feat(core): brahman-broker — matching híbrido
- Crate nuevo
crates/core/brahman-broker.
- 3 estrategias de matching:
Exact, Structural, ExactThenStructural
(default). Devuelven Match::via con la estrategia que ganó.
- Override
pin_to: el consumer pide un productor por label; si la
pista no resuelve, cae en type-search.
- Tiebreak por
Card.priority desc, luego label asc (estable y
determinista).
- API:
register, unregister, find_producer_for, all_matches,
cards, sessions, len, is_empty.
- 11 tests (matching, pin_to, priority, no-self-loops, all-matches).
814390f feat(core): brahman-handshake — protocolo runtime
- Crate nuevo
crates/core/brahman-handshake con server y client
Rust↔Rust sobre Unix socket.
- Frames length-prefixed (4 bytes LE) + cuerpo postcard.
- Mensajes:
Hello, HelloAck, Ping, Pong, Farewell, Error.
MAX_FRAME_BYTES = 4 MiB para evitar reservas absurdas.
- Tradeoff: drop
extensions/extra de Card por incompat
postcard ↔ serde_json::Value. Forward-compat queda en
schema_version + protocol_version negotiation.
- 4 tests integ + 1 unit en codec.
ed0e973 refactor(arje): migra ente-card a re-export de brahman-card
ente-card/src/lib.rs reescrito como crate-shim de re-export
(327 LOC → 25 LOC).
EntityCard ≡ brahman_card::Card por type alias.
ente-card/Cargo.toml: deps reducidas a brahman-card.
Card impl Default (Ulid::nil(), label vacío) para que
..Default::default() funcione en struct-literals.
- 4 sitios en
ente-zero/src/seed.rs actualizados con
..Default::default() para los campos aditivos.
- Los 21 consumidores arje compilan sin tocar fuente.
0feba74 feat(core): brahman-card — Tarjeta canónica híbrida
- Crate nuevo
crates/core/brahman-card.
- Hereda de arje:
id: Ulid, lineage, Capability tipado,
Payload::{Wasm, Native, Virtual, Legacy}, SomaSpec
(namespaces, cgroups, rlimits, cpu_affinity), Supervision
(Restart con backoff, OneShot, Delegate), genesis recursivo.
- Aditivo brahman:
Permissions enumerados (NetworkingPolicy,
FsPolicy, IpcPolicy), Lifecycle ortogonal a Supervision,
Priority de scheduling, Flows con TypeRef discriminado
(Primitive | Wit), pin_to opcional.
TrustLevel derivado de Permissions (no declarado).
ResolvedCard { card, wit: Option<WitInterface>, trust }.
- Soporta JSON (canónico) + TOML (auto-detectado por extensión).
- 8 tests incluido
arje_seed_format_compatible que valida que
el JSON de arje sigue parseando con defaults para los aditivos.
4d50bfc chore: absorbe nakui (ERP matemático) en modules/nakui
~/nakui → crates/modules/nakui/{core,modules}.
core/: el crate nakui-core con 4 bins (nakui, demo,
inventory_demo, sales_demo) y tests.
modules/{inventory,sales,treasury}/: data declarativa
(nsmc.json, schema.k, morphisms/) que el crate consume.
No son crates Cargo.
- Deps directas (no
workspace = true): thiserror v1, surrealdb,
rhai, petgraph. No conflicto con el resto del workspace.
53dbdf0 chore: monorepo inicial con arje + minga + yahweh absorbidos
- 45 crates absorbidos en 4 ejes:
crates/core/: 24 crates de arje (Init systemd-compatible:
ente-card, ente-zero, ente-kernel, ente-bus, ente-cas,
ente-soma, ente-wasm, ente-snapshot, ente-brain,
ente-echo, ente-policy-provider, + 12 *-compat).
crates/modules/semantic_dht/: 5 crates de minga (minga-core
con AST/CAS/MST, minga-p2p con libp2p Kad, minga-store,
minga-vfs, minga-cli).
crates/modules/ui_engine/: 11 crates de yahweh (libs/{core,
theme, bus, providers}, widgets/{tree, splitter, tabs, tiled,
container_core, text_input}).
crates/apps/: 5 crates de yahweh (file_explorer,
database_explorer, text_viewer, image_viewer, yahweh-shell).
shared_wit/protocol.wit con handshake/lifecycle inicial.
Cargo.toml unificado: thiserror bumped a 2 (transparente para
arje), tokio "full", paths intra-workspace de yahweh redirigidos.
cargo check --workspace: 0 errores (sólo dead-code warnings
preexistentes en ente-zero).