99838b849b
Iter 12. Hasta ahora minga-explorer mostraba sólo counts. Ahora cada stat card muestra un sample de los 5 primeros items: hashes truncados de nodos AST (con kind), atestaciones (content_hash ← did_short) y claves MST. - RepoSnapshot agrega 3 Vec<String/(String,String)> con recent items, cap RECENT_LIMIT=5. - load_snapshot itera los stores con filter_map(Result::ok) + take(5). Errores per-item silenciados — dashboard tolerante a corrupción puntual. - short_hash(&str) local: trunca a 12 chars (48 bits). - stat_card extendido con recent_items: &[String]. Si no vacío, agrega "recent (N de TOTAL):" + una linea por item. Tests: 2 → 4 (sanity defaults + short_hash). Beneficio: tras `minga ingest`, el explorer muestra los hashes de los nodos creados sin necesitar queries SQL. Limitación: los "recent" no son cronológicos (sled ordena lex por hash). Timeline real requiere timestamp al schema. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4616 lines
216 KiB
Markdown
4616 lines
216 KiB
Markdown
# 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-10
|
||
|
||
### feat(minga-explorer): listings de items recientes en cada stat card
|
||
Iter 12. Hasta ahora minga-explorer mostraba sólo counts (3
|
||
números). Ahora cada stat card muestra también un sample de los
|
||
items dentro: hashes truncados de los 5 primeros nodos AST
|
||
(con su `kind`), atestaciones (`content_hash ← did_short`) y
|
||
claves MST. Mucho más útil para debugging que el "tengo N items".
|
||
|
||
Cambios en `minga-explorer`:
|
||
- **`RepoSnapshot` extendido** con 3 nuevos `Vec<...>`:
|
||
- `recent_nodes: Vec<(String, String)>` — `(hash_short, kind)`.
|
||
- `recent_attestations: Vec<(String, String)>` —
|
||
`(content_hash_short, did_short)`.
|
||
- `recent_mst_keys: Vec<String>` — `hash_short`.
|
||
- Cap de 5 items por sección via `RECENT_LIMIT` const.
|
||
- **`load_snapshot` itera los stores** y toma los primeros 5
|
||
items via `iter().filter_map(Result::ok).take(RECENT_LIMIT)`.
|
||
Errores per-item se silencian (`filter_map`) — el dashboard
|
||
muestra lo que pueda; un par de items corruptos no debería
|
||
tirar el panel.
|
||
- **`short_hash(&str)` helper local**: trunca un hex a sus
|
||
primeros 12 chars (48 bits, distintivo dentro de un repo
|
||
single-machine).
|
||
- **`stat_card` extendido**: nuevo arg `recent_items: &[String]`.
|
||
Si no está vacío, agrega un sub-header `"recent (N de TOTAL):"`
|
||
+ una linea por item. Cada line es texto pequeño (`px(11)`)
|
||
con el color principal del theme — visualmente queda como
|
||
monospace listing aunque no use mono font (no hay todavía
|
||
en el theme).
|
||
|
||
Tests: 2 → **4** (+2 sanity de los nuevos defaults + del
|
||
`short_hash`).
|
||
|
||
Beneficio operativo:
|
||
- Después de `minga ingest archivo.rs`, el explorer muestra
|
||
inmediatamente los hashes de los nodos AST creados, qué `kind`
|
||
tienen, y las atestaciones firmadas — sin necesitar `minga
|
||
status` o queries SQL.
|
||
- "5 de 247" da contexto del crecimiento sin overwhelm de
|
||
listing completo.
|
||
|
||
Limitación documentada: los "recent" no son cronológicos — sled
|
||
ordena lexicográfico por hash. Para timeline real, agregar
|
||
timestamp al schema (cambio breaking del store, scope futuro).
|
||
|
||
### feat(minga-explorer): nueva app dashboard del repo Minga sobre stack yahweh
|
||
Iter 11. Cierra el último frente identificado: integración del
|
||
módulo Minga (VCS semántico P2P) al ecosistema GUI. Antes Minga
|
||
sólo tenía CLI (`minga init/status/ingest/listen/sync/watch`).
|
||
Ahora hay un **dashboard GPUI** que muestra los counts del repo
|
||
en vivo, sobre el mismo stack themed que las otras dos apps
|
||
explorer.
|
||
|
||
Crate nuevo `crates/apps/minga-explorer/`:
|
||
- **Deps**: `minga-store` (para `PersistentRepo::open`) +
|
||
`yahweh-theme` + `yahweh-widget-{banner,card,theme-switcher}`.
|
||
Sin `minga-cli` (no necesita prompts de passphrase) ni
|
||
`minga-core` (counts no requieren parsear AST).
|
||
- **Lectura sin passphrase**: el `PersistentRepo` se abre directo
|
||
desde `<repo>/repo` sled. Los counts (`nodes.len()`,
|
||
`attestations.len()`, `mst.len()`) son lectura pública. Para el
|
||
DID se sigue necesitando `minga status` (CLI con passphrase).
|
||
- **Refresh por polling cada 2s**: mismo pattern que
|
||
`nakui-explorer`/`nouser-explorer`.
|
||
- **3 stat cards** una por dimensión:
|
||
- Nodos AST (cyan) — fragments parseados del código.
|
||
- Atestaciones (verde) — firmas Ed25519 sobre los nodos.
|
||
- Claves MST (purple) — entradas del Merkle Search Tree.
|
||
- **Helper `stat_card(cx, label, value, description, accent, ...)`**:
|
||
fabrica una card con border-l colored + label tenue + número
|
||
grande (`px(28)`) + descripción. Reutilizable.
|
||
- **Header**: título dinámico (`Repo: <path> · reload <ms> ms`)
|
||
+ theme switcher derecha.
|
||
- **Error banner**: themed Banner::Error si el repo no abre.
|
||
- 2 tests: `load_snapshot_errors_on_missing_dir` (msg claro
|
||
cuando el dir no existe) + sanity del `RepoSnapshot::default`.
|
||
|
||
Workspace: nueva entry en `members[]`.
|
||
|
||
Smoke run del binario verificado: bootstrap completo OK, panic
|
||
esperado en open_window por falta de display.
|
||
|
||
Beneficio operativo:
|
||
- Un usuario corre `minga init` + `minga ingest archivo.rs` desde
|
||
CLI, después abre `minga-explorer` y ve los counts crecer en
|
||
vivo cuando ingiere más archivos.
|
||
- Comparte theme switcher con `nakui-explorer` y
|
||
`nouser-explorer` — cualquier preset elegido se aplica
|
||
visualmente igual cross-app.
|
||
- `minga` deja de ser sólo CLI; gana presencia GUI sin tocar
|
||
el resto del módulo.
|
||
|
||
Apps GUI integradas al stack themed: **4** (nakui-ui, nakui-explorer,
|
||
nouser-explorer, minga-explorer).
|
||
|
||
### 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(yahweh): caret blinking + slots ornament en theme + MetaApp full themed
|
||
Iters 8-9 combinadas. Tres mejoras pequeñas que cierran la
|
||
integración del theme:
|
||
|
||
**1. Caret blinking en text-input** (`yahweh-widget-text-input`):
|
||
- Nuevo field `caret_visible: bool` que toggea cada 500ms.
|
||
- Nuevo field `_blink_task: Task<()>` mantiene el loop de blink
|
||
vivo y lo cancela al drop del widget.
|
||
- En `new()`, `cx.spawn(...)` arranca el loop: `timer.timer(500ms)`
|
||
+ `this.update(...)` que toggea + `cx.notify()`. Si el update
|
||
falla (entity drop), break.
|
||
- En `render()`, caret `|` se dibuja sólo si
|
||
`is_focused && self.caret_visible`. Familiar feel del SO.
|
||
|
||
**2. Slots ornament en yahweh-theme** (5 nuevos):
|
||
- `bg_input() -> Hsla` — bg sutil para fields editables.
|
||
- `bg_button() -> Hsla` + `bg_button_hover() -> Hsla` — controls
|
||
clickable secundarios.
|
||
- `accent_destructive() -> Hsla` — rojo para acciones peligrosas.
|
||
- `bg_destructive_hover() -> Hsla` — bg de hover sobre destructive.
|
||
- Implementados como **methods** del `Theme` (no fields del
|
||
struct), derivados via `ornament_slots(self.is_dark)`. Esto
|
||
evita modificar los 6 presets — el slot vive donde uno lo
|
||
invoca.
|
||
|
||
**3. MetaApp ornament cleanup** (`yahweh-widget-meta-form`):
|
||
- 11 colores hardcoded `gpui::rgb(0x...)` migrados a slots del
|
||
theme:
|
||
- Sidebar menu items (selected/hover) → `bg_row_active` /
|
||
`bg_row_hover`.
|
||
- List row separator + button bgs → `bg_row_active` /
|
||
`bg_button()` / `bg_button_hover()`.
|
||
- Icon ✕ delete + hover → `accent_destructive()` /
|
||
`bg_destructive_hover()`.
|
||
- EntityRef selector hover/selected → `bg_row_active` /
|
||
`bg_row_hover`.
|
||
- EntityRef selector border → `theme.border` (slot existente).
|
||
- Form fallback input bg + submit button → `bg_input()` /
|
||
`bg_button()` / `bg_button_hover()`.
|
||
- Confirm modal hint subtitle + hovers de Cancel/Confirm →
|
||
`theme.fg_muted` / `bg_button_hover()` / `bg_destructive_hover()`.
|
||
- Pattern: `let X = theme.slot()` antes de las closures + `move |d|
|
||
d.bg(X)` en hover/when para que el cierre tome ownership.
|
||
|
||
Antes de este commit MetaApp tenía la **paleta principal** themed
|
||
(iter 5) pero el ornament secundario (hovers, separators, botones
|
||
inline) seguía hardcoded. Ahora el theme switcher cambia
|
||
**absolutamente todo** el chrome del MetaApp en runtime.
|
||
|
||
Tests: 117 verdes (sin cambios numéricos, pero downstream sigue
|
||
compilando). Smoke run de nakui-ui: bootstrap completo OK.
|
||
|
||
Limitación restante: `nouser-explorer` todavía no migra al stack
|
||
yahweh themed — patrón idéntico a `nakui-explorer` aplicado pero
|
||
más nuevo. Próxima iter.
|
||
|
||
### feat(yahweh-widget-text-input): focus-aware border + caret sólo on focus
|
||
Iter 7 (mini-iter — el text-input ya estaba themed, faltaba sólo
|
||
el polish de focus visibility). Antes el border era siempre
|
||
`accent_strong` y el caret `|` siempre estaba presente — imposible
|
||
distinguir cuál input está activo en un form con varios fields.
|
||
|
||
Cambios en `yahweh-widget-text-input`:
|
||
- **Border focus-aware**: cuando el input está focused, border =
|
||
`theme.accent_strong` (color vivo). Cuando no, border =
|
||
`theme.border` (color tenue del chrome). Se obtiene via
|
||
`self.focus_handle.is_focused(window)`.
|
||
- **Caret `|` sólo on focus**: cuando el input no tiene focus, se
|
||
muestra el texto plano sin caret. Reduce el "ruido visual" en
|
||
forms con muchos fields.
|
||
- `render` ahora usa el `Window` arg (antes `_w`) para chequear
|
||
focus.
|
||
|
||
Sin cambios en API pública — todo es interno al `render`. El
|
||
binario no requiere migración.
|
||
|
||
Tests: sin cambios (los tests del crate son struct constructors,
|
||
no rendering). Tests downstream del widget (`yahweh-widget-meta-form`,
|
||
`nakui-ui`) siguen verdes — el cambio es backward compatible.
|
||
|
||
Beneficio operativo:
|
||
- Forms con 5+ fields ahora son navegables: el usuario ve cuál
|
||
input recibe sus teclas via el border highlighted.
|
||
- Cambio de theme afecta también a inputs (ya estaban themed; ahora
|
||
además respetan el `accent_strong` específico del preset
|
||
cuando focused, vs el `border` cuando no).
|
||
|
||
Limitación pendiente: el caret `|` literal no parpadea (no hay
|
||
animation timer). Cuando emerja la necesidad, agregar via
|
||
`cx.spawn` con un loop de toggle. Por ahora el caret estático on
|
||
focus es suficiente signal.
|
||
|
||
### feat(yahweh-widget-theme-switcher): control para ciclar themes en runtime
|
||
Iter 6. Cierra el ciclo del theme: ya teníamos paleta themed +
|
||
widgets que la consumen, faltaba el control UI para rotar entre
|
||
presets en vivo. Ahora hay un botón yahweh que muestra el theme
|
||
actual y al click avanza al siguiente. `nakui-ui` y `nakui-explorer`
|
||
lo incrustan en sus headers — un click cambia toda la paleta.
|
||
|
||
Crate nuevo: `crates/modules/ui_engine/widgets/theme-switcher/`
|
||
(`yahweh-widget-theme-switcher`):
|
||
- **Deps**: `gpui` + `yahweh-theme`. Sin nada más.
|
||
- **`pub fn theme_switcher(cx: &mut App) -> impl IntoElement`**:
|
||
botón clickable con `id`, padding consistente (`px(8/4)`),
|
||
bg = `theme.bg_panel_alt`, hover = `bg_row_hover`. Muestra
|
||
`"Tema: <name> ▸"` y al click hace
|
||
`Theme::set(cx, Theme::next_after(current.name))`.
|
||
- 2 tests `#[gpui::test]`:
|
||
- `switcher_constructs_with_theme_installed` — smoke: el
|
||
constructor lee el global y devuelve un IntoElement sin panic.
|
||
- `theme_set_changes_global` — verifica que `Theme::set` reemplaza
|
||
el global y que el siguiente `Theme::global` devuelve el nuevo.
|
||
- Dev-dep `gpui` con `test-support` para habilitar TestAppContext.
|
||
|
||
Migración de consumers:
|
||
- **`nakui-explorer`**: nueva dep `yahweh-widget-theme-switcher`.
|
||
El header pasa de `div().px().py()...child(text)` a
|
||
`div().flex_row().child(div().flex_grow().child(text)).child(theme_switcher(cx))`.
|
||
El switcher queda alineado a la derecha vía `flex_grow` del label.
|
||
- **`yahweh-widget-meta-form`**: nueva dep. El sidebar header
|
||
("Nakui" + 12px padding) gana el switcher con el mismo patrón
|
||
flex_row + flex_grow.
|
||
|
||
Tests stack: 115 → **117** (+2 del switcher). Cada crate compila
|
||
individualmente.
|
||
|
||
Beneficio operativo:
|
||
- Click en el switcher cambia toda la paleta en vivo: bg del app,
|
||
panels, banners (los que usan `_themed`), confirm modal, todo.
|
||
- 6 presets disponibles via `Theme::all()` (Nebula, Aurora,
|
||
Sunset, Flat Dark, Solarized Light, High Contrast). El switcher
|
||
cicla circularmente.
|
||
- Apps adoptantes del `Theme` heredan el switch sin esfuerzo.
|
||
|
||
Decisión técnica: el handler usa `Theme::set(cx, ...)` que
|
||
invalida el global. GPUI marca todos los views como dirty y
|
||
re-renderea — los widgets que leen `Theme::global` en su `render`
|
||
ven el nuevo automáticamente. No requiere `cx.observe_global`
|
||
explícito en cada widget consumidor.
|
||
|
||
Limitación: TextInput entities ya creadas no se actualizan visualmente
|
||
si el theme cambia los colors del input bg/border (esos colors
|
||
están hardcoded en `yahweh-widget-text-input`). Migrar text_input
|
||
al theme es una iter futura — bajo scope porque actualmente vive
|
||
suficientemente bien con sus defaults dark.
|
||
|
||
### feat(yahweh-widget-meta-form): paleta del chrome migrada a `Theme::global(cx)`
|
||
Iter 5 de integración. El `MetaApp::render` tenía 7 vars locales
|
||
con colors hardcoded (`bg/panel/border/text/text_dim/accent/
|
||
accent_active`) que se pasaban a las funciones internas
|
||
(`render_sidebar`/`render_main`/`render_list`/`render_form`/
|
||
`render_entity_ref_selector`). Ahora salen del `Theme::global(cx)`
|
||
que el binario shell instala al boot. El `confirm_delete_banner`
|
||
también usa `themed_colors(Banner::Warning)` / `themed_colors(Banner::Error)`
|
||
para sus colors base.
|
||
|
||
Cambios en `MetaApp::render`:
|
||
- 7 `let X = gpui::rgb(0x...)` → derive del theme:
|
||
- `bg` ← `theme.bg_app` (Background, soporta gradientes).
|
||
- `panel` ← `theme.bg_panel`.
|
||
- `border` ← `theme.border` (Hsla).
|
||
- `text` ← `theme.fg_text`.
|
||
- `text_dim` ← `theme.fg_muted`.
|
||
- `accent` ← `theme.accent`.
|
||
- `accent_active` ← `theme.accent_strong`.
|
||
- `toast_div` y `error_banner`: `banner(...)` → `banner_themed(cx, ...)`.
|
||
|
||
Cambios de firma (internas, no API público):
|
||
- `render_sidebar` / `render_main` / `render_list` /
|
||
`render_entity_ref_selector` / `render_form` cambian Rgba →
|
||
Hsla en sus parámetros de color (Background donde aplica para
|
||
`panel`). Los métodos `bg/text_color/border_color` de gpui::Div
|
||
aceptan ambos via `Into`, así que el uso interno no cambia.
|
||
|
||
Cambios en `render_confirm_delete_banner`:
|
||
- 6 colors hardcoded amber/red/gray → `themed_colors(Warning)` para
|
||
banner base, `themed_colors(Error)` para botón Confirm,
|
||
`theme.bg_panel_alt + fg_text` para botón Cancel.
|
||
- Cambiar de Theme ahora cambia toda la paleta del modal.
|
||
|
||
Lo que **NO** migra esta iter (queda como ornament hardcoded; iter
|
||
futura si emerge la necesidad):
|
||
- Row hovers misceláneos en `render_list` (px 0x232a36 / 0x1f2630
|
||
para selected/hover de filas).
|
||
- Borders sutiles entre filas (px 0x232a36).
|
||
- Bg de inputs custom (px 0x171a20).
|
||
- Bg de botones en `render_entity_ref_selector` (px 0x2c3540).
|
||
- Color rojo del icon `✕` de delete (px 0xd07070) y su hover
|
||
(px 0x4a2020).
|
||
|
||
Estos son detalles ornamentales que un theme switcher real
|
||
querría integrar; los aislo para una pasada futura cuando esté
|
||
claro qué slots semánticos del theme conviene agregar (ej.
|
||
`bg_row_selected` distinto de `bg_row_hover`, `accent_destructive`,
|
||
etc.).
|
||
|
||
`nakui-ui` shell ya instalaba `Theme::install_default(cx)` desde la
|
||
iter pasada — sigue siendo el contract entre el shell y el widget.
|
||
Smoke test del binario verificado: bootstrap completo OK, panic
|
||
esperado en open_window sin display.
|
||
|
||
Tests stack: 115 verdes (sin cambio — los tests del widget no
|
||
acceden al render).
|
||
|
||
Beneficio operativo:
|
||
- El theme switcher (cuando llegue) cambia toda la paleta principal
|
||
de `MetaApp` con 1 sola llamada `Theme::set(cx, ...)`.
|
||
- `MetaApp` y `nakui-explorer` comparten el mismo theme global en
|
||
un mismo proceso (si llegan a vivir juntos).
|
||
- Los `confirm_delete_banner` y los toasts del MetaApp respetan
|
||
is_dark: el contrast ajusta automatic.
|
||
|
||
### feat(yahweh): theme integration en `banner` + `card` + `nakui-explorer` consume themed
|
||
Iter 4 de la integración. Los widgets `banner` y `card` ahora
|
||
ofrecen variants `_themed(cx, ...)` que leen `Theme::global(cx)`.
|
||
Las versiones sin theme se preservan para apps sin theme global.
|
||
`nakui-explorer` migra a versiones themed + `Theme::install_default`
|
||
al boot — el chrome hardcoded del explorer (5 variables `let bg =
|
||
rgb(...)`) sale del theme.
|
||
|
||
Cambios en `yahweh-widget-card`:
|
||
- **Nueva dep**: `yahweh-theme`.
|
||
- **`pub fn card_themed(cx: &App) -> Div`**: devuelve [`card`]
|
||
pre-aplicado con `bg(theme.bg_panel)`. El caller sigue componiendo
|
||
con borders, accents, children.
|
||
|
||
Cambios en `yahweh-widget-banner`:
|
||
- **Nueva dep**: `yahweh-theme`.
|
||
- **`pub fn banner_themed(cx: &App, kind, message) -> Div`**:
|
||
deriva `(bg, fg)` según `kind` + `theme.is_dark`:
|
||
- `Info`: `theme.bg_panel_alt` + `theme.accent`.
|
||
- `Success` / `Warning` / `Error`: hue fijo (verde/amber/rojo)
|
||
+ lightness flippeada según `is_dark` (dark = bg low, fg high;
|
||
light = invertido).
|
||
- **`pub fn themed_colors(kind, theme) -> (Background, Hsla)`**:
|
||
helper público para callers que quieren computar el par sin
|
||
construir el div.
|
||
- 3 tests nuevos del derivation: dark/light lightness contrast,
|
||
kinds distinguidos por hue.
|
||
|
||
Migración de `nakui-explorer`:
|
||
- Nueva dep `yahweh-theme`.
|
||
- `main()` llama `Theme::install_default(cx)` antes de open_window
|
||
(el theme default es Nebula).
|
||
- `render`:
|
||
- 5 `let bg/text/text_dim/card_bg/border = rgb(...)` colors
|
||
locales → `theme.bg_app/fg_text/fg_muted/bg_panel/border`.
|
||
- `card().bg(card_bg)` → `card_themed(cx)` (borra los locales).
|
||
- `banner(Banner::Error, ...)` → `banner_themed(cx, Banner::Error, ...)`.
|
||
- Los accents `accent_seed` / `accent_morphism` se preservan
|
||
locales: son **señales semánticas del log** (azul=seed,
|
||
verde=morphism), no chrome del app.
|
||
|
||
Distribución de tests: 112 → **115** (+3 del banner derivation).
|
||
Workspace stack pasó por la migración sin errores.
|
||
|
||
Beneficio operativo:
|
||
- Cambiar de Theme (Nebula → Aurora → Solarized Light, etc.) ahora
|
||
refleja en `nakui-explorer` automáticamente. Antes había que
|
||
buscar y reemplazar los hex codes uno a uno.
|
||
- Apps que adopten el patrón `_themed` heredan el switcher de
|
||
theme cuando emerja.
|
||
|
||
Decisiones:
|
||
- **Hue fijo por kind**: Success siempre verde, Error siempre rojo,
|
||
etc. La lightness se ajusta al theme; el hue se mantiene como
|
||
invariante semántico cross-theme.
|
||
- **API dual**: `banner` (defaults) + `banner_themed` (theme).
|
||
Apps sin theme global pueden seguir con la versión simple.
|
||
- **Acentos semánticos del explorer (seed/morphism) NO migran**:
|
||
pertenecen al dominio del log, no al chrome.
|
||
|
||
Próximas integraciones pendientes:
|
||
- `MetaApp` (en `yahweh-widget-meta-form`) tiene su propia paleta
|
||
hardcoded de 6 colors que podría migrarse al theme. Scope mayor
|
||
que esta iter; queda como candidato.
|
||
- Theme switcher widget (botón/menú en chrome para ciclar themes).
|
||
Cuando emerja la necesidad real.
|
||
|
||
### feat(yahweh-widget-card): container card-shape compartido para timeline entries
|
||
Iteración 3 de la integración nakui ↔ yahweh. El "card visual"
|
||
pattern (padding consistente + rounded + flex_col + gap) que vivía
|
||
duplicado en cada timeline entry de `nakui-explorer` ahora es un
|
||
widget yahweh reusable. Sin acoplamiento a colores: el caller
|
||
decide bg/border/accent.
|
||
|
||
Crate nuevo: `crates/modules/ui_engine/widgets/card/`
|
||
(`yahweh-widget-card`):
|
||
- **Dep**: solo `gpui`. App-agnostic.
|
||
- **`pub fn card() -> Div`**: container con `flex_col` + `px(12)`
|
||
+ `py(8)` + `mb(4)` + `rounded(4)` + `gap(2)`. Sin colores
|
||
aplicados.
|
||
- El return es `Div` GPUI — el caller compone con `.bg(...)`,
|
||
`.border_l_4()`, `.border_color(...)`, `.child(...)`, hover,
|
||
on_click, etc., según necesite.
|
||
- 1 test smoke (constructor no panicea).
|
||
|
||
Migración de `nakui-explorer`:
|
||
- Nueva dep `yahweh-widget-card`.
|
||
- Los 2 patterns de timeline entry (Seed y Morphism) pasan de:
|
||
```rust
|
||
div().flex().flex_col().px(12).py(8).mb(4).bg(card_bg)
|
||
.rounded(4).border_l_4().border_color(accent).gap(2)...
|
||
```
|
||
a:
|
||
```rust
|
||
card().bg(card_bg).border_l_4().border_color(accent)...
|
||
```
|
||
- Reducción ~7 calls → ~3 por entry; legibilidad mejor (la
|
||
intención "card with accent" emerge del nombre `card()`).
|
||
|
||
Tests stack: 111 → **112 verdes** (+1 del crate card). Cada crate
|
||
afectado compila y testea individualmente.
|
||
|
||
Beneficio operativo:
|
||
- Si `MetaApp` o cualquier futura app necesita un container
|
||
card-shape (ej. info card, expanded list row), `card()` está
|
||
ya disponible.
|
||
- Cambiar el padding/rounded/gap canónico = un cambio en un solo
|
||
lugar.
|
||
- El widget no impone colores → no fuerza una paleta y permite
|
||
themes diversos por app/contexto.
|
||
|
||
### feat(yahweh-widget-banner): widget compartido para toasts/errores cross-app
|
||
Patrón visual común a `yahweh-widget-meta-form` (toast success +
|
||
error_banner) y `nakui-explorer` (error_banner): un `div` con bg
|
||
+ text colored según severidad. Antes vivía duplicado con colores
|
||
hardcoded en cada consumer; ahora hay un widget yahweh con presets
|
||
consistentes.
|
||
|
||
Crate nuevo: `crates/modules/ui_engine/widgets/banner/`
|
||
(`yahweh-widget-banner`):
|
||
- **Dep**: solo `gpui` (sin nakui, sin runtime). Reusable por
|
||
cualquier app GPUI que necesite tiras de status.
|
||
- **`pub enum Banner`** con 4 variants:
|
||
- `Info` (azul tenue, mensajes neutros).
|
||
- `Success` (verde, confirmaciones).
|
||
- `Warning` (amber, llamadas de atención).
|
||
- `Error` (rojo, errores fatales).
|
||
- **Métodos `Banner::bg()` y `Banner::fg()`**: paleta hardcoded por
|
||
variant (sin tema dinámico todavía — cuando emerja, se
|
||
inyecta vía `yahweh-theme`).
|
||
- **`pub fn banner(kind, message) -> Div`**: constructor que
|
||
devuelve el div ya con padding/text_size defaults; el caller
|
||
puede agregar children, override pads/sizes, attach handlers.
|
||
- 2 tests sanity: ningún kind comparte bg, ningún kind comparte fg.
|
||
|
||
Migración de consumers:
|
||
- **`yahweh-widget-meta-form`**: nueva dep `yahweh-widget-banner`.
|
||
El `toast_div` (Success) y `error_banner` (Error) en
|
||
`MetaApp::render` pasan de 2x6 líneas hardcoded a una llamada
|
||
a `banner(...)` cada uno (~12 líneas → 2).
|
||
- **`nakui-explorer`**: nueva dep. El error banner local pasa a
|
||
`banner(Banner::Error, e).px(16).py(8).text_size(12)` —
|
||
preserva el padding/size custom del header del explorer via
|
||
override builder.
|
||
|
||
Tests stack: 109 → **111 verdes** (+2 del crate banner).
|
||
|
||
Beneficio operativo:
|
||
- Si emerge un tercer consumer, importa la dep + 1 llamada.
|
||
- Cambiar la paleta de un kind = un cambio en un solo lugar
|
||
(ej. ajustar tono del Error o el contraste del Warning).
|
||
- Composición preservada: el `banner()` devuelve un `Div` directo,
|
||
el caller modifica con builder calls (`.child()`, `.px()`,
|
||
`.on_click()`, etc.) sin rewrap.
|
||
|
||
Próximo candidato natural: el `confirm_delete_banner` de MetaApp
|
||
es Banner::Warning + 2 botones embedded. Cuando emerja un segundo
|
||
consumer de modal-style banners, extraer un widget compositivo
|
||
arriba del `Banner` base.
|
||
|
||
### feat(yahweh): `MockBackend` público + tests E2E del widget con `gpui::TestAppContext`
|
||
Cierra el ciclo de testabilidad del widget metainterfaz. Hasta
|
||
ahora los tests del trait `MetaBackend` vivían como impl privada
|
||
en `backend.rs`; el widget no tenía forma de testear handlers
|
||
reales sin levantar `NakuiBackend` (que depende de event log +
|
||
Rhai + nakui-core). Ahora el mock es público y los tests del widget
|
||
lo consumen con `TestAppContext`.
|
||
|
||
Cambios en `yahweh-meta-runtime`:
|
||
- **Nuevo módulo `pub mod testing`** con
|
||
`pub struct MockBackend`. Exporta:
|
||
- `MockBackend::new()` — vacío.
|
||
- `MockBackend::with_records(iter)` — pre-poblado con
|
||
`(entity, uuid, value)` tuples.
|
||
- `MockBackend::with_morphism(name, |inputs, params| -> Result<usize>)` —
|
||
builder para registrar handlers callable de morphism (sin
|
||
handler, `morphism()` rebota con error claro).
|
||
- Métodos de inspección `total_records()` / `records_for(entity)`
|
||
(último devuelve `Vec<(Uuid, &Value)>` sin clones).
|
||
- `impl MetaBackend` completo: seed/load/list/update/delete con
|
||
semantica documentada.
|
||
- **Tests del trait en `backend.rs` simplificados**: el `MemBackend`
|
||
duplicado se borra; los tests pasan a usar `MockBackend::new()`
|
||
importado de `crate::testing`. 8 tests del backend.rs intactos +
|
||
9 tests propios del mock en `testing.rs`.
|
||
- Bajo `pub mod testing` (no `#[cfg(test)]`) deliberadamente: los
|
||
crates downstream pueden importarlo en sus dev/integ tests
|
||
vía `yahweh_meta_runtime::testing::MockBackend`.
|
||
|
||
Cambios en `yahweh-widget-meta-form`:
|
||
- **Dev-dep nueva**: `gpui = { workspace = true, features = ["test-support"] }`.
|
||
Habilita `TestAppContext` para tests sin abrir window real.
|
||
- **`MetaApp::apply_action` ahora `pub`** (era privado). Necesario
|
||
para que los tests E2E lo invoquen desde fuera. La function ya
|
||
era el entry point de los click handlers internos; exponerla no
|
||
cambia el contract.
|
||
- **Nuevo archivo `tests/widget_with_mock_backend.rs`** con 4 tests
|
||
`#[gpui::test]`:
|
||
- `meta_app_constructs_with_mock_backend_and_initial_state`:
|
||
instancia `MetaApp<MockBackend>` con records pre-poblados +
|
||
toast inicial; valida que la window construye sin panic.
|
||
- `open_view_action_does_not_panic`: invoca
|
||
`apply_action(OpenView)` real a través de
|
||
`window.update(cx, |meta, _, cx| ...)` → state machine corre
|
||
sin crash.
|
||
- `backend_state_visible_from_widget_perspective`: demuestra el
|
||
patrón "backend pre-poblado para fixtures" (typical para
|
||
screenshots / demos).
|
||
- `morphism_handler_can_be_registered_and_called_via_widget`:
|
||
`MockBackend::with_morphism` registra un counter callback;
|
||
`apply_action(Morphism)` lo dispara via `commit_morphism`
|
||
sin tocar nakui-core / Rhai.
|
||
|
||
Helpers de tests:
|
||
- `customers_module()`: fixture local de un `Module` con entity
|
||
Customer + view list + view form. Reusable cross-test.
|
||
|
||
Distribución de tests:
|
||
- `yahweh-meta-runtime`: 47 → **56** (+9 del nuevo testing
|
||
module).
|
||
- `yahweh-widget-meta-form`: 3 → **7** (+4 E2E reales).
|
||
- Total stack: **109 tests verdes** (56 runtime + 31 cards + 12
|
||
nakui-ui + 3 explorer + 7 widget).
|
||
|
||
Beneficio operativo:
|
||
- El widget tiene cobertura runtime real, no sólo type-check.
|
||
- Cualquier app que tome `B: MetaBackend` puede testarse con
|
||
`MockBackend` en sus dev-deps sin re-implementar el mock.
|
||
- Fixtures pre-pobladas habilitan demos/screenshots/CI con state
|
||
conocido.
|
||
|
||
Limitaciones:
|
||
- `render()` no se invoca en los tests (requiere window context
|
||
más rico). Los tests verifican state machine, no pixels. Pixel
|
||
comparison (snapshot tests) es scope futuro si emerge la
|
||
necesidad.
|
||
- `apply_action(Morphism)` con un module que no declara
|
||
`nakui_module_dir` rebota antes de llamar al mock handler. El
|
||
4to test acepta ambos outcomes (counter 0 o 1) — si en el futuro
|
||
agregamos un módulo de fixture con nakui_module_dir poblado, el
|
||
test puede aserta exactamente.
|
||
|
||
### feat(yahweh-meta-runtime): promover `short_hash` y `preview_value` desde nakui-explorer
|
||
Continúa la integración de las apps nakui al stack yahweh. Los
|
||
helpers visuales que `nakui-explorer` tenía locales y son reusables
|
||
suben a `yahweh-meta-runtime/format` para que cualquier app pueda
|
||
consumirlos sin duplicar.
|
||
|
||
Cambios en `yahweh-meta-runtime`:
|
||
- **`pub fn short_hash(h: &[u8; 32]) -> String`**: hex de los
|
||
primeros 4 bytes (8 chars). Útil para mostrar bundle/schema
|
||
hashes en UI sin quemar pantalla.
|
||
- **`pub fn preview_value(v: &Value, max: usize) -> String`**:
|
||
JSON one-liner truncado con `...` al final si excede `max`
|
||
chars. Edge case: `max < 3` devuelve los primeros `max` chars
|
||
sin sufijo.
|
||
- Re-exports en lib.
|
||
- 5 tests nuevos: 4 tests + 1 sanity para el caso `max < ellipsis`.
|
||
|
||
Migración de `nakui-explorer`:
|
||
- Nueva dep `yahweh-meta-runtime` en Cargo.toml.
|
||
- Borrado helpers locales `short_uuid`, `short_hash`,
|
||
`preview_value` (~30 líneas).
|
||
- `use yahweh_meta_runtime::{preview_value, short_hash, short_uuid}`.
|
||
- Borrados 4 tests duplicados (los runtime los testea).
|
||
|
||
Tests:
|
||
- `yahweh-meta-runtime`: 42 → **47** (+5 helpers nuevos).
|
||
- `nakui-explorer`: 7 → **3** (–4 duplicados; quedan los 3
|
||
específicos: load_log, breakdown, missing_file).
|
||
- Resto del workspace intacto.
|
||
|
||
Beneficio operativo: 3 helpers visuales centralizados. Cualquier
|
||
app nueva que muestre UUIDs/hashes/JSON-previews los importa sin
|
||
re-implementar la heurística de truncamiento.
|
||
|
||
Pendiente arquitectural: el render del card timeline en
|
||
`nakui-explorer` (border-l-4 colored + flex_col + texto en niveles)
|
||
es un pattern reusable que también aparece en `yahweh-widget-meta-form`
|
||
(render_list filas). Cuando aparezca un tercer consumer de ese
|
||
pattern se extrae a un widget yahweh.
|
||
|
||
### feat(brahman-cards): templates Nickel canónicos para cada body kind
|
||
Materializa el patrón "import + override" del brazo: hasta ahora
|
||
`BRAHMAN_CARDS_TEMPLATES_DIR` existía como mecanismo pero el repo
|
||
no shippeaba ningún template. Ahora hay 3 templates basic (uno
|
||
por body kind del CardBody) bajo
|
||
`crates/core/brahman-cards/templates/`:
|
||
|
||
- **`ente_basic.ncl`** — Card runtime mínima: `payload="Virtual"`,
|
||
`supervision="OneShot"`, `schema_version=1`. Override típico:
|
||
`id` + `label`.
|
||
- **`monad_basic.ncl`** — agrupación semántica de archivos
|
||
(Mónada Nouser): metadata vacía, `dominant_lens="grid"` (lowercase
|
||
por convención serde rename_all). Override típico: `id`, `label`,
|
||
`members`, `cardinality`.
|
||
- **`ui_module_basic.ncl`** — descriptor UI con `entities=[]`,
|
||
`menu=[]`, `views={}`. Override típico: `id`, `label` y los
|
||
3 payloads.
|
||
|
||
Cada field override-able marcada `| default` (sin eso Nickel
|
||
rebota merge de strings/numbers no-iguales).
|
||
|
||
API nueva en `lib.rs`:
|
||
- **`pub fn canonical_templates_dir() -> PathBuf`**: devuelve el
|
||
directorio de templates del crate (resuelto via
|
||
`CARGO_MANIFEST_DIR`). Útil para apuntar el env
|
||
`BRAHMAN_CARDS_TEMPLATES_DIR` en runtime/tests sin hardcoding
|
||
del path.
|
||
- Doc explica que para distribución del binary standalone (cuando
|
||
emerja), incluir templates como recursos via `include_dir!` o
|
||
instalar el directorio junto al ejecutable.
|
||
|
||
5 tests E2E (`tests/templates.rs`) que cubren:
|
||
- `ente_basic` import + override `id`+`label` → Card body Ente
|
||
con `payload=Virtual` (default preserved).
|
||
- `monad_basic` import + override `id`+`label`+`cardinality` →
|
||
Card body Monad con members=[] y summary="" (defaults).
|
||
- `ui_module_basic` import + override de `id`+`label`+menu+views
|
||
→ Card body UiModule con entities=[] (default).
|
||
- Sanity: import sin override → defaults `"TEMPLATE_ID"` /
|
||
`"TEMPLATE_LABEL"` pasan al wrapper sin error.
|
||
- Sanity: el path de `canonical_templates_dir()` apunta a un
|
||
directorio existente con los 3 archivos esperados.
|
||
|
||
Helper de test `with_canonical_templates(F)` setea/restaura el
|
||
env localmente; cada test single-thread-safe.
|
||
|
||
Tests suite brahman-cards: 26 → **31** verdes (+5). El resto del
|
||
workspace intacto.
|
||
|
||
Beneficio operativo:
|
||
- Un usuario que quiera declarar un Card nuevo puede empezar con
|
||
un override de 2 líneas (`id` + `label`) en lugar de copiar el
|
||
shape full desde cero.
|
||
- Templates auto-documentan la convención `| default` para que
|
||
copiar uno y agregar fields propios "just works" en merge.
|
||
- El brazo sigue siendo agnostic — los templates son sólo
|
||
archivos `.ncl` resueltos via el import resolver Nickel; nada
|
||
hardcoded en código Rust.
|
||
|
||
Limitaciones:
|
||
- No hay templates "ricos" tipo `crud_basic.ncl` que parametricen
|
||
por entity name. Nickel no expone funciones-templates de la
|
||
forma típica de templating engines; lo más cercano sería un
|
||
template con un field `entity_name | String` y references
|
||
internas via `me.entity_name`. Cuando aparezca el caso de uso
|
||
real (e.g., un módulo donde el patrón list+form es repetitivo),
|
||
se diseña el template paramétrico.
|
||
- `canonical_templates_dir()` resuelve via `CARGO_MANIFEST_DIR` —
|
||
funciona en `cargo` (test/run/build) pero no para un binary
|
||
instalado fuera del workspace. Para release distribution la API
|
||
necesitará un fallback (resources embedded o convención de
|
||
install path).
|
||
|
||
### refactor(nakui-core): KCL → Nickel — `kcl_wrapper` reemplazado por evaluación in-process
|
||
Cierra el ciclo: el motor de validación de entities deja de
|
||
shellear el binario externo `kcl` y pasa a evaluar **Nickel
|
||
contracts** in-process via la dep `nickel-lang` (la misma que ya
|
||
usa `brahman-cards` para sus templates). Los 3 schemas de los
|
||
módulos sales/inventory/treasury migran de `.k` a `.ncl`.
|
||
Además se borran los 2 archivos `.k` doc-only del repo
|
||
(`ente-card/schema/card.k`, `ente-brain/schema/rule.k` — ambos
|
||
estaban marcados "REFERENCE ONLY. NOT LOADED").
|
||
|
||
Cambios en **nakui-core**:
|
||
- **Nueva dep**: `nickel-lang = "2.0.0"` (interfaz estable).
|
||
- **Borrado** `kcl_wrapper.rs` (43 líneas) — shellear el binario
|
||
desaparece.
|
||
- **Nuevo** `nickel_validator.rs`:
|
||
- `pub fn vet(schema_path, state, schema_name) -> Result<(), NickelError>`
|
||
evalúa `let bundle = (import "<schema>") in
|
||
(std.deserialize 'Json m%%"<json>"%%) | bundle.<entity>`.
|
||
- El state JSON va dentro de un raw string Nickel
|
||
(`m%%"..."%%`) y se deserialize via `std.deserialize 'Json`.
|
||
No embebemos el state como record literal Nickel directo
|
||
porque la sintaxis JSON usa `:` (Nickel records usan `=`).
|
||
- 5 tests propios cubriendo happy path + 4 fallure modes
|
||
(field missing, predicate fails, cross-field invariant
|
||
fails, optional field present/absent).
|
||
- **`executor.rs`**:
|
||
- `kcl_wrapper::vet` → `nickel_validator::vet`.
|
||
- `KclError` → `NickelError`.
|
||
- `ExecError::KclPre/KclPost/KclPostCreate` → `SchemaPre/Post/PostCreate`
|
||
(más neutro, ya no menciona KCL).
|
||
- `kcl_check` (privado) → `validate_entity`.
|
||
- `build_schema_bundle` ahora emite un archivo Nickel con
|
||
`(import "X") & (import "Y") & ...` en lugar de concatenar
|
||
bytes (cada `.ncl` es una expresión record completa, no
|
||
juntable como texto plano).
|
||
- **`manifest.rs`**:
|
||
- `effective_schemas` default `"schema.k"` → `"schema.ncl"`.
|
||
- `extract_schema_names` reescrito: ahora detecta keys
|
||
CapitalCase con 2 spaces de indent (convención de los
|
||
`schema.ncl`), no más patrón `schema X:` de KCL.
|
||
- Tests del extractor actualizados (1 test reemplazado por 2:
|
||
`_handles_nickel_record_top_level` + `_skips_let_bindings_and_lowercase`).
|
||
|
||
Cambios en **schemas de módulos**:
|
||
- **`sales/schema.ncl`**: contracts Nickel para `Venta`. Usa
|
||
`std.contract.Sequence [record_contract, from_predicate]`
|
||
para combinar shape + invariante cross-field
|
||
(`total == cantidad * precio_unitario`). El patrón directo
|
||
`record | from_predicate` rebota con "missing definition" porque
|
||
el predicate evalúa el contract antes de que el value lo
|
||
populate; documentado en el comment.
|
||
- **`inventory/schema.ncl`**: `Stock`, `MovimientoStock`,
|
||
`TransferenciaStock` (esta última con cross-field
|
||
`source != dest` via Sequence).
|
||
- **`treasury/schema.ncl`**: `Caja`, `Movimiento`,
|
||
`Transferencia` (con cross-field via Sequence).
|
||
- Helpers locales en cada archivo: `positive_int`,
|
||
`non_negative_int`, `currency_iso`, etc. via
|
||
`std.contract.from_predicate`.
|
||
- Los 3 `schema.k` viejos **borrados**.
|
||
- `sales/nsmc.json` actualizado: paths `schema.k` →
|
||
`schema.ncl`.
|
||
|
||
Cambios en **tests**:
|
||
- `sales.rs`, `inventory.rs`: `KclPost` → `SchemaPost`.
|
||
- `kernel_guards.rs`: `KclPostCreate` → `SchemaPostCreate`,
|
||
path del schema directo `treasury/schema.k` →
|
||
`treasury/schema.ncl`.
|
||
- `graph.rs`, `manifest_validation.rs`: tests que escriben
|
||
`schema.k` inline cambian a `schema.ncl` con sintaxis Nickel.
|
||
- `schema_versioning.rs`: refs `schema.k` → `schema.ncl`.
|
||
|
||
Cambios documentales:
|
||
- **Borrado** `crates/core/ente-card/schema/card.k` (1 archivo,
|
||
REFERENCE ONLY documentado en su header).
|
||
- **Borrado** `crates/core/ente-brain/schema/rule.k` (REFERENCE
|
||
ONLY documentado en su header).
|
||
|
||
Tests:
|
||
- **nakui-core**: 84 tests verdes (41 unit + 43 integration en
|
||
graph/event_log/manifest_validation/schema_versioning/
|
||
inventory/sales/kernel_guards). Suite full pasa.
|
||
- **nakui-ui**, **brahman-cards**, **yahweh-***: sin cambios,
|
||
todos verdes.
|
||
- Total cubriendo el área: 174 tests.
|
||
|
||
Beneficios:
|
||
- **Sin dep externa**: el binario `kcl` ya no es requisito de
|
||
runtime ni de tests. Build limpio en CI sin instalar KCL.
|
||
- **Errores en línea**: Nickel reporta contract violations con
|
||
caret pointing al field exacto del schema y el value que
|
||
falló. KCL daba mensajes textuales menos navegables.
|
||
- **Mismo motor que el brazo de cards**: una sola dependencia
|
||
Nickel para todo el repo (validación + templates de cards).
|
||
- **Sin tempfile JSON intermedio**: el state se evalúa
|
||
directamente en memoria; no hay `std::fs::write` por cada
|
||
validate.
|
||
|
||
Limitaciones / decisiones:
|
||
- El comentario "REFERENCE ONLY" de los `.k` borrados ya estaba
|
||
marcado en sus headers; eran sólo notas de diseño para humanos.
|
||
La autoridad real (Rust validate methods) sigue intacta.
|
||
- La sintaxis Nickel `record_contract | from_predicate` no
|
||
funciona — hay que envolver en `std.contract.Sequence [record,
|
||
from_predicate]`. Documentado en cada schema y en el doc del
|
||
validator.
|
||
|
||
**Pendientes restantes**: ninguno del refactor original. Los
|
||
yahweh + KCL + card.k cierran. Próximos pendientes salen de
|
||
nuevo trabajo (no del plan que arrastrábamos).
|
||
|
||
### refactor(yahweh): Fase 2c — extracción del widget al crate `yahweh-widget-meta-form`
|
||
Cierra el refactor de UI: el widget render (forms, lists, modal de
|
||
delete, EntityRef selector, sidebar, key handlers) deja de vivir en
|
||
el binario nakui-ui y pasa a un crate yahweh nuevo, genérico sobre
|
||
`MetaBackend`. nakui-ui queda como un shell de bootstrap de 424
|
||
líneas.
|
||
|
||
Crate nuevo: `crates/modules/ui_engine/widgets/meta-form/`
|
||
(`yahweh-widget-meta-form`):
|
||
- **Deps**: gpui, yahweh-meta-schema, yahweh-meta-runtime, yahweh-theme,
|
||
yahweh-widget-text-input, serde_json, uuid. **Cero deps a nakui** o
|
||
brahman-cards — reusable por cualquier app.
|
||
- **`MetaApp<B: MetaBackend>`** público: estructura genérica con
|
||
`modules`, `backend: B`, `active`, `form_inputs`, `editing`,
|
||
`pending_delete`, `toast`, `load_error`. El bound `B: MetaBackend`
|
||
se propaga a todos los `impl MetaApp<B>` y al `impl Render for
|
||
MetaApp<B>`.
|
||
- **`MetaApp::new(modules, backend, initial_toast, initial_error, cx)`**:
|
||
constructor sin lógica de bootstrap. El caller pre-construye
|
||
modules + backend + cualquier mensaje inicial. La active view
|
||
default es la primera entry del menú del primer módulo.
|
||
- **Methods preservados** del original (rename simbólico): select_view,
|
||
open_edit, commit_seed, commit_morphism, commit_delete, apply_action,
|
||
list_rows, render_*, tick interno via WriteOutcome.post_status.
|
||
- **Helpers locales del widget**: `lookup_field` (path walker JSON
|
||
por la lista renderer), `append_compact_msg` (concatenador del
|
||
toast), `format_seed_toast` (decide "creado/actualizado/sin cambios"
|
||
según `WriteOutcome`).
|
||
- **3 tests funcionales puros**: `lookup_field`, `append_compact_msg`,
|
||
`format_seed_toast`. Tests con GPUI cx no son posibles sin un
|
||
TestAppContext setup; quedan implícitos vía type-check del trait
|
||
bound.
|
||
|
||
Cambios en `nakui-ui` (shell):
|
||
- **main.rs**: 1959 → **424** líneas (78% reducción). Ahora sólo:
|
||
1. Carga modules via `brahman_cards::load_cards_from_dir` +
|
||
`load_ui_modules` (filtra UiModule body, valida, dedup).
|
||
2. Carga executors para módulos con `nakui_module_dir`.
|
||
3. `NakuiBackend::open(...)` para inicializar el backend.
|
||
4. `cx.open_window(...)` con `MetaApp::<NakuiBackend>::new(...)`
|
||
como root view.
|
||
- **`use yahweh_widget_meta_form::MetaApp`** + dep nueva en
|
||
Cargo.toml. Los imports de yahweh-meta-runtime/schema desaparecen
|
||
de main (los consume el widget internamente).
|
||
- **Tests del shell**: 4 tests E2E que tocan nakui-core directamente
|
||
(event_log_replay, morphism_pipeline_real_sales_vender,
|
||
load_ui_modules x3). Los tests del NakuiBackend impl quedan en
|
||
`backend.rs` (8 tests). Los tests del widget viven en su propio
|
||
crate.
|
||
- **`backend.rs`**: sin cambios (NakuiBackend ya estaba aislado en
|
||
Fase 2b).
|
||
|
||
Distribución final del refactor yahweh:
|
||
- `yahweh-meta-schema`: 8 tests (data puro).
|
||
- `yahweh-meta-runtime`: 42 tests (helpers + trait MetaBackend).
|
||
- `yahweh-widget-meta-form`: 3 tests (widget genérico).
|
||
- `brahman-cards`: 26 tests (loader unificado).
|
||
- `nakui-ui`: 12 tests (4 shell + 8 backend impl).
|
||
- **Total: 91 tests** cubriendo el área.
|
||
|
||
Cada crate compila individualmente. El widget consume el trait sin
|
||
saber qué backend hay debajo; `nakui-ui` provee el trait wireado a
|
||
nakui-core; cualquier futuro shell (mock para tests, otro stack de
|
||
storage) puede reusar el widget sin cambio.
|
||
|
||
Lo que NO hace Fase 2c:
|
||
- No mueve `format_seed_toast`/`append_compact_msg`/`lookup_field`
|
||
a `yahweh-meta-runtime`. Son lo bastante widget-flavored
|
||
(`SharedString` de gpui, decisiones de UX del toast, etc.) que
|
||
preferí dejarlos al lado del render.
|
||
- No introduce un `MetaApp::with_status` builder pattern. La
|
||
signature de `new` con 5 args es manejable; si crece, se refactor
|
||
después.
|
||
- No expone configuración del widget (theme override, layout
|
||
custom, etc.). Cuando emerja una segunda app que use el widget
|
||
con preferencias distintas, se agregan opts.
|
||
|
||
**Pendientes**:
|
||
1. **KCL → Nickel**: kcl_wrapper en nakui-core reemplazado por
|
||
evaluación de Nickel contracts. Migrar los 3 schemas .k de
|
||
sales/inventory/treasury a .ncl.
|
||
2. **`card.k` eliminado** (REFERENCE ONLY documentado en su header).
|
||
|
||
### refactor(yahweh): Fase 2b — `MetaBackend` trait + `NakuiBackend` + MetaUi consume el backend
|
||
Materialización del trait que diseñamos en charla. Tres pasos
|
||
combinados en un solo commit:
|
||
|
||
**Step A** — trait + WriteOutcome en `yahweh-meta-runtime`:
|
||
- Nuevo módulo `backend.rs` con:
|
||
- `pub trait MetaBackend: 'static` con 6 métodos:
|
||
`list_records`, `load_record`, `seed`, `update`, `delete`,
|
||
`morphism`. Convención de ids como `Uuid` canónico (los
|
||
backends que internamente usan otros tipos mapean), `set+clear`
|
||
pre-computados por el caller (no double-roundtrip al store),
|
||
threshold `'static` sin Send/Sync (suficiente para handlers
|
||
GPUI single-threaded).
|
||
- `pub struct WriteOutcome { id, changed, post_status }` con
|
||
constructor `no_change(id)`. La UI usa `changed = 0` para
|
||
"sin cambios", `post_status` para concatenar mensajes
|
||
auto-emitidos por el backend (compact, etc.).
|
||
- 9 tests con un `MemBackend` mínimo (HashMap por
|
||
`(entity, uuid)`): seed/load round-trip, list/filter/order,
|
||
update set/clear/no-op, delete/missing, object-safety check.
|
||
|
||
**Step B** — `NakuiBackend` en `nakui-ui/src/backend.rs`:
|
||
- Estructura que ownea `Arc<Mutex<MemoryStore>>`,
|
||
`Option<Arc<Mutex<EventLog>>>`, `BTreeMap<id, Arc<Executor>>`,
|
||
`snap_path`, `snapshot_threshold`, `writes_since_compact`.
|
||
- `NakuiBackend::open(log_path, threshold, executors) -> (Self, OpenStatus)`:
|
||
abre log, carga snapshot, replay, auto-compact si threshold
|
||
cruzado; devuelve `OpenStatus { init_toast, load_error }` para
|
||
que el caller agregue al banner.
|
||
- `tick_compact()` privado que cada write public method invoca
|
||
tras éxito; devuelve `Option<String>` que se mete en
|
||
`WriteOutcome.post_status`.
|
||
- `impl MetaBackend for NakuiBackend`:
|
||
- `seed`: WAL order (log first, store after), `tick_compact`,
|
||
devuelve `WriteOutcome { id: Some(uuid), changed: 1, post_status }`.
|
||
- `update`: si `set+clear` vacíos devuelve `WriteOutcome::no_change`;
|
||
si no construye `FieldOp::Set`+`FieldOp::Clear`, log Morphism
|
||
`ui.edit_record` con `params.fields/cleared`, store.apply, tick.
|
||
- `delete`: `FieldOp::Delete`, log Morphism `ui.delete_record`,
|
||
store.apply, tick.
|
||
- `morphism`: locks log + store, `execute_and_log_with_recovery`,
|
||
tick. `WriteOutcome { id: None, changed: ops.len(), post_status }`.
|
||
- Funciones `snapshot_path_for` y `maybe_compact_log` movidas acá
|
||
desde main.rs (ahora son detalle del backend).
|
||
- 7 tests del impl: round-trip via trait, set+clear, no-op edit
|
||
no escribe, delete/load, list_records, morphism sin executor da
|
||
error claro, threshold dispara snapshot.
|
||
|
||
**Step C** — `MetaUi` consume el backend:
|
||
- Reemplaza fields `store` / `event_log` / `executors` /
|
||
`snap_path` / `snapshot_threshold` / `writes_since_compact`
|
||
por un único `backend: NakuiBackend`.
|
||
- `MetaUi::new` colapsa el wiring de persistencia en
|
||
`NakuiBackend::open(...)` — pasó de ~150 líneas a ~10 líneas.
|
||
- `commit_seed` ya no construye `LogEntry`/`FieldOp` directos:
|
||
- SEED → `self.backend.seed(entity, obj)`.
|
||
- EDIT → `self.backend.load_record + compute_field_delta +
|
||
compute_clear_fields → self.backend.update(set, clear)`.
|
||
- Devuelve `WriteOutcome` (reemplaza el viejo enum `CommitOutcome`).
|
||
- `commit_morphism` parsea inputs/params del form y delega a
|
||
`self.backend.morphism(...)`.
|
||
- `commit_delete` es one-liner: `self.backend.delete(entity, id)`.
|
||
- `tick_runtime_compact` eliminado (ahora interno al backend; el
|
||
msg viaja en `WriteOutcome.post_status`).
|
||
- `list_rows` queda como proxy `self.backend.list_records(entity)`.
|
||
- `validate_entity_refs` callsite usa cierre sobre
|
||
`backend.load_record` (en vez de `&Store`).
|
||
- Nuevo helper `format_seed_toast(entity, was_editing, &outcome)`
|
||
reemplaza el match sobre `CommitOutcome`.
|
||
- Imports limpiados: no más `nakui_core::delta::FieldOp`/`FieldPath`,
|
||
no más `nakui_core::event_log::*` en main.rs (sólo en tests E2E).
|
||
No más `Arc/Mutex` (vive en backend).
|
||
|
||
Distribución de tests post-refactor:
|
||
- `yahweh-meta-runtime`: 33 → **42** (+9 trait tests con MemBackend).
|
||
- `nakui-ui`: 14 → **21** (+7 tests del NakuiBackend impl).
|
||
- `yahweh-meta-schema`: 8 (sin cambio).
|
||
- `brahman-cards`: 26 (sin cambio).
|
||
- Total: **97**.
|
||
|
||
Build: cada crate compila individualmente.
|
||
|
||
Nota sobre Fase 2b/c estado:
|
||
- ✅ Backend trait + impl + MetaUi usa backend.
|
||
- ⏭ Falta extraer los **widgets render** (form/list/modal/EntityRef
|
||
selector) de nakui-ui a un crate yahweh nuevo
|
||
(sugerencia: `yahweh-widget-meta-form`). Esa extracción ahora es
|
||
trivial: el render code ya consume sólo `&self.modules` +
|
||
`self.backend` (vía trait). Lo dejo para próximo commit.
|
||
|
||
**Pendientes**:
|
||
1. **Fase 2c**: extraer widget render al crate yahweh
|
||
(`yahweh-widget-meta-form` o similar) — `MetaApp<B: MetaBackend>`
|
||
genérico, `nakui-ui` queda como ~50 líneas de shell con
|
||
`MetaApp::<NakuiBackend>::new(...)`.
|
||
2. **KCL → Nickel**: kcl_wrapper reemplazado por evaluación de
|
||
Nickel contracts.
|
||
3. **`card.k` eliminado** (REFERENCE ONLY).
|
||
|
||
### refactor(yahweh): Fase 2 — extraer helpers puros a `yahweh-meta-runtime`
|
||
Sigue de la Fase 1 (lift del schema a yahweh). Ahora extraemos los
|
||
**helpers puros** que cualquier widget renderer o backend ejecutor
|
||
necesita sobre el schema: parse, delta, validation, format. Sin
|
||
GPUI, sin acoplamiento a un backend específico.
|
||
|
||
Crate nuevo: `crates/modules/ui_engine/libs/meta-runtime/`
|
||
(`yahweh-meta-runtime`):
|
||
- **Deps**: `serde_json`, `thiserror`, `uuid`, `yahweh-meta-schema`.
|
||
NO GPUI, NO nakui.
|
||
- **Módulos**:
|
||
- `parse.rs` — `parse_field_value(kind, raw)`,
|
||
`infer_param_value(raw)`, `resolve_param_value(name, raw, spec)`.
|
||
- `delta.rs` — `compute_field_delta(current, proposed)`,
|
||
`compute_clear_fields(current, to_clear)`.
|
||
- `refs.rs` — `validate_entity_refs(load: F, refs)` donde `F`
|
||
es un cierre `Fn(&str, Uuid) -> Option<Value>`. Decoupling vía
|
||
closure en lugar de trait — evita atar el crate a cualquier
|
||
backend específico (no hay `Store` trait acá), y los callers
|
||
pasan `|e, id| store.load(e, id)` trivialmente.
|
||
- `format.rs` — `human_label_for_record(value, id)`,
|
||
`render_value(opt_value)`, `value_to_input_text(value)`,
|
||
`short_uuid(id)`.
|
||
- **33 tests propios** en el crate nuevo (cubren todos los helpers
|
||
movidos + edge cases).
|
||
|
||
Cambios en `nakui-ui`:
|
||
- **Nueva dep** `yahweh-meta-runtime` en `Cargo.toml`.
|
||
- **Imports**: agrega `use yahweh_meta_runtime::{...}` con todos los
|
||
helpers extraídos. Borrado el código local equivalente
|
||
(~200 líneas).
|
||
- **`validate_entity_refs` callsite**: pasa de
|
||
`validate_entity_refs(&*store, &refs)` a
|
||
`validate_entity_refs(|e, id| store.load(e, id), &refs)` — el
|
||
closure es ergonómico sobre cualquier `Store`.
|
||
- **Tests duplicados borrados** (~34 tests que ahora viven en
|
||
`yahweh-meta-runtime`):
|
||
- `parse_field_*` (text/number/boolean variants)
|
||
- `infer_param_value_*`
|
||
- `delta_*` (5 tests)
|
||
- `clear_fields_*` (3 tests)
|
||
- `validate_entity_refs_*` (5 tests)
|
||
- `resolve_param_*` (6 tests)
|
||
- `parse_field_entity_ref_*` (4 tests)
|
||
- `human_label_*` (3 tests), `render_value_*`,
|
||
`value_to_input_text_inverse_of_parse`
|
||
- **Tests que se quedan en nakui-ui** (runtime-específicos):
|
||
- `lookup_field_simple_and_nested` — helper local del list renderer.
|
||
- `append_compact_msg_handles_both_branches`,
|
||
`runtime_compact_cycle_resets_counter_after_threshold`,
|
||
`snapshot_path_for_replaces_extension`,
|
||
`maybe_compact_log_*` (3) — wiring de persistencia a EventLog.
|
||
- `load_ui_modules_via_brahman_cards_*` (3) — integración con el
|
||
brazo de cards.
|
||
- `value_to_input_then_parse_round_trip` — round-trip del par
|
||
`value_to_input_text + parse_field_value` (toca ambos lados).
|
||
- `event_log_replay_restores_memory_store`,
|
||
`morphism_pipeline_executes_real_sales_vender`,
|
||
`event_log_replay_handles_full_crud_cycle` — E2E nakui-core.
|
||
|
||
Distribución de tests:
|
||
- `nakui-ui`: 48 → 14 (los 34 movidos viven en runtime).
|
||
- `yahweh-meta-runtime`: 33 (nuevos).
|
||
- `yahweh-meta-schema`: 8 (sin cambio).
|
||
- `brahman-cards`: 26 (sin cambio).
|
||
- Total cubriendo el área: 81.
|
||
|
||
Build: cada crate afectado compila y testea limpio individualmente.
|
||
Workspace build full no se completó esta corrida por OOM al
|
||
compilar `surrealdb-core` (problema ambiental no relacionado al
|
||
refactor).
|
||
|
||
Lo que NO hace Fase 2:
|
||
- No mueve los widgets render (`render_form`/`render_list`/
|
||
`render_entity_ref_selector`/`render_confirm_delete_banner`) a
|
||
yahweh — eso es Fase 2b/3, requiere diseñar el `MetaBackend`
|
||
trait porque las render functions tocan el state de `MetaUi`
|
||
(form_inputs, pending_delete, executors).
|
||
|
||
**Pendientes** (orden):
|
||
1. **Fase 2b**: extraer widget render a un crate yahweh nuevo
|
||
(sugerencia: `yahweh-widget-meta-form`). Requiere diseñar
|
||
`MetaBackend` trait.
|
||
2. **Fase 3**: thin shell — `nakui-ui` queda reducido a una impl
|
||
de backend wireada a `nakui-core`.
|
||
3. **KCL → Nickel** + **card.k eliminado**.
|
||
|
||
### refactor(yahweh): Fase 1 — `nakui-ui-schema` → `yahweh-meta-schema`
|
||
Primer paso del refactor yahweh. El schema de UI declarativa
|
||
(entities, menús, listas, formularios, acciones) vivía bajo
|
||
`crates/modules/nakui/ui-schema/` y se llamaba `nakui-ui-schema` —
|
||
un nombre que sugería acoplamiento con Nakui que en realidad no
|
||
existe (el crate sólo depende de `serde`/`serde_json`/`thiserror`).
|
||
Lo movemos a yahweh para que sea consumible por cualquier app de UI
|
||
metadata-driven sin hacer pasar la dep "rara" por nakui.
|
||
|
||
Cambios mecánicos:
|
||
- **`git mv`**: `crates/modules/nakui/ui-schema/` →
|
||
`crates/modules/ui_engine/libs/meta-schema/`.
|
||
- **Cargo.toml del crate movido**:
|
||
- `name = "nakui-ui-schema"` → `name = "yahweh-meta-schema"`.
|
||
- Description actualizada: "Yahweh — meta-schema: descriptores
|
||
declarativos de UI ... independiente del backend".
|
||
- **Workspace `Cargo.toml`**: la entry del members[] pasa de
|
||
`crates/modules/nakui/ui-schema` a
|
||
`crates/modules/ui_engine/libs/meta-schema` (en su sección
|
||
yahweh, no en la sección nakui).
|
||
- **`brahman-cards`**:
|
||
- Cargo.toml: dep path/name a `yahweh-meta-schema`.
|
||
- lib.rs: `pub use nakui_ui_schema::Module` →
|
||
`pub use yahweh_meta_schema::Module`.
|
||
- readers.rs: comment + doc-link al nuevo nombre.
|
||
- **`nakui-ui`**:
|
||
- Cargo.toml: dep path/name a `yahweh-meta-schema`.
|
||
- main.rs: `use nakui_ui_schema::{...}` →
|
||
`use yahweh_meta_schema::{...}`.
|
||
- **Self-test del crate movido**
|
||
(`tests/example_modules.rs`): `nakui_ui_schema` → `yahweh_meta_schema`,
|
||
y se rebasa el path del repo root (5 niveles arriba ahora, era 4).
|
||
|
||
Cambios documentales:
|
||
- **Doc de crate** (`lib.rs`): "Schema declarativo de la metainterfaz
|
||
Nakui" → "Schema declarativo de la metainterfaz (yahweh
|
||
meta-schema)" + "backend-agnostic" en la filosofía. La sección
|
||
Persistencia universal pasa de "el runtime conecta cada vista al
|
||
`nakui_core::store::Store`" a un wording neutro: "el runtime que
|
||
consume este schema conecta vistas a su backend".
|
||
- **Doc del field `Module.nakui_module_dir`**: ahora marcado como
|
||
"path opaco al backend, lo interpreta el runtime concreto". Se
|
||
describe la convención actual de Nakui (nsmc.json + KCL + Rhai)
|
||
como ejemplo, no como contrato del schema. El nombre del campo
|
||
se mantiene por compat con módulos ya escritos; agregado
|
||
`#[serde(alias = "backend_module_dir")]` para que un futuro
|
||
rename no rompa los actuales.
|
||
|
||
Tests:
|
||
- yahweh-meta-schema (crate movido): 13 tests propios siguen
|
||
verdes tras el path rebase.
|
||
- brahman-cards: 26/26 verdes (17 integration + 9 nickel).
|
||
- nakui-ui: 48/48 verdes.
|
||
- Workspace build verde.
|
||
|
||
Lo que NO hace Fase 1:
|
||
- No mueve los widgets de UI (form/list/modal/EntityRef selector)
|
||
a yahweh — eso es Fase 2.
|
||
- No introduce un trait `MetaBackend` para desacoplar la lógica
|
||
de runtime de Nakui — eso es Fase 3.
|
||
- No renombra el field `nakui_module_dir`. Se hará cuando aparezca
|
||
un segundo backend que también lo necesite.
|
||
|
||
**Pendientes** (orden):
|
||
1. **Fase 2**: extraer widgets render (form/list/modal/EntityRef
|
||
selector + helpers parse_field_value/render_value/etc.) a un
|
||
nuevo crate `yahweh-widget-meta-form` (o nombre similar).
|
||
2. **Fase 3**: trait `MetaBackend` + thin shell — `nakui-ui` queda
|
||
reducido a una impl de backend wireada a `nakui-core`.
|
||
3. **KCL → Nickel**: kcl_wrapper reemplazado por evaluación de
|
||
Nickel contracts.
|
||
4. **card.k eliminado** (REFERENCE ONLY).
|
||
|
||
### feat(nakui-ui): migrar consumer al brazo unificado `brahman_cards::load_cards_from_dir`
|
||
Primera consumer migration del brazo. `nakui-ui` ya no llama a
|
||
`nakui_ui_schema::load_modules_from_dir` directamente — pasa por
|
||
`brahman_cards::load_cards_from_dir` y extrae el variant `UiModule`
|
||
del `CardBody` de cada Card. Beneficios concretos:
|
||
|
||
- **Soporta `.ncl` además de `.json`**: el usuario puede dropear un
|
||
`card.ncl` (con templates Nickel + merge) en cualquier subdir y
|
||
el runtime lo levanta automáticamente. El layout legacy
|
||
`examples/nakui-modules/<id>/module.json` sigue funcionando vía
|
||
los filenames default `[card.ncl, card.json, module.ncl, module.json]`.
|
||
- **Cards de otros body kinds (Ente/Monad) se skipean limpio**:
|
||
si el dir contiene Cards no-UiModule, se reportan en un toast
|
||
informativo en lugar de fallar la carga.
|
||
|
||
Cambios en `brahman-cards`:
|
||
- **Nuevo `load_cards_from_dir(dir)`** + variante con readers/filenames
|
||
custom. Walkea subdirs (orden lexicográfico), busca el primero de
|
||
`DEFAULT_CARD_FILENAMES`, dispatcha al reader. Subdirs sin ningún
|
||
filename matching se skipean silenciosamente (permite assets/fixtures
|
||
sueltos al lado de los cards). Errores per-file se propagan loud
|
||
(sin ocultar corrupción).
|
||
- **`pub const DEFAULT_CARD_FILENAMES`**: lista canónica probada en
|
||
orden. `card.ncl` tiene prioridad sobre `card.json` y sobre los
|
||
legacy `module.*`.
|
||
- **4 tests nuevos del helper**: walk + skip de subdirs sin
|
||
card, prioridad ncl > json, propagación loud de errores per-file,
|
||
custom filenames.
|
||
|
||
Cambios en `nakui-ui`:
|
||
- **Nueva dep** `brahman-cards` en `Cargo.toml`.
|
||
- **Nuevo helper `load_ui_modules(dir) -> (Vec<Module>, Vec<String>)`**
|
||
que envuelve `brahman_cards::load_cards_from_dir`, filtra a
|
||
UiModule body, valida cada Module con su `validate()`, ordena
|
||
por id, y detecta duplicados. El callsite en `MetaUi::new` pasa
|
||
a usarlo y al ver Cards skipped emite un toast informativo.
|
||
- **3 tests nuevos**:
|
||
- `load_ui_modules_via_brahman_cards_returns_ui_modules_and_skips_others`
|
||
— verifica que un dir con UiModule + Ente carga el primero y
|
||
reporta el segundo en `skipped`.
|
||
- `load_ui_modules_via_brahman_cards_rejects_invalid_module` —
|
||
`Module::validate()` se sigue aplicando (menu apuntando a view
|
||
inexistente rebota).
|
||
- `load_ui_modules_detects_duplicate_id` — dos UiModule con
|
||
mismo id rebotan con mensaje claro.
|
||
|
||
Tests totales:
|
||
- `brahman-cards`: 22 → 26 (+4 helper directorio).
|
||
- `nakui-ui`: 45 → 48 (+3 e2e migración).
|
||
- Workspace build verde.
|
||
|
||
Lo que NO cambió:
|
||
- `nakui_ui_schema::load_modules_from_dir` se mantiene intacto (sus
|
||
propios tests lo siguen usando, y otros consumers futuros podrían
|
||
preferir su error-typing más específico). La migración es opt-in:
|
||
`nakui-ui` usa el brazo, ui-schema sigue siendo una API válida.
|
||
- Layout actual de `examples/nakui-modules/<id>/module.json` no
|
||
requiere cambio. Un usuario puede convertir cualquier módulo a
|
||
`card.ncl` sin tocar el dir layout.
|
||
|
||
**Pendientes para próximos commits** (orden):
|
||
1. **Yahweh refactor**: lift del MetaUi runtime a
|
||
`crates/modules/ui_engine/` para reuso. El brazo + canónico ya
|
||
estables, ahora puede extraerse el meta-form widget genérico.
|
||
2. **KCL → Nickel**: kcl_wrapper reemplazado por Nickel contracts;
|
||
los 3 schemas .k de nakui modules pasan a .ncl.
|
||
3. **card.k eliminado** (es REFERENCE ONLY documentado).
|
||
|
||
### feat(brahman-cards): Nickel reader + templates con merge nativo (V2)
|
||
Sigue al V1 (readers JSON). Ahora el brazo acepta inputs `.ncl`:
|
||
los evalúa via `nickel-lang` 2.0, exporta a JSON, y dispatcha por
|
||
los mismos readers JSON estándar. Un `.ncl` puede producir
|
||
cualquier `CardBody` siempre que su shape sea reconocida. Los
|
||
templates funcionan con los `import` + `&` merge nativos de
|
||
Nickel — el brazo no inventa una mecánica paralela.
|
||
|
||
Cambios:
|
||
- **Dep `nickel-lang = "2.0.0"`** (interfaz estable, no
|
||
`nickel-lang-core` que es internal/inestable). Compila clean
|
||
pero suma ~1 min al build cold del crate.
|
||
- **Nuevo módulo `nickel_eval.rs`** con `eval_nickel_file(path) ->
|
||
Result<Value, NickelEvalError>`. Errores tipados:
|
||
`Io`, `Eval`, `Export`, `JsonReparse` — el mensaje de Nickel se
|
||
formatea como texto plano (sin ANSI) para que sea legible en
|
||
logs y toasts.
|
||
- **`load_card_with` añade `"ncl"`**: lee archivo → eval Nickel →
|
||
exporta a JSON → parsea de vuelta a Value → dispatch a los
|
||
readers JSON. Pipeline simétrico a `"json"`.
|
||
- **`CardLoadError::Nickel(NickelEvalError)`**: el error de
|
||
Nickel se propaga limpio al error público del brazo.
|
||
- **Resolución de imports**:
|
||
- El parent dir del input se agrega como import path → `import
|
||
"./template.ncl"` resuelve sin config.
|
||
- El env `BRAHMAN_CARDS_TEMPLATES_DIR` (constante exportada
|
||
`BRAHMAN_CARDS_TEMPLATES_ENV`) agrega un registry global →
|
||
`import "ui_module_minimal.ncl"` desde cualquier ubicación.
|
||
- No hay magic resolución por kind. El autor del Card decide
|
||
qué template importa.
|
||
|
||
**Convención obligatoria de templates** (documentada en
|
||
`nickel_eval.rs`): las fields que el usuario va a sobrescribir
|
||
deben marcarse `| default` (o `| optional`). Sin ese marker
|
||
Nickel rechaza el merge de strings/numbers no-iguales con la
|
||
misma prioridad. Patrón canónico:
|
||
|
||
```nickel
|
||
# template ui_module_basic.ncl
|
||
{
|
||
id | String | default = "TEMPLATE_ID",
|
||
label | String | default = "TEMPLATE_LABEL",
|
||
...
|
||
}
|
||
|
||
# uso concreto
|
||
let base = import "ui_module_basic.ncl" in
|
||
base & { id = "my_id", label = "Mi Label" }
|
||
```
|
||
|
||
9 tests nuevos en `tests/nickel.rs`:
|
||
- `eval_nickel_file_returns_value_for_valid_input` — happy path.
|
||
- `eval_nickel_file_surfaces_evaluation_error` — variant `Eval`
|
||
con path + message.
|
||
- `load_card_dispatches_ncl_to_ui_module_variant` — pipeline
|
||
e2e a UiModule.
|
||
- `load_card_dispatches_ncl_to_ente_variant` — pipeline e2e a
|
||
Ente.
|
||
- `template_merge_overrides_id_and_label_only` — el caso del
|
||
user: template + override de id+label, resto del template
|
||
intacto.
|
||
- `template_resolves_via_env_registry` — uso del env como
|
||
registry global.
|
||
- `load_card_wraps_nickel_error_in_card_load_error` — wrap
|
||
limpio del error.
|
||
- `nickel_contract_violation_caught_at_eval_time` — value-add
|
||
concreto: `id | String = 42` falla en eval, no en deserialize
|
||
ni aguas abajo.
|
||
- `ncl_evaluating_to_unknown_shape_returns_no_matching_reader`
|
||
— sanity de coherencia con dispatcher JSON.
|
||
|
||
22 tests en total en `brahman-cards` (13 JSON V1 + 9 Nickel V2).
|
||
Workspace build verde tras la dep nueva.
|
||
|
||
**Lo que NO hace V2** (sigue pendiente):
|
||
- No migra consumers — `nakui-ui` sigue cargando con
|
||
`nakui_ui_schema::load_modules_from_dir`. La migración a
|
||
`brahman_cards::load_card` queda para después.
|
||
- No define un set canonical de templates en el repo (algo
|
||
como `templates/ente_basic.ncl`, `templates/ui_module_minimal.ncl`).
|
||
Eso emerge cuando aparezcan los primeros casos de uso reales
|
||
donde dos cards comparten estructura.
|
||
- No hace cross-validation entre template + override (ej:
|
||
detectar que un override saca un campo required del template).
|
||
Nickel ya lo hace via contracts si el template tiene un schema.
|
||
- No expone una API streaming (load N cards en paralelo). El
|
||
use case actual es one-shot al boot.
|
||
|
||
**Pendientes para próximos commits** (orden):
|
||
1. Migrar consumers (`nakui-ui` consume `brahman_cards::load_card`).
|
||
2. Yahweh refactor: lift del MetaUi runtime a `crates/modules/ui_engine/`.
|
||
3. KCL → Nickel: kcl_wrapper reemplazado por evaluación de Nickel
|
||
contracts; los 3 schemas .k de nakui modules pasan a .ncl.
|
||
4. card.k eliminado (es REFERENCE ONLY documentado).
|
||
|
||
### feat(brahman-cards): brazo unificado V1 — readers JSON + estructura canónica
|
||
**Pivote arquitectónico** decidido en charla: Brahman maneja varios
|
||
formatos legítimos de "Card" (cada formato vive en su crate origen y
|
||
conserva su shape público), y un **único brazo** los lee, completa
|
||
desde templates si vienen simplificados, y los proyecta a UNA sola
|
||
estructura interna canónica que consumen UI runtime / storage / DHT /
|
||
wire. Agregar un formato nuevo = agregar un reader, sin tocar
|
||
consumers.
|
||
|
||
**V1 en este commit**: estructura canónica + readers para los 3
|
||
formatos JSON existentes en el monorepo. Sin Nickel todavía (aislado
|
||
para próximo commit).
|
||
|
||
Crate nuevo `crates/core/brahman-cards/`:
|
||
- **`Card { id, schema_version, lineage, label, extensions, body }`**:
|
||
wrapper común con identidad legible + extensiones forward-compat.
|
||
`id` como String (no `Ulid`) porque cada body variant usa un tipo
|
||
de id distinto (Ulid para Ente/Monad, slug human-friendly para
|
||
UiModule). PartialEq omitido del derive porque `MonadManifest` y
|
||
`nakui_ui_schema::Module` no lo implementan en sus crates origen.
|
||
- **`CardBody`** enum etiquetado `kind`:
|
||
- `Ente(brahman_card::Card)` — entidad runtime con
|
||
payload/soma/supervision.
|
||
- `Monad(nouser_card::MonadManifest)` — agrupación semántica de
|
||
archivos.
|
||
- `UiModule(nakui_ui_schema::Module)` — descriptor de UI con
|
||
entities/views/menu.
|
||
- Convención: agregar variant nuevo + reader; los consumers que
|
||
sólo manejen algunos hacen `match { Ente(..) => ..., _ => skip }`.
|
||
- **`trait CardReader`**: `name()` + `can_read(&Value) -> bool` +
|
||
`read(Value) -> Result<Card>`. El dispatcher prueba en orden y
|
||
delega al primero que matchee.
|
||
- **3 readers concretos** (en `readers.rs`):
|
||
- `EnteJsonReader` — heurística: `payload` Y `supervision`
|
||
presentes simultáneamente.
|
||
- `MonadJsonReader` — heurística: `members` Y `cardinality`.
|
||
- `UiModuleJsonReader` — heurística: `entities` Y `views` Y
|
||
`menu`. El más específico, va primero en `default_readers()`.
|
||
- **Entry points**:
|
||
- `load_card(path)` — abre archivo, dispatcha por extensión, dentro
|
||
de JSON prueba los readers default.
|
||
- `load_card_with(path, readers)` — variante con set custom para
|
||
apps que quieren restringir formatos.
|
||
- **Errores tipados** vía `CardLoadError`: `Io`, `JsonParse`,
|
||
`NoMatchingReader`, `ReaderFailed { reader, message }`,
|
||
`UnsupportedExtension { ext, supported }`.
|
||
|
||
13 tests integration:
|
||
- 3 detection tests (cada reader matchea sólo su shape, rechaza los
|
||
otros 2 + non-object).
|
||
- 3 dispatch+projection tests (cada formato JSON cargado produce el
|
||
variant esperado con campos del wrapper bien derivados).
|
||
- 2 negative cases (NoMatchingReader, non-object input).
|
||
- 1 sanity de orden (UiModule gana cuando el shape acepta múltiples
|
||
readers — defiende el contrato de orden documentado).
|
||
- 1 e2e desde disco con `load_card_with`.
|
||
- 1 unsupported extension.
|
||
- 1 custom reader set (restringir a sólo Ente).
|
||
- 1 documented invariant (extensions vacío en V1; si cambia, este
|
||
test se rompe como signal).
|
||
|
||
13/13 verdes. Workspace build verde tras agregar el crate al
|
||
`members[]` del workspace Cargo.toml.
|
||
|
||
**Lo que NO hace V1** (explícito):
|
||
- No carga Nickel — próximo commit. La dep `nickel-lang-core` queda
|
||
aislada para no inflar este commit.
|
||
- No define templates — los templates Nickel se diseñan junto al
|
||
reader Nickel (necesitan `merge` nativo de Nickel para fusionar
|
||
override + base).
|
||
- No migra consumers. `nakui-ui` sigue cargando `module.json` con
|
||
`nakui_ui_schema::load_modules_from_dir` directo. La migración a
|
||
`brahman_cards::load_card` viene cuando V1 + Nickel + templates
|
||
estén estables.
|
||
- No mueve los `extensions` del input a `Card.extensions` — los crates
|
||
origen ya tienen sus propios `extensions` internos (`#[serde(flatten)]`).
|
||
Documentado como decisión consciente.
|
||
|
||
**Pendientes para próximos commits** (orden):
|
||
1. Reader Nickel + template merge.
|
||
2. Migrar consumers (`nakui-ui` consume `brahman_cards::load_card`).
|
||
3. Yahweh refactor: lift del MetaUi runtime a `crates/modules/ui_engine/`
|
||
(esperando hasta que el brazo + canónico estén estables).
|
||
4. KCL → Nickel: kcl_wrapper reemplazado por evaluación de Nickel
|
||
contracts; los 3 schemas .k de nakui modules pasan a .ncl.
|
||
5. card.k eliminado (es REFERENCE ONLY documentado).
|
||
|
||
### feat(nakui-ui): validación cross-field del EntityRef (existence en store)
|
||
Cierra otro pendiente. Hasta ahora `parse_field_value(EntityRef, raw)`
|
||
sólo validaba **forma** (UUID parseable + trim de whitespace) — un
|
||
UUID válido pero inexistente en el store pasaba silenciosamente al
|
||
log/store, dejando dangling references. Ahora validamos también
|
||
**existencia** contra la entity declarada en `FieldSpec.ref_entity`.
|
||
|
||
Cambios:
|
||
- **Nuevo helper `validate_entity_refs<S: Store>(store, refs)`**:
|
||
- `refs: &[(label, target_entity, uuid)]`.
|
||
- Loop fail-fast: primer record ausente → error con label
|
||
legible + UUID corto + target entity en el msg
|
||
`"campo 'Stock': record abc12345 de 'Stock' no existe en el store"`.
|
||
- Pure (toma `&S: Store`), totalmente testable sin GPUI.
|
||
- **Wireup en `commit_seed`**:
|
||
- Durante el parse loop, cuando un field es EntityRef + tiene
|
||
`ref_entity` declarado + value parseado a UUID, lo encolamos
|
||
en `entity_refs: Vec<(String, String, Uuid)>`.
|
||
- Después del parse loop (antes del seed/edit branch), si
|
||
`entity_refs` no está vacío, una sola toma del store lock
|
||
para validar todos via el helper.
|
||
- Falla early: ningún log entry, ningún apply.
|
||
- **Cobertura**:
|
||
- SEED path: alta nueva con EntityRef → validamos antes de
|
||
`Seed { data }`.
|
||
- EDIT path: edit con EntityRef → validamos antes de calcular
|
||
delta. Una optional empty (que iría a clear) no cuenta como
|
||
EntityRef (raw vacío skipea el push).
|
||
- Morphism inputs: NO se duplica acá. `Executor::compute` ya
|
||
valida cada input via `store.load(...).ok_or(EntityMissing)`
|
||
antes de correr el script Rhai. Documentado en el doc del
|
||
helper.
|
||
|
||
5 tests nuevos:
|
||
- `validate_entity_refs_passes_when_all_records_exist` — happy path.
|
||
- `validate_entity_refs_fails_on_first_missing` — fail-fast con
|
||
msg que incluye entity + UUID corto.
|
||
- `validate_entity_refs_uses_label_not_entity_in_msg` — el label
|
||
legible (ej: "Stock origen") aparece en el error, no la entity
|
||
desnuda.
|
||
- `validate_entity_refs_empty_list_is_ok` — lista vacía es Ok.
|
||
- `validate_entity_refs_distinguishes_target_from_other_entities` —
|
||
un UUID que existe bajo Customer pero NO Stock falla la
|
||
validación contra Stock.
|
||
|
||
45 tests verdes en nakui-ui (+5). Workspace build verde.
|
||
|
||
Comportamiento esperado:
|
||
- **Selector clickable es happy path**: el dropdown sólo lista
|
||
records existentes, así que clickearlo nunca debería disparar
|
||
el error. Sólo dispara con paste manual de UUID que no existe
|
||
o records borrados después de la selección (timing race).
|
||
- **Optional empty no se valida**: si el field es EntityRef
|
||
optional y el form lo deja vacío, lo manejamos como "no value"
|
||
(skipea el push a `entity_refs`); la lógica de Clear se
|
||
encarga del resto.
|
||
- **Lock contention**: una sola toma del store lock por
|
||
`commit_seed`, no una por field. La validación es O(refs) reads.
|
||
|
||
Pendientes restantes:
|
||
- **Validación KCL del record post-edit** antes de emitir Set/Clear
|
||
(hoy `ui.edit_record` no pasa por `Executor::compute`).
|
||
- **EntityRef cross-module** (referenciar records de OTRO módulo
|
||
por nombre, no sólo por entity local).
|
||
|
||
### feat(nakui-core,nakui-ui): FieldOp::Clear — borrar values vía form vacío
|
||
Cierra el último pendiente de UX del round. El edit no podía
|
||
"borrar" un value vaciando el input — empty optional fields
|
||
hacían `continue` en `commit_seed`, así que el current value
|
||
quedaba intacto. Para honrar el intent del usuario ("este field
|
||
ya no aplica") necesitábamos un FieldOp explícito que remueva
|
||
la key del map.
|
||
|
||
Cambios en **nakui-core** (la variante es semántica del kernel,
|
||
no específica de la UI):
|
||
|
||
- **`delta::FieldOp::Clear { path }`** — nueva variante.
|
||
Distinta de `Set { value: Null }`: Clear borra la clave; Set
|
||
Null deja la clave con valor literal `null`. Importa para
|
||
downstream que diferencia "ausente" vs "presente como null"
|
||
(ej: serde con `skip_serializing_if = "Option::is_none"`).
|
||
- **`capability_token`** — Clear devuelve `entity.field`,
|
||
mismo shape que Set. Una capability `writes: ["Customer.notes"]`
|
||
autoriza tanto Set como Clear sobre ese field.
|
||
- **`simulate_on`** — Clear remueve la key del Object si el
|
||
state es Some(Object). Skip silente si el state es None
|
||
(deleted) o no-objeto.
|
||
- **`MemoryStore::apply_dry_run`** — Set y Clear comparten
|
||
pre-condición (record padre existe + es objeto). Pattern
|
||
combinado con `|`.
|
||
- **`MemoryStore::apply`** — Clear hace `map.remove(field)`.
|
||
Field ausente = no-op silencioso (post-state idéntico).
|
||
- **`SurrealStore::apply_dry_run`** — Set/Clear combinados.
|
||
- **`SurrealStore::apply`** — Clear emite
|
||
`UPDATE type::thing UNSET <field>`. El field name viene de
|
||
un FieldSpec validado upstream; SurrealQL no soporta binding
|
||
de identifiers, así que va inline (con la advertencia
|
||
documentada en el comment).
|
||
- **`Executor` capability check** — Set/Clear comparten match
|
||
(mismo token shape, misma resolución a binding role).
|
||
- **Conservation rules** (en `check_conservation`) NO consideran
|
||
Clear — sólo Set. Documentado: morphism authors que querían
|
||
clear de un field con conservation tienen que ser cuidadosos;
|
||
KCL post-checks pueden capturar violations.
|
||
|
||
Cambios en **nakui-ui**:
|
||
|
||
- **`commit_seed` loop** acumula `to_clear: Vec<String>` con
|
||
los nombres de fields optional empty (en lugar de hacer
|
||
`continue` silencioso).
|
||
- **EDIT branch**:
|
||
- Computa `set_delta` (igual que antes) + `clear_fields` via
|
||
nuevo helper `compute_clear_fields(current, to_clear)`.
|
||
- Helper filtra a sólo los fields que actualmente tienen
|
||
valor non-null — Clear de un field ausente o ya null no
|
||
se emite (sería no-op semántico). Preserva el orden del
|
||
input para estabilidad del log entry.
|
||
- Construye `ops` combinando Set + Clear.
|
||
- NoChange ahora requiere AMBOS vacíos (set_delta y
|
||
clear_fields).
|
||
- `params` del log entry incluye `cleared: ["field1", ...]`
|
||
sólo si non-empty (preserva la shape `fields:` para
|
||
edits sin clears).
|
||
- `CommitOutcome::Updated.changed = sets + clears` para
|
||
que el toast `"actualizado X (N campo(s))"` siga siendo
|
||
preciso.
|
||
|
||
Tests nuevos:
|
||
- **delta.rs**: `simulate_clear_removes_field`,
|
||
`simulate_clear_then_set_same_field_keeps_set`,
|
||
`clear_capability_token_matches_set_shape`.
|
||
- **store.rs**: `apply_clear_removes_field_key`,
|
||
`apply_clear_on_absent_field_is_noop`,
|
||
`dry_run_rejects_clear_on_missing_record`,
|
||
`dry_run_rejects_clear_on_non_object`.
|
||
- **nakui-ui main.rs**: `clear_fields_skips_absent_and_null`,
|
||
`clear_fields_preserves_input_order`,
|
||
`clear_fields_empty_when_current_is_null`.
|
||
|
||
34 tests verdes en nakui-core (+7), 40 en nakui-ui (+3).
|
||
Workspace build verde. E2E del morphism real
|
||
`morphism_pipeline_executes_real_sales_vender` intacto — `vender`
|
||
no usa Clear.
|
||
|
||
Implicaciones:
|
||
- **El log puede crecer con entries `ui.edit_record` que sólo
|
||
tienen `cleared: [...]`** sin `fields`. Esperado y esperable.
|
||
- **Replay**: las entries con Clear se aplican en orden via
|
||
`store.apply(&ops)`. La semantic es deterministic.
|
||
- **Si un módulo tiene KCL invariants sobre la presencia de un
|
||
field**, el usuario podría romper el record vaciando ese
|
||
field via UI. Hoy esto NO se chequea — `ui.edit_record` es
|
||
un morphism manual que no pasa por `Executor::compute`. Si
|
||
esto es un problema, el camino futuro es validar contra el
|
||
KCL del entity al submit (otro pendiente).
|
||
|
||
Pendientes restantes:
|
||
- **Validación cross-field** (ej: UUID del EntityRef existe en
|
||
la entity referida).
|
||
- **Validación KCL del record post-edit** antes de emitir Set/Clear.
|
||
|
||
### feat(nakui-ui): snapshot/compaction durante runtime cada N writes
|
||
Cierra el último pending del round de persistencia. Antes el compact
|
||
sólo corría al startup — para una sesión larga con muchas escrituras,
|
||
el log crecía sin tope hasta el próximo restart, y el siguiente boot
|
||
pagaba el costo lineal del replay.
|
||
|
||
Cambios:
|
||
- **Nuevos fields en `MetaUi`**:
|
||
- `snap_path: PathBuf` — cacheado del init para que el tick no
|
||
tenga que recomputarlo.
|
||
- `snapshot_threshold: usize` — leído del env en `new()` y
|
||
cacheado. `0` desactiva runtime compact (mismo env y
|
||
semantic que el threshold de startup).
|
||
- `writes_since_compact: u64` — contador que incrementa por cada
|
||
write efectivo y se resetea cuando el threshold dispara
|
||
`maybe_compact_log`.
|
||
- **Nuevo método `tick_runtime_compact()`**:
|
||
- Early return si `threshold == 0`.
|
||
- Increment + check vs threshold.
|
||
- Si cruza: lock log + store, llama `maybe_compact_log`.
|
||
- **Si compactó OK**: counter = 0, devuelve msg.
|
||
- **Si `maybe_compact_log` returned None** (counter dijo "go"
|
||
pero entries < 2): counter = 0 (no re-entrar cada write).
|
||
- **Si error**: counter NO se resetea (próximo write reintenta),
|
||
devuelve el error.
|
||
- **Nuevo helper `append_compact_msg(base, opt)`**: concatena el
|
||
msg del compact al toast del op original con `";"` separator.
|
||
- **Wireup en 3 callsites de write efectivo**:
|
||
- `apply_action::SeedEntity`: tick si outcome != NoChange.
|
||
- `apply_action::Morphism`: tick siempre que Ok.
|
||
- Click handler `[Confirmar]` del delete modal: tick si commit_delete Ok.
|
||
- **NoChange no cuenta**: un edit que no cambia nada no escribe al
|
||
log, así que tampoco debería avanzar el counter — preserva la
|
||
semantic "1 write = 1 log entry = 1 tick".
|
||
|
||
2 tests nuevos:
|
||
- `append_compact_msg_handles_both_branches` — base solo vs base
|
||
+ compact, formato del separator.
|
||
- `runtime_compact_cycle_resets_counter_after_threshold` — E2E
|
||
estilo simulación: 7 writes con threshold=3 → 2 compacts (en
|
||
write 3 y 6), counter residual = 1, log final con 2 entries
|
||
(1 anchor + 1 write residual). Reproduce el algoritmo del tick
|
||
sin GPUI cx; si la lógica del método cambia, se rompe como signal.
|
||
|
||
37 tests verdes (+2). Workspace build verde.
|
||
|
||
Trade-offs:
|
||
- **Counter en memoria, no persistido**: si la app crashea entre
|
||
compacts, al próximo boot el counter parte de 0. El startup
|
||
compact (basado en entry_count del log file) compensa esto:
|
||
si quedó mucho post-último-compact, se compacta al boot.
|
||
- **Lock orden**: tick toma log lock primero, store lock después.
|
||
Misma orden que `commit_seed` y `commit_morphism`, no debería
|
||
haber deadlock.
|
||
- **Costo del tick**: 1 increment + 1 compare por write. Cuando
|
||
cruza threshold, 1 read del log (entries) + 1 snapshot write +
|
||
1 compact. Para threshold=50 es ~1 fsync cada 50 writes —
|
||
amortiza bien.
|
||
|
||
Pendientes restantes:
|
||
- **`FieldOp::Clear`** — para soportar borrar un value vía form vacío.
|
||
- **Validación cross-field** (UUID del EntityRef existe en la
|
||
entity referida).
|
||
|
||
### feat(nakui-ui): atajo Esc para cancelar el modal de delete
|
||
Cierra otro pendiente de UX. El banner de confirmación de delete
|
||
ya tenía botones [Cancelar] / [Confirmar], pero la acción más
|
||
natural para cancelar un dialog es Esc — y no la teníamos wireada.
|
||
|
||
Cambios:
|
||
- **`capture_key_down` en el root div** del `MetaUi::render`. Capture
|
||
phase (no bubble) para interceptar el Esc *antes* que cualquier
|
||
TextInput descendiente lo consuma. Sin pending el handler es
|
||
no-op y el evento sigue su flujo normal.
|
||
- **Match `event.keystroke.key == "escape"`** + `pending_delete.take()`
|
||
→ toast `"delete cancelado (Entity) [esc]"` (sufijo `[esc]` para
|
||
diferenciar visualmente del botón). Si no hay pending, return
|
||
temprano sin tocar nada.
|
||
- **Hint visual en el banner**: subtítulo en amber tenue debajo del
|
||
título: `"Esc para cancelar · click [Confirmar] para borrar"`.
|
||
Que el usuario descubra el atajo sin RTFM.
|
||
|
||
35 tests verdes — el handler de Esc es 8 líneas no-testeables sin
|
||
GPUI cx (la lógica de pending_delete + toast vive dentro del
|
||
listener); el wireup compila por type-check.
|
||
|
||
Pendientes restantes:
|
||
- **`FieldOp::Clear`** — para soportar borrar un value vía form vacío.
|
||
- **Snapshot durante runtime** (cada N writes, no sólo al startup).
|
||
- **Validación cross-field** (UUID del EntityRef existe en la
|
||
entity referida).
|
||
|
||
### feat(nakui-ui): EntityRef validation en parse_field_value (UUID al submit)
|
||
Cierra otro pendiente: `parse_field_value(FieldKind::EntityRef, raw)`
|
||
devolvía `Ok(json!(raw))` blindly — el value entraba al log/store
|
||
incluso si era basura. La validación de UUID sólo ocurría cuando el
|
||
field se usaba como **input** del morphism (línea ~540 de
|
||
`commit_morphism`); como **seed field** o como **param**, garbage
|
||
pasaba silenciosamente.
|
||
|
||
Cambios:
|
||
- **`parse_field_value(EntityRef, raw)`** ahora hace
|
||
`Uuid::parse_str(raw.trim())` y devuelve error claro si falla:
|
||
`"'<raw>' no es UUID válido (usá el selector de records)"`. En
|
||
caso de éxito, devuelve el UUID **trimmed** como string —
|
||
protege contra paste manual con whitespace.
|
||
- **Doble cobertura**: este path cubre seed fields (commit_seed via
|
||
obj.insert) y morphism params (resolve_param_value lo invoca por
|
||
cada FieldSpec con kind=EntityRef). El path de morphism inputs
|
||
ya validaba antes con `Uuid::parse_str` directo — sigue intacto,
|
||
no hay double-validation.
|
||
- **Selector clickable es happy path**: el dropdown setea valores
|
||
bien-formados, así que el usuario nunca debería ver el error en
|
||
uso normal. Sólo dispara con paste manual o si el usuario escribe
|
||
garbage en el input — defensivo.
|
||
|
||
5 tests nuevos (reemplazan al obsoleto `parse_field_entity_ref_returns_string`):
|
||
- `parse_field_entity_ref_accepts_valid_uuid` — happy path.
|
||
- `parse_field_entity_ref_trims_whitespace` — `" uuid\n"` → `"uuid"`.
|
||
- `parse_field_entity_ref_rejects_non_uuid` — `"abc-123"` → error
|
||
con el value y la palabra "UUID" en el mensaje.
|
||
- `parse_field_entity_ref_rejects_empty_string` — `""` → rebota.
|
||
- `resolve_param_strict_entity_ref_propagates_error` — sanity de
|
||
que el wireup en resolve_param_value hereda el strict checking,
|
||
con label del FieldSpec en el mensaje.
|
||
|
||
35 tests verdes (+4 net). El E2E del morphism real
|
||
`morphism_pipeline_executes_real_sales_vender` sigue verde — sus
|
||
inputs van por el path dedicado, no por parse_field_value.
|
||
|
||
Pendientes restantes:
|
||
- **Atajo Esc para Cancelar** del modal de delete.
|
||
- **`FieldOp::Clear`** — para soportar borrar un value vía form.
|
||
- **Snapshot durante runtime** (cada N writes, no sólo al startup).
|
||
- **Validación cross-field** (ej: el UUID del EntityRef existe en
|
||
la entity referida) — hoy sólo validamos forma; un UUID válido
|
||
pero inexistente sí pasa.
|
||
|
||
### feat(nakui-ui): snapshot/compaction automático del event log al startup
|
||
Cierra el último gran pendiente del round: el replay full cada
|
||
startup escala lineal en el log. Con 60+ entries el costo de boot
|
||
se nota; con 10k entries es prohibitivo. Wireamos el snapshot
|
||
machinery que ya estaba en `nakui-core` (`Snapshot`,
|
||
`replay_with_snapshot_into`, `EventLog::compact_through`) al
|
||
runtime de la UI.
|
||
|
||
Cambios:
|
||
- **Path del snapshot**: sibling del log, extensión `.snap.json`.
|
||
`nakui-ui-state.jsonl` ↔ `nakui-ui-state.snap.json`.
|
||
- **Nuevo helper `snapshot_path_for(log_path)`** — derivación
|
||
pura, testeable.
|
||
- **Nuevo helper `maybe_compact_log(log, snap_path, store, threshold)`**:
|
||
- Si `entry_count >= threshold` y `>= 2`, captura
|
||
`Snapshot::from_memory_store(store, next_seq - 1)`, lo escribe
|
||
atómicamente, y compacta el log dejando la última entry como
|
||
anchor.
|
||
- Anchor invariant: `EventLog::open` deriva `next_seq` del primer
|
||
entry del archivo. Si compactáramos *todo* el log file, al
|
||
reabrir el cursor volvería a 0 y el próximo append crashearía
|
||
con `NonMonotonic`. Por eso compactamos sólo hasta
|
||
`next_seq - 2` — la entry del `snap.seq` queda como anchor del
|
||
cursor; `replay_with_snapshot_into` la skipea porque snap ya
|
||
cubre hasta ese seq inclusive.
|
||
- Threshold via env `NAKUI_SNAPSHOT_THRESHOLD`, default 50.
|
||
`0` desactiva por completo.
|
||
- Devuelve `Result<Option<msg>, String>`: `Ok(Some)` si compactó,
|
||
`Ok(None)` si no había payoff, `Err` si snap o compact fallaron.
|
||
- **`MetaUi::new` reescrito**:
|
||
- Carga snapshot al inicio (Some/None según exista).
|
||
- `replay_with_snapshot_into(&log, snapshot.as_ref(), &mut store)`
|
||
en lugar de `replay_into`.
|
||
- Después del replay corre `maybe_compact_log` con el threshold.
|
||
- Toast inicial menciona snapshot loaded si aplica
|
||
("snapshot @ seq K") y la compactación si ocurrió.
|
||
- Errores de snapshot load **no son fatales**: cae a full replay
|
||
con un msg en el banner.
|
||
- Errores de auto-compact **no son fatales**: el log + snap
|
||
quedan como estaban, msg al banner.
|
||
|
||
5 tests nuevos:
|
||
- `snapshot_path_for_replaces_extension` — `.jsonl` → `.snap.json`,
|
||
edge case sin extensión.
|
||
- `maybe_compact_log_below_threshold_noops` — 5 entries vs threshold
|
||
50: no toca nada, no escribe snap.
|
||
- `maybe_compact_log_threshold_zero_noops` — threshold 0 = disabled.
|
||
- `maybe_compact_log_then_reopen_preserves_records` — E2E:
|
||
- Escribe 60 seeds (log + store en sync).
|
||
- Compacta (60 >= 50): snap escrito, log queda con 1 anchor entry,
|
||
msg reporta "59 entries dropped (1 anchor kept)".
|
||
- Reopen: `next_seq=60` se preserva via anchor, `entries.len()=1`.
|
||
- Replay con snap loadado en store fresco: los 60 records están.
|
||
- Segunda corrida del compact con threshold=1: no-op (idempotente).
|
||
|
||
31 tests verdes (+5). Workspace build verde tras la nueva firma.
|
||
|
||
Trade-offs y notas:
|
||
- **Fail-soft**: cualquier error de snap/compact no rompe el boot;
|
||
la UI sigue funcionando con full replay y el toast lo reporta.
|
||
Sólo `EventLog::open` failing es no-recoverable (pierde
|
||
persistencia).
|
||
- **Crash-safety**: WAL order preservado — escribimos snap (atómico
|
||
via tempfile + fsync + rename) ANTES de compactar el log
|
||
(atómico igual). Si crasheamos entre los dos, próximo boot ve
|
||
snap@K + log con todas las entries 0..N — replay skippea las que
|
||
snap cubre, outcome idéntico.
|
||
- **Sólo en startup**: no hay snapshot durante runtime. Para sesiones
|
||
largas con muchas escrituras, el log puede crecer arbitrariamente
|
||
hasta el próximo restart. Pendiente futuro: snapshot N writes
|
||
desde el último compact.
|
||
- **Anchor entry sobrevive sin uso útil**: el costo es 1 línea JSON
|
||
por compact. No es preocupación a menos que el threshold sea
|
||
muy chico (cada compact deja 1 línea de basura).
|
||
|
||
Pendientes restantes:
|
||
- **EntityRef validation post-submit** — validar UUID parseable al
|
||
submit en lugar de al execute del morphism.
|
||
- **Atajo Esc para Cancelar** del modal de delete.
|
||
- **`FieldOp::Clear`** — para soportar borrar un value vía form vacío.
|
||
- **Snapshot durante runtime** (cada N writes, no sólo al startup).
|
||
|
||
### feat(nakui-ui): edit delta-only — sólo campos modificados al log/store
|
||
Antes de este cambio, editar un record emitía un `FieldOp::Set` por
|
||
**cada field del form**, incluso los no tocados. Eso bloata el log
|
||
(replay tenía que aplicar N ops cuando 1 alcanzaba) y oscurece el
|
||
intent en una auditoría posterior. Con delta-only, el edit emite
|
||
sólo los Sets cuyo value nuevo difiere del actual; un edit que no
|
||
cambia nada deja el log intacto.
|
||
|
||
Cambios:
|
||
- **Nuevo helper `compute_field_delta(current, proposed)`** — toma
|
||
el record actual del store (un `Value`, posible `Null` si el
|
||
record no existe) y el `Map` propuesto desde el form, y devuelve
|
||
sólo las entries que difieren. Comparación: `PartialEq` estructural
|
||
de `serde_json::Value` (un `Null` en current = todos los proposed
|
||
son nuevos).
|
||
- **Nuevo enum `CommitOutcome`**:
|
||
- `Created(Uuid)` — alta nueva.
|
||
- `Updated { id, changed }` — edit con N campos modificados.
|
||
- `NoChange(Uuid)` — edit sin diferencias (el toast lo refleja
|
||
como "X sin cambios — no log entry").
|
||
- **`commit_seed` en path EDIT**:
|
||
- Carga current via `store.load(entity, id)` con fallback a
|
||
`Value::Null`.
|
||
- Calcula delta. Si vacío → return early sin tocar log ni store.
|
||
- Si no vacío → emite `Morphism { ui.edit_record, ops: [Set...] }`
|
||
con `params.fields` reflejando el delta (no todo el form),
|
||
haciendo la auditoría grep-able por field cambiado.
|
||
- **Toast del callsite**:
|
||
- `creado X uuid` (Created)
|
||
- `actualizado X uuid (N campo(s))` (Updated)
|
||
- `X uuid sin cambios — no log entry` (NoChange)
|
||
- **`editing` se limpia incluso en NoChange** — el modo edit cierra,
|
||
el form vuelve al state limpio.
|
||
|
||
5 tests nuevos del helper:
|
||
- delta vacío cuando todo coincide.
|
||
- delta sólo con el field cambiado.
|
||
- delta full cuando current = Null (record no existe).
|
||
- distingue int 100 de string "100".
|
||
- ignora fields del current que no están en proposed.
|
||
|
||
27 tests verdes (+5). El path SEED no cambió; el E2E del morphism
|
||
real sigue verde.
|
||
|
||
Limitación conocida (consistente con pre-delta): el form no puede
|
||
**borrar** un value vaciando el input — empty optional fields hacen
|
||
`continue` antes de llegar al delta. Para clearear un value hay que
|
||
declarar el field como required, o esperar a un `FieldOp::Clear`
|
||
futuro (no necesario hoy: ningún demo lo requiere).
|
||
|
||
Pendientes restantes:
|
||
- **Snapshot/compaction** del log (replay full cada startup escala
|
||
mal con repos grandes).
|
||
- **EntityRef validation post-submit** — validar UUID parseable al
|
||
submit en lugar de al execute del morphism.
|
||
- **Atajo Esc para Cancelar** del modal de delete.
|
||
- **`FieldOp::Clear`** — para soportar borrar un value vía form.
|
||
|
||
### feat(nakui-ui): confirmación de delete vía banner modal antes de borrar
|
||
Cierra el primer pending del último round: borrar un record pedía un
|
||
solo click en `✕` y se ejecutaba inmediatamente (irreversible —
|
||
queda en el log como `Morphism { name: "ui.delete_record" }`). Ahora
|
||
hay un paso intermedio: el click marca el record como pendiente y el
|
||
banner amber al tope de la ventana ofrece [Cancelar] o [Confirmar].
|
||
|
||
Cambios:
|
||
- **Nuevo state `MetaUi.pending_delete: Option<(String, Uuid)>`**.
|
||
Set en el click del `✕`; limpiado por:
|
||
- [Cancelar] → toast "delete cancelado (Entity)".
|
||
- [Confirmar] → llama `commit_delete` (igual que antes) y emite
|
||
el toast usual.
|
||
- Navegación a otra view (`select_view`) — el record marcado
|
||
podría no estar visible en la nueva pantalla.
|
||
- **Click handler de `✕` ya no llama `commit_delete`**: sólo setea
|
||
`pending_delete` y limpia toast. La acción destructiva ahora vive
|
||
exclusivamente en el botón [Confirmar] del banner.
|
||
- **Nuevo método `render_confirm_delete_banner`**: devuelve
|
||
`Option<Div>` (None si no hay pending). Banner amber con el texto
|
||
`¿Borrar {Entity} {short_uuid}?` + dos botones. Renderea como
|
||
sibling del row sidebar+main en `flex_col` raíz — no es overlay
|
||
flotante (GPUI no expone z-index trivialmente), pero la posición
|
||
fija al tope + color amber lo hacen imposible de ignorar.
|
||
- **Limpieza pre-commit**: `pending_delete = None` se ejecuta antes
|
||
de `commit_delete`, así un fallo del commit no deja el banner
|
||
colgado además del toast de error.
|
||
|
||
22 tests verdes — la lógica del store/log no cambió, sólo el state
|
||
machine de UI. La confirmación es puramente UX/state, no testable
|
||
sin GPUI cx, pero la compilación garantiza wireup correcto de las
|
||
closures.
|
||
|
||
Pendientes restantes:
|
||
- **Snapshot/compaction** del log para repos grandes (replay full
|
||
cada startup escala mal).
|
||
- **Edit delta-only** — sólo campos modificados, no todos.
|
||
- **EntityRef validation post-submit** — validar UUID parseable
|
||
al submit en lugar de al execute del morphism.
|
||
- **Atajo de teclado Esc para Cancelar** — requiere event
|
||
dispatcher de GPUI, fuera de scope inmediato.
|
||
|
||
### feat(nakui-ui): validación estricta de params del morphism vía FieldKind del FieldSpec
|
||
Cierra el último trade-off documentado: `infer_param_value` adivinaba
|
||
el tipo de cada param por la shape del string (i64 → f64 → bool →
|
||
string). Ahora cuando hay `FieldSpec` declarado, usamos
|
||
`parse_field_value(spec.kind, raw)` — un Boolean field con value
|
||
"abc" rebota con mensaje claro en la UI antes de llegar al morphism
|
||
Rhai (donde el error sería opaco como "Function not found: * ((), ())").
|
||
|
||
Cambios:
|
||
- **Nuevo helper `resolve_param_value(field_name, raw, spec)`**:
|
||
- Si hay `FieldSpec`: validación de `required` (rebota empty con
|
||
"param 'X' es obligatorio y está vacío") + parseo estricto via
|
||
`parse_field_value(spec.kind, raw)`. Errores incluyen el `label`
|
||
del spec para que el toast sea interpretable.
|
||
- Si NO hay spec (param declarado en `Action::Morphism.params`
|
||
que no existe en `form.fields` — módulo mal-formado): fallback
|
||
a `infer_param_value` como red de seguridad.
|
||
- Empty + opcional → `Value::Null`.
|
||
- **`commit_morphism` simplificado**: el loop de params ahora es
|
||
3 líneas (lookup spec + llamada a `resolve_param_value` +
|
||
inserción al map). La lógica vive en el helper standalone,
|
||
testable sin GPUI.
|
||
|
||
Tests: 6 nuevos en `tests` mod, todos contra `resolve_param_value`:
|
||
- `resolve_param_strict_number_parses_i64` — happy path.
|
||
- `resolve_param_strict_boolean_rejects_non_boolean` — un Boolean
|
||
con "abc" rebota con mensaje que incluye el label.
|
||
- `resolve_param_strict_number_rejects_garbage` — Number con "abc"
|
||
rebota.
|
||
- `resolve_param_required_empty_rejected` — required vacío rebota
|
||
con "obligatorio".
|
||
- `resolve_param_optional_empty_returns_null` — optional vacío
|
||
→ null.
|
||
- `resolve_param_no_spec_falls_back_to_infer` — el fallback
|
||
preserva el comportamiento anterior para back-compat.
|
||
|
||
22 tests verdes en nakui-ui (+6). E2E del morphism real
|
||
(`morphism_pipeline_executes_real_sales_vender`) sigue verde — la
|
||
validación estricta no rompe el path correcto, sólo agrega rebotes
|
||
tempranos a values mal-tipados.
|
||
|
||
Beneficio operativo:
|
||
- Mensaje de error en la UI ahora identifica el field problemático
|
||
por su label legible ("param 'Cantidad': 'abc' no es número")
|
||
en lugar del error opaco del morphism Rhai.
|
||
- Errores se ven antes de tocar el log o el store — ningún cambio
|
||
parcial.
|
||
- El módulo Nakui ya no tiene que defender contra inputs garbage
|
||
desde la UI: la metainterfaz se vuelve la primera línea de
|
||
validación tipada.
|
||
|
||
Pendientes futuros (orden de prioridad):
|
||
- **Confirmación de delete** — modal antes de borrar.
|
||
- **Snapshot/compaction** del log para repos grandes.
|
||
- **Edit delta-only** — sólo campos modificados, no todos.
|
||
- **EntityRef validation post-submit**: hoy `parse_field_value`
|
||
para EntityRef devuelve string raw; el commit_morphism luego
|
||
valida como Uuid sólo cuando es input del morphism. Para
|
||
EntityRef como param, podríamos validar UUID al submit.
|
||
|
||
### feat(nakui-ui): FieldKind::EntityRef — selector clickable de records existentes
|
||
Cierra el principal trade-off documentado del commit anterior:
|
||
"Inputs UUID a mano (no dropdown)". Los formularios pueden declarar
|
||
un campo `entity_ref` que apunta a una entity y el runtime renderea
|
||
una lista clickable de records existentes; click selecciona, el
|
||
UUID queda guardado para el submit.
|
||
|
||
Schema `nakui-ui-schema`:
|
||
- **Nueva variante `FieldKind::EntityRef`** (serializa como
|
||
`"entity_ref"` en JSON).
|
||
- **`FieldSpec.ref_entity: Option<String>`** nuevo. Indica qué
|
||
entity ofrecer en el selector. `validate()` chequea que cualquier
|
||
field con `kind=entity_ref` tenga `ref_entity` set.
|
||
- Nuevo error tipado `SchemaError::EntityRefMissingTarget`.
|
||
|
||
Runtime `nakui-ui`:
|
||
- **`render_entity_ref_selector(field_name, target_entity, ...)`** —
|
||
helper que arma la lista debajo del input. Cada item:
|
||
- Etiqueta humana via `human_label_for_record` (heurística:
|
||
`name` → `label` → `title` → `sku` → `sku_id` → fallback al
|
||
UUID corto).
|
||
- Click handler vía `cx.listener` que llama
|
||
`input.set_text(uuid_completo)` — el TextInput interno queda
|
||
como source-of-truth, así que `commit_seed` y `commit_morphism`
|
||
leen el UUID seleccionado sin saber que vino de un selector.
|
||
- Highlight en accent color cuando el item es el actualmente
|
||
seleccionado (compara contra el contenido del TextInput).
|
||
- **`parse_field_value(EntityRef, raw)`** devuelve string del raw
|
||
(la validación como Uuid ocurre downstream en `commit_morphism`).
|
||
- Mensaje "(sin {entity}: creá uno antes para referenciar)" cuando
|
||
la lista está vacía — el user sabe qué hacer en lugar de quedarse
|
||
trabado.
|
||
|
||
Demo actualizado: `examples/nakui-modules/sales_engine/module.json`:
|
||
- `vender_form.fields.stock_id_input` y `caja_id_input` cambian de
|
||
`kind: "text"` a `kind: "entity_ref"` con `ref_entity: "Stock"`
|
||
y `"Caja"` respectivamente.
|
||
- Ahora el flujo "Vender" es: (1) click en una Stock listada bajo
|
||
el input, (2) click en una Caja, (3) escribir venta_id/cantidad/
|
||
precio_unitario/timestamp, (4) submit. Sin copiar UUIDs.
|
||
|
||
Tests:
|
||
- 2 nuevos en schema: `validate_catches_entity_ref_without_target`
|
||
y `entity_ref_with_target_validates_clean`. 8 totales.
|
||
- 4 nuevos en runtime: `parse_field_entity_ref_returns_string`,
|
||
`human_label_for_record_prefers_name_over_id`,
|
||
`human_label_falls_back_through_label_title_sku`,
|
||
`human_label_falls_back_to_id_when_no_known_keys`. 16 totales.
|
||
- Integration de los 7 demos sigue verde — el demo `sales_engine`
|
||
ahora valida con EntityRef + ref_entity correctamente set.
|
||
|
||
29 tests totales nakui-ui + schema, 100% verde. El demo
|
||
`sales_engine` carga limpio con la nueva forma del schema.
|
||
|
||
Pendientes futuros:
|
||
- **Confirmación de delete** — modal antes de borrar.
|
||
- **Snapshot/compaction** del log para repos grandes.
|
||
- **Edit delta-only** (sólo campos modificados).
|
||
- **Validación de tipos en params del morphism**: `FieldKind`
|
||
declarado en el FieldSpec se podría usar para forzar parseo
|
||
estricto en `commit_morphism` en lugar de la heurística
|
||
`infer_param_value`.
|
||
|
||
### feat(nakui-ui): Action::Morphism wired al pipeline real (compute → log → apply)
|
||
Cierra el último gran TODO de la metainterfaz Nakui: las acciones
|
||
`Action::Morphism` ya no son un toast informativo; despachan al
|
||
`Executor` cargado del manifest nakui-core (`nsmc.json` + schemas
|
||
KCL + scripts Rhai), pasando por el pipeline completo de Nakui:
|
||
compute (con dry-run + KCL post-checks) → log append → store apply.
|
||
|
||
Schema `nakui-ui-schema` extendido:
|
||
- **`Module.nakui_module_dir: Option<String>`** nuevo. Path
|
||
(relativo al directorio del `module.json` o absoluto) a un módulo
|
||
nakui-core. Sin esto, las Action::Morphism del módulo quedan
|
||
no-op con toast informativo. Las Action::SeedEntity siguen
|
||
funcionando sin manifest (alta administrativa).
|
||
- **`Action::Morphism`** ganó dos campos opcionales:
|
||
- `inputs: BTreeMap<String, String>` — mapeo `role → field_name`.
|
||
Por cada input declarado en el `MorphismSpec.inputs`, indica
|
||
qué field del form contiene el UUID del record. El runtime
|
||
parsea como `Uuid` y lo pasa al `execute_and_log`.
|
||
- `params: Vec<String>` — lista de fields cuyos values van al
|
||
`params` JSON. Si vacío, todos los fields no-input van a params.
|
||
|
||
Runtime `nakui-ui`:
|
||
- **`MetaUi.executors: BTreeMap<String, Arc<Executor>>`** nuevo.
|
||
Carga `Executor::load_module(nakui_module_dir)` en `MetaUi::new`
|
||
por cada módulo UI que declare la entry. Errores de carga van al
|
||
banner; el módulo sigue cargado para SeedEntity, sólo Morphism
|
||
queda no-op.
|
||
- **`commit_morphism(mod_idx, name, inputs_map, params_fields)`** nuevo.
|
||
Resuelve inputs (parsea cada field como Uuid), arma params (Value
|
||
object con tipos inferidos via `infer_param_value` — int/float/
|
||
bool/string), llama `execute_and_log_with_recovery`. Toast con
|
||
cantidad de ops aplicadas o el error tipado.
|
||
- **`infer_param_value`** nuevo helper: heurística simple para
|
||
pasar values del form al morphism con tipo inferido (i64 → f64 →
|
||
bool → string).
|
||
|
||
Tests: 2 nuevos:
|
||
- `infer_param_value_int_then_float_then_bool_then_string` —
|
||
cobertura de la heurística.
|
||
- **E2E `morphism_pipeline_executes_real_sales_vender`** —
|
||
carga el módulo real `crates/modules/nakui/modules/sales`,
|
||
arma store + log, ejecuta el morphism `vender` con inputs
|
||
Stock+Caja y params (cantidad=5, precio_unitario=200,
|
||
venta_id, timestamp). Asserta:
|
||
- el morphism produce ops (no vacío).
|
||
- stock.cantidad bajó 100 → 95.
|
||
- caja.saldo subió 1_000_000 → 1_001_000.
|
||
|
||
12 tests verdes en nakui-ui (+1 vs commit anterior). Schema
|
||
extension no rompió nada (6 unit + 5 integration siguen verdes).
|
||
|
||
Demo nuevo: **`examples/nakui-modules/sales_engine/module.json`**
|
||
- Apunta a `crates/modules/nakui/modules/sales` vía `nakui_module_dir`.
|
||
- 6 vistas: list + form para cada Stock, Caja, Venta + form
|
||
"Vender" con `Action::Morphism { name: "vender", inputs: {stock,
|
||
caja}, params: [venta_id, cantidad, precio_unitario, timestamp] }`.
|
||
- El user crea Stocks + Cajas con seed_entity, copia los UUIDs
|
||
cortos a los inputs de "Vender", y ejecuta el morphism real:
|
||
stock baja, caja sube, Venta se persiste, todo loggeado.
|
||
- Validaciones KCL fallan limpio (toast con error) si el morphism
|
||
rebota — p. ej. cantidad > stock disponible.
|
||
|
||
Activación full:
|
||
```sh
|
||
NAKUI_EVENT_LOG=~/.nakui/state.jsonl \
|
||
NAKUI_MODULES_DIR=examples/nakui-modules \
|
||
cargo run -p nakui-ui
|
||
# Sidebar gana "Ventas (con morphism)" — los 6 menús aparecen y
|
||
# el form "Vender" dispara el pipeline nakui-core completo.
|
||
```
|
||
|
||
Trade-offs documentados:
|
||
- **Inputs UUID a mano**: el form pide que el user copie el UUID de
|
||
un Stock/Caja existente. Para UX seria habría que agregar
|
||
`FieldKind::EntityRef { entity }` que renderiza un dropdown — no
|
||
hecho por scope, queda como nice-to-have.
|
||
- **Inferencia de tipo en params**: `infer_param_value` adivina
|
||
por shape del string. Para casos sutiles (ej. "true" como string
|
||
literal vs bool), el módulo nakui-core puede explicitar tipos
|
||
via `kind` en el FieldSpec — el form lo respeta para validación
|
||
pre-submit; la inferencia final sigue siendo heurística.
|
||
|
||
### feat(nakui-ui): edit + delete de records (ciclo CRUD completo)
|
||
Cierra "no hay UI para editar/borrar records existentes" del commit
|
||
anterior. Cada fila de la lista gana dos botones (✎ edit, ✕ delete);
|
||
el form view se reusa para alta y para edit; el delete es inline.
|
||
Las mutaciones pasan por `LogEntry::Morphism` con sus ops, así el
|
||
replay restaura el estado correcto.
|
||
|
||
Cambios:
|
||
|
||
- **`MetaUi.editing: Option<(String, Uuid)>`** nuevo. Set al click
|
||
en ✎; cleared al cambiar de view o tras submit exitoso.
|
||
- **`open_edit(mod_idx, entity, id, cx)`**: setea `editing`, busca la
|
||
primera Form view del módulo cuya `entity` matchee, navega ahí. Si
|
||
el módulo no tiene Form para esa entity → toast con error
|
||
("no hay form view para entity X").
|
||
- **`select_view`** extendido: cuando carga un Form, si `editing`
|
||
matchea esa entity y el record existe en el store, pre-llena cada
|
||
input con el valor del record (vía nuevo helper
|
||
`value_to_input_text` — inverso de `parse_field_value`).
|
||
- **`commit_seed`** ramifica:
|
||
- **Edit path** (cuando `editing.is_some()` y entity matchea):
|
||
emite `LogEntry::Morphism { name: "ui.edit_record", ops:
|
||
[Set { path, value } for each field], params: { entity, id,
|
||
fields } }`. Aplica al store via `apply(&ops)`.
|
||
- **Seed path** (alta nueva): comportamiento previo.
|
||
- **`commit_delete(entity, id)`**: emite `LogEntry::Morphism {
|
||
name: "ui.delete_record", ops: [Delete { entity, id }] }` + apply.
|
||
- **Render del form**: título cambia a "Editar customer abc12345"
|
||
cuando `editing` matchea; submit label cambia a "Guardar cambios
|
||
en customer".
|
||
- **Render de la lista**: dos columnas nuevas — "id" y "acciones".
|
||
Cada fila tiene ✎ (accent color, click → open_edit) y ✕ (rojo,
|
||
click → commit_delete). Hover states.
|
||
|
||
Ramificación visible en el event log:
|
||
```
|
||
{"kind":"seed","seq":0,"entity":"customer","id":"abc...","data":{"name":"Acme"}}
|
||
{"kind":"morphism","seq":1,"morphism":"ui.edit_record","ops":[
|
||
{"op":"set","path":{"entity":"customer","id":"abc...","field":"name"},
|
||
"value":"Acme S.A."}
|
||
]}
|
||
{"kind":"morphism","seq":2,"morphism":"ui.delete_record","ops":[
|
||
{"op":"delete","entity":"customer","id":"abc..."}
|
||
]}
|
||
```
|
||
Coherente con el modelo de Nakui — todo cambio post-seed pasa por
|
||
ops dentro de Morphism. `nakui-explorer` muestra estos morphisms
|
||
con sus ops claros en su timeline.
|
||
|
||
Trade-offs documentados:
|
||
- **`schema_hash: None`** sigue para los morphism de la UI (legacy/
|
||
pre-versioning path) hasta que `Action::Morphism` cargue Manifest
|
||
schemas.
|
||
- **Delete sin confirmación**: 1 click, sin modal. Para MVP es OK
|
||
(los records son recuperables vía replay parcial), pero un futuro
|
||
iter agregaría confirmación.
|
||
- **Edit sobreescribe TODOS los campos del form**, no sólo los
|
||
cambiados — emite N ops Set, una por field. Adecuado para forms
|
||
chicos; para forms con muchos campos optimizar a delta-only.
|
||
|
||
Tests: 3 nuevos (10 totales en nakui-ui):
|
||
- `value_to_input_text_inverse_of_parse` y
|
||
`value_to_input_then_parse_round_trip` — la propiedad fundamental
|
||
del pre-llenado: text → parse devuelve el Value original.
|
||
- `event_log_replay_handles_full_crud_cycle` — E2E del log: escribe
|
||
Seed + Morphism(Set ops) + Morphism(Delete op), replay desde cero,
|
||
verifica que el store termina vacío (delete fue el último). Verifica
|
||
además que un replay parcial (sin el delete) deja los valores
|
||
editados.
|
||
|
||
Activación:
|
||
```sh
|
||
NAKUI_EVENT_LOG=~/.nakui/state.jsonl \
|
||
NAKUI_MODULES_DIR=examples/nakui-modules \
|
||
cargo run -p nakui-ui
|
||
# Crear un customer, click ✎ en su fila, modificar campos,
|
||
# "Guardar cambios". Click ✕ en otra fila para borrar.
|
||
# Cerrar y reabrir: el state persiste con todos los cambios.
|
||
```
|
||
|
||
### feat(nakui-ui): persistencia con event log + replay al startup
|
||
Cierra "sin persistencia entre runs" del commit anterior. Cada
|
||
`SeedEntity` se appendea al `nakui_core::event_log::EventLog` con
|
||
WAL semantics (log antes que store) y al re-abrir el binario el
|
||
replay reconstruye el `MemoryStore` desde cero. Cerrar y volver a
|
||
abrir ya no borra el data.
|
||
|
||
Cambios:
|
||
- **`MetaUi.event_log: Option<Arc<Mutex<EventLog>>>`** nuevo.
|
||
Compartido bajo `Mutex` para que el commit_seed pueda mutar.
|
||
- **Apertura + replay al startup** (`MetaUi::new`): path por env
|
||
`NAKUI_EVENT_LOG`, default `./nakui-ui-state.jsonl`.
|
||
`EventLog::open` + `replay_into` reconstruyen el store.
|
||
Toast informativo: "log nuevo" o "log X cargado: N evento(s)
|
||
replayed".
|
||
- **WAL en `commit_seed`**: si `event_log.is_some()`, primero
|
||
`log.append(LogEntry::Seed { ..., schema_hash: None })`, después
|
||
`store.seed`. Si el append falla, cancela toda la operación
|
||
(el user reintenta sin haber dejado state inconsistente).
|
||
- **`schema_hash: None`**: documentado como path "legacy /
|
||
pre-versioning" para seeds que no pasan por un Manifest+Executor.
|
||
Es el path correcto para alta administrativa vía la metainterfaz
|
||
hasta que `Action::Morphism` wireé el Manifest loader.
|
||
- **Degradación grácil**: si abrir log falla (permisos, disco),
|
||
toast con error pero el runtime sigue en modo in-memory.
|
||
|
||
Tests: 1 nuevo E2E `event_log_replay_restores_memory_store` que
|
||
escribe 2 seeds via `EventLog::append`, re-abre y `replay_into` un
|
||
store fresh, verifica que ambos records están con sus values
|
||
correctos. Reproduce el flujo del startup de `MetaUi::new` sin
|
||
necesitar GPUI. 7 tests verdes en nakui-ui.
|
||
|
||
Activación con persistencia explícita:
|
||
```sh
|
||
NAKUI_EVENT_LOG=~/.nakui/state.jsonl \\
|
||
NAKUI_MODULES_DIR=examples/nakui-modules \\
|
||
cargo run -p nakui-ui
|
||
# Crear varios records vía el form, cerrar el binario, abrir de
|
||
# nuevo: los records están.
|
||
```
|
||
|
||
Limitaciones que **siguen** (próximos iters):
|
||
- **`Action::Morphism`** sigue como TODO: requiere cargar el
|
||
`Manifest` de nakui-core junto al `Module` UI para conocer los
|
||
inputs/params declarados y poder llamar `execute_and_log`.
|
||
- **No hay snapshot/compaction**: el log crece append-only para
|
||
siempre. Para repos grandes habría que integrar `Snapshot` de
|
||
nakui_core (existe, no se usa todavía).
|
||
- **No hay UI para borrar/editar** records existentes — sólo alta
|
||
vía form. Edit + delete en futuras iteraciones.
|
||
- **Widget input simple** (sin selection/IME/clipboard) — heredado
|
||
de la limitación documentada de `yahweh-widget-text-input`.
|
||
|
||
### feat(nakui-ui): inputs reales con yahweh-widget-text-input + click handlers funcionales
|
||
Cierra dos limitaciones documentadas en el commit anterior de la
|
||
metainterfaz: los formularios ahora aceptan teclado real, y los
|
||
clicks en menús + botones mutan estado correctamente.
|
||
|
||
Cambios:
|
||
- **Inputs vivos**: cada `FieldSpec` del Form view materializa un
|
||
`Entity<TextInput>` (de `yahweh-widget-text-input`) al entrar a la
|
||
vista. Los entities se reemplazan al cambiar de view (drop limpio).
|
||
El widget soporta: escribir caracteres, Backspace, Enter (Confirmed
|
||
event — no usado todavía; el submit va por botón), Escape
|
||
(Cancelled). El cursor se renderea como `|` al final.
|
||
- **Click handlers wired vía `cx.listener`**: menús del sidebar
|
||
invocan `select_view`; botones de acción (header de list, submit
|
||
de form) invocan `apply_action`. Los handlers tienen acceso real
|
||
al `Context<MetaUi>` y mutan el modelo + emiten `cx.notify()`.
|
||
- **Submit lee texto de los inputs**: `commit_seed` reemplaza el
|
||
buffer ad-hoc anterior por `input.read(cx).text()` por cada
|
||
field. El value parseado va al `MemoryStore` con su tipo correcto
|
||
(text/number/boolean/date).
|
||
- **Reset de inputs tras submit**: si la acción no tiene `next_view`,
|
||
los inputs se vacían (`set_text("")`) para alta consecutiva sin
|
||
re-tipear.
|
||
- **Hover states**: items del sidebar y botones cambian de bg al
|
||
pasar el mouse, feedback visual consistente con el resto del
|
||
ecosistema yahweh.
|
||
- **Theme global**: `Theme::install_default(cx)` al inicio (lo
|
||
requiere el text_input para sus colores).
|
||
|
||
Wire en Cargo:
|
||
- Deps nuevas: `yahweh-widget-text-input`, `yahweh-theme` (paths
|
||
relativos al monorepo).
|
||
|
||
Limitaciones que **siguen abiertas** (próximos iters):
|
||
- **`Action::Morphism`** sigue como TODO: requiere cargar el
|
||
`Manifest` de nakui-core junto al `Module` UI para conocer los
|
||
inputs/params declarados.
|
||
- **Sin persistencia entre runs**: `MemoryStore` en RAM. Wire con
|
||
`EventLog` o `SurrealStore` queda para cuando exista el daemon
|
||
Nakui.
|
||
- **Inputs simples**: el widget no soporta cursor positioning,
|
||
selection, copy/paste, IME, multilínea. Para edits serios habrá
|
||
que portar `gpui::examples::input` o adoptar `gpui-input` cuando
|
||
exista upstream.
|
||
- **Enter no envía**: el `TextInputEvent::Confirmed` que emite el
|
||
widget no está suscrito todavía; el submit va por click. Trivial
|
||
de wirear si lo necesitamos.
|
||
|
||
Tests: los 6 unit del runtime siguen verdes (parse_field_value para
|
||
los 5 kinds, lookup_field nested, render_value). El comportamiento
|
||
visual requiere correr el binario con `cargo run -p nakui-ui` y
|
||
probar a mano — GPUI no provee harness de UI testing en CI hoy.
|
||
|
||
Activación full:
|
||
```sh
|
||
NAKUI_MODULES_DIR=examples/nakui-modules cargo run -p nakui-ui
|
||
# Click en un menú → carga vista. Click en "Nuevo" → form.
|
||
# Tipear en cada campo → ver el `|` al final. Click "Crear customer"
|
||
# → record aparece en la lista.
|
||
```
|
||
|
||
### feat(nakui): metainterfaz declarativa + 6 módulos ERP estándar
|
||
Salto cualitativo: Nakui pasa de "library + demos + read-only viewer
|
||
del event log" a **plataforma ERP con UI dirigida por datos**. Cada
|
||
módulo de negocio se declara como un `module.json` (sin código Rust
|
||
nuevo) y el runtime GPUI lo carga dinámicamente: sidebar de menús,
|
||
listas con columnas configurables, formularios de alta.
|
||
|
||
Tres entregables:
|
||
|
||
**1) Crate nuevo `nakui-ui-schema`** (datos puros, ~250 LOC + 200
|
||
LOC tests):
|
||
- `Module { id, label, entities, menu, views }`.
|
||
- `View::List { entity, columns, actions, search_in }` o
|
||
`View::Form { entity, fields, on_submit }`.
|
||
- `FieldSpec { name, label, kind, default, required, help }` con
|
||
`FieldKind = Text|Multiline|Number|Boolean|Date`.
|
||
- `Action::OpenView | SeedEntity | Morphism` — el runtime las
|
||
dispara desde botones / submits.
|
||
- `Module::from_path` parsea un JSON; `Module::validate` chequea que
|
||
cada `MenuItem.view` exista en `views`.
|
||
- `load_modules_from_dir(dir)` busca `dir/<modulo>/module.json`,
|
||
parsea, valida, detecta IDs duplicados, devuelve ordenado.
|
||
- 6 tests unit + 4 integration (los 6 demos cargan limpio, todos
|
||
tienen list+form, kinds reconocidos, validate pasa).
|
||
|
||
**2) Crate nuevo `nakui-ui`** (binario GPUI, ~700 LOC + 100 LOC tests):
|
||
- Carga módulos desde `NAKUI_MODULES_DIR` (default `./nakui-modules`).
|
||
- Sidebar con módulos + sus menús; click en menu cambia la vista activa.
|
||
- **List view**: tabla de instancias del entity con columnas
|
||
weighted (header de columnas + filas + id corto).
|
||
- **Form view**: campos labeled + botón submit que dispara la action
|
||
declarada (`SeedEntity` mete el record al `MemoryStore`
|
||
in-process; `Morphism` queda como TODO hasta integrar el manifest
|
||
loader nakui-core).
|
||
- `MemoryStore` compartido entre todas las vistas (Arc<Mutex>); el
|
||
cambio en un módulo se refleja en otro inmediato.
|
||
- Toast + error banner para feedback.
|
||
- 6 tests unit (parse_field_value para los 5 kinds, lookup_field
|
||
nested, render_value).
|
||
|
||
**3) 6 módulos demo** en `examples/nakui-modules/` que cubren un
|
||
ERP estándar:
|
||
- **customers**: nombre, email, teléfono, activo, límite de
|
||
crédito, notas.
|
||
- **products**: SKU, nombre, categoría, precio, stock, activo.
|
||
- **suppliers**: razón social, ID fiscal, contacto, email,
|
||
teléfono, términos de pago.
|
||
- **inventory_movements**: fecha, tipo (in/out/adjustment), SKU
|
||
producto, cantidad, costo unitario, motivo, doc. referencia.
|
||
- **sales_orders**: número, cliente, emisión, vencimiento,
|
||
estado, subtotal, impuestos, total, notas.
|
||
- **invoices**: número, cliente, emisión, vencimiento, subtotal,
|
||
impuestos, total, pagado, estado, moneda, orden referenciada.
|
||
|
||
Cada módulo tiene su `list` (catálogo) + `form` (alta), con search
|
||
field y columns weighted. Los 6 cubren un setup de ERP de ventas
|
||
chico funcional para demo.
|
||
|
||
Filosofía documentada:
|
||
- **UI como datos**: agregar un módulo = escribir un JSON, no
|
||
recompilar el binario.
|
||
- **Persistencia universal**: el runtime conecta cada vista al
|
||
`nakui_core::store::Store`; cambiar de MemoryStore a SurrealStore
|
||
no toca los module.json.
|
||
- **Schema primero, semántica después**: `nakui-ui-schema` sólo
|
||
define la forma; validación de referencias rotas (entity inexistente,
|
||
morphism faltante) vive en el runtime.
|
||
|
||
Activación:
|
||
```sh
|
||
NAKUI_MODULES_DIR=examples/nakui-modules cargo run -p nakui-ui
|
||
```
|
||
|
||
Limitaciones conocidas (próximas iteraciones):
|
||
- **Inputs sin teclado**: GPUI no incluye text input; los forms
|
||
muestran los `default` del schema y el submit usa esos. Próximo
|
||
iter: integración con `yahweh-widget-text-input`.
|
||
- **Click handlers no wired**: GPUI necesita pasar `Entity<MetaUi>`
|
||
a los handlers para mutar estado; refactor con `cx.listener` +
|
||
weak refs queda para el próximo iter. Hoy la navegación es
|
||
visual; el código de mutación sí funciona via API programática
|
||
(los tests lo cubren).
|
||
- **Acción `Morphism`**: pendiente de cargar el `Manifest` de
|
||
nakui-core junto con el `Module` UI para wirear `execute_and_log`.
|
||
- **Sin persistencia entre runs**: el `MemoryStore` se pierde al
|
||
cerrar. Wire con `EventLog` o `SurrealStore` queda para cuando
|
||
el daemon Nakui exista.
|
||
|
||
Tests: 16 totales nuevos (10 schema + 6 runtime). 100% verde.
|
||
|
||
Lo que esto desbloquea: cualquiera puede escribir un `module.json`
|
||
para su dominio (pacientes médicos, alumnos de escuela,
|
||
reservaciones de hotel) y aparece en la UI sin tocar Rust ni
|
||
recompilar. La forma de extender Nakui dejó de ser "agregar código
|
||
al ERP" y pasó a ser "escribir el contrato del módulo".
|
||
|
||
### feat(nakui-explorer): nuevo binario GPUI — Nakui visible en la interfaz
|
||
Cierra "nakui no tiene UI propia" del audit. Nuevo binario standalone
|
||
`nakui-explorer` (paralelo a `nouser-explorer`) que renderea el
|
||
event log de un repo Nakui: timeline scrollable de seeds + morphisms
|
||
con sus parámetros, breakdown por entity type, polling cada 2s para
|
||
detectar nuevos eventos appended sin restart del explorer.
|
||
|
||
Diseño:
|
||
- Lee directamente el archivo `.jsonl` del `nakui_core::event_log::EventLog`.
|
||
Path por env `NAKUI_EVENT_LOG`, default `nakui.jsonl` en pwd.
|
||
- Sin discovery vía broker brahman porque nakui hoy es CLI/library/
|
||
demos, no daemon. Cuando se daemonice, sustituir el lector de
|
||
archivo por un sidecar consumer (mismo patrón que nouser-explorer
|
||
actualmente usa).
|
||
|
||
UI:
|
||
- **Header**: path del log, count total + breakdown seeds/morphisms,
|
||
tiempo del último reload en ms.
|
||
- **Breakdown line**: top 5 buckets por frecuencia (entities + nombres
|
||
de morphisms con prefijo `→`).
|
||
- **Timeline**: tarjetas color-coded por kind (azul=seed,
|
||
verde=morphism). Cada tarjeta muestra `#seq`, kind, entity/morphism
|
||
name, id corto (8 hex), preview del data/params (80 chars), schema
|
||
hash corto (8 hex) o `(legacy)` si pre-versioning. Mostradas
|
||
más-recientes-primero, hasta 200 visibles (suficiente para
|
||
navegación; sin scroll virtualizado por ahora).
|
||
- **Error banner**: si la lectura falla (archivo inexistente o
|
||
corrupto), banner rojo con el motivo. El explorer NO crashea —
|
||
sigue intentando cada 2s.
|
||
|
||
Wire en workspace:
|
||
- Nuevo `crates/apps/nakui-explorer/` agregado a `[workspace] members`.
|
||
- Deps mínimas: `nakui-core` (para EventLog + LogEntry), `gpui`,
|
||
`serde_json`, `uuid` (con feature serde para parsear los IDs).
|
||
- Sin deps de brahman por ahora (Nakui standalone).
|
||
|
||
Tests: 7 unitarios en `tests` mod del bin:
|
||
- `load_log_returns_all_entries_in_order` — cargar un .jsonl
|
||
generado a mano, asserta que devuelve 5 entries con seqs 0..4
|
||
contiguous.
|
||
- `breakdown_counts_seeds_morphisms_and_buckets` — verifica el
|
||
conteo (3 seeds + 2 morphisms) y los buckets esperados.
|
||
- `load_missing_file_yields_empty_not_error` — archivo inexistente
|
||
devuelve `[]` sin error (delegado al contrato de `EventLog::open`).
|
||
- `preview_value_truncates_long_strings` y `_keeps_short_strings_intact`.
|
||
- `short_uuid_takes_first_8_chars` y `short_hash_takes_first_4_bytes_hex`.
|
||
|
||
Activación:
|
||
```sh
|
||
NAKUI_EVENT_LOG=/tmp/nakui_inv_xxx.jsonl cargo run -p nakui-explorer
|
||
```
|
||
|
||
Estado del CHANGELOG global tras este commit: cero pendientes
|
||
fundamentados activos. Lo único que queda es `minga-vfs` (FUSE,
|
||
explícitamente diferido por el usuario) y mejoras nice-to-have
|
||
(cobertura adicional per-lenguaje, daemon-ización de nakui para
|
||
sidecar discovery).
|
||
|
||
### feat(minga-core): α-hashing per-language para Python, TypeScript, JavaScript, Go
|
||
Cierra el último pendiente fundamentado del CHANGELOG. Cada lenguaje
|
||
soportado por `minga` tiene ahora su propio profile α-equivalente —
|
||
dos versiones del mismo programa que difieren sólo en nombres de
|
||
variables ligadas producen el mismo hash, no importa el lenguaje.
|
||
Refactorings tipo "rename variable" no inflan el storage del repo
|
||
en ningún dialecto.
|
||
|
||
Refactor de `alpha.rs` (639 LOC) a módulo `alpha/`:
|
||
- **`alpha/common.rs`**: primitives compartidos (TAG_*, write_kind_and_field,
|
||
emit_leaf_marker, emit_binder_body, emit_identifier_ref, push_identifier_name).
|
||
Garantiza que el formato wire del hash sea bit-equivalente entre
|
||
todos los profiles.
|
||
- **`alpha/rust.rs`**: la lógica de Rust (movida desde alpha.rs sin
|
||
cambios funcionales).
|
||
- **`alpha/python.rs`**: nuevo.
|
||
- **`alpha/ecmascript.rs`**: nuevo (cubre TypeScript + JavaScript;
|
||
comparten la mayoría de los kinds).
|
||
- **`alpha/go.rs`**: nuevo.
|
||
- **`alpha/mod.rs`**: re-exporta `hash_node_alpha` (Rust legacy) +
|
||
expone `hash_alpha_with(dialect, node)` que despacha al profile
|
||
correspondiente.
|
||
|
||
Cobertura per-language:
|
||
|
||
**Python** (`def`, `lambda`, `for`, comprehensions, `with`):
|
||
- `function_definition` y `lambda`: parámetros (incluyendo
|
||
typed_parameter, default_parameter, *args, **kwargs) introducen
|
||
binders al body. El nombre de la función NO es α-anónimo.
|
||
- `for_statement`: el `left` (identifier o tuple) introduce
|
||
binder(es) al body.
|
||
- `list_comprehension`, `set_comprehension`, `dictionary_comprehension`,
|
||
`generator_expression`: cada `for_in_clause` añade binders que
|
||
viven en el body + clauses siguientes (semántica de scope
|
||
incremental de Python).
|
||
- `with_statement`: `as` introduce binder al body (recursando en
|
||
`as_pattern_target` para llegar al identifier).
|
||
|
||
**ECMAScript** (TS + JS):
|
||
- `function_declaration`, `function_expression`, `method_definition`,
|
||
`generator_function_*`: parameters → body. Soporta TS
|
||
`required_parameter` y `optional_parameter` (`x: number`,
|
||
`x?: number`).
|
||
- `arrow_function`: tanto `(x, y) => body` como shorthand `x => body`.
|
||
- `statement_block`: `lexical_declaration` (let/const) y
|
||
`variable_declaration` (var) introducen binders al resto del block.
|
||
- `for_in_statement` (cubre `for-of` y `for-in`): `left` → body.
|
||
- `for_statement` (C-style): initializer (lexical decl) introduce
|
||
binders al condition + increment + body.
|
||
- `catch_clause`: parameter → body.
|
||
|
||
**Go**:
|
||
- `function_declaration`, `method_declaration`, `func_literal` (closure):
|
||
`parameter_list` → body. `parameter_declaration` con varios names
|
||
agrupa varios binders bajo un mismo tipo (`a, b int`).
|
||
- `block`: `short_var_declaration` (`x := ...`) introduce binders
|
||
al resto.
|
||
- `for_statement` con `range_clause` (`for k, v := range m`): los
|
||
identifiers del `left` son binders al body.
|
||
- `for_statement` con `for_clause` (C-style): initializer → body.
|
||
- `if_statement` con `initializer` (`if x := init(); x > 0`):
|
||
binders viven en condition + consequence + alternative.
|
||
|
||
API:
|
||
- `hash_alpha_with(Dialect, &SemanticNode) -> ContentHash` —
|
||
despacho per-dialect.
|
||
- `hash_node_alpha(&SemanticNode) -> ContentHash` — alias histórico
|
||
asume Rust (back-compat).
|
||
|
||
Tests: 26 nuevos en `tests/alpha_polyglot.rs`:
|
||
- Python (9): def rename, lambda rename, for-loop rename, list comp,
|
||
nested comp, with rename, function name matters, iterable name
|
||
matters, sanity negativo (operación distinta → hash distinto).
|
||
- JS/TS (9): function rename, function name matters, arrow rename,
|
||
arrow shorthand rename, let/const rename, for-of rename, classic
|
||
for rename, catch rename, TS typed param rename, TS type matters.
|
||
- Go (6): function rename, function name matters, short var decl
|
||
rename, range_clause rename, if-init rename, func_literal closure
|
||
rename.
|
||
- Cross-language (1): mismos shapes en lenguajes distintos
|
||
producen hashes distintos (sanity para evitar colisiones).
|
||
|
||
141 tests verdes en minga-core (115 antes; +26 polyglot). Refactor
|
||
sin regresión: 36 α-Rust tests siguen pasando.
|
||
|
||
Pendientes que quedan en Minga (orden de prioridad):
|
||
- `minga-vfs` FUSE (proyecto independiente, scope grande).
|
||
- Cobertura adicional por-lenguaje: Python class, JS destructuring,
|
||
Go type_switch, etc. — cada uno pequeño, no urgente.
|
||
|
||
### feat(minga-core): cierre del α-hashing de Rust — if let, while let, let-else, or-pattern, let-chains
|
||
Cierra los 5 pendientes documentados en `alpha.rs`. El hash
|
||
α-equivalente ahora es estable bajo renombre de TODOS los binders
|
||
de Rust, no sólo los del MVP (parámetros, let, for, match arms).
|
||
|
||
Pendientes cerrados:
|
||
- **`if let X = expr { ... }`**: `if_expression` detecta
|
||
`let_condition` en su `condition`, recolecta los binders del
|
||
pattern, los propaga al `consequence`. El `alternative` (else)
|
||
NO los ve.
|
||
- **`while let X = expr { ... }`**: simétrico al if-let, propaga al
|
||
`body`. El `condition` mismo se evalúa con scope previo (los
|
||
binders todavía no existen).
|
||
- **`let-else`**: `let_declaration` con campo `alternative`. El
|
||
alternative se procesa con el scope ANTES de los binders (ya
|
||
funcionaba: `feed_let` llama `feed` para no-pattern children con
|
||
el scope actual; `feed_block` extiende el scope DESPUÉS de
|
||
`feed_let`).
|
||
- **`or_pattern`**: en `pat1 | pat2` (Rust enforcement: ambos lados
|
||
introducen los mismos binders). Para emit, recorremos cada lado
|
||
con `feed_pattern`. Para collect, sólo el primer lado — iterar
|
||
todos duplicaría binders y rompería los índices de Bruijn.
|
||
- **let-chains** (`if let X = a && let Y = b { ... }`): el
|
||
`collect_let_condition_binders` recursa en el árbol del condition,
|
||
capturando todos los `let_condition` (vivan dentro de
|
||
`binary_expression` u otros nodos). Ambos binders quedan en scope
|
||
del consequence.
|
||
|
||
Helper nuevo: `feed_let_condition` para que el `pattern` del
|
||
let_condition pase por `feed_pattern` (que distingue binders vs
|
||
constructors). Sin esto, los identifiers del pattern se hasheaban
|
||
como variables libres y `Some(x)` ≠ `Some(y)` aún teniendo el
|
||
mismo significado.
|
||
|
||
Tests: 6 nuevos en `tests/alpha_invariants.rs`:
|
||
- `alpha_if_let_binder_rename_invariant`
|
||
- `alpha_if_let_else_does_not_see_binder` (sanity)
|
||
- `alpha_while_let_binder_rename_invariant`
|
||
- `alpha_let_else_binder_rename_invariant`
|
||
- `alpha_or_pattern_binder_rename_invariant`
|
||
- `alpha_let_chain_binders_propagate_to_consequence`
|
||
- `alpha_if_let_does_not_collide_with_unrelated_program` (negativo:
|
||
programas distintos NO deben dar el mismo hash)
|
||
|
||
36 tests α verdes (eran 30). 115 tests totales en minga-core.
|
||
|
||
Lo que esto significa: el hash α-equivalente de Rust en minga es
|
||
**completo** — cubre todos los constructos del lenguaje que
|
||
introducen bindings. Dos versiones del mismo programa que difieren
|
||
sólo en nombres de variables (incluyendo en `if let`, `while let`,
|
||
`or-pattern`, etc.) producen el mismo hash y por tanto la misma
|
||
identidad CAS. Refactorings del tipo "rename variable" no inflan
|
||
el storage del repo.
|
||
|
||
Pendientes futuros:
|
||
- α-hashing per-language (Python: def/lambda/comprehensions; TS/JS:
|
||
function/arrow/destructuring; Go: func/closure). Cada uno
|
||
requiere conocimiento profundo de la gramática y tests
|
||
exhaustivos. Plantilla genérica no aplica.
|
||
|
||
### feat(minga): multi-lenguaje en parser — Python, TypeScript, JavaScript, Go
|
||
Minga deja de ser Rust-only. Cualquiera de los cinco dialectos
|
||
(Rust + 4 nuevos) se ingresa al CAS por su AST normalizado, hashea
|
||
estructuralmente, sincroniza por DHT como cualquier nodo. La
|
||
auto-detección por extensión hace que `minga ingest archivo.py` o
|
||
`.ts` o `.go` "simplemente funcione".
|
||
|
||
API nueva en `minga_core::parse`:
|
||
- Funciones por dialecto (~6 LOC c/u sobre el `parse_with` común):
|
||
`python`, `typescript`, `javascript`, `go`. Más la `rust` existente.
|
||
- Enum `Dialect` con `parse(source) -> Result<SemanticNode>` y
|
||
`name() -> &'static str` para logging.
|
||
- `detect_by_extension(ext) -> Option<Dialect>`: mapea `rs`/`py`/
|
||
`pyi`/`ts`/`js`/`mjs`/`cjs`/`go` (case-insensitive). `None` para
|
||
extensiones desconocidas — el caller decide si es error o se
|
||
ignora silente.
|
||
|
||
Wire en `minga-cli`:
|
||
- `cmd_ingest` deja de hardcodear `parse::rust` — usa
|
||
`detect_dialect(file)?.parse(...)`. Acepta `.py`, `.ts`, `.js`,
|
||
`.go` además de `.rs`.
|
||
- `initial_scan` y `cmd_watch` cambian `is_rs_file` → `is_supported_source`
|
||
para incluir todas las extensiones soportadas en el filtro.
|
||
- `CliError::UnsupportedLanguage { path, extension }` nuevo, con
|
||
mensaje que lista las extensiones reconocidas.
|
||
|
||
Notas sobre hashing:
|
||
- El AST normalizado (`SemanticNode`) descarta whitespace y
|
||
comentarios — propiedad universal de tree-sitter (extras). Misma
|
||
lógica para los 5 dialectos.
|
||
- Hashing **estructural** (`cas::hash_node`) funciona para todos:
|
||
dos textos semánticamente equivalentes-por-estructura producen el
|
||
mismo hash. NO α-equivalente (las variables ligadas distinguen).
|
||
- Hashing **α-equivalente** (`alpha::hash_node_alpha`) sigue siendo
|
||
Rust-only: cada lenguaje tiene reglas distintas para qué es
|
||
binder vs. constructor (def/lambda en Python, arrow functions en
|
||
TS/JS, func + closures en Go). Implementación per-language queda
|
||
como work futuro — requiere conocimiento profundo de cada
|
||
gramática y no se plantilla genéricamente.
|
||
- Sanity test `structural_hash_distinguishes_languages` verifica
|
||
que `x = 1` parseado como Python ≠ parseado como JavaScript: las
|
||
gramáticas no comparten kinds y los hashes salen distintos.
|
||
Importante para evitar colisiones cuando el mismo source se
|
||
ingresa bajo dialectos distintos.
|
||
|
||
Deps nuevas (workspace + minga-core):
|
||
- `tree-sitter-python = "0.23"`
|
||
- `tree-sitter-typescript = "0.23"` (sólo el modo `LANGUAGE_TYPESCRIPT`,
|
||
no TSX — bumpear a TSX es agregar otro dialecto cuando se necesite).
|
||
- `tree-sitter-javascript = "0.23"`
|
||
- `tree-sitter-go = "0.23"`
|
||
|
||
Tests:
|
||
- 9 nuevos en `parse::tests`: parse básico para los 5 dialectos
|
||
(Python con type hints, TS con tipos, JS sin tipos, Go con
|
||
package declaration), `detect_by_extension` canonical +
|
||
case-insensitive, `dialect_name`, `structural_hash_distinguishes_languages`.
|
||
- 108 tests verdes en minga-core (39 → 48 unit + integration tests
|
||
pre-existentes intactos).
|
||
- 10 tests verdes en minga-cli (sin regresión en el path Rust;
|
||
el refactor a `detect_dialect`/`is_supported_source` no rompe
|
||
nada).
|
||
|
||
Pendientes futuros del changelog:
|
||
- α-hashing per-language (Python: def/lambda/comprehensions;
|
||
TS/JS: function/arrow/destructuring; Go: func/closure). Trabajo
|
||
profundo, scope independiente.
|
||
- α-Rust pendientes documentados en `alpha.rs`: `if let`,
|
||
`while let`, `let-else`, let-chains, `or_pattern` con bindings.
|
||
|
||
### feat(brahman-handshake): multi-key identity — rotación de session sin perder peer_id lógico
|
||
Cierra el último pendiente del plan de red P2P. Hasta ahora, rotar
|
||
la keypair libp2p de un nodo cambiaba su `peer_id`, lo que
|
||
invalidaba todas las allowlists/denylists remotas que lo
|
||
referenciaban. Imposible rotar sin coordinar con todos los pares.
|
||
|
||
Solución: separar **identity master** (Ed25519 persistente forever,
|
||
identifica al nodo como entidad lógica) de **session libp2p**
|
||
(Ed25519 efímera, rotable). El master firma certs de session con
|
||
expiración. La política de admisión se evalúa contra el
|
||
`master_peer_id` del cert — el session peer_id puede cambiar
|
||
libremente sin tocar las allowlists.
|
||
|
||
API nueva en `brahman_handshake::identity`:
|
||
- `Identity::from_keypair(master)` — wrapper sobre la master kp.
|
||
- `Identity::master_peer_id()` — el peer_id estable del nodo.
|
||
- `Identity::issue_session_cert(session_kp, ttl) -> SessionCert` —
|
||
firma un cert que vincula session_pubkey + expires_at_ms.
|
||
- `SessionCert::verify()` — chequea versión, firma criptográfica,
|
||
no expiración. Devuelve `(master_peer_id, session_peer_id)`.
|
||
- `SessionCert::verify_against_session(expected_session_pk)` — verify
|
||
+ exige que el cert vincule esa session pubkey (previene reuso de
|
||
certs ajenos con keypairs distintas).
|
||
- `CertError` tipado: `UnknownVersion`, `DecodeMaster`,
|
||
`DecodeSession`, `InvalidSignature`, `Expired`, `SessionMismatch`,
|
||
`Sign`.
|
||
- `DEFAULT_SESSION_TTL = 24h`.
|
||
|
||
Wire:
|
||
- `Hello.identity_cert: Option<SessionCert>` agregado (default None,
|
||
back-compat).
|
||
- `Client::connect_with_stream_signed_with_cert(stream, card, wit,
|
||
session_kp, cert)` — variante que adjunta el cert.
|
||
- `network::connect_libp2p_with_cert(net, peer, card, wit,
|
||
session_kp, cert)` — paralelo a `connect_libp2p`.
|
||
|
||
Server (`do_handshake`):
|
||
- Nuevo paso ANTES del policy gate: si `Hello.identity_cert.is_some()`,
|
||
se verifica con `verify_against_session(&hello.signature.public_key)`.
|
||
El `logical_peer` que se evalúa contra la policy es el
|
||
`master_peer_id` derivado, NO el session peer_id.
|
||
- Sin cert (path Fase 3): `logical_peer = expected_peer` (compat).
|
||
- Si el cert es inválido (firma rota, expirado, session mismatch),
|
||
rechazo con `Unauthorized` antes de evaluar policy.
|
||
- Migración gradual: clientes sin cert siguen funcionando contra
|
||
servers con policy basada en session peer_ids.
|
||
|
||
Canonicalización del payload firmado:
|
||
```
|
||
[u8 version][b"sess"][u32 LE session_pubkey_len][session_pubkey][u64 LE expires_at_ms]
|
||
```
|
||
`SESSION_CERT_VERSION = 1` documenta el esquema; cualquier cambio
|
||
fuerza bump (clientes viejos no validan certs nuevos).
|
||
|
||
Sobre el swarm-level deny:
|
||
- El `block_list` del swarm sigue operando con session peer_ids
|
||
(Noise sólo conoce eso). Si la operatoria lista master_peer_ids
|
||
en deny, el handshake-level gate los para; el swarm-level no.
|
||
El operador elige granularity: listar masters = robust a
|
||
rotaciones; listar sessions = rechazo más temprano.
|
||
|
||
Tests: 8 unit en `identity::tests`:
|
||
- `issue_and_verify_cert` — roundtrip básico, peer_ids derivados.
|
||
- `verify_against_session_admits_matching` y
|
||
`_rejects_mismatch` — el cert vincula 1 sola session pubkey.
|
||
- `cert_with_zero_ttl_is_expired` — expiración chequeada con tiempo
|
||
real.
|
||
- `tampered_signature_rejected` y `tampered_expires_at_rejected` —
|
||
cualquier mutación del cert post-firma falla.
|
||
- `unknown_version_rejected` — schema versionado defensivamente.
|
||
- `rotated_session_with_same_master_yields_same_master_peer_id` —
|
||
la propiedad fundamental: rotar session NO cambia master_peer_id.
|
||
|
||
Plus 1 E2E definitivo en `network_libp2p.rs`:
|
||
`identity_cert_allows_session_rotation_without_policy_change`.
|
||
- A configura `policy = allowlist[B.master_peer_id]` (master, no
|
||
session).
|
||
- B se conecta con session1 + cert(master, session1) → admitido.
|
||
Sesión registrada, farewell limpio.
|
||
- B "rota": genera session2 ≠ session1, mismo master, emite cert2.
|
||
- B se conecta con session2 + cert2 → admitido también, **sin que
|
||
A toque su allowlist**.
|
||
- Sanity: una session sin cert (cuyo session_peer NO está en allow)
|
||
es rechazada.
|
||
|
||
40 tests verdes en brahman-handshake + brahman-net (24 unit
|
||
incluyendo identity + 7 handshake + 3 discovery + 6 libp2p
|
||
incluyendo rotation E2E). Ningún regreso.
|
||
|
||
Wire en Arje queda como follow-up: ente-zero hoy es server-only y
|
||
no necesita identity (su keypair libp2p ya es estable vía
|
||
keypair_store). Cuando algún módulo de Arje haga conexiones
|
||
salientes con cert, se cargará la identity master separada de la
|
||
session vía nueva env `BRAHMAN_IDENTITY_PATH`. La API ya está
|
||
lista.
|
||
|
||
### feat(brahman-net+handshake): swarm-level deny — la denylist se proyecta al block_list de libp2p
|
||
Optimización de seguridad: la denylist ya no espera al handshake
|
||
brahman para rechazar — ahora se proyecta al `block_list` behaviour
|
||
del swarm libp2p. Conexiones desde peers baneados son rechazadas
|
||
**antes del Noise handshake**, ahorrando el round-trip TCP+Noise
|
||
por cada intento denegado.
|
||
|
||
Wire de bajo nivel (`brahman-net`):
|
||
- Nuevo behaviour `block_list: allow_block_list::Behaviour<BlockedPeers>`
|
||
añadido al `BrahmanBehaviour` derivado. Vive junto a `stream`,
|
||
`kad`, `identify`. Default vacío al construir.
|
||
- Nuevos comandos `BlockPeer(PeerId)` y `UnblockPeer(PeerId)` en el
|
||
enum interno + handlers que llaman
|
||
`swarm.behaviour_mut().block_list.{block_peer,unblock_peer}`.
|
||
- API pública: `BrahmanNet::block_peer(peer)` y
|
||
`BrahmanNet::unblock_peer(peer)`. Idempotentes.
|
||
- Dep nueva: `libp2p-allow-block-list = "0.6"` (sub-crate, no es
|
||
feature de `libp2p` en 0.56).
|
||
|
||
Wire en la política (`brahman_handshake::peer_policy`):
|
||
- `PeerPolicy` gana campo opcional `net: Arc<RwLock<Option<Arc<BrahmanNet>>>>`.
|
||
Default `None` para preservar callers existentes.
|
||
- Nuevo método `attach_to_net(net: Arc<BrahmanNet>)`:
|
||
- Sincronización inicial: itera la deny actual y llama
|
||
`net.block_peer(p)` por cada uno.
|
||
- Guarda el net para diff-sync en cada `reload`.
|
||
- `reload()` extendido: snapshot de `prev_deny` ANTES de mutar el
|
||
inner. Tras la mutación, llama `sync_deny_to_swarm(prev, new)`
|
||
que aplica `block_peer` por cada added y `unblock_peer` por cada
|
||
removed.
|
||
- Atomicidad preservada: si un archivo falla al parsear, el sync
|
||
no ocurre y la versión anterior persiste tanto en la policy
|
||
como en el block_list del swarm.
|
||
|
||
Wire en Arje (`ente-zero`):
|
||
- Tras setup_brahman_net + setup_brahman_policy, si AMBOS están
|
||
presentes se llama `policy.attach_to_net(net.clone())` con un log
|
||
informativo. Sin policy o sin net, no hay attach (modo abierto
|
||
o solo gate-level deny).
|
||
|
||
Tests: 1 nuevo E2E en `network_libp2p.rs`:
|
||
`swarm_level_deny_blocks_before_noise`. A configura policy con
|
||
deny de un peer + attach_to_net. Cliente baneado intenta
|
||
`connect_libp2p`; en lugar del `HandshakeError::Unauthorized` que
|
||
recibíamos antes (que requería completar Noise primero), ahora
|
||
falla con error de transporte/stream (o timeout, según timing) —
|
||
el dial nunca completa porque el swarm rechaza la conexión.
|
||
|
||
5 tests verdes en `network_libp2p.rs` (roundtrip, mismatched signing,
|
||
allowlist, denylist handshake-level, denylist swarm-level). 31 tests
|
||
totales en brahman-handshake + brahman-net. Sin regresión en
|
||
ente-zero.
|
||
|
||
Trade-offs:
|
||
- **Más eficiente** contra DoS: un atacante que prueba miles de
|
||
peer_ids no consume CPU del Noise handshake.
|
||
- **Misma fuente de verdad**: la denylist sigue viviendo en
|
||
`PeerPolicy` (un solo archivo, hot-reloadable). El swarm es un
|
||
cache derivado que se actualiza vía diff. No hay drift posible —
|
||
cada reload re-sincroniza atómicamente.
|
||
- **El handshake-level gate sigue activo** como segunda línea: si
|
||
por alguna razón un peer baneado pasa el block_list (race entre
|
||
reload y nueva conexión, o bug del crate), el handshake brahman
|
||
igual lo rechaza con `Unauthorized`. Defensa en profundidad.
|
||
|
||
Pendientes futuros del changelog:
|
||
- Rotación de keypair sin perder peer_id (multi-key identity).
|
||
|
||
### feat(brahman-handshake+ente-zero): denylist + hot reload de la política de peers
|
||
Consolida `PeerAllowlist` + nueva `PeerDenylist` en un único
|
||
`PeerPolicy` con allow + deny + hot reload vía `notify`. Cubre los
|
||
dos pendientes documentados en el commit anterior y simplifica la
|
||
API hacia un sólo punto de entrada.
|
||
|
||
API consolidada en `brahman_handshake::peer_policy`:
|
||
- `PeerPolicy::open()` — todo permitido (default).
|
||
- `PeerPolicy::from_sets(allow: Option<BTreeSet<PeerId>>, deny: BTreeSet<PeerId>)`
|
||
— política inline para tests.
|
||
- `PeerPolicy::from_files(allow_path?, deny_path?)` — carga ambos
|
||
archivos opcionales.
|
||
- `PeerPolicy::evaluate(peer) -> Decision` — `Admit |
|
||
DeniedByDenylist | NotInAllowlist`. Decision lleva su `reason()`
|
||
para logging consistente.
|
||
- `PeerPolicy::reload()` — recarga atómica desde los paths
|
||
asociados. **Si un archivo falla, conserva la versión anterior**
|
||
(un typo no debe tirar al Init en modo inseguro).
|
||
- `PeerPolicy::spawn_watcher() -> JoinHandle` — vigila los
|
||
archivos vía `notify`, debounce 250ms (coalesce de los varios
|
||
eventos típicos de un save), recarga atómica al detectar cambio.
|
||
|
||
Orden de evaluación (deny-first):
|
||
1. Si `peer ∈ denylist` → `DeniedByDenylist`.
|
||
2. Si hay allowlist y `peer ∉ allowlist` → `NotInAllowlist`.
|
||
3. Resto → `Admit`.
|
||
|
||
Esto significa que **deny gana sobre allow**: un peer en ambas listas
|
||
es rechazado. Diseño explícito para que la denylist sea la primitiva
|
||
de "kill switch" — agregar un peer al deny lo banea inmediatamente
|
||
sin importar dónde más esté listado.
|
||
|
||
Watcher: vigila el **directorio padre** del archivo, no el archivo
|
||
mismo. Razón: editores típicos hacen rename-and-replace (escriben
|
||
a tmp y rename al destino), lo que rompe el watch del archivo pero
|
||
no el del dir. Filtra eventos por path al procesar.
|
||
|
||
Wire en server:
|
||
- `ServerConfig.allowlist` → `ServerConfig.policy: Option<PeerPolicy>`
|
||
(breaking rename, scope local al monorepo). Gate en `do_handshake`
|
||
llama `policy.evaluate(&peer)` y usa `decision.reason()` para el
|
||
mensaje de error tipado.
|
||
|
||
Wire en Arje (`ente-zero`):
|
||
- Nueva env `BRAHMAN_PEER_DENYLIST` complementa
|
||
`BRAHMAN_PEER_ALLOWLIST`. Cualquiera (o ambas) activa la política.
|
||
- `setup_brahman_policy()` carga + arranca watcher. Devuelve
|
||
`(policy, JoinHandle)`; el handle se guarda en main para que el
|
||
thread no se aborte.
|
||
- Failure modes degradan a "modo abierto" (sin política) con log,
|
||
preservando la doctrina PID 1.
|
||
|
||
Activación end-to-end con todas las capas activas:
|
||
```sh
|
||
BRAHMAN_LISTEN_MULTIADDR=/ip4/0.0.0.0/tcp/4101 \
|
||
BRAHMAN_PEER_ALLOWLIST=/etc/brahman/allow.txt \
|
||
BRAHMAN_PEER_DENYLIST=/etc/brahman/deny.txt \
|
||
ente-zero
|
||
# El operador puede editar deny.txt en caliente y la nueva regla
|
||
# entra en efecto en ~250ms sin restart del Init.
|
||
```
|
||
|
||
Tests: 10 unit en `peer_policy::tests`:
|
||
- `open_admits_anyone`, `allow_only_admits_listed`,
|
||
`deny_overrides_open`, `deny_overrides_allow` (deny-first
|
||
semantics).
|
||
- `from_files_with_both_lists`, `from_files_only_deny`,
|
||
`invalid_file_rejected_at_load`.
|
||
- `reload_picks_up_changes` — manualmente recarga y verifica.
|
||
- `reload_failure_preserves_previous_state` — invariante de
|
||
seguridad: archivo roto NO baja la política activa.
|
||
- `watcher_reloads_on_file_change` — E2E del watcher con notify
|
||
real: muta archivo, espera < debounce + margen, verifica que
|
||
la política refleja el cambio sin haber llamado reload manualmente.
|
||
|
||
Plus 1 E2E nuevo en `network_libp2p.rs`:
|
||
`libp2p_handshake_denylist_blocks_listed_peer` — A configura
|
||
`policy = PeerPolicy::from_sets(None, [banned_peer])`. Cliente
|
||
con keypair baneada es rechazado; cliente con keypair distinta
|
||
pasa el handshake.
|
||
|
||
30 tests verdes en brahman-handshake (16 unit + 7 handshake + 3
|
||
discovery + 4 libp2p incluyendo allowlist + denylist E2E). Sin
|
||
regresión en ente-zero.
|
||
|
||
Lo que cierra esta entrega:
|
||
- Política completa de admisión: open / allow-only / deny-only /
|
||
allow+deny.
|
||
- Hot reload sin restart del Init — el operador puede banear/admitir
|
||
peers en caliente editando archivos.
|
||
- Atomicidad: la recarga es del paquete `(allow, deny)` completo, no
|
||
de cada lista por separado. No hay momento donde una lista esté
|
||
vieja y la otra nueva.
|
||
- Resiliencia: errores de parseo NO bajan la política activa.
|
||
|
||
Pendientes futuros del changelog:
|
||
- Aplicar la política a nivel de swarm vía `libp2p_allow_block_list::
|
||
Behaviour` (rechazar ANTES del Noise handshake, ahorrar el
|
||
round-trip TCP+Noise por intento denegado).
|
||
- Rotación de keypair sin perder peer_id (multi-key identity).
|
||
|
||
### feat(brahman-handshake+ente-zero): allowlist explícita de peers libp2p
|
||
Capa de política sobre el trust criptográfico de Fase 3. Hasta ahora
|
||
cualquier peer con keypair Ed25519 válida pasaba el handshake remoto;
|
||
con allowlist activa, sólo los peers explícitamente listados. Aplica
|
||
únicamente al path libp2p — el path Unix sigue usando SO_PEERCRED
|
||
del kernel, que es autenticación de proceso local, no de red.
|
||
|
||
API nueva en `brahman_handshake::peer_allowlist`:
|
||
- `PeerAllowlist::from_iter([peer_id, ...])` para tests/inline.
|
||
- `PeerAllowlist::from_file(path)` parsea texto plano: un PeerId
|
||
base58 por línea, `#` para comentarios (línea entera o inline),
|
||
líneas vacías ignoradas. Errores de parseo incluyen número de
|
||
línea para debug rápido.
|
||
- `is_allowed(peer)`, `len()`, `is_empty()`, `iter()`.
|
||
- `AllowlistError { Io, InvalidPeerId }`.
|
||
|
||
Wire en el server:
|
||
- `ServerConfig.allowlist: Option<PeerAllowlist>`. `None` = modo
|
||
abierto (compat con todo lo anterior). `Some` = sólo los listados.
|
||
- Gate en `do_handshake` ANTES de la verificación de firma — la
|
||
comparación O(log n) en BTreeSet es más barata que crypto, así
|
||
que rechazamos peers inválidos antes de gastar ciclos. Se devuelve
|
||
`HandshakeError::Unauthorized("peer X no está en la allowlist")`.
|
||
|
||
Wire en Arje (`ente-zero`):
|
||
- Nueva env var `BRAHMAN_PEER_ALLOWLIST` apuntando a un archivo.
|
||
- `setup_brahman_allowlist()` carga al startup; degrada a `None`
|
||
(modo abierto) si el archivo falla, consistente con la doctrina
|
||
PID 1 de no romper por subsistemas opcionales.
|
||
|
||
Ejemplo de archivo de allowlist:
|
||
```text
|
||
# Peers permitidos en la malla brahman de prod-eu-1
|
||
# Generados con: ente-zero (peer_id loggeado al arrancar)
|
||
12D3KooWFooBarBazFooBarBazFooBarBazFooBarBazFooBarBaz
|
||
12D3KooWQuxQuxQuxQuxQuxQuxQuxQuxQuxQuxQuxQuxQuxQuxQux # operador 2
|
||
```
|
||
|
||
Activación end-to-end:
|
||
```sh
|
||
BRAHMAN_LISTEN_MULTIADDR=/ip4/0.0.0.0/tcp/4101 \\
|
||
BRAHMAN_PEER_ALLOWLIST=/etc/brahman/allowlist.txt \\
|
||
ente-zero
|
||
```
|
||
|
||
Tests:
|
||
- 6 unit en `peer_allowlist::tests`: from_iter, parse limpio, parse
|
||
con comentarios inline, parse rechaza PeerId inválido (y reporta
|
||
número de línea), I/O error en archivo faltante, empty list
|
||
rechaza todo.
|
||
- 1 E2E en `network_libp2p.rs`:
|
||
`libp2p_handshake_allowlist_admits_listed_rejects_others`. A
|
||
configura `allowlist = [allowed_peer]`. Cliente con keypair
|
||
permitida pasa el handshake (sesión registrada, farewell limpio).
|
||
Segundo cliente con keypair distinta es rechazado con error
|
||
ANTES de que se le verifique la firma. Sanidad: el conteo de
|
||
sesiones del server queda en 0 tras el rechazo.
|
||
|
||
25 tests verdes en brahman-handshake (12 unit + 7 handshake legacy
|
||
+ 3 discovery + 3 libp2p). Ningún regreso en ente-zero (4/4
|
||
keypair_store).
|
||
|
||
Pendiente futuro:
|
||
- Denylist explícita (negada — banear peers específicos sin tener
|
||
que listar a todos los demás).
|
||
- Hot reload de la allowlist sin restart del Init (signal SIGHUP o
|
||
watch del archivo).
|
||
- Aplicar la política a nivel de swarm vía
|
||
`libp2p_allow_block_list::Behaviour` para rechazar conexiones
|
||
ANTES del Noise handshake (hoy se rechaza después, gastando un
|
||
round-trip TCP+Noise por cada intento denegado).
|
||
|
||
### feat(brahman-net+handshake): stop_providing automático en cleanup de sesión
|
||
Cierra el pendiente conocido del DHT: hasta ahora cuando una sesión
|
||
con outputs cerraba (Farewell, EOF, error), el record que la
|
||
anunciaba en el DHT seguía vivo hasta su TTL natural (~24h en kad
|
||
default). Consumers remotos podían descubrir un peer "vivo" que ya
|
||
no servía nada.
|
||
|
||
Cambios:
|
||
- **`BrahmanNet::stop_providing(key)`** (nuevo): contraparte simétrica
|
||
de `start_providing`. Manda `Command::StopProviding` al swarm que
|
||
llama `kad.stop_providing(&key)`. Borra el record del provider
|
||
store local al instante; replicas en peers remotos siguen
|
||
expirando por TTL (kad no expone retracción cross-peer, simétrico
|
||
al hecho de que `start_providing` también propaga eventualmente).
|
||
- **`brahman_handshake::network::withdraw_outputs(net, card)`**
|
||
(nuevo): contraparte de `announce_outputs`. Itera `card.flow.output`
|
||
y llama `net.stop_providing(flow_dht_key(...))` por cada uno.
|
||
- **`server::cleanup`**: extrae la `ResolvedCard` removida del registro
|
||
de sesiones (en lugar de descartarla con `remove`) y, si
|
||
`config.net` está set, llama `withdraw_outputs(net, &card)` antes
|
||
de `broadcast_match_diffs`.
|
||
|
||
Tests: nuevo E2E `dht_discovery_withdraws_on_session_cleanup`:
|
||
1. A registra Card con `flow.output = monad-list:json`.
|
||
2. B descubre a A vía `find_remote_providers` — confirma
|
||
`before.contains(&a_peer)`.
|
||
3. Cliente local de A hace `farewell` → cleanup → withdraw_outputs.
|
||
4. Espera a que la sesión salga del registro (señal de cleanup
|
||
completado) + 100ms para que el swarm procese el Command.
|
||
5. Nueva query desde B: `after` NO debe contener `a_peer`.
|
||
|
||
3 tests verdes en `network_discovery.rs` (positivo, negativo,
|
||
withdraw). 18 tests totales en handshake + net.
|
||
|
||
Pendiente futuro: retracción cross-peer en kad (requeriría extensión
|
||
del protocolo libp2p, no soportada hoy). Aceptable: simétrico al
|
||
modelo de propagación eventual del DHT.
|
||
|
||
### feat(ente-zero): wire de Arje con brahman-net (red P2P opcional + identidad persistente)
|
||
Cierra el último pendiente del plan de red: Arje ahora puede arrancar
|
||
opcionalmente con `BrahmanNet` configurado, persistir su identidad
|
||
libp2p entre reboots, y participar en la malla brahman como nodo
|
||
público. Sin breaking changes: usuarios actuales (sin env vars) siguen
|
||
viendo el comportamiento Unix-only de antes.
|
||
|
||
Activación por env vars:
|
||
- **`BRAHMAN_LISTEN_MULTIADDR`** — si set, activa la red P2P. Ej:
|
||
`/ip4/0.0.0.0/tcp/4101` (público), `/ip4/127.0.0.1/tcp/0` (loopback,
|
||
port aleatorio). Sin la var, `brahman_net = None` y todo sigue
|
||
como antes.
|
||
- **`BRAHMAN_KEYPAIR_PATH`** — override del path donde se persiste
|
||
la keypair Ed25519 de identidad libp2p del nodo. Defaults sensatos:
|
||
- PID 1 (root): `/var/lib/brahman/init-keypair.bin`.
|
||
- Dev mode: `$XDG_DATA_HOME/brahman/init-keypair.bin` →
|
||
`$HOME/.local/share/brahman/init-keypair.bin` →
|
||
`/tmp/brahman-init-keypair.bin` (último recurso).
|
||
- **`BRAHMAN_BOOTSTRAP_PEERS`** — lista coma-separada de multiaddrs
|
||
para dial-ear al arranque y entrar al DHT. Sin esto, el nodo
|
||
arranca aislado hasta que alguien se conecte a él.
|
||
|
||
Comportamiento al activarse:
|
||
1. `keypair_store::load_or_generate(path)` carga la keypair de disco
|
||
o genera+persiste una nueva (32 bytes raw, permisos 0o600,
|
||
atomic rename). Reboots conservan el `peer_id`.
|
||
2. `BrahmanNet::with_keypair(kp)` arma el swarm con esa identidad.
|
||
3. `net.listen(multiaddr)` espera dirección resuelta y la loggea.
|
||
4. `BRAHMAN_BOOTSTRAP_PEERS` (si set) → dial a cada multiaddr.
|
||
5. El handshake server se levanta con `ServerConfig.net = Some(net)`,
|
||
que activa `announce_outputs` automático en el DHT por cada Card
|
||
con outputs.
|
||
6. Además del Unix accept loop (existing), se monta un libp2p accept
|
||
loop sobre el mismo `Server` compartido. Sesiones locales y
|
||
remotas conviven en las mismas tablas (sessions, push_table,
|
||
broker, last_matches).
|
||
|
||
Refactor del Unix accept loop: antes consumía el server vía
|
||
`server.run().await`; ahora usa `Arc<Server>::accept_one().await` en
|
||
loop para coexistir con el libp2p accept loop sin moverse el server.
|
||
|
||
Degradación grácil en cada paso: si la keypair no carga, si el
|
||
multiaddr es inválido, si el listen falla, si el bootstrap dial
|
||
revienta — loggeamos y seguimos en modo Unix-only. La doctrina de
|
||
PID 1 ("ningún subsistema opcional rompe el bucle primordial") se
|
||
mantiene.
|
||
|
||
Tests: 4 unit en `keypair_store`:
|
||
- `generate_persist_and_reload_yields_same_peer_id` — peer_id estable
|
||
across reloads (la propiedad fundamental).
|
||
- `rejects_corrupted_file` — archivo de tamaño incorrecto rechazado.
|
||
- `persisted_file_is_owner_only` — permisos 0o600 verificados.
|
||
- `default_path_honors_env` — `BRAHMAN_KEYPAIR_PATH` override
|
||
respeta tanto dev como root mode.
|
||
|
||
Ente-zero compila clean. Ningún test del workspace regresa.
|
||
|
||
Lo que esto desbloquea hoy:
|
||
- Para activar Arje como nodo público, basta:
|
||
```sh
|
||
BRAHMAN_LISTEN_MULTIADDR=/ip4/0.0.0.0/tcp/4101 ente-zero
|
||
```
|
||
- Cualquier consumer (en otra máquina) puede luego dial-ar a ese
|
||
multiaddr + descubrir Cards anunciadas via DHT + abrir handshake
|
||
remoto firmado.
|
||
- La identidad del nodo (su `peer_id`) sobrevive reboots, así que
|
||
los nodos remotos pueden cachear "este peer_id es Arje en
|
||
máquina X" sin invalidarse cada vez.
|
||
|
||
Pendientes futuros:
|
||
- `stop_providing` al cleanup de sesión (records DHT con TTL ~24h).
|
||
- Allowlist/Denylist de peers (PKI explícito).
|
||
- Rotación de keypair sin perder peer_id (multi-key identity).
|
||
|
||
### feat(brahman-handshake): Fase 3 — trust remoto vía firma Ed25519 anclada al peer libp2p
|
||
Cuarto y último paso del plan "el encuentro entre Entes no se
|
||
restringe a local". Cierra la falla de seguridad que dejaba la red
|
||
P2P abierta: hasta ahora, un atacante que pudiera dial-ar al
|
||
multiaddr del Init podía registrar cualquier Card con cualquier
|
||
label/flow. Fase 3 cierra esto exigiendo que el Hello vía libp2p
|
||
venga firmado con la **misma keypair Ed25519 que produce el
|
||
`peer_id` autenticado por Noise**.
|
||
|
||
Modelo:
|
||
- **Local Unix**: SO_PEERCRED del kernel autentica al cliente. Firma
|
||
opcional. Si está presente, igual se verifica (defensa en
|
||
profundidad).
|
||
- **Remoto libp2p**: firma obligatoria. La public key del Hello debe
|
||
derivar al `peer_id` que Noise ya autenticó. Si falta o no coincide
|
||
→ `HandshakeError::Unauthorized`.
|
||
|
||
Wire (`brahman_handshake::messages`):
|
||
- `Hello.signature: Option<HelloSignature>` (nuevo, default None).
|
||
- `HelloSignature { public_key: Vec<u8>, signature: Vec<u8> }` —
|
||
public_key en formato canónico libp2p (`encode_protobuf`), firma
|
||
Ed25519 sobre `(SIGNATURE_VERSION, WireCard, Option<WitInterface>)`
|
||
serializado postcard.
|
||
- `SIGNATURE_VERSION = 1` documenta el esquema del payload firmado;
|
||
bump al cambiar.
|
||
|
||
Nuevo módulo `brahman_handshake::signature`:
|
||
- `sign_hello(keypair, card, wit) -> HelloSignature`.
|
||
- `verify_hello(sig, card, wit, expected_peer) -> Result<(), SignatureError>`.
|
||
- `SignatureError` tipado (`DecodeKey`, `EncodePayload`, `Invalid`,
|
||
`PeerMismatch`, `Missing`, `Unexpected`).
|
||
|
||
Server:
|
||
- `Session<S>` gana `expected_peer: Option<PeerId>`.
|
||
- `Server::session_from_libp2p_stream(stream, peer)` (nuevo)
|
||
construye Session con `expected_peer = Some(peer)`.
|
||
`session_from_stream` (Unix/in-memory) sigue con `None`.
|
||
- `do_handshake` exige firma + verifica peer match cuando
|
||
`expected_peer.is_some()`. Si no, verifica firma presente por
|
||
consistencia interna pero no exige que esté.
|
||
- `network::run_libp2p_accept_loop` ahora usa
|
||
`session_from_libp2p_stream(stream.compat(), peer)` para propagar
|
||
la identidad libp2p al gate de trust.
|
||
|
||
Client:
|
||
- `Client::connect_with_stream_signed(stream, card, wit, &Keypair)`
|
||
(nuevo) firma el Hello antes de mandarlo.
|
||
- `Client::connect_with_stream` sigue existiendo sin firma (path
|
||
Unix / tests).
|
||
- `Client::connect`/`connect_with` (Unix) no cambian — siguen sin
|
||
firma porque SO_PEERCRED autentica.
|
||
- `network::connect_libp2p(net, peer, card, wit, keypair)`
|
||
**breaking change**: gana parámetro `keypair: &Keypair`.
|
||
|
||
BrahmanNet:
|
||
- Almacena la `Keypair` en `Arc<Keypair>` (libp2p Keypair no es
|
||
Clone; el truco es duplicar el `ed25519::Keypair` interno que sí
|
||
es Clone, una copia para Noise/swarm y otra para signing).
|
||
- `BrahmanNet::keypair() -> Arc<Keypair>` accessor para que callers
|
||
puedan firmar con la misma identidad libp2p del nodo sin tener
|
||
que mantener la keypair por separado.
|
||
- `with_keypair` rechaza keypairs no-Ed25519 (RSA/ECDSA/Secp256k1
|
||
vendrían a futuro si se necesitan).
|
||
|
||
Tests:
|
||
- 4 unit en `signature::tests`: roundtrip propio, peer mismatch,
|
||
card tampered, signature flipped.
|
||
- 1 E2E nuevo en `tests/network_libp2p.rs`:
|
||
`libp2p_handshake_rejects_mismatched_signing_key` — el cliente
|
||
intenta firmar con keypair distinta a la del net; server rechaza.
|
||
- E2E positivo (`libp2p_handshake_roundtrip`) ahora pasa la keypair
|
||
del client_net y debe verificar OK.
|
||
- Discovery + handshake legacy + signature: 90+ tests verdes en
|
||
brahman-handshake/brahman-net/brahman-card/minga-p2p.
|
||
|
||
Lo que esto cierra:
|
||
- Brahman-net es una malla públicamente dial-able **con
|
||
autenticación criptográfica end-to-end**: Noise para el transport,
|
||
Ed25519 para las Cards.
|
||
- La cadena completa de discovery + connect + trust funciona
|
||
cross-machine sin paths hardcodeados ni confianza implícita.
|
||
- El plan original ("el encuentro entre Entes no se restringe a
|
||
local, la ejecución remota está pensada desde el principio")
|
||
está implementado y testeado.
|
||
|
||
Pendientes (futuro, no de hoy):
|
||
- `stop_providing` al cleanup de sesión (records DHT viven hasta
|
||
TTL ~24h).
|
||
- Wire de Arje (`ente-zero`) para arrancar opcionalmente con
|
||
`BrahmanNet` configurado y `ServerConfig.net = Some(...)`.
|
||
- Allowlist/Denylist de peers (hoy cualquier peer Ed25519-válido
|
||
pasa el trust gate; producción podría querer un PKI explícito).
|
||
- Persistencia de la keypair de identidad del nodo entre reboots.
|
||
|
||
### feat(brahman-handshake): Fase 2 — discovery remoto vía DHT por flow type
|
||
Tercer paso del plan "el encuentro entre Entes no se restringe a
|
||
local". Cuando un Init local acepta una sesión cuya Card declara
|
||
outputs, anuncia al DHT (Kademlia, vía `brahman-net`) que él provee
|
||
esos flow types. Cualquier nodo conectado al mismo DHT puede
|
||
consultar y obtener la lista de `PeerId`s que sirven el flow.
|
||
|
||
API nueva en `brahman_handshake::network`:
|
||
- `flow_dht_key(flow_name, type_ref) -> [u8; 32]`: blake3 hash de
|
||
`"brahman-flow|v1|{flow}|{type_canon}"`. Determinístico cross-host.
|
||
Cambiar la canonicalización rompe compatibilidad — el prefijo `v1`
|
||
documenta la versión del esquema y obliga a bump al modificar.
|
||
- `announce_outputs(net, card)`: llama `start_providing` en el DHT
|
||
por cada `Flow` en `card.flow.output`. Idempotente, fire-and-forget.
|
||
- `find_remote_providers(net, flow_name, type_ref) -> Vec<PeerId>`:
|
||
query DHT por la key derivada. Lista vacía si nadie anuncia o si
|
||
la query no resuelve dentro del timeout interno de Kad.
|
||
|
||
Wire en el server:
|
||
- `ServerConfig` gana `pub net: Option<Arc<BrahmanNet>>`. Si está set,
|
||
cada Card registrada con outputs se anuncia automáticamente al DHT
|
||
desde `register_session`. `None` = server "ciego al DHT" (correcto
|
||
cuando no hay conectividad o el operador no quiere exponer).
|
||
- `ServerConfig` ahora tiene `Debug` manual (BrahmanNet no implementa
|
||
Debug; loggeamos sólo presencia/ausencia).
|
||
|
||
Canonicalización del TypeRef:
|
||
- `Primitive { name }` → `prim:{name}`
|
||
- `Wit { package, interface, name }` → `wit:{package}#{interface_or_empty}#{name}`
|
||
|
||
Tests: 2 nuevos en `tests/network_discovery.rs`:
|
||
- `dht_discovery_finds_remote_provider`: dos nodos, A registra Card
|
||
con `flow.output = monad-list:json`, B dial-ea a A y descubre el
|
||
`peer_id` de A vía `find_remote_providers`. Asserts contains.
|
||
- `dht_discovery_negative_unknown_flow`: B busca un flow que nadie
|
||
anunció, devuelve lista vacía sin colgarse.
|
||
|
||
Lo que esto desbloquea:
|
||
- Un `nouser daemon` corriendo en máquina A puede ser descubierto por
|
||
un `nouser-explorer` en máquina B sin conocimiento previo del peer
|
||
— sólo necesitan compartir DHT (vía bootstrap inicial).
|
||
- La cadena completa "explorer → daemon → llm-provider" puede cruzar
|
||
máquinas, no sólo procesos.
|
||
|
||
Lo que queda para Fase 3 (trust):
|
||
- Cards remotas se aceptan hoy sin verificación. Para producción se
|
||
necesita firma Ed25519 sobre la Card y verificación antes de
|
||
aceptar el Hello remoto. Local sigue confiando en SO_PEERCRED.
|
||
- Stop-providing al cleanup de sesión (hoy records DHT viven hasta
|
||
TTL ~24h aunque la sesión cierre).
|
||
|
||
### feat(brahman-handshake): Fase 1 — handshake brahman sobre stream libp2p
|
||
Segundo paso del plan "el encuentro entre Entes no se restringe a
|
||
local". El protocolo brahman (Hello / HelloAck / Ping / Pong /
|
||
MatchEvent / Farewell, frames postcard length-prefixed) ahora también
|
||
viaja sobre streams libp2p de la malla `brahman-net` — el mismo Init
|
||
acepta sesiones por Unix socket Y por libp2p indistintamente, y un
|
||
consumer remoto puede dial-ar al multiaddr y completar handshake.
|
||
|
||
Cambios:
|
||
- **`Session<S>` y `Client<S>` genéricos**: ambos dejan de estar atados
|
||
a `UnixStream` y pasan a ser genéricos sobre `S: AsyncRead +
|
||
AsyncWrite + Unpin + Send + 'static`. El path Unix queda como
|
||
`Client = Client<UnixStream>` (default genérico). Constructores
|
||
nuevos: `Server::session_from_stream(stream)`,
|
||
`Client::connect_with_stream(stream, card, wit)`.
|
||
- **Refactor del post-handshake con split**: `tokio::select!` sobre
|
||
`&mut self.stream` requería `S: Sync` indirectamente, y
|
||
`libp2p::Stream` no es Sync. Reemplazado por
|
||
`tokio::io::split(stream)` → reader loop principal + writer task
|
||
separada que drena el push channel. Writer compartido bajo
|
||
`Arc<Mutex<WriteHalf<S>>>` para serializar Pong/Error inline con
|
||
los MatchEvents pusheados. Cleanup garantizado en todas las ramas.
|
||
La lógica del post-handshake migra a funciones libres
|
||
(`run_post_handshake`, `handle_inbound_frame`, `cleanup`,
|
||
`broadcast_match_diffs`, `do_handshake`, `register_session`,
|
||
`validate_hello`).
|
||
- **Nuevo módulo `brahman-handshake::network`**:
|
||
- `BRAHMAN_HANDSHAKE_PROTOCOL = "/brahman/handshake/1.0.0"`.
|
||
- `LibP2pHandshakeStream = Compat<libp2p::Stream>` (alias del
|
||
stream una vez convertido al mundo `tokio::io`).
|
||
- `run_libp2p_accept_loop(server, net)`: bucle accept sobre el
|
||
protocolo que delega cada stream entrante a una `Session`
|
||
construida vía `server.session_from_stream(stream.compat())`.
|
||
Sesiones libp2p y Unix conviven en el mismo `Server` —
|
||
comparten broker, push table, last_matches.
|
||
- `connect_libp2p(net, peer, card, wit)`: abre stream libp2p al
|
||
`peer` y arranca handshake.
|
||
- `NetworkError` tipado (`OpenStream`, `Handshake`, `AcceptStream`).
|
||
|
||
Deps: `brahman-handshake` gana `brahman-net`, `futures`, `tokio-util`.
|
||
`brahman-net` re-exporta `Multiaddr`, `PeerId`, `Stream`,
|
||
`StreamProtocol`, `Protocol`, `OpenStreamError` para que callers no
|
||
necesiten dep directa a libp2p.
|
||
|
||
Tests:
|
||
- 9 tests unit + integration verdes (sin regresión en el path Unix).
|
||
- Nuevo `tests/network_libp2p.rs`: test E2E que arma server con
|
||
Unix socket + BrahmanNet, hace listen TCP, monta el accept loop;
|
||
cliente con su propio BrahmanNet dial-ea al peer_id, completa
|
||
handshake remoto, pinguea, farewell. Verifica que la sesión se
|
||
registró durante la conversación y se removió tras farewell.
|
||
|
||
Próximo: Fase 2 (discovery remoto vía DHT — anunciar Cards bajo
|
||
flow type, broker query local + remoto).
|
||
|
||
### feat(brahman-net): capa P2P compartida — Fase 0 (extracción del swarm libp2p)
|
||
Primer paso del plan "el encuentro entre Entes no se restringe a local".
|
||
El swarm libp2p que vivía dentro de `minga-p2p::network` (282 LOC) sale
|
||
a una crate compartida `brahman-net` para que cualquier protocolo de la
|
||
familia (handshake brahman remoto en Fase 1, sync minga, futuros) reuse
|
||
una sola malla TCP+Noise+Yamux+Kad+Identify+Stream.
|
||
|
||
Diseño:
|
||
- `BrahmanNet::{new, with_keypair}` arma el swarm con DHT en modo
|
||
Server, Identify auto-poblando el routing table de Kad, y un
|
||
`stream::Control` accesible para que cada protocolo registre su
|
||
`StreamProtocol` y abra/acepte streams sin acoplarse al swarm.
|
||
- API de comandos uniforme: `dial`, `listen`, `add_dht_peer`,
|
||
`find_closest_peers`, `start_providing`, `find_providers`.
|
||
- Pública: `peer_id` (libp2p) + `control` (stream::Control).
|
||
- Re-exporta `Stream` y `StreamProtocol` para que callers no necesiten
|
||
importar libp2p directo.
|
||
|
||
Migración:
|
||
- `minga-p2p::network` reduce de 282 LOC a 22: ahora sólo re-exporta
|
||
`BrahmanNet` bajo el alias histórico `LibP2pNode` (zero churn en
|
||
`MingaPeer`) y declara la const `SYNC_PROTOCOL = "/minga/sync/1.0.0"`
|
||
específica del sub-protocolo Minga.
|
||
- Cualquier consumer que necesite armar un nodo P2P puede importar
|
||
`brahman_net::BrahmanNet` directo sin pasar por minga.
|
||
- Deps de `minga-p2p` ganan `brahman-net`; el resto del grafo
|
||
(libp2p, libp2p-stream, futures, tokio-util) sigue igual porque
|
||
`MingaPeer` aún los usa para la lógica específica de sync.
|
||
|
||
Aclaración semántica anclada por el usuario: **Arje** es el init
|
||
(PID 1, runtime, ente-zero/kernel/soma); **Brahman** es el encuentro
|
||
entre Entes (handshake/broker/card/sidecar/ahora también net). El
|
||
nombre de la crate refleja que la malla pertenece al encuentro, no
|
||
al runtime — Arje puede usar la malla, Minga usa la malla, cualquier
|
||
futuro módulo (Nakui remoto, p.ej.) la usa, sin acoplarse a Minga.
|
||
|
||
Tests: minga-p2p completo verde (58 tests, sin regresión). Behavior
|
||
verificado idéntico — sólo se movió código, ningún cambio funcional.
|
||
Próximo: Fase 1 (handshake brahman sobre libp2p stream).
|
||
|
||
### refactor(explorer+card): independencia jerárquica enforced — cliente con los wire types + fallback al default path
|
||
Cierra el único debt estructural detectado en el audit de
|
||
independencia: `nouser-explorer` ya no arrastra `nouser-core`
|
||
(que aportaba `notify`/`walkdir`/`sled`/`blake3` al grafo de
|
||
compilación de una UI que sólo habla JSON contra un socket).
|
||
|
||
Cambios:
|
||
- **Cliente movido**: `engine_socket::client::list_monads` (~60 LOC,
|
||
std + serde_json puros) emigra de `nouser_core::engine_socket` a
|
||
`nouser_card::query::client`. Vive donde viven los wire types,
|
||
consistente con el principio "un consumer importa el contrato,
|
||
no el runtime del productor".
|
||
- **Drop dep**: `nouser-explorer` deja de dependener de
|
||
`nouser-core`. Verificado con `cargo tree`: `notify`, `sled`,
|
||
`blake3` desaparecen del grafo del binario. (`walkdir` sigue
|
||
pero llega vía `gpui_util` → `rust-embed`, fuera de nuestro
|
||
control y pre-existente.)
|
||
- **Fallback "falla hacia la simplicidad"**: nueva función
|
||
`resolve_socket()` en el explorer intenta primero broker
|
||
discovery; si el broker no responde / no hay init vivo,
|
||
fallback directo a `nouser_card::query::transport::default_socket_path()`.
|
||
El explorer queda funcional contra un daemon "huérfano"
|
||
(corriendo standalone sin init) — completa la cadena
|
||
"consciente cuando hay ecosistema, soberano cuando está solo".
|
||
- `socket_source` en el header gana un tercer estado
|
||
`"default-path"` para que el usuario vea por dónde se conectó.
|
||
|
||
Audit estructural confirmó que el resto del ecosistema ya
|
||
respeta el principio: todos los `yahweh-*` viewers, `minga-cli`,
|
||
`minga-core`, `nouser-card`, `nouser-nous`, los providers
|
||
`nouser-nous-{mock,real}` y `nakui-core` corren standalone con
|
||
soft-fail hacia infra brahman cuando está ausente. Brahman es
|
||
"pegamento opcional, no chasis obligatorio" — y ahora el grafo
|
||
de Cargo lo enforcea, no sólo la convención.
|
||
|
||
Tests: 4 (sidecar) + 10 (nouser-card) + 27 (nouser-core) verdes.
|
||
El cliente movido se ejercita end-to-end por los 3 tests integración
|
||
de `engine_socket` (importa ahora `nouser_card::query::client`).
|
||
|
||
### feat(explorer+daemon): discovery dinámico vía broker + query socket
|
||
La UI deja de hardcodear el socket admin: ahora descubre al daemon
|
||
nouser vía `MatchEvent::Available` del broker brahman y le consulta
|
||
sus Mónadas directo, sin pasar por brahman-admin. Cierra el "explorer
|
||
encuentra al daemon de forma totalmente dinámica" del meta-plan.
|
||
|
||
Pipeline end-to-end:
|
||
- Daemon publica engine Card con `service_socket = $XDG_RUNTIME_DIR/nouser-engine.sock`
|
||
y `flow.output = monad-list:json`.
|
||
- Daemon binda un Unix socket en ese path y monta un listener
|
||
blocking que sirve `nouser_card::query::QueryRequest::ListMonads`,
|
||
responde `ListMonadsResponse { engine, monads: Vec<MonadView> }`.
|
||
- Explorer construye un consumer Card con `flow.input = monad-list:json`
|
||
vía `brahman_sidecar::build_consumer_card`, llama
|
||
`await_provider_blocking(card, 3s)` y recibe el socket descubierto.
|
||
- Cachea ese socket; cada poll (2s) llama
|
||
`nouser_core::engine_socket::client::list_monads(socket, 2s)`.
|
||
Fallo de query → invalida cache → próximo tick re-descubre.
|
||
|
||
Wire types nuevos en `nouser_card::query`:
|
||
- `QueryRequest::ListMonads` (single variant por ahora).
|
||
- `ListMonadsResponse { engine: EngineInfo, monads: Vec<MonadView> }`.
|
||
- `MonadView`: proyección slim de `MonadManifest` SIN `centroid` ni
|
||
`members` — la UI no los necesita y eran KB por Mónada que no
|
||
tenían por qué viajar cada poll.
|
||
- `transport::default_socket_path()` con env override
|
||
`NOUSER_ENGINE_SOCKET`.
|
||
- Const `FLOW_MONAD_LIST = "monad-list"`, `FLOW_TYPE_NAME = "json"`.
|
||
|
||
Listener en `nouser_core::engine_socket`:
|
||
- `spawn_listener(config, db)` arma std::os::unix::net::UnixListener
|
||
en thread blocking dedicado. Frecuencia esperada (UI cada 2s) no
|
||
amerita tokio.
|
||
- `client::list_monads(socket, timeout)` — cliente blocking con
|
||
`QueryError` tipado (Connect / Io / Serde / Daemon / Timeout / Empty).
|
||
- 3 tests integración: roundtrip vacío, Mónadas reales, request
|
||
inválido devuelve ErrorResponse.
|
||
|
||
Refactor explorer:
|
||
- Drop dep `brahman-admin`, add deps `brahman-sidecar`, `nouser-card`,
|
||
`nouser-core`.
|
||
- State: `socket: Option<PathBuf>` cache + `snapshot: Option<ListMonadsResponse>`
|
||
+ `socket_source: "discovery"|"cache"` (sólo informativo).
|
||
- Tick: `tick(prior_socket)` separado del UI, devuelve un enum
|
||
`TickOutcome::{Ok, DiscoveryFailed, QueryFailed}`. Cualquier
|
||
fallo invalida la cache → re-discovery automática.
|
||
- Header reformulado: `Engine 'nouser_engine' · N mónada(s) ·
|
||
socket: /... (cache|discovery) · watching: /tmp/x`.
|
||
- Render pintado de un engine card + Mónadas, sin ya iterar
|
||
`BrokeredCard` del admin.
|
||
|
||
Trade-offs aceptados:
|
||
- Polling 2s (no streaming). El broker no empuja cambios de Data
|
||
cards hoy; agregar streaming requiere extender el protocolo
|
||
handshake. Para snapshot UI, polling 2s es suficiente.
|
||
- Re-descubrimiento full en cada error de query (en lugar de retry
|
||
con backoff). Discovery es barato (~ms vs broker), no vale la
|
||
pena la complejidad.
|
||
|
||
Tests: 10 (nouser-card, +3 query) + 27 (nouser-core, +3 engine_socket)
|
||
+ 4 (sidecar) verdes. Explorer compila clean.
|
||
|
||
### 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.
|
||
|
||
### 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):
|
||
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).
|
||
|
||
### 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:
|
||
|
||
- `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).
|
||
|
||
### 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).
|