# Changelog — nahual Motor GPUI: libs + widgets. Renombrado de `yahweh` el 2026-05-19. ### feat(yahweh-launcher): F3 — extracción del shell standard de explorers Iter 19. Patrón con 4 consumers idénticos (nakui-explorer, nouser-explorer, minga-explorer, brahman-broker-explorer) declaraban ~20 líneas de boot: `Application::new + Theme::install_default + cx.open_window + WindowOptions{ window_bounds, titlebar } + cx.activate(true)`. Todo idéntico salvo título, tamaño, y root entity factory. Crate nuevo `crates/modules/ui_engine/libs/launcher/` (`yahweh-launcher`): - **Deps**: `gpui`, `yahweh-theme`. Sin más. - **`pub fn launch_app(title, size, root_factory)`**: 1-line boot. - **`pub fn launch_app_with(config, root_factory)`**: variante con `AppLaunchConfig` armado afuera, para casos donde título/tamaño se computan condicionalmente (env var, etc). - **`AppLaunchConfig::new(title, size)`**: builder normalizador. - 2 tests `#[test]` cubren la normalización de config (no se testea `launch_app` per se porque bloquea el thread main hasta que la ventana se cierra). Migración 4 consumers: - `main()` pasa de 20 líneas a 1: `launch_app("Title", (W, H), Explorer::new)`. - Imports de gpui se podan: ya no necesitan `App, Application, Bounds, WindowBounds, WindowOptions` ni `SharedString` ni `prelude::*`. - Cada consumer agrega dep `yahweh-launcher` (path local). Naming: el slot natural era `yahweh-shell`, pero ya existe un crate `yahweh-shell` en `crates/apps/` (bootstrap heavyweight con file explorer + DB explorer + viewers). El helper es liviano y específico al patrón de launch, así que `yahweh-launcher` evita confusión. Total ahorro: ~75 líneas hardcoded de boilerplate UI a 4 líneas en total. Cambios de boot (window opts, theme, etc) ahora viven en un solo lugar. Tests stack: 2 nuevos en launcher; suites de los 4 consumers intactas. Todo verde. ### feat(yahweh-widget-app-header): promover el header standard de explorers Iter 16. Patrón con 4 consumers idénticos: `nakui-explorer`, `nouser-explorer`, `minga-explorer`, `brahman-broker-explorer` todos declaraban un header `flex_row + flex_grow(label) + theme_switcher + bg(panel) + border-bottom + text_size(14) + padding(16/12)`. Ahora es 1 línea. Crate nuevo `crates/modules/ui_engine/widgets/app-header/` (`yahweh-widget-app-header`): - **Deps**: `gpui`, `yahweh-theme`, `yahweh-widget-theme-switcher`. El switcher se incluye automáticamente. - **`pub fn app_header(cx: &mut App, label: impl Into) -> impl IntoElement`**: caso simple con texto plano. - **`pub fn app_header_with(cx, label_child: impl IntoElement)`**: variante para cuando el lado izquierdo no es texto plano (icon + text, múltiples spans, etc.). - 3 tests `#[gpui::test]`: smoke con string label, con custom child IntoElement, type-check de label con literal/owned/format!. Migración de los 4 consumers: - Cada uno reemplaza un bloque `let header = div().flex().flex_row()... .child(theme_switcher(cx))` (~13 líneas) por `let header = app_header(cx, header_text)` (~1 línea). - Cada uno borra dep `yahweh-widget-theme-switcher` (ya no la necesita directo — el `app_header` la incluye internamente). - Cada uno reemplaza `use yahweh_widget_theme_switcher::theme_switcher` por `use yahweh_widget_app_header::app_header`. Total ahorro: ~50 líneas de código UI hardcoded en consumers. Cambios visuales en el header (padding, border, text_size) ahora viven en un solo lugar. Tests stack: 3 nuevos en app-header; suites de los 4 consumers intactas. Todo verde. Decisión: el sidebar header del `MetaApp` (que también incluye theme_switcher) NO se migra — es un header de sidebar, no de app top, y tiene styling distinto (px(12/10/13), sin bg/border-bottom porque ya está dentro del panel). Diferente patrón → diferente widget si emerge segundo consumer. ### feat(yahweh-widget-stat-card): promover el patrón stat card como widget Iter 15. El patrón "tarjeta de dashboard con border-l accent + label + valor grande + descripción + listing opcional" tenía 2 consumers (`minga-explorer` y `brahman-broker-explorer`); ahora vale extraer al stack yahweh para reusabilidad y mantenimiento single-place. Crate nuevo `crates/modules/ui_engine/widgets/stat-card/` (`yahweh-widget-stat-card`): - **Deps**: `gpui` + `yahweh-widget-card` (compone `card_themed`). Sin theme directo — el caller pasa `text` y `text_dim` ya resueltos del theme. - **`pub fn stat_card(cx, label, value, description, accent, text, text_dim, recent_items)`**: - `cx: &App` (acepta `&Context` por deref auto-coerce). - `value: impl Into` — sirve para counts (`"3"`), status text (`"UP"`), o cualquier label corto. - `recent_items: &[String]` — si no vacío, agrega sub-header `"recent (N):"` + una linea por item. - 3 tests `#[gpui::test]` con TestAppContext: smoke con/sin recent_items, type-check de `value` con literal/format/owned. - Dev-deps: gpui con `test-support` + yahweh-theme para construir el cx con un theme global. Cambios consumer: - **`minga-explorer`**: sustituye su `fn stat_card` local (~60 líneas) por `use yahweh_widget_stat_card::stat_card`. Borra dep `yahweh-widget-card` (ya no se usa directo). Adapta los 3 callsites para pasar `value.to_string()` (el widget acepta `Into`). - **`brahman-broker-explorer`**: refactoriza su `fn state_card` para que sea un wrap de `stat_card` con la traducción `ProbeState → (accent, value, description)`. La función queda como helper local porque la mapping del enum es app-specific, pero el rendering pasa por el widget compartido. Borra dep `yahweh-widget-card`. Tests stack: nuevos 3 del widget. Suites de los 2 consumers intactas (4 minga-explorer, 2 broker-explorer). Stack total ~120 verdes (varía por compilation cache). Beneficio operativo: - Cualquier app nueva que necesite cards de dashboard usa `stat_card(...)` directo; no re-implementa el pattern. - Cambios visuales (text sizes, padding, sub-header format) ahora viven en un solo lugar. - `value: impl Into` es más expressive que el `usize` rígido del original local. Pequeña simplificación documentada: el sub-header del listing pasa de `"recent (N de TOTAL):"` a `"recent (N):"`. El "TOTAL" ya no se calcula porque el widget no lo conoce — el caller que quiera mostrarlo lo formatea en el label/value (ej. label `"Nodos AST (5 de 247)"`). Acceptable trade-off por la reusabilidad genérica. ### feat(yahweh-theme): persistencia de la preferencia de theme entre runs Iter 13. El theme switcher ya cambiaba el chrome en runtime, pero al cerrar y reabrir la app el theme volvía a Nebula default. Ahora el name del theme se persiste en `$XDG_CONFIG_HOME/yahweh/theme` (default `~/.config/yahweh/theme`) y se restaura al boot. Cambios en `yahweh-theme`: - **`pub fn config_path() -> Option`**: resuelve el path XDG. Devuelve `None` si ni `XDG_CONFIG_HOME` ni `HOME` están set (sandbox/CI). - **`pub fn load_persisted() -> Option`**: lee el archivo, trim, busca el theme por name vía `Theme::by_name`. `None` si el file no existe, lectura falla, o el name no matchea ningún preset (e.g. preset renombrado entre versiones). - **`pub fn persist(theme: &Theme) -> io::Result<()>`**: escribe el name al config file. Crea el dir parent si no existe. - **`pub fn load_from_path` y `pub fn persist_to_path`**: variantes con path explícito — útiles para tests con tempfile y para apps que quieren un path custom (multi-user, staging, etc.). - **`Theme::install_default(cx)` cambia**: antes hardcoded `nebula()`. Ahora intenta `load_persisted()`, fallback a Nebula. - **`Theme::set(cx, theme)` cambia**: antes sólo `cx.set_global`. Ahora también `persist(&theme)` antes (best-effort: ignora io errors). El `theme_switcher` widget ya consume `Theme::set`, así que sin cambios en su código el switching ahora persiste. 5 tests nuevos (`persistence_tests`): - `persist_then_load_round_trip` — escribir + leer Aurora. - `load_from_missing_file_returns_none` — no rebota. - `load_from_unknown_name_returns_none` — name desconocido → `None` (degrada a default cuando se usa). - `persist_creates_parent_dir_if_missing` — crea `~/.config/yahweh/` si no existe. - `config_path_uses_xdg_config_home_when_set` — respeta el env. Tests stack: ~5 nuevos en yahweh-theme. Todos los downstream (nakui-ui, *-explorer) compilan sin tocar nada — la API pública de `Theme::install_default` y `Theme::set` no cambió shape. Smoke run del binario verificado: bootstrap OK, panic esperado sin display. Beneficio operativo: - Usuario abre `nakui-ui`, cicla a Aurora con el switcher, cierra app. Próxima apertura: Aurora cargado del disco. Todas las apps yahweh-themed (4 del repo) comparten la misma preferencia. - Failure mode benigno: sin home dir o sin permisos de write, el theme cambia in-memory pero no se persiste — el switcher sigue usable, sólo no sobrevive al close. - Path canónico documentado: usuarios que quieran preset el theme antes de abrir la app pueden hacer `echo Aurora > ~/.config/yahweh/theme`. ### 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: ▸"` 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)` — 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` 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. ### 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`** 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` y al `impl Render for MetaApp`. - **`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::::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>`, `Option>>`, `BTreeMap>`, `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` 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` genérico, `nakui-ui` queda como ~50 líneas de shell con `MetaApp::::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`. 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).