# Changelog — nahual

Motor GPUI: libs + widgets. Renombrado de `yahweh` el 2026-05-19.

### feat(meta-form): pulido de formularios (Fase 7 del ERP nakui)

Cierre del plan ERP. Pulido de la metainterfaz:

- **Validación inline** — al fallar un submit por campos `required`
  vacíos, el form ya no sólo muestra un toast genérico: marca cada
  campo faltante (label en color destructivo + mensaje debajo).
  `MetaApp.form_errors` + `validate_required_fields`. `AutoId` se
  exime (se autogenera).
- **Secciones de formulario** — `FieldSpec.section` agrupa campos
  consecutivos bajo un encabezado en el render del formulario.

### feat(meta-form): export CSV de listas (Fase 6 del ERP nakui)

- Toda vista de lista gana un botón «⬇ CSV» que exporta las filas
  **filtradas y ordenadas** (todas, no sólo la página) a un archivo
  `<entity>-<timestamp>.csv` en el directorio de trabajo; avisa por
  toast con la ruta.
- Las celdas exportadas usan el mismo render que la lista —
  referencias resueltas a su nombre, montos formateados.
- Serializador `to_csv` (RFC 4180, con escape de comas/comillas/saltos)
  en el módulo nuevo `meta-runtime/csv.rs`, con tests.
- Refactor: `list_filtered_sorted` extraído como helper compartido
  entre el render de la lista y el export.

### feat(meta-*): tablero de KPIs (Fase 5 del ERP nakui)

Cuarta clase de vista de la metainterfaz:

- **`View::Dashboard(DashboardView)`** — grilla de tarjetas de KPI.
  Cada `DashboardCard` declara su `entity`, su `metric` y un `filter`
  opcional.
- **`Metric`** — `Count`, `Sum { field }` y `GroupBy { field }`.
  `compute_metric` (nuevo módulo `meta-runtime/metric.rs`) los computa:
  `MetricResult::Scalar` para conteos/sumas, `Breakdown` (ranking) para
  agrupaciones. `CardFilter` restringe los records contados.
- `meta-form`: `render_dashboard` — tarjetas con el número grande
  (formateado vía `format_value`) o un breakdown con barras de texto.

Tests de `compute_metric` en `meta-runtime`; verificación del tablero
del CRM en `nakui-ui`.

### feat(meta-form): listas profesionales — orden, búsqueda, paginación

Fase 4 del ERP nakui. Las vistas de lista de `meta-form` ganan:

- **Orden por columna** — clic en un header cicla ascendente →
  descendente → sin orden (indicador ▲/▼). Comparación de valores con
  `cmp_values` (nuevo en `meta-runtime`): números por valor, strings
  case-insensitive, `null` primero.
- **Búsqueda en vivo** — caja 🔍 que filtra por substring contra las
  columnas de `search_in` mientras se teclea (vía `cx.observe` del
  `TextInput`). `search_in` ya existía en el schema; ahora se renderiza.
- **Paginación** — 25 filas por página, controles ◀ ▶ y «página N/M».

Sin cambios de schema: orden y página son estado del widget, se
reinician al navegar. Helpers puros `cmp_values` y `next_sort` con
tests.

### feat(meta-*): ficha de detalle (Fase 3 del ERP nakui)

La metainterfaz gana una tercera clase de vista:

- **`View::Detail(DetailView)`** — ficha de un record: sus `fields`
  (reusan `Column`, con resolución de refs y formato) + `related`
  (listas de back-references) + botones «← Volver» / «✎ Editar».
- **`RelatedList`** — declara una lista de records relacionados por
  `via_field`: el runtime filtra los records de otra entity cuyo campo
  apunta al record que se ve (las oportunidades de un cliente, etc.).
- **`ListView.row_detail`** — enlaza lista → ficha: cada fila gana un
  botón 👁 que abre la ficha del record. `Module::validate` exige que
  apunte a una vista `Detail`.
- `meta-form`: `render_detail` + `render_related`, navegación
  `select_detail` con retorno a la lista de origen.

Tests en `meta-schema` y `nakui-ui`.

### feat(meta-*): relaciones legibles + formato (Fase 2 del ERP nakui)

- **`Column.ref_entity`** — una columna de lista con esto resuelve su
  valor (un UUID) al label legible del record referido, en vez de
  mostrar el UUID crudo. `meta-form` carga el record vía el backend y
  usa `human_label_for_record`.
- **`Column.format`** (`ValueFormat::{Plain, Number, Currency}`) —
  formato de la celda: separador de miles, símbolo de moneda
  (`12000` → `$12,000`). Helper `format_value` en `meta-runtime`.
- El campo `entity_ref` en formularios ahora muestra el **label del
  record elegido** (read-only) + el selector, no el UUID crudo.
- `human_label_for_record` reconoce campos de nombre en español
  (`nombre`, `titulo`), no sólo inglés.

Tests nuevos en `meta-runtime` (`format_value`, labels ES) y
`meta-schema`. Ver el changelog de `nakui` para el plan maestro.

### feat(meta-*): FieldKind Select y AutoId (Fase 1 del ERP nakui)

La metainterfaz declarativa gana dos tipos de campo:

- **`Select`** — valor de un conjunto cerrado. `FieldSpec.options`
  (valor + etiqueta opcional); `Module::validate` exige `options` no
  vacío. `meta-form` lo renderiza como chips clickables (el chip
  elegido resaltado), no texto libre.
- **`AutoId`** — UUID v4 autogenerado. `meta-form` lo rellena al abrir
  el formulario y lo muestra read-only; el usuario no teclea ids de
  idempotencia. En modo edición conserva el id del record.

`parse_field_value` trata ambos como string passthrough. Tests nuevos
en `meta-schema` (validación de Select) y `meta-runtime` (parseo).

### feat(nahual-widget-text-input): modo enmascarado para contraseñas

`TextInput::with_mask()` dibuja el contenido como puntos (`•`, uno por
carácter Unicode) en vez del texto real; `text()` sigue devolviendo el
contenido crudo. Lo usa el campo de contraseña de `mirada-greeter`.
Lógica de enmascarado en la función pura `display_text`, con tests.

### feat(nahual-theme): exportación del tema a GTK (módulo toolkit)

Módulo nuevo `nahual-theme/src/toolkit.rs`: traduce el `Theme` activo a
`~/.config/gtk-3.0/gtk.css` y `gtk-4.0/gtk.css` con overrides
`@define-color`. Las apps GTK adoptan el acento exacto del tema + un
neutro claro/oscuro coherente. Los fondos en gradiente de nahual no se
pueden reproducir en ventanas GTK sólidas, así que el neutro se sintetiza
con un ramp de luminancia tintado por el matiz del borde del tema.

- `gtk4_css` / `gtk3_css` — generadores puros (nombres de color de
  libadwaita y de Adwaita 3 respectivamente).
- `export_toolkit_configs` + `export_toolkit_configs_to(base)` —
  escritura; el segundo con directorio base explícito para tests.
- **Guarda de no-pisar**: si un `gtk.css` ya existe sin la marca de
  nahual, es del usuario y se respeta (`ExportReport.skipped`).
- `Theme::set` y `Theme::install_default` exportan best-effort: cambiar
  de tema en cualquier app GPUI actualiza GTK al instante.
- `config_path` refactorizado sobre un nuevo `config_home()`.
- Ejemplo `dump-toolkit-css` para inspeccionar el CSS generado.
- 8 tests nuevos en `toolkit`.

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

