# 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<SharedString>)
  -> 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<T>` por deref auto-coerce).
  - `value: impl Into<SharedString>` — 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<SharedString>`).
- **`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<SharedString>` 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<PathBuf>`**: resuelve el path
  XDG. Devuelve `None` si ni `XDG_CONFIG_HOME` ni `HOME` están
  set (sandbox/CI).
- **`pub fn load_persisted() -> Option<Theme>`**: 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: <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.

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

