refactor(monorepo): reorganización lógica + renames + SDDs + split CHANGELOG
Reorganización física de crates/: - core/ (mezclaba 6 propósitos) se divide en protocol/, init/, runtime/, compat/ - shared/ (3 crates) se redistribuye en protocol/ e init/ - lapaloma (sub-módulo de ui_engine) se promueve a modules/pineal/ Renames de proyectos: - shipote → shuma (runtime de sandboxes) - nouser → akasha (explorador de Mónadas) - yahweh → nahual (motor GPUI, antes ui_engine/) - lapaloma → pineal (data-viz agnóstica) Fraccionamiento UI → core agnóstico: - vista-core (DeckState + snap, 175 LOC, 5 tests verdes) - barra-core (Task + render_html + sanitize, 90 LOC, 5 tests verdes) - vista-web y barra-web ahora son thin DOM bindings Documentación nueva: - 16 SDDs por subdirectorio (≤80 LOC c/u): protocol/init/runtime/compat + 10 módulos + apps/ - docs/STATUS.md con cifras reales por proyecto - docs/ROADMAP.md con plan a finalización (6 hitos, ~6-8 semanas) - CHANGELOG.md particionado en docs/changelog/<proyecto>.md (7 buckets) Automatización: - scripts/reorg.py — script idempotente que: git mv directorios, renombra package names, recomputa path = refs, reescribe imports rust, actualiza workspace Cargo.toml. Soporta --dry-run. - scripts/split-changelog.py — particiona CHANGELOG por componente. Validación: - cargo check --workspace pasa (124 crates + 2 nuevos cores). - 10 tests adicionales (5 en vista-core + 5 en barra-core) verdes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,723 @@
|
||||
# Changelog — akasha
|
||||
|
||||
Explorador semántico de Mónadas. Renombrado de `nouser` el 2026-05-19.
|
||||
|
||||
### feat(nouser-explorer): integración al stack yahweh themed
|
||||
Iter 10. `nouser-explorer` (la app paralela a `nakui-explorer`
|
||||
para ver Mónadas via daemon nouser) tenía colors hardcoded
|
||||
idénticos al patrón previo. Aplico el mismo refactor que se hizo
|
||||
para `nakui-explorer` en iter 4: instala el theme global, migra
|
||||
chrome a slots, usa los widgets `banner_themed` / `card_themed` /
|
||||
`theme_switcher`.
|
||||
|
||||
Cambios en `nouser-explorer`:
|
||||
- **Nuevas deps**: `yahweh-theme`, `yahweh-widget-banner`,
|
||||
`yahweh-widget-card`, `yahweh-widget-theme-switcher`.
|
||||
- **`main()`**: `Theme::install_default(cx)` antes de
|
||||
`cx.open_window`.
|
||||
- **`render`**: 4 vars `let X = rgb(...)` (chrome) → theme slots
|
||||
(`bg_app`/`fg_text`/`fg_muted`/`bg_panel`/`border`).
|
||||
- **Header**: gana flex_row + theme switcher en la derecha (mismo
|
||||
pattern que nakui-explorer).
|
||||
- **`error_banner`**: pasa de div hardcoded a `banner_themed(cx,
|
||||
Banner::Error, ...)` con override de padding (16/8) por
|
||||
convención del header.
|
||||
- **2 cards de Engine y Monad**: pasan de `div().flex().flex_col()
|
||||
.p().mb().bg(card_bg).rounded().border_l_4().border_color()...`
|
||||
a `card_themed(cx).border_l_4().border_color(accent)...`.
|
||||
- **Acentos semánticos**: `accent_engine` (cyan, las "máquinas")
|
||||
y `accent_data` (purple, las Mónadas) quedan locales — son
|
||||
señales del dominio nouser, no del chrome.
|
||||
|
||||
Tests: workspace stack intacto. nouser-explorer no tiene tests
|
||||
propios (siempre fue una vista live del daemon, sin lógica
|
||||
testable separada).
|
||||
|
||||
Beneficio operativo: las dos apps explorer del repo
|
||||
(`nakui-explorer` para event log + `nouser-explorer` para Mónadas)
|
||||
ahora comparten la misma paleta themed + el mismo control de
|
||||
switcher. Si un usuario las corre lado a lado, la consistencia
|
||||
visual emerge sola.
|
||||
|
||||
### feat(nous-real): cache de embeddings + write-through al CAS de arje
|
||||
Cierra el ciclo de la crítica del usuario: "Si un archivo no ha
|
||||
cambiado su hash en el CAS, Nouser ni siquiera debería pedirle al
|
||||
LLM que re-genere el embedding". El modelo real
|
||||
(`fastembed-allMiniLML6V2-384d`, ~1-50ms por archivo) era invocado
|
||||
ciegamente en cada re-cluster del watcher. Ahora se cachea por
|
||||
`sha256(bytes-vistos) + model_id`.
|
||||
|
||||
Pipeline en `handle_file`:
|
||||
1. Lee primeros 8 KiB (igual que antes).
|
||||
2. `file_sha = ente_cas::sha256_of(buf)` — hash de los bytes que el
|
||||
modelo *realmente* verá (no del archivo completo). Garantiza
|
||||
que un archivo creciendo más allá de la ventana sin tocar la
|
||||
cabeza siga sirviendo cache hits.
|
||||
3. Cache lookup: HIT → respuesta en ~µs.
|
||||
4. MISS → `ente_cas::store(&buf)` (write-through al CAS de arje,
|
||||
no-fatal si falla) → `backend.embed_one(text)` → `cache.put(...)`.
|
||||
|
||||
Backend de cache: sled local en
|
||||
`$XDG_CACHE_HOME/brahman/nouser-nous-real-embed-cache.sled`. Tree
|
||||
versionado `embed_cache_v1`; el `MODEL_ID` viaja en la key, así que
|
||||
cambiar de modelo invalida el cache implícitamente. Override por env
|
||||
`NOUSER_NOUS_REAL_CACHE`.
|
||||
|
||||
Encoding compacto: cada `Vec<f32>` se serializa como bytes
|
||||
little-endian (4B por f32, sin overhead). Para el modelo default
|
||||
(384-d) son 1.5 KiB por entry. Decode tolera bytes corruptos
|
||||
(longitud no-múltiplo de 4 → `None`, no panic).
|
||||
|
||||
Por qué sled y no `ente-cas` directo: el CAS de arje es flat
|
||||
sha256-keyed; la cache necesita un mapeo `(file_sha, model_id) →
|
||||
embedding`, no expresable como entry CAS. El write-through a CAS
|
||||
queda como registro consultable + futura GC.
|
||||
|
||||
API:
|
||||
- `EmbedCache::open()` → abre sled, idempotente.
|
||||
- `EmbedCache::open_at(dir)` para tests.
|
||||
- `EmbedCache::get(sha, model)` → `Option<Vec<f32>>`.
|
||||
- `EmbedCache::put(sha, model, &[f32])` → no-fatal en error.
|
||||
- `EmbedCache::len()` → contador para logs (best-effort).
|
||||
|
||||
Mock NO se modifica — su embedding pseudo-32d es metadata-hashing
|
||||
puro, sin costo. Cachearlo sería overhead.
|
||||
|
||||
Tests: 5 unitarios (`roundtrip_returns_same_vector`, `miss_returns_none`,
|
||||
`different_models_do_not_collide`, `different_content_different_keys`,
|
||||
`corrupted_value_returns_none`). Verdes con `--features embeddings`;
|
||||
stub mode (sin feature) sigue compilando sin tocar cache.
|
||||
|
||||
### feat(nouser+sidecar): watcher con debounce + re-publish al broker
|
||||
Cierra las dos limitaciones del watcher previo: ya no spamea N veces por
|
||||
una sola edición, y el broker ve los cambios estructurales en lugar de
|
||||
quedarse con manifests congelados al arranque.
|
||||
|
||||
$ nouser daemon /tmp/x &
|
||||
$ touch /tmp/x/src/a.rs /tmp/x/src/b.rs /tmp/x/src/c.rs
|
||||
# daemon log (un solo batch, no 9 reacciones):
|
||||
[watcher] ⚙ batch: 6 path(s) coalescidos → re-scan
|
||||
[watcher] ✦ x/src nace (3 miembros, lens=Code)
|
||||
[watcher] ⌃ delta: 1 nuevas, 0 refrescadas, 0 cerradas — 3 sesiones vivas
|
||||
|
||||
Mecánica del debounce (150ms):
|
||||
- `spawn_fs_watcher` arma dos threads: **dispatcher** filtra eventos
|
||||
notify Create/Modify/Remove a un canal de paths; **coordinator**
|
||||
mantiene `HashMap<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):
|
||||
1. Open sled si NOUSER_DB_PATH existe.
|
||||
2. Publica las Mónadas previas con `centroid_model` válido (las
|
||||
inválidas se descartan con log explícito).
|
||||
3. Re-scan + `by_directory_hydrated(prior=&db)`.
|
||||
4. Sólo spawnea sidecars para Mónadas con id que NO estaba en la
|
||||
hidratación inicial. Los path_hints existentes preservan identidad,
|
||||
evitando duplicados en el broker.
|
||||
5. Persiste el set actualizado.
|
||||
|
||||
Validación end-to-end:
|
||||
|
||||
$ NOUSER_DB_PATH=/tmp/h.sled nouser daemon crates/core
|
||||
# arranque 1: DB vacía
|
||||
re-scan 102 archivos → 5 mónadas
|
||||
1 ente + 5 mónadas vivas (5 nuevas vs hidratación)
|
||||
|
||||
$ NOUSER_DB_PATH=/tmp/h.sled nouser daemon crates/core
|
||||
# arranque 2: DB poblada
|
||||
hidratadas 5 mónadas previas en O(1)
|
||||
re-scan 102 archivos → 5 mónadas
|
||||
1 ente + 5 mónadas vivas (0 nuevas vs hidratación)
|
||||
|
||||
Costo del arranque 2: ~0.06s user CPU. Antes (sin hidratación) era
|
||||
re-scan + cluster + spawn x N — segundos enteros para árboles grandes.
|
||||
|
||||
Tests: 7 (card) + 24 (core) verdes.
|
||||
|
||||
### feat(nouser): centroid_model — versionado de embeddings
|
||||
Protege contra el bug silencioso de mezclar centroides de modelos
|
||||
distintos (mock 32-d vs real 384-d), que daba scores sin sentido.
|
||||
|
||||
- `MonadManifest.centroid_model: Option<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:
|
||||
|
||||
1. Si `NOUSER_NOUS_SOCKET` está set, lo usa directo (atajo explícito).
|
||||
2. Si no, abre `brahman_handshake::client::Client` al `brahman-init`,
|
||||
anuncia un consumer Card mínimo con `flow.input = embed-result:json`,
|
||||
espera 3s por el primer `MatchEvent::Available`, y usa el
|
||||
`producer_service_socket` que viaja en el evento.
|
||||
|
||||
Esto activa el swap automático mock↔real:
|
||||
- `BRAHMAN_BROKER_CONTEXT=test`: el bias `+1 en test` del mock lo hace
|
||||
ganar; consumer recibe el socket del mock.
|
||||
- `BRAHMAN_BROKER_CONTEXT=prod`: el bias del real lo hace ganar.
|
||||
- Sin contexto: empate alfabético entre los presentes.
|
||||
|
||||
Validación end-to-end:
|
||||
|
||||
$ ente-zero & nouser-nous-mock &
|
||||
$ # Sin NOUSER_NOUS_SOCKET:
|
||||
$ nouser attract --remote crates/core archivo.rs
|
||||
embed: remote
|
||||
🧲 0.9058 ente-brain/src ...
|
||||
(mock log confirma "embed_file path=...")
|
||||
|
||||
Cambios:
|
||||
- `nouser-core` Cargo.toml: deps directas brahman-handshake + tokio.
|
||||
- `cmd_attract` resuelve el socket por discovery antes de llamar a
|
||||
`embed_via(&path, file)` (mini-runtime tokio current_thread inline).
|
||||
|
||||
Bug que se descubrió en el camino: la "flakiness" reportada de
|
||||
`cargo test --workspace` era disco lleno (24 GB en `target/`), no
|
||||
condición de carrera. Con `cargo clean` + profile slim, todos los
|
||||
tests pasan deterministas.
|
||||
|
||||
### feat(nouser): yahweh widget — `nouser-explorer` panel GPUI
|
||||
Bin GPUI standalone que consulta `brahman-admin` cada 2s y renderea
|
||||
todas las sesiones del Init como cards. Cierra el círculo visual del
|
||||
ecosistema brahman.
|
||||
|
||||
- Crate nuevo `crates/apps/nouser-explorer` (deps: brahman-admin,
|
||||
brahman-card, gpui).
|
||||
- Ventana 900×640 con header del estado del Init, banner de error
|
||||
cuando no conecta, y lista de cards (una por sesión).
|
||||
- Cada card muestra: kind + label + lifecycle, ULID corto, summary
|
||||
(si data), keywords, lens hint, service_socket si está, y refs
|
||||
(RelationshipKind → target_label). El borde izquierdo coloreado
|
||||
diferencia ente (azul) de data (lavanda).
|
||||
- `cx.spawn(async move |this, cx| { … })` corre el loop de refresh
|
||||
en el GPUI executor; `query_blocking` se usa porque GPUI no provee
|
||||
un runtime tokio.
|
||||
- Nuevo helper en brahman-admin: `client::query_blocking(path)` —
|
||||
versión sync de `query()`, para callers con su propio executor.
|
||||
|
||||
Uso:
|
||||
|
||||
$ ente-zero & nouser daemon crates/core &
|
||||
$ cargo run -p nouser-explorer
|
||||
# ventana muestra ~6 cards en vivo, refrescando cada 2s.
|
||||
|
||||
cargo check --workspace: 0 errores, 0 warnings.
|
||||
|
||||
### feat(nouser): persistencia sled write-through del MonadDb
|
||||
`MonadDb` ahora soporta backend dual:
|
||||
|
||||
- `MonadDb::new()` → memoria pura (default, back-compat).
|
||||
- `MonadDb::open(path)` → sled-backed con cache en memoria. Carga
|
||||
contenido existente al abrir; cada `insert_*` hace write-through
|
||||
(cache + sled).
|
||||
|
||||
Diseño:
|
||||
- 2 trees sled: `files` y `monads`.
|
||||
- Wire format: serde_json (ergonomía + inspectability con sled-cli;
|
||||
los manifests son chicos, JSON gana sobre postcard aquí).
|
||||
- Reads SIEMPRE desde la cache — sled se consulta sólo al abrir.
|
||||
- `replace_monads()` purga el tree de sled antes de escribir.
|
||||
|
||||
Bin nouser: nueva env var `NOUSER_DB_PATH`. Si está set, persiste
|
||||
en esa ruta; si no, in-memory:
|
||||
|
||||
$ NOUSER_DB_PATH=/tmp/monads.sled nouser scan crates/core
|
||||
scan: 102 archivos en crates/core, 5 mónadas
|
||||
$ ls /tmp/monads.sled
|
||||
blobs conf
|
||||
$ NOUSER_DB_PATH=/tmp/monads.sled nouser scan crates/core
|
||||
# segunda corrida re-escribe la DB con el nuevo scan
|
||||
|
||||
Tests nuevos en db.rs:
|
||||
- `persistence_roundtrip` — escribe, cierra, reabre, datos están.
|
||||
- `replace_monads_purges_persistent_tree` — replace limpia el tree.
|
||||
|
||||
24 tests en nouser-core (era 22, +2).
|
||||
|
||||
### refactor(nouser): labels de Mónada con 2 componentes del path
|
||||
Resuelve la fricción visual de monorepos donde múltiples Mónadas se
|
||||
llamaban "src". Nueva función `label_from_path` toma los últimos hasta
|
||||
2 componentes normales del path y los une con `/`.
|
||||
|
||||
$ nouser scan crates/core
|
||||
[01K..] brahman-admin/src card=5
|
||||
[01K..] brahman-handshake/src card=6
|
||||
[01K..] ente-brain/src card=11
|
||||
[01K..] ente-kernel/src card=4
|
||||
...
|
||||
|
||||
Tests añadidos: `label_from_root_only_one_component`,
|
||||
`label_from_deep_path_takes_last_two`. Tests existentes actualizados
|
||||
con los nuevos labels.
|
||||
|
||||
### feat(nouser): Phase D-2 — proveedor Nous real (LLM) detrás de feature flag
|
||||
Cierra el ciclo del módulo Nous: existe un proveedor que produce
|
||||
embeddings reales con un modelo LLM, mientras que `cargo build` sin
|
||||
features sigue siendo liviano (no descarga ni compila ML deps).
|
||||
|
||||
Crate nuevo:
|
||||
|
||||
- `crates/modules/nouser/nous-real`: bin con dos modos según feature.
|
||||
- **Sin feature (default)**: stub. Bin compila en ~10s, arranca,
|
||||
sidecarea a brahman-init declarando la Card de real-nous, escucha
|
||||
en el socket Nous, y rechaza toda request con `ErrorResponse {
|
||||
error: "compilado sin la feature embeddings. Rebuild con
|
||||
cargo build -p nouser-nous-real --features embeddings" }`.
|
||||
`cargo build --workspace` sigue siendo limpio.
|
||||
- **Con `--features embeddings`**: pulls `fastembed = "4"`. Ese crate
|
||||
arrastra `ort 2.0.0-rc.9` (ONNX Runtime con binarios descargados
|
||||
por Cargo) + `tokenizers 0.21` + ~30 deps más. Compila en ~50s.
|
||||
Modelo default: `all-MiniLM-L6-v2` (384-d, descargado a
|
||||
`~/.cache/fastembed` la primera vez).
|
||||
- `EmbedText`: pasa el texto al modelo, devuelve vector 384-d.
|
||||
- `EmbedFile`: lee primeros 8KiB con UTF-8 lossy, embed como texto.
|
||||
Para binarios el resultado no es semánticamente útil — caller
|
||||
decide.
|
||||
- `Ping`: devuelve `model_id` y `embed_dim` reales.
|
||||
|
||||
- Card de real-nous:
|
||||
- label `nouser.nous_real` (distinto del mock para coexistir).
|
||||
- `priority_contexts.prod = { priority_offset: +1 }`. En contexto
|
||||
prod gana sobre el mock; en `test` el mock gana por su propio
|
||||
`+1`. Sin contexto activo, empate alfabético entre ambos.
|
||||
|
||||
Validación end-to-end con modelo real:
|
||||
|
||||
$ cargo build -p nouser-nous-real --features embeddings # ~50s
|
||||
$ ente-zero & nouser-nous-real &
|
||||
$ # probe vía python al socket Unix:
|
||||
$ echo '{"kind":"embed_text","payload":{"text":"hello brahman"}}' \
|
||||
| python3 -c "..." | head
|
||||
model: real-fastembed-allMiniLML6V2-384d
|
||||
elapsed_ms: 8
|
||||
embed_dim: 384
|
||||
first 5 values: [0.0034, -0.0036, 0.0078, -0.0218, -0.0162]
|
||||
|
||||
Tradeoff conocido: las dimensiones del mock (32-d) y real (384-d) son
|
||||
incompatibles. Cambiar de proveedor invalida los centroides cacheados
|
||||
de Mónadas. Documentar como "limpiar DB al cambiar proveedor".
|
||||
|
||||
Workspace state:
|
||||
- cargo build --workspace sigue limpio sin features (no ML).
|
||||
- cargo build -p nouser-nous-real --features embeddings funciona.
|
||||
- 0 errores, 0 warnings en ambos modos.
|
||||
|
||||
Pendientes para D-3 / futuro:
|
||||
- Discovery de socket: hoy el consumer hardcodea NOUSER_NOUS_SOCKET.
|
||||
Para que el broker brahman elija real vs mock per-contexto, falta
|
||||
inyectar el socket del provider electo en el MatchEvent o exponer
|
||||
un broker query "dame el socket de la sesión X".
|
||||
- Coexistencia: hoy los dos providers compiten por el mismo socket
|
||||
path por default. Habría que parametrizarlos a sockets distintos
|
||||
cuando coexistan.
|
||||
|
||||
### feat(nouser): Phase D — proveedor Nous mock + cliente remoto
|
||||
Cierra el patrón "Nous como módulo aparte intercambiable": el contrato
|
||||
del proveedor de embeddings vive en su crate, el mock determinístico
|
||||
implementa ese contrato sirviéndolo por Unix socket, y `nouser-core`
|
||||
sabe consumirlo remotamente. El switch entre mock y real (futuro) se
|
||||
hará vía priority_contexts en el broker.
|
||||
|
||||
Crates nuevos:
|
||||
|
||||
- `crates/modules/nouser/nous`: contrato compartido. Tipos
|
||||
`EmbedRequest`, `RequestKind { EmbedFile, EmbedText, Ping }`,
|
||||
`EmbedFilePayload`, `EmbedTextPayload`, `EmbedResponse`,
|
||||
`PingResponse`, `ErrorResponse`. Wire format: line-delimited JSON
|
||||
por Unix socket, single-shot per conexión. Constants para los nombres
|
||||
de flow (`embed-request`/`embed-result`) y el tipo (`json`). Helper
|
||||
`transport::default_socket_path()` con env var
|
||||
`NOUSER_NOUS_SOCKET`.
|
||||
- `crates/modules/nouser/nous-mock`: bin `nouser-nous-mock`. Sidecarea
|
||||
a brahman-init con Card kind=Ente declarando los flows
|
||||
`embed-request:json`/`embed-result:json` y un
|
||||
`priority_contexts.test = { priority_offset: +1 }` (gana sobre
|
||||
cualquier real-nous en contexto test). Bind del socket Nous, accept
|
||||
loop, despacha por `RequestKind`. EmbedFile usa
|
||||
`nouser_core::embed::embed` (los pseudo-embeddings de Phase C).
|
||||
Modelo: `mock-pseudo-32d`.
|
||||
|
||||
Cambios:
|
||||
|
||||
- `nouser-core`: dep nueva `nouser-nous`. Subcomando `attract` ahora
|
||||
acepta `--remote` que abre un socket UnixStream blocking, envía un
|
||||
`EmbedRequest` y lee la response. Imprime `embed: local|remote`
|
||||
para que se vea cuál ruta corrió.
|
||||
|
||||
Validación end-to-end (un solo terminal, varios procesos):
|
||||
|
||||
$ ente-zero &
|
||||
$ nouser-nous-mock &
|
||||
$ NOUSER_MIN_FILES=5 nouser daemon crates/core &
|
||||
$ brahman-status
|
||||
|
||||
Sessions (7):
|
||||
[ente] nouser.nous_mock flows: embed-request, embed-result
|
||||
[ente] brahman.nouser_engine
|
||||
[data] src summary: 6 archivos en crates/core/brahman-handshake/src
|
||||
[data] graph summary: 7 archivos en crates/core/ente-zero/src/graph
|
||||
...
|
||||
|
||||
$ nouser attract --remote crates/core <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).
|
||||
|
||||
Reference in New Issue
Block a user