# Changelog — nakui

ERP categórico.

### fix(nakui-core): schema_bundle_hash debe reflejar el contenido real del schema
Iter 17. Regresión surfaceada por el workspace test
`verify_log_rejects_seed_after_schema_kcl_changes` (rebautizado a
`verify_log_rejects_seed_after_schema_changes`).

**Bug**: `compute_schema_bundle_hash` operaba sobre los bytes del
bundle compilado, que `build_schema_bundle` arma como
`(import "/abs/path/schema.ncl") & ...`. Los bytes del bundle nunca
cambian cuando se edita el archivo apuntado — sólo cambian si se
agregan/quitan schemas o se mueve el módulo de path. Resultado: el
hash quedaba pegado y los seeds firmados bajo schema vN se aceptaban
silenciosamente como válidos bajo schema vN+1, aunque las invariantes
hubieran cambiado.

**Fix**: nueva fn `read_schema_files_concat(module_dir, schemas)`
que lee los bytes de cada schema declarado y los concatena con
framing `\0NCL:<name>\0` (separador no ambiguo + nombre relativo, no
absoluto, para que el hash sea estable entre máquinas). Esos bytes
alimentan `compute_schema_bundle_hash` y `compute_morphism_schema_hash`
en lugar de los bytes del bundle. El bundle compilado sigue siendo
imports-style (necesario para que Nickel resuelva paths relativos);
sólo la fuente del hash cambió.

**Impacto en logs existentes**: como cualquier cambio al insumo del
hash, los seeds y morphisms versados bajo el bundle hash anterior
fallarán `SchemaMismatch` al verificarse contra un binario nuevo. No
hay migración — esto es exactamente el comportamiento que el hash
busca: re-seed.

Tests: 10/10 en `schema_versioning` (era 9/10 con 1 FAILED).

### refactor(nakui-core): KCL → Nickel — `kcl_wrapper` reemplazado por evaluación in-process
Cierra el ciclo: el motor de validación de entities deja de
shellear el binario externo `kcl` y pasa a evaluar **Nickel
contracts** in-process via la dep `nickel-lang` (la misma que ya
usa `brahman-cards` para sus templates). Los 3 schemas de los
módulos sales/inventory/treasury migran de `.k` a `.ncl`.
Además se borran los 2 archivos `.k` doc-only del repo
(`ente-card/schema/card.k`, `ente-brain/schema/rule.k` — ambos
estaban marcados "REFERENCE ONLY. NOT LOADED").

Cambios en **nakui-core**:
- **Nueva dep**: `nickel-lang = "2.0.0"` (interfaz estable).
- **Borrado** `kcl_wrapper.rs` (43 líneas) — shellear el binario
  desaparece.
- **Nuevo** `nickel_validator.rs`:
  - `pub fn vet(schema_path, state, schema_name) -> Result<(), NickelError>`
    evalúa `let bundle = (import "<schema>") in
    (std.deserialize 'Json m%%"<json>"%%) | bundle.<entity>`.
  - El state JSON va dentro de un raw string Nickel
    (`m%%"..."%%`) y se deserialize via `std.deserialize 'Json`.
    No embebemos el state como record literal Nickel directo
    porque la sintaxis JSON usa `:` (Nickel records usan `=`).
  - 5 tests propios cubriendo happy path + 4 fallure modes
    (field missing, predicate fails, cross-field invariant
    fails, optional field present/absent).
- **`executor.rs`**:
  - `kcl_wrapper::vet` → `nickel_validator::vet`.
  - `KclError` → `NickelError`.
  - `ExecError::KclPre/KclPost/KclPostCreate` → `SchemaPre/Post/PostCreate`
    (más neutro, ya no menciona KCL).
  - `kcl_check` (privado) → `validate_entity`.
  - `build_schema_bundle` ahora emite un archivo Nickel con
    `(import "X") & (import "Y") & ...` en lugar de concatenar
    bytes (cada `.ncl` es una expresión record completa, no
    juntable como texto plano).
- **`manifest.rs`**:
  - `effective_schemas` default `"schema.k"` → `"schema.ncl"`.
  - `extract_schema_names` reescrito: ahora detecta keys
    CapitalCase con 2 spaces de indent (convención de los
    `schema.ncl`), no más patrón `schema X:` de KCL.
  - Tests del extractor actualizados (1 test reemplazado por 2:
    `_handles_nickel_record_top_level` + `_skips_let_bindings_and_lowercase`).

Cambios en **schemas de módulos**:
- **`sales/schema.ncl`**: contracts Nickel para `Venta`. Usa
  `std.contract.Sequence [record_contract, from_predicate]`
  para combinar shape + invariante cross-field
  (`total == cantidad * precio_unitario`). El patrón directo
  `record | from_predicate` rebota con "missing definition" porque
  el predicate evalúa el contract antes de que el value lo
  populate; documentado en el comment.
- **`inventory/schema.ncl`**: `Stock`, `MovimientoStock`,
  `TransferenciaStock` (esta última con cross-field
  `source != dest` via Sequence).
- **`treasury/schema.ncl`**: `Caja`, `Movimiento`,
  `Transferencia` (con cross-field via Sequence).
- Helpers locales en cada archivo: `positive_int`,
  `non_negative_int`, `currency_iso`, etc. via
  `std.contract.from_predicate`.
- Los 3 `schema.k` viejos **borrados**.
- `sales/nsmc.json` actualizado: paths `schema.k` →
  `schema.ncl`.

Cambios en **tests**:
- `sales.rs`, `inventory.rs`: `KclPost` → `SchemaPost`.
- `kernel_guards.rs`: `KclPostCreate` → `SchemaPostCreate`,
  path del schema directo `treasury/schema.k` →
  `treasury/schema.ncl`.
- `graph.rs`, `manifest_validation.rs`: tests que escriben
  `schema.k` inline cambian a `schema.ncl` con sintaxis Nickel.
- `schema_versioning.rs`: refs `schema.k` → `schema.ncl`.

Cambios documentales:
- **Borrado** `crates/core/ente-card/schema/card.k` (1 archivo,
  REFERENCE ONLY documentado en su header).
- **Borrado** `crates/core/ente-brain/schema/rule.k` (REFERENCE
  ONLY documentado en su header).

Tests:
- **nakui-core**: 84 tests verdes (41 unit + 43 integration en
  graph/event_log/manifest_validation/schema_versioning/
  inventory/sales/kernel_guards). Suite full pasa.
- **nakui-ui**, **brahman-cards**, **yahweh-***: sin cambios,
  todos verdes.
- Total cubriendo el área: 174 tests.

Beneficios:
- **Sin dep externa**: el binario `kcl` ya no es requisito de
  runtime ni de tests. Build limpio en CI sin instalar KCL.
- **Errores en línea**: Nickel reporta contract violations con
  caret pointing al field exacto del schema y el value que
  falló. KCL daba mensajes textuales menos navegables.
- **Mismo motor que el brazo de cards**: una sola dependencia
  Nickel para todo el repo (validación + templates de cards).
- **Sin tempfile JSON intermedio**: el state se evalúa
  directamente en memoria; no hay `std::fs::write` por cada
  validate.

Limitaciones / decisiones:
- El comentario "REFERENCE ONLY" de los `.k` borrados ya estaba
  marcado en sus headers; eran sólo notas de diseño para humanos.
  La autoridad real (Rust validate methods) sigue intacta.
- La sintaxis Nickel `record_contract | from_predicate` no
  funciona — hay que envolver en `std.contract.Sequence [record,
  from_predicate]`. Documentado en cada schema y en el doc del
  validator.

**Pendientes restantes**: ninguno del refactor original. Los
yahweh + KCL + card.k cierran. Próximos pendientes salen de
nuevo trabajo (no del plan que arrastrábamos).

### feat(nakui-ui): migrar consumer al brazo unificado `brahman_cards::load_cards_from_dir`
Primera consumer migration del brazo. `nakui-ui` ya no llama a
`nakui_ui_schema::load_modules_from_dir` directamente — pasa por
`brahman_cards::load_cards_from_dir` y extrae el variant `UiModule`
del `CardBody` de cada Card. Beneficios concretos:

- **Soporta `.ncl` además de `.json`**: el usuario puede dropear un
  `card.ncl` (con templates Nickel + merge) en cualquier subdir y
  el runtime lo levanta automáticamente. El layout legacy
  `examples/nakui-modules/<id>/module.json` sigue funcionando vía
  los filenames default `[card.ncl, card.json, module.ncl, module.json]`.
- **Cards de otros body kinds (Ente/Monad) se skipean limpio**:
  si el dir contiene Cards no-UiModule, se reportan en un toast
  informativo en lugar de fallar la carga.

Cambios en `brahman-cards`:
- **Nuevo `load_cards_from_dir(dir)`** + variante con readers/filenames
  custom. Walkea subdirs (orden lexicográfico), busca el primero de
  `DEFAULT_CARD_FILENAMES`, dispatcha al reader. Subdirs sin ningún
  filename matching se skipean silenciosamente (permite assets/fixtures
  sueltos al lado de los cards). Errores per-file se propagan loud
  (sin ocultar corrupción).
- **`pub const DEFAULT_CARD_FILENAMES`**: lista canónica probada en
  orden. `card.ncl` tiene prioridad sobre `card.json` y sobre los
  legacy `module.*`.
- **4 tests nuevos del helper**: walk + skip de subdirs sin
  card, prioridad ncl > json, propagación loud de errores per-file,
  custom filenames.

Cambios en `nakui-ui`:
- **Nueva dep** `brahman-cards` en `Cargo.toml`.
- **Nuevo helper `load_ui_modules(dir) -> (Vec<Module>, Vec<String>)`**
  que envuelve `brahman_cards::load_cards_from_dir`, filtra a
  UiModule body, valida cada Module con su `validate()`, ordena
  por id, y detecta duplicados. El callsite en `MetaUi::new` pasa
  a usarlo y al ver Cards skipped emite un toast informativo.
- **3 tests nuevos**:
  - `load_ui_modules_via_brahman_cards_returns_ui_modules_and_skips_others`
    — verifica que un dir con UiModule + Ente carga el primero y
    reporta el segundo en `skipped`.
  - `load_ui_modules_via_brahman_cards_rejects_invalid_module` —
    `Module::validate()` se sigue aplicando (menu apuntando a view
    inexistente rebota).
  - `load_ui_modules_detects_duplicate_id` — dos UiModule con
    mismo id rebotan con mensaje claro.

Tests totales:
- `brahman-cards`: 22 → 26 (+4 helper directorio).
- `nakui-ui`: 45 → 48 (+3 e2e migración).
- Workspace build verde.

Lo que NO cambió:
- `nakui_ui_schema::load_modules_from_dir` se mantiene intacto (sus
  propios tests lo siguen usando, y otros consumers futuros podrían
  preferir su error-typing más específico). La migración es opt-in:
  `nakui-ui` usa el brazo, ui-schema sigue siendo una API válida.
- Layout actual de `examples/nakui-modules/<id>/module.json` no
  requiere cambio. Un usuario puede convertir cualquier módulo a
  `card.ncl` sin tocar el dir layout.

**Pendientes para próximos commits** (orden):
1. **Yahweh refactor**: lift del MetaUi runtime a
   `crates/modules/ui_engine/` para reuso. El brazo + canónico ya
   estables, ahora puede extraerse el meta-form widget genérico.
2. **KCL → Nickel**: kcl_wrapper reemplazado por Nickel contracts;
   los 3 schemas .k de nakui modules pasan a .ncl.
3. **card.k eliminado** (es REFERENCE ONLY documentado).

### feat(nakui-ui): validación cross-field del EntityRef (existence en store)
Cierra otro pendiente. Hasta ahora `parse_field_value(EntityRef, raw)`
sólo validaba **forma** (UUID parseable + trim de whitespace) — un
UUID válido pero inexistente en el store pasaba silenciosamente al
log/store, dejando dangling references. Ahora validamos también
**existencia** contra la entity declarada en `FieldSpec.ref_entity`.

Cambios:
- **Nuevo helper `validate_entity_refs<S: Store>(store, refs)`**:
  - `refs: &[(label, target_entity, uuid)]`.
  - Loop fail-fast: primer record ausente → error con label
    legible + UUID corto + target entity en el msg
    `"campo 'Stock': record abc12345 de 'Stock' no existe en el store"`.
  - Pure (toma `&S: Store`), totalmente testable sin GPUI.
- **Wireup en `commit_seed`**:
  - Durante el parse loop, cuando un field es EntityRef + tiene
    `ref_entity` declarado + value parseado a UUID, lo encolamos
    en `entity_refs: Vec<(String, String, Uuid)>`.
  - Después del parse loop (antes del seed/edit branch), si
    `entity_refs` no está vacío, una sola toma del store lock
    para validar todos via el helper.
  - Falla early: ningún log entry, ningún apply.
- **Cobertura**:
  - SEED path: alta nueva con EntityRef → validamos antes de
    `Seed { data }`.
  - EDIT path: edit con EntityRef → validamos antes de calcular
    delta. Una optional empty (que iría a clear) no cuenta como
    EntityRef (raw vacío skipea el push).
  - Morphism inputs: NO se duplica acá. `Executor::compute` ya
    valida cada input via `store.load(...).ok_or(EntityMissing)`
    antes de correr el script Rhai. Documentado en el doc del
    helper.

5 tests nuevos:
- `validate_entity_refs_passes_when_all_records_exist` — happy path.
- `validate_entity_refs_fails_on_first_missing` — fail-fast con
  msg que incluye entity + UUID corto.
- `validate_entity_refs_uses_label_not_entity_in_msg` — el label
  legible (ej: "Stock origen") aparece en el error, no la entity
  desnuda.
- `validate_entity_refs_empty_list_is_ok` — lista vacía es Ok.
- `validate_entity_refs_distinguishes_target_from_other_entities` —
  un UUID que existe bajo Customer pero NO Stock falla la
  validación contra Stock.

45 tests verdes en nakui-ui (+5). Workspace build verde.

Comportamiento esperado:
- **Selector clickable es happy path**: el dropdown sólo lista
  records existentes, así que clickearlo nunca debería disparar
  el error. Sólo dispara con paste manual de UUID que no existe
  o records borrados después de la selección (timing race).
- **Optional empty no se valida**: si el field es EntityRef
  optional y el form lo deja vacío, lo manejamos como "no value"
  (skipea el push a `entity_refs`); la lógica de Clear se
  encarga del resto.
- **Lock contention**: una sola toma del store lock por
  `commit_seed`, no una por field. La validación es O(refs) reads.

Pendientes restantes:
- **Validación KCL del record post-edit** antes de emitir Set/Clear
  (hoy `ui.edit_record` no pasa por `Executor::compute`).
- **EntityRef cross-module** (referenciar records de OTRO módulo
  por nombre, no sólo por entity local).

### feat(nakui-core,nakui-ui): FieldOp::Clear — borrar values vía form vacío
Cierra el último pendiente de UX del round. El edit no podía
"borrar" un value vaciando el input — empty optional fields
hacían `continue` en `commit_seed`, así que el current value
quedaba intacto. Para honrar el intent del usuario ("este field
ya no aplica") necesitábamos un FieldOp explícito que remueva
la key del map.

Cambios en **nakui-core** (la variante es semántica del kernel,
no específica de la UI):

- **`delta::FieldOp::Clear { path }`** — nueva variante.
  Distinta de `Set { value: Null }`: Clear borra la clave; Set
  Null deja la clave con valor literal `null`. Importa para
  downstream que diferencia "ausente" vs "presente como null"
  (ej: serde con `skip_serializing_if = "Option::is_none"`).
- **`capability_token`** — Clear devuelve `entity.field`,
  mismo shape que Set. Una capability `writes: ["Customer.notes"]`
  autoriza tanto Set como Clear sobre ese field.
- **`simulate_on`** — Clear remueve la key del Object si el
  state es Some(Object). Skip silente si el state es None
  (deleted) o no-objeto.
- **`MemoryStore::apply_dry_run`** — Set y Clear comparten
  pre-condición (record padre existe + es objeto). Pattern
  combinado con `|`.
- **`MemoryStore::apply`** — Clear hace `map.remove(field)`.
  Field ausente = no-op silencioso (post-state idéntico).
- **`SurrealStore::apply_dry_run`** — Set/Clear combinados.
- **`SurrealStore::apply`** — Clear emite
  `UPDATE type::thing UNSET <field>`. El field name viene de
  un FieldSpec validado upstream; SurrealQL no soporta binding
  de identifiers, así que va inline (con la advertencia
  documentada en el comment).
- **`Executor` capability check** — Set/Clear comparten match
  (mismo token shape, misma resolución a binding role).
- **Conservation rules** (en `check_conservation`) NO consideran
  Clear — sólo Set. Documentado: morphism authors que querían
  clear de un field con conservation tienen que ser cuidadosos;
  KCL post-checks pueden capturar violations.

Cambios en **nakui-ui**:

- **`commit_seed` loop** acumula `to_clear: Vec<String>` con
  los nombres de fields optional empty (en lugar de hacer
  `continue` silencioso).
- **EDIT branch**:
  - Computa `set_delta` (igual que antes) + `clear_fields` via
    nuevo helper `compute_clear_fields(current, to_clear)`.
  - Helper filtra a sólo los fields que actualmente tienen
    valor non-null — Clear de un field ausente o ya null no
    se emite (sería no-op semántico). Preserva el orden del
    input para estabilidad del log entry.
  - Construye `ops` combinando Set + Clear.
  - NoChange ahora requiere AMBOS vacíos (set_delta y
    clear_fields).
  - `params` del log entry incluye `cleared: ["field1", ...]`
    sólo si non-empty (preserva la shape `fields:` para
    edits sin clears).
  - `CommitOutcome::Updated.changed = sets + clears` para
    que el toast `"actualizado X (N campo(s))"` siga siendo
    preciso.

Tests nuevos:
- **delta.rs**: `simulate_clear_removes_field`,
  `simulate_clear_then_set_same_field_keeps_set`,
  `clear_capability_token_matches_set_shape`.
- **store.rs**: `apply_clear_removes_field_key`,
  `apply_clear_on_absent_field_is_noop`,
  `dry_run_rejects_clear_on_missing_record`,
  `dry_run_rejects_clear_on_non_object`.
- **nakui-ui main.rs**: `clear_fields_skips_absent_and_null`,
  `clear_fields_preserves_input_order`,
  `clear_fields_empty_when_current_is_null`.

34 tests verdes en nakui-core (+7), 40 en nakui-ui (+3).
Workspace build verde. E2E del morphism real
`morphism_pipeline_executes_real_sales_vender` intacto — `vender`
no usa Clear.

Implicaciones:
- **El log puede crecer con entries `ui.edit_record` que sólo
  tienen `cleared: [...]`** sin `fields`. Esperado y esperable.
- **Replay**: las entries con Clear se aplican en orden via
  `store.apply(&ops)`. La semantic es deterministic.
- **Si un módulo tiene KCL invariants sobre la presencia de un
  field**, el usuario podría romper el record vaciando ese
  field via UI. Hoy esto NO se chequea — `ui.edit_record` es
  un morphism manual que no pasa por `Executor::compute`. Si
  esto es un problema, el camino futuro es validar contra el
  KCL del entity al submit (otro pendiente).

Pendientes restantes:
- **Validación cross-field** (ej: UUID del EntityRef existe en
  la entity referida).
- **Validación KCL del record post-edit** antes de emitir Set/Clear.

### feat(nakui-ui): snapshot/compaction durante runtime cada N writes
Cierra el último pending del round de persistencia. Antes el compact
sólo corría al startup — para una sesión larga con muchas escrituras,
el log crecía sin tope hasta el próximo restart, y el siguiente boot
pagaba el costo lineal del replay.

Cambios:
- **Nuevos fields en `MetaUi`**:
  - `snap_path: PathBuf` — cacheado del init para que el tick no
    tenga que recomputarlo.
  - `snapshot_threshold: usize` — leído del env en `new()` y
    cacheado. `0` desactiva runtime compact (mismo env y
    semantic que el threshold de startup).
  - `writes_since_compact: u64` — contador que incrementa por cada
    write efectivo y se resetea cuando el threshold dispara
    `maybe_compact_log`.
- **Nuevo método `tick_runtime_compact()`**:
  - Early return si `threshold == 0`.
  - Increment + check vs threshold.
  - Si cruza: lock log + store, llama `maybe_compact_log`.
  - **Si compactó OK**: counter = 0, devuelve msg.
  - **Si `maybe_compact_log` returned None** (counter dijo "go"
    pero entries < 2): counter = 0 (no re-entrar cada write).
  - **Si error**: counter NO se resetea (próximo write reintenta),
    devuelve el error.
- **Nuevo helper `append_compact_msg(base, opt)`**: concatena el
  msg del compact al toast del op original con `";"` separator.
- **Wireup en 3 callsites de write efectivo**:
  - `apply_action::SeedEntity`: tick si outcome != NoChange.
  - `apply_action::Morphism`: tick siempre que Ok.
  - Click handler `[Confirmar]` del delete modal: tick si commit_delete Ok.
- **NoChange no cuenta**: un edit que no cambia nada no escribe al
  log, así que tampoco debería avanzar el counter — preserva la
  semantic "1 write = 1 log entry = 1 tick".

2 tests nuevos:
- `append_compact_msg_handles_both_branches` — base solo vs base
  + compact, formato del separator.
- `runtime_compact_cycle_resets_counter_after_threshold` — E2E
  estilo simulación: 7 writes con threshold=3 → 2 compacts (en
  write 3 y 6), counter residual = 1, log final con 2 entries
  (1 anchor + 1 write residual). Reproduce el algoritmo del tick
  sin GPUI cx; si la lógica del método cambia, se rompe como signal.

37 tests verdes (+2). Workspace build verde.

Trade-offs:
- **Counter en memoria, no persistido**: si la app crashea entre
  compacts, al próximo boot el counter parte de 0. El startup
  compact (basado en entry_count del log file) compensa esto:
  si quedó mucho post-último-compact, se compacta al boot.
- **Lock orden**: tick toma log lock primero, store lock después.
  Misma orden que `commit_seed` y `commit_morphism`, no debería
  haber deadlock.
- **Costo del tick**: 1 increment + 1 compare por write. Cuando
  cruza threshold, 1 read del log (entries) + 1 snapshot write +
  1 compact. Para threshold=50 es ~1 fsync cada 50 writes —
  amortiza bien.

Pendientes restantes:
- **`FieldOp::Clear`** — para soportar borrar un value vía form vacío.
- **Validación cross-field** (UUID del EntityRef existe en la
  entity referida).

### feat(nakui-ui): atajo Esc para cancelar el modal de delete
Cierra otro pendiente de UX. El banner de confirmación de delete
ya tenía botones [Cancelar] / [Confirmar], pero la acción más
natural para cancelar un dialog es Esc — y no la teníamos wireada.

Cambios:
- **`capture_key_down` en el root div** del `MetaUi::render`. Capture
  phase (no bubble) para interceptar el Esc *antes* que cualquier
  TextInput descendiente lo consuma. Sin pending el handler es
  no-op y el evento sigue su flujo normal.
- **Match `event.keystroke.key == "escape"`** + `pending_delete.take()`
  → toast `"delete cancelado (Entity) [esc]"` (sufijo `[esc]` para
  diferenciar visualmente del botón). Si no hay pending, return
  temprano sin tocar nada.
- **Hint visual en el banner**: subtítulo en amber tenue debajo del
  título: `"Esc para cancelar · click [Confirmar] para borrar"`.
  Que el usuario descubra el atajo sin RTFM.

35 tests verdes — el handler de Esc es 8 líneas no-testeables sin
GPUI cx (la lógica de pending_delete + toast vive dentro del
listener); el wireup compila por type-check.

Pendientes restantes:
- **`FieldOp::Clear`** — para soportar borrar un value vía form vacío.
- **Snapshot durante runtime** (cada N writes, no sólo al startup).
- **Validación cross-field** (UUID del EntityRef existe en la
  entity referida).

### feat(nakui-ui): EntityRef validation en parse_field_value (UUID al submit)
Cierra otro pendiente: `parse_field_value(FieldKind::EntityRef, raw)`
devolvía `Ok(json!(raw))` blindly — el value entraba al log/store
incluso si era basura. La validación de UUID sólo ocurría cuando el
field se usaba como **input** del morphism (línea ~540 de
`commit_morphism`); como **seed field** o como **param**, garbage
pasaba silenciosamente.

Cambios:
- **`parse_field_value(EntityRef, raw)`** ahora hace
  `Uuid::parse_str(raw.trim())` y devuelve error claro si falla:
  `"'<raw>' no es UUID válido (usá el selector de records)"`. En
  caso de éxito, devuelve el UUID **trimmed** como string —
  protege contra paste manual con whitespace.
- **Doble cobertura**: este path cubre seed fields (commit_seed via
  obj.insert) y morphism params (resolve_param_value lo invoca por
  cada FieldSpec con kind=EntityRef). El path de morphism inputs
  ya validaba antes con `Uuid::parse_str` directo — sigue intacto,
  no hay double-validation.
- **Selector clickable es happy path**: el dropdown setea valores
  bien-formados, así que el usuario nunca debería ver el error en
  uso normal. Sólo dispara con paste manual o si el usuario escribe
  garbage en el input — defensivo.

5 tests nuevos (reemplazan al obsoleto `parse_field_entity_ref_returns_string`):
- `parse_field_entity_ref_accepts_valid_uuid` — happy path.
- `parse_field_entity_ref_trims_whitespace` — `"  uuid\n"` → `"uuid"`.
- `parse_field_entity_ref_rejects_non_uuid` — `"abc-123"` → error
  con el value y la palabra "UUID" en el mensaje.
- `parse_field_entity_ref_rejects_empty_string` — `""` → rebota.
- `resolve_param_strict_entity_ref_propagates_error` — sanity de
  que el wireup en resolve_param_value hereda el strict checking,
  con label del FieldSpec en el mensaje.

35 tests verdes (+4 net). El E2E del morphism real
`morphism_pipeline_executes_real_sales_vender` sigue verde — sus
inputs van por el path dedicado, no por parse_field_value.

Pendientes restantes:
- **Atajo Esc para Cancelar** del modal de delete.
- **`FieldOp::Clear`** — para soportar borrar un value vía form.
- **Snapshot durante runtime** (cada N writes, no sólo al startup).
- **Validación cross-field** (ej: el UUID del EntityRef existe en
  la entity referida) — hoy sólo validamos forma; un UUID válido
  pero inexistente sí pasa.

### feat(nakui-ui): snapshot/compaction automático del event log al startup
Cierra el último gran pendiente del round: el replay full cada
startup escala lineal en el log. Con 60+ entries el costo de boot
se nota; con 10k entries es prohibitivo. Wireamos el snapshot
machinery que ya estaba en `nakui-core` (`Snapshot`,
`replay_with_snapshot_into`, `EventLog::compact_through`) al
runtime de la UI.

Cambios:
- **Path del snapshot**: sibling del log, extensión `.snap.json`.
  `nakui-ui-state.jsonl` ↔ `nakui-ui-state.snap.json`.
- **Nuevo helper `snapshot_path_for(log_path)`** — derivación
  pura, testeable.
- **Nuevo helper `maybe_compact_log(log, snap_path, store, threshold)`**:
  - Si `entry_count >= threshold` y `>= 2`, captura
    `Snapshot::from_memory_store(store, next_seq - 1)`, lo escribe
    atómicamente, y compacta el log dejando la última entry como
    anchor.
  - Anchor invariant: `EventLog::open` deriva `next_seq` del primer
    entry del archivo. Si compactáramos *todo* el log file, al
    reabrir el cursor volvería a 0 y el próximo append crashearía
    con `NonMonotonic`. Por eso compactamos sólo hasta
    `next_seq - 2` — la entry del `snap.seq` queda como anchor del
    cursor; `replay_with_snapshot_into` la skipea porque snap ya
    cubre hasta ese seq inclusive.
  - Threshold via env `NAKUI_SNAPSHOT_THRESHOLD`, default 50.
    `0` desactiva por completo.
  - Devuelve `Result<Option<msg>, String>`: `Ok(Some)` si compactó,
    `Ok(None)` si no había payoff, `Err` si snap o compact fallaron.
- **`MetaUi::new` reescrito**:
  - Carga snapshot al inicio (Some/None según exista).
  - `replay_with_snapshot_into(&log, snapshot.as_ref(), &mut store)`
    en lugar de `replay_into`.
  - Después del replay corre `maybe_compact_log` con el threshold.
  - Toast inicial menciona snapshot loaded si aplica
    ("snapshot @ seq K") y la compactación si ocurrió.
  - Errores de snapshot load **no son fatales**: cae a full replay
    con un msg en el banner.
  - Errores de auto-compact **no son fatales**: el log + snap
    quedan como estaban, msg al banner.

5 tests nuevos:
- `snapshot_path_for_replaces_extension` — `.jsonl` → `.snap.json`,
  edge case sin extensión.
- `maybe_compact_log_below_threshold_noops` — 5 entries vs threshold
  50: no toca nada, no escribe snap.
- `maybe_compact_log_threshold_zero_noops` — threshold 0 = disabled.
- `maybe_compact_log_then_reopen_preserves_records` — E2E:
  - Escribe 60 seeds (log + store en sync).
  - Compacta (60 >= 50): snap escrito, log queda con 1 anchor entry,
    msg reporta "59 entries dropped (1 anchor kept)".
  - Reopen: `next_seq=60` se preserva via anchor, `entries.len()=1`.
  - Replay con snap loadado en store fresco: los 60 records están.
  - Segunda corrida del compact con threshold=1: no-op (idempotente).

31 tests verdes (+5). Workspace build verde tras la nueva firma.

Trade-offs y notas:
- **Fail-soft**: cualquier error de snap/compact no rompe el boot;
  la UI sigue funcionando con full replay y el toast lo reporta.
  Sólo `EventLog::open` failing es no-recoverable (pierde
  persistencia).
- **Crash-safety**: WAL order preservado — escribimos snap (atómico
  via tempfile + fsync + rename) ANTES de compactar el log
  (atómico igual). Si crasheamos entre los dos, próximo boot ve
  snap@K + log con todas las entries 0..N — replay skippea las que
  snap cubre, outcome idéntico.
- **Sólo en startup**: no hay snapshot durante runtime. Para sesiones
  largas con muchas escrituras, el log puede crecer arbitrariamente
  hasta el próximo restart. Pendiente futuro: snapshot N writes
  desde el último compact.
- **Anchor entry sobrevive sin uso útil**: el costo es 1 línea JSON
  por compact. No es preocupación a menos que el threshold sea
  muy chico (cada compact deja 1 línea de basura).

Pendientes restantes:
- **EntityRef validation post-submit** — validar UUID parseable al
  submit en lugar de al execute del morphism.
- **Atajo Esc para Cancelar** del modal de delete.
- **`FieldOp::Clear`** — para soportar borrar un value vía form vacío.
- **Snapshot durante runtime** (cada N writes, no sólo al startup).

### feat(nakui-ui): edit delta-only — sólo campos modificados al log/store
Antes de este cambio, editar un record emitía un `FieldOp::Set` por
**cada field del form**, incluso los no tocados. Eso bloata el log
(replay tenía que aplicar N ops cuando 1 alcanzaba) y oscurece el
intent en una auditoría posterior. Con delta-only, el edit emite
sólo los Sets cuyo value nuevo difiere del actual; un edit que no
cambia nada deja el log intacto.

Cambios:
- **Nuevo helper `compute_field_delta(current, proposed)`** — toma
  el record actual del store (un `Value`, posible `Null` si el
  record no existe) y el `Map` propuesto desde el form, y devuelve
  sólo las entries que difieren. Comparación: `PartialEq` estructural
  de `serde_json::Value` (un `Null` en current = todos los proposed
  son nuevos).
- **Nuevo enum `CommitOutcome`**:
  - `Created(Uuid)` — alta nueva.
  - `Updated { id, changed }` — edit con N campos modificados.
  - `NoChange(Uuid)` — edit sin diferencias (el toast lo refleja
    como "X sin cambios — no log entry").
- **`commit_seed` en path EDIT**:
  - Carga current via `store.load(entity, id)` con fallback a
    `Value::Null`.
  - Calcula delta. Si vacío → return early sin tocar log ni store.
  - Si no vacío → emite `Morphism { ui.edit_record, ops: [Set...] }`
    con `params.fields` reflejando el delta (no todo el form),
    haciendo la auditoría grep-able por field cambiado.
- **Toast del callsite**:
  - `creado X uuid` (Created)
  - `actualizado X uuid (N campo(s))` (Updated)
  - `X uuid sin cambios — no log entry` (NoChange)
- **`editing` se limpia incluso en NoChange** — el modo edit cierra,
  el form vuelve al state limpio.

5 tests nuevos del helper:
- delta vacío cuando todo coincide.
- delta sólo con el field cambiado.
- delta full cuando current = Null (record no existe).
- distingue int 100 de string "100".
- ignora fields del current que no están en proposed.

27 tests verdes (+5). El path SEED no cambió; el E2E del morphism
real sigue verde.

Limitación conocida (consistente con pre-delta): el form no puede
**borrar** un value vaciando el input — empty optional fields hacen
`continue` antes de llegar al delta. Para clearear un value hay que
declarar el field como required, o esperar a un `FieldOp::Clear`
futuro (no necesario hoy: ningún demo lo requiere).

Pendientes restantes:
- **Snapshot/compaction** del log (replay full cada startup escala
  mal con repos grandes).
- **EntityRef validation post-submit** — validar UUID parseable al
  submit en lugar de al execute del morphism.
- **Atajo Esc para Cancelar** del modal de delete.
- **`FieldOp::Clear`** — para soportar borrar un value vía form.

### feat(nakui-ui): confirmación de delete vía banner modal antes de borrar
Cierra el primer pending del último round: borrar un record pedía un
solo click en `✕` y se ejecutaba inmediatamente (irreversible —
queda en el log como `Morphism { name: "ui.delete_record" }`). Ahora
hay un paso intermedio: el click marca el record como pendiente y el
banner amber al tope de la ventana ofrece [Cancelar] o [Confirmar].

Cambios:
- **Nuevo state `MetaUi.pending_delete: Option<(String, Uuid)>`**.
  Set en el click del `✕`; limpiado por:
  - [Cancelar] → toast "delete cancelado (Entity)".
  - [Confirmar] → llama `commit_delete` (igual que antes) y emite
    el toast usual.
  - Navegación a otra view (`select_view`) — el record marcado
    podría no estar visible en la nueva pantalla.
- **Click handler de `✕` ya no llama `commit_delete`**: sólo setea
  `pending_delete` y limpia toast. La acción destructiva ahora vive
  exclusivamente en el botón [Confirmar] del banner.
- **Nuevo método `render_confirm_delete_banner`**: devuelve
  `Option<Div>` (None si no hay pending). Banner amber con el texto
  `¿Borrar {Entity} {short_uuid}?` + dos botones. Renderea como
  sibling del row sidebar+main en `flex_col` raíz — no es overlay
  flotante (GPUI no expone z-index trivialmente), pero la posición
  fija al tope + color amber lo hacen imposible de ignorar.
- **Limpieza pre-commit**: `pending_delete = None` se ejecuta antes
  de `commit_delete`, así un fallo del commit no deja el banner
  colgado además del toast de error.

22 tests verdes — la lógica del store/log no cambió, sólo el state
machine de UI. La confirmación es puramente UX/state, no testable
sin GPUI cx, pero la compilación garantiza wireup correcto de las
closures.

Pendientes restantes:
- **Snapshot/compaction** del log para repos grandes (replay full
  cada startup escala mal).
- **Edit delta-only** — sólo campos modificados, no todos.
- **EntityRef validation post-submit** — validar UUID parseable
  al submit en lugar de al execute del morphism.
- **Atajo de teclado Esc para Cancelar** — requiere event
  dispatcher de GPUI, fuera de scope inmediato.

### feat(nakui-ui): validación estricta de params del morphism vía FieldKind del FieldSpec
Cierra el último trade-off documentado: `infer_param_value` adivinaba
el tipo de cada param por la shape del string (i64 → f64 → bool →
string). Ahora cuando hay `FieldSpec` declarado, usamos
`parse_field_value(spec.kind, raw)` — un Boolean field con value
"abc" rebota con mensaje claro en la UI antes de llegar al morphism
Rhai (donde el error sería opaco como "Function not found: * ((), ())").

Cambios:
- **Nuevo helper `resolve_param_value(field_name, raw, spec)`**:
  - Si hay `FieldSpec`: validación de `required` (rebota empty con
    "param 'X' es obligatorio y está vacío") + parseo estricto via
    `parse_field_value(spec.kind, raw)`. Errores incluyen el `label`
    del spec para que el toast sea interpretable.
  - Si NO hay spec (param declarado en `Action::Morphism.params`
    que no existe en `form.fields` — módulo mal-formado): fallback
    a `infer_param_value` como red de seguridad.
  - Empty + opcional → `Value::Null`.
- **`commit_morphism` simplificado**: el loop de params ahora es
  3 líneas (lookup spec + llamada a `resolve_param_value` +
  inserción al map). La lógica vive en el helper standalone,
  testable sin GPUI.

Tests: 6 nuevos en `tests` mod, todos contra `resolve_param_value`:
- `resolve_param_strict_number_parses_i64` — happy path.
- `resolve_param_strict_boolean_rejects_non_boolean` — un Boolean
  con "abc" rebota con mensaje que incluye el label.
- `resolve_param_strict_number_rejects_garbage` — Number con "abc"
  rebota.
- `resolve_param_required_empty_rejected` — required vacío rebota
  con "obligatorio".
- `resolve_param_optional_empty_returns_null` — optional vacío
  → null.
- `resolve_param_no_spec_falls_back_to_infer` — el fallback
  preserva el comportamiento anterior para back-compat.

22 tests verdes en nakui-ui (+6). E2E del morphism real
(`morphism_pipeline_executes_real_sales_vender`) sigue verde — la
validación estricta no rompe el path correcto, sólo agrega rebotes
tempranos a values mal-tipados.

Beneficio operativo:
- Mensaje de error en la UI ahora identifica el field problemático
  por su label legible ("param 'Cantidad': 'abc' no es número")
  en lugar del error opaco del morphism Rhai.
- Errores se ven antes de tocar el log o el store — ningún cambio
  parcial.
- El módulo Nakui ya no tiene que defender contra inputs garbage
  desde la UI: la metainterfaz se vuelve la primera línea de
  validación tipada.

Pendientes futuros (orden de prioridad):
- **Confirmación de delete** — modal antes de borrar.
- **Snapshot/compaction** del log para repos grandes.
- **Edit delta-only** — sólo campos modificados, no todos.
- **EntityRef validation post-submit**: hoy `parse_field_value`
  para EntityRef devuelve string raw; el commit_morphism luego
  valida como Uuid sólo cuando es input del morphism. Para
  EntityRef como param, podríamos validar UUID al submit.

### feat(nakui-ui): FieldKind::EntityRef — selector clickable de records existentes
Cierra el principal trade-off documentado del commit anterior:
"Inputs UUID a mano (no dropdown)". Los formularios pueden declarar
un campo `entity_ref` que apunta a una entity y el runtime renderea
una lista clickable de records existentes; click selecciona, el
UUID queda guardado para el submit.

Schema `nakui-ui-schema`:
- **Nueva variante `FieldKind::EntityRef`** (serializa como
  `"entity_ref"` en JSON).
- **`FieldSpec.ref_entity: Option<String>`** nuevo. Indica qué
  entity ofrecer en el selector. `validate()` chequea que cualquier
  field con `kind=entity_ref` tenga `ref_entity` set.
- Nuevo error tipado `SchemaError::EntityRefMissingTarget`.

Runtime `nakui-ui`:
- **`render_entity_ref_selector(field_name, target_entity, ...)`** —
  helper que arma la lista debajo del input. Cada item:
  - Etiqueta humana via `human_label_for_record` (heurística:
    `name` → `label` → `title` → `sku` → `sku_id` → fallback al
    UUID corto).
  - Click handler vía `cx.listener` que llama
    `input.set_text(uuid_completo)` — el TextInput interno queda
    como source-of-truth, así que `commit_seed` y `commit_morphism`
    leen el UUID seleccionado sin saber que vino de un selector.
  - Highlight en accent color cuando el item es el actualmente
    seleccionado (compara contra el contenido del TextInput).
- **`parse_field_value(EntityRef, raw)`** devuelve string del raw
  (la validación como Uuid ocurre downstream en `commit_morphism`).
- Mensaje "(sin {entity}: creá uno antes para referenciar)" cuando
  la lista está vacía — el user sabe qué hacer en lugar de quedarse
  trabado.

Demo actualizado: `examples/nakui-modules/sales_engine/module.json`:
- `vender_form.fields.stock_id_input` y `caja_id_input` cambian de
  `kind: "text"` a `kind: "entity_ref"` con `ref_entity: "Stock"`
  y `"Caja"` respectivamente.
- Ahora el flujo "Vender" es: (1) click en una Stock listada bajo
  el input, (2) click en una Caja, (3) escribir venta_id/cantidad/
  precio_unitario/timestamp, (4) submit. Sin copiar UUIDs.

Tests:
- 2 nuevos en schema: `validate_catches_entity_ref_without_target`
  y `entity_ref_with_target_validates_clean`. 8 totales.
- 4 nuevos en runtime: `parse_field_entity_ref_returns_string`,
  `human_label_for_record_prefers_name_over_id`,
  `human_label_falls_back_through_label_title_sku`,
  `human_label_falls_back_to_id_when_no_known_keys`. 16 totales.
- Integration de los 7 demos sigue verde — el demo `sales_engine`
  ahora valida con EntityRef + ref_entity correctamente set.

29 tests totales nakui-ui + schema, 100% verde. El demo
`sales_engine` carga limpio con la nueva forma del schema.

Pendientes futuros:
- **Confirmación de delete** — modal antes de borrar.
- **Snapshot/compaction** del log para repos grandes.
- **Edit delta-only** (sólo campos modificados).
- **Validación de tipos en params del morphism**: `FieldKind`
  declarado en el FieldSpec se podría usar para forzar parseo
  estricto en `commit_morphism` en lugar de la heurística
  `infer_param_value`.

### feat(nakui-ui): Action::Morphism wired al pipeline real (compute → log → apply)
Cierra el último gran TODO de la metainterfaz Nakui: las acciones
`Action::Morphism` ya no son un toast informativo; despachan al
`Executor` cargado del manifest nakui-core (`nsmc.json` + schemas
KCL + scripts Rhai), pasando por el pipeline completo de Nakui:
compute (con dry-run + KCL post-checks) → log append → store apply.

Schema `nakui-ui-schema` extendido:
- **`Module.nakui_module_dir: Option<String>`** nuevo. Path
  (relativo al directorio del `module.json` o absoluto) a un módulo
  nakui-core. Sin esto, las Action::Morphism del módulo quedan
  no-op con toast informativo. Las Action::SeedEntity siguen
  funcionando sin manifest (alta administrativa).
- **`Action::Morphism`** ganó dos campos opcionales:
  - `inputs: BTreeMap<String, String>` — mapeo `role → field_name`.
    Por cada input declarado en el `MorphismSpec.inputs`, indica
    qué field del form contiene el UUID del record. El runtime
    parsea como `Uuid` y lo pasa al `execute_and_log`.
  - `params: Vec<String>` — lista de fields cuyos values van al
    `params` JSON. Si vacío, todos los fields no-input van a params.

Runtime `nakui-ui`:
- **`MetaUi.executors: BTreeMap<String, Arc<Executor>>`** nuevo.
  Carga `Executor::load_module(nakui_module_dir)` en `MetaUi::new`
  por cada módulo UI que declare la entry. Errores de carga van al
  banner; el módulo sigue cargado para SeedEntity, sólo Morphism
  queda no-op.
- **`commit_morphism(mod_idx, name, inputs_map, params_fields)`** nuevo.
  Resuelve inputs (parsea cada field como Uuid), arma params (Value
  object con tipos inferidos via `infer_param_value` — int/float/
  bool/string), llama `execute_and_log_with_recovery`. Toast con
  cantidad de ops aplicadas o el error tipado.
- **`infer_param_value`** nuevo helper: heurística simple para
  pasar values del form al morphism con tipo inferido (i64 → f64 →
  bool → string).

Tests: 2 nuevos:
- `infer_param_value_int_then_float_then_bool_then_string` —
  cobertura de la heurística.
- **E2E `morphism_pipeline_executes_real_sales_vender`** —
  carga el módulo real `crates/modules/nakui/modules/sales`,
  arma store + log, ejecuta el morphism `vender` con inputs
  Stock+Caja y params (cantidad=5, precio_unitario=200,
  venta_id, timestamp). Asserta:
  - el morphism produce ops (no vacío).
  - stock.cantidad bajó 100 → 95.
  - caja.saldo subió 1_000_000 → 1_001_000.

12 tests verdes en nakui-ui (+1 vs commit anterior). Schema
extension no rompió nada (6 unit + 5 integration siguen verdes).

Demo nuevo: **`examples/nakui-modules/sales_engine/module.json`**
- Apunta a `crates/modules/nakui/modules/sales` vía `nakui_module_dir`.
- 6 vistas: list + form para cada Stock, Caja, Venta + form
  "Vender" con `Action::Morphism { name: "vender", inputs: {stock,
  caja}, params: [venta_id, cantidad, precio_unitario, timestamp] }`.
- El user crea Stocks + Cajas con seed_entity, copia los UUIDs
  cortos a los inputs de "Vender", y ejecuta el morphism real:
  stock baja, caja sube, Venta se persiste, todo loggeado.
- Validaciones KCL fallan limpio (toast con error) si el morphism
  rebota — p. ej. cantidad > stock disponible.

Activación full:
```sh
NAKUI_EVENT_LOG=~/.nakui/state.jsonl \
NAKUI_MODULES_DIR=examples/nakui-modules \
cargo run -p nakui-ui
# Sidebar gana "Ventas (con morphism)" — los 6 menús aparecen y
# el form "Vender" dispara el pipeline nakui-core completo.
```

Trade-offs documentados:
- **Inputs UUID a mano**: el form pide que el user copie el UUID de
  un Stock/Caja existente. Para UX seria habría que agregar
  `FieldKind::EntityRef { entity }` que renderiza un dropdown — no
  hecho por scope, queda como nice-to-have.
- **Inferencia de tipo en params**: `infer_param_value` adivina
  por shape del string. Para casos sutiles (ej. "true" como string
  literal vs bool), el módulo nakui-core puede explicitar tipos
  via `kind` en el FieldSpec — el form lo respeta para validación
  pre-submit; la inferencia final sigue siendo heurística.

### feat(nakui-ui): edit + delete de records (ciclo CRUD completo)
Cierra "no hay UI para editar/borrar records existentes" del commit
anterior. Cada fila de la lista gana dos botones (✎ edit, ✕ delete);
el form view se reusa para alta y para edit; el delete es inline.
Las mutaciones pasan por `LogEntry::Morphism` con sus ops, así el
replay restaura el estado correcto.

Cambios:

- **`MetaUi.editing: Option<(String, Uuid)>`** nuevo. Set al click
  en ✎; cleared al cambiar de view o tras submit exitoso.
- **`open_edit(mod_idx, entity, id, cx)`**: setea `editing`, busca la
  primera Form view del módulo cuya `entity` matchee, navega ahí. Si
  el módulo no tiene Form para esa entity → toast con error
  ("no hay form view para entity X").
- **`select_view`** extendido: cuando carga un Form, si `editing`
  matchea esa entity y el record existe en el store, pre-llena cada
  input con el valor del record (vía nuevo helper
  `value_to_input_text` — inverso de `parse_field_value`).
- **`commit_seed`** ramifica:
  - **Edit path** (cuando `editing.is_some()` y entity matchea):
    emite `LogEntry::Morphism { name: "ui.edit_record", ops:
    [Set { path, value } for each field], params: { entity, id,
    fields } }`. Aplica al store via `apply(&ops)`.
  - **Seed path** (alta nueva): comportamiento previo.
- **`commit_delete(entity, id)`**: emite `LogEntry::Morphism {
  name: "ui.delete_record", ops: [Delete { entity, id }] }` + apply.
- **Render del form**: título cambia a "Editar customer abc12345"
  cuando `editing` matchea; submit label cambia a "Guardar cambios
  en customer".
- **Render de la lista**: dos columnas nuevas — "id" y "acciones".
  Cada fila tiene ✎ (accent color, click → open_edit) y ✕ (rojo,
  click → commit_delete). Hover states.

Ramificación visible en el event log:
```
{"kind":"seed","seq":0,"entity":"customer","id":"abc...","data":{"name":"Acme"}}
{"kind":"morphism","seq":1,"morphism":"ui.edit_record","ops":[
  {"op":"set","path":{"entity":"customer","id":"abc...","field":"name"},
   "value":"Acme S.A."}
]}
{"kind":"morphism","seq":2,"morphism":"ui.delete_record","ops":[
  {"op":"delete","entity":"customer","id":"abc..."}
]}
```
Coherente con el modelo de Nakui — todo cambio post-seed pasa por
ops dentro de Morphism. `nakui-explorer` muestra estos morphisms
con sus ops claros en su timeline.

Trade-offs documentados:
- **`schema_hash: None`** sigue para los morphism de la UI (legacy/
  pre-versioning path) hasta que `Action::Morphism` cargue Manifest
  schemas.
- **Delete sin confirmación**: 1 click, sin modal. Para MVP es OK
  (los records son recuperables vía replay parcial), pero un futuro
  iter agregaría confirmación.
- **Edit sobreescribe TODOS los campos del form**, no sólo los
  cambiados — emite N ops Set, una por field. Adecuado para forms
  chicos; para forms con muchos campos optimizar a delta-only.

Tests: 3 nuevos (10 totales en nakui-ui):
- `value_to_input_text_inverse_of_parse` y
  `value_to_input_then_parse_round_trip` — la propiedad fundamental
  del pre-llenado: text → parse devuelve el Value original.
- `event_log_replay_handles_full_crud_cycle` — E2E del log: escribe
  Seed + Morphism(Set ops) + Morphism(Delete op), replay desde cero,
  verifica que el store termina vacío (delete fue el último). Verifica
  además que un replay parcial (sin el delete) deja los valores
  editados.

Activación:
```sh
NAKUI_EVENT_LOG=~/.nakui/state.jsonl \
NAKUI_MODULES_DIR=examples/nakui-modules \
cargo run -p nakui-ui
# Crear un customer, click ✎ en su fila, modificar campos,
# "Guardar cambios". Click ✕ en otra fila para borrar.
# Cerrar y reabrir: el state persiste con todos los cambios.
```

### feat(nakui-ui): persistencia con event log + replay al startup
Cierra "sin persistencia entre runs" del commit anterior. Cada
`SeedEntity` se appendea al `nakui_core::event_log::EventLog` con
WAL semantics (log antes que store) y al re-abrir el binario el
replay reconstruye el `MemoryStore` desde cero. Cerrar y volver a
abrir ya no borra el data.

Cambios:
- **`MetaUi.event_log: Option<Arc<Mutex<EventLog>>>`** nuevo.
  Compartido bajo `Mutex` para que el commit_seed pueda mutar.
- **Apertura + replay al startup** (`MetaUi::new`): path por env
  `NAKUI_EVENT_LOG`, default `./nakui-ui-state.jsonl`.
  `EventLog::open` + `replay_into` reconstruyen el store.
  Toast informativo: "log nuevo" o "log X cargado: N evento(s)
  replayed".
- **WAL en `commit_seed`**: si `event_log.is_some()`, primero
  `log.append(LogEntry::Seed { ..., schema_hash: None })`, después
  `store.seed`. Si el append falla, cancela toda la operación
  (el user reintenta sin haber dejado state inconsistente).
- **`schema_hash: None`**: documentado como path "legacy /
  pre-versioning" para seeds que no pasan por un Manifest+Executor.
  Es el path correcto para alta administrativa vía la metainterfaz
  hasta que `Action::Morphism` wireé el Manifest loader.
- **Degradación grácil**: si abrir log falla (permisos, disco),
  toast con error pero el runtime sigue en modo in-memory.

Tests: 1 nuevo E2E `event_log_replay_restores_memory_store` que
escribe 2 seeds via `EventLog::append`, re-abre y `replay_into` un
store fresh, verifica que ambos records están con sus values
correctos. Reproduce el flujo del startup de `MetaUi::new` sin
necesitar GPUI. 7 tests verdes en nakui-ui.

Activación con persistencia explícita:
```sh
NAKUI_EVENT_LOG=~/.nakui/state.jsonl \\
NAKUI_MODULES_DIR=examples/nakui-modules \\
cargo run -p nakui-ui
# Crear varios records vía el form, cerrar el binario, abrir de
# nuevo: los records están.
```

Limitaciones que **siguen** (próximos iters):
- **`Action::Morphism`** sigue como TODO: requiere cargar el
  `Manifest` de nakui-core junto al `Module` UI para conocer los
  inputs/params declarados y poder llamar `execute_and_log`.
- **No hay snapshot/compaction**: el log crece append-only para
  siempre. Para repos grandes habría que integrar `Snapshot` de
  nakui_core (existe, no se usa todavía).
- **No hay UI para borrar/editar** records existentes — sólo alta
  vía form. Edit + delete en futuras iteraciones.
- **Widget input simple** (sin selection/IME/clipboard) — heredado
  de la limitación documentada de `yahweh-widget-text-input`.

### feat(nakui-ui): inputs reales con yahweh-widget-text-input + click handlers funcionales
Cierra dos limitaciones documentadas en el commit anterior de la
metainterfaz: los formularios ahora aceptan teclado real, y los
clicks en menús + botones mutan estado correctamente.

Cambios:
- **Inputs vivos**: cada `FieldSpec` del Form view materializa un
  `Entity<TextInput>` (de `yahweh-widget-text-input`) al entrar a la
  vista. Los entities se reemplazan al cambiar de view (drop limpio).
  El widget soporta: escribir caracteres, Backspace, Enter (Confirmed
  event — no usado todavía; el submit va por botón), Escape
  (Cancelled). El cursor se renderea como `|` al final.
- **Click handlers wired vía `cx.listener`**: menús del sidebar
  invocan `select_view`; botones de acción (header de list, submit
  de form) invocan `apply_action`. Los handlers tienen acceso real
  al `Context<MetaUi>` y mutan el modelo + emiten `cx.notify()`.
- **Submit lee texto de los inputs**: `commit_seed` reemplaza el
  buffer ad-hoc anterior por `input.read(cx).text()` por cada
  field. El value parseado va al `MemoryStore` con su tipo correcto
  (text/number/boolean/date).
- **Reset de inputs tras submit**: si la acción no tiene `next_view`,
  los inputs se vacían (`set_text("")`) para alta consecutiva sin
  re-tipear.
- **Hover states**: items del sidebar y botones cambian de bg al
  pasar el mouse, feedback visual consistente con el resto del
  ecosistema yahweh.
- **Theme global**: `Theme::install_default(cx)` al inicio (lo
  requiere el text_input para sus colores).

Wire en Cargo:
- Deps nuevas: `yahweh-widget-text-input`, `yahweh-theme` (paths
  relativos al monorepo).

Limitaciones que **siguen abiertas** (próximos iters):
- **`Action::Morphism`** sigue como TODO: requiere cargar el
  `Manifest` de nakui-core junto al `Module` UI para conocer los
  inputs/params declarados.
- **Sin persistencia entre runs**: `MemoryStore` en RAM. Wire con
  `EventLog` o `SurrealStore` queda para cuando exista el daemon
  Nakui.
- **Inputs simples**: el widget no soporta cursor positioning,
  selection, copy/paste, IME, multilínea. Para edits serios habrá
  que portar `gpui::examples::input` o adoptar `gpui-input` cuando
  exista upstream.
- **Enter no envía**: el `TextInputEvent::Confirmed` que emite el
  widget no está suscrito todavía; el submit va por click. Trivial
  de wirear si lo necesitamos.

Tests: los 6 unit del runtime siguen verdes (parse_field_value para
los 5 kinds, lookup_field nested, render_value). El comportamiento
visual requiere correr el binario con `cargo run -p nakui-ui` y
probar a mano — GPUI no provee harness de UI testing en CI hoy.

Activación full:
```sh
NAKUI_MODULES_DIR=examples/nakui-modules cargo run -p nakui-ui
# Click en un menú → carga vista. Click en "Nuevo" → form.
# Tipear en cada campo → ver el `|` al final. Click "Crear customer"
# → record aparece en la lista.
```

### feat(nakui): metainterfaz declarativa + 6 módulos ERP estándar
Salto cualitativo: Nakui pasa de "library + demos + read-only viewer
del event log" a **plataforma ERP con UI dirigida por datos**. Cada
módulo de negocio se declara como un `module.json` (sin código Rust
nuevo) y el runtime GPUI lo carga dinámicamente: sidebar de menús,
listas con columnas configurables, formularios de alta.

Tres entregables:

**1) Crate nuevo `nakui-ui-schema`** (datos puros, ~250 LOC + 200
LOC tests):
- `Module { id, label, entities, menu, views }`.
- `View::List { entity, columns, actions, search_in }` o
  `View::Form { entity, fields, on_submit }`.
- `FieldSpec { name, label, kind, default, required, help }` con
  `FieldKind = Text|Multiline|Number|Boolean|Date`.
- `Action::OpenView | SeedEntity | Morphism` — el runtime las
  dispara desde botones / submits.
- `Module::from_path` parsea un JSON; `Module::validate` chequea que
  cada `MenuItem.view` exista en `views`.
- `load_modules_from_dir(dir)` busca `dir/<modulo>/module.json`,
  parsea, valida, detecta IDs duplicados, devuelve ordenado.
- 6 tests unit + 4 integration (los 6 demos cargan limpio, todos
  tienen list+form, kinds reconocidos, validate pasa).

**2) Crate nuevo `nakui-ui`** (binario GPUI, ~700 LOC + 100 LOC tests):
- Carga módulos desde `NAKUI_MODULES_DIR` (default `./nakui-modules`).
- Sidebar con módulos + sus menús; click en menu cambia la vista activa.
- **List view**: tabla de instancias del entity con columnas
  weighted (header de columnas + filas + id corto).
- **Form view**: campos labeled + botón submit que dispara la action
  declarada (`SeedEntity` mete el record al `MemoryStore`
  in-process; `Morphism` queda como TODO hasta integrar el manifest
  loader nakui-core).
- `MemoryStore` compartido entre todas las vistas (Arc<Mutex>); el
  cambio en un módulo se refleja en otro inmediato.
- Toast + error banner para feedback.
- 6 tests unit (parse_field_value para los 5 kinds, lookup_field
  nested, render_value).

**3) 6 módulos demo** en `examples/nakui-modules/` que cubren un
ERP estándar:
- **customers**: nombre, email, teléfono, activo, límite de
  crédito, notas.
- **products**: SKU, nombre, categoría, precio, stock, activo.
- **suppliers**: razón social, ID fiscal, contacto, email,
  teléfono, términos de pago.
- **inventory_movements**: fecha, tipo (in/out/adjustment), SKU
  producto, cantidad, costo unitario, motivo, doc. referencia.
- **sales_orders**: número, cliente, emisión, vencimiento,
  estado, subtotal, impuestos, total, notas.
- **invoices**: número, cliente, emisión, vencimiento, subtotal,
  impuestos, total, pagado, estado, moneda, orden referenciada.

Cada módulo tiene su `list` (catálogo) + `form` (alta), con search
field y columns weighted. Los 6 cubren un setup de ERP de ventas
chico funcional para demo.

Filosofía documentada:
- **UI como datos**: agregar un módulo = escribir un JSON, no
  recompilar el binario.
- **Persistencia universal**: el runtime conecta cada vista al
  `nakui_core::store::Store`; cambiar de MemoryStore a SurrealStore
  no toca los module.json.
- **Schema primero, semántica después**: `nakui-ui-schema` sólo
  define la forma; validación de referencias rotas (entity inexistente,
  morphism faltante) vive en el runtime.

Activación:
```sh
NAKUI_MODULES_DIR=examples/nakui-modules cargo run -p nakui-ui
```

Limitaciones conocidas (próximas iteraciones):
- **Inputs sin teclado**: GPUI no incluye text input; los forms
  muestran los `default` del schema y el submit usa esos. Próximo
  iter: integración con `yahweh-widget-text-input`.
- **Click handlers no wired**: GPUI necesita pasar `Entity<MetaUi>`
  a los handlers para mutar estado; refactor con `cx.listener` +
  weak refs queda para el próximo iter. Hoy la navegación es
  visual; el código de mutación sí funciona via API programática
  (los tests lo cubren).
- **Acción `Morphism`**: pendiente de cargar el `Manifest` de
  nakui-core junto con el `Module` UI para wirear `execute_and_log`.
- **Sin persistencia entre runs**: el `MemoryStore` se pierde al
  cerrar. Wire con `EventLog` o `SurrealStore` queda para cuando
  el daemon Nakui exista.

Tests: 16 totales nuevos (10 schema + 6 runtime). 100% verde.

Lo que esto desbloquea: cualquiera puede escribir un `module.json`
para su dominio (pacientes médicos, alumnos de escuela,
reservaciones de hotel) y aparece en la UI sin tocar Rust ni
recompilar. La forma de extender Nakui dejó de ser "agregar código
al ERP" y pasó a ser "escribir el contrato del módulo".

### feat(nakui-explorer): nuevo binario GPUI — Nakui visible en la interfaz
Cierra "nakui no tiene UI propia" del audit. Nuevo binario standalone
`nakui-explorer` (paralelo a `nouser-explorer`) que renderea el
event log de un repo Nakui: timeline scrollable de seeds + morphisms
con sus parámetros, breakdown por entity type, polling cada 2s para
detectar nuevos eventos appended sin restart del explorer.

Diseño:
- Lee directamente el archivo `.jsonl` del `nakui_core::event_log::EventLog`.
  Path por env `NAKUI_EVENT_LOG`, default `nakui.jsonl` en pwd.
- Sin discovery vía broker brahman porque nakui hoy es CLI/library/
  demos, no daemon. Cuando se daemonice, sustituir el lector de
  archivo por un sidecar consumer (mismo patrón que nouser-explorer
  actualmente usa).

UI:
- **Header**: path del log, count total + breakdown seeds/morphisms,
  tiempo del último reload en ms.
- **Breakdown line**: top 5 buckets por frecuencia (entities + nombres
  de morphisms con prefijo `→`).
- **Timeline**: tarjetas color-coded por kind (azul=seed,
  verde=morphism). Cada tarjeta muestra `#seq`, kind, entity/morphism
  name, id corto (8 hex), preview del data/params (80 chars), schema
  hash corto (8 hex) o `(legacy)` si pre-versioning. Mostradas
  más-recientes-primero, hasta 200 visibles (suficiente para
  navegación; sin scroll virtualizado por ahora).
- **Error banner**: si la lectura falla (archivo inexistente o
  corrupto), banner rojo con el motivo. El explorer NO crashea —
  sigue intentando cada 2s.

Wire en workspace:
- Nuevo `crates/apps/nakui-explorer/` agregado a `[workspace] members`.
- Deps mínimas: `nakui-core` (para EventLog + LogEntry), `gpui`,
  `serde_json`, `uuid` (con feature serde para parsear los IDs).
- Sin deps de brahman por ahora (Nakui standalone).

Tests: 7 unitarios en `tests` mod del bin:
- `load_log_returns_all_entries_in_order` — cargar un .jsonl
  generado a mano, asserta que devuelve 5 entries con seqs 0..4
  contiguous.
- `breakdown_counts_seeds_morphisms_and_buckets` — verifica el
  conteo (3 seeds + 2 morphisms) y los buckets esperados.
- `load_missing_file_yields_empty_not_error` — archivo inexistente
  devuelve `[]` sin error (delegado al contrato de `EventLog::open`).
- `preview_value_truncates_long_strings` y `_keeps_short_strings_intact`.
- `short_uuid_takes_first_8_chars` y `short_hash_takes_first_4_bytes_hex`.

Activación:
```sh
NAKUI_EVENT_LOG=/tmp/nakui_inv_xxx.jsonl cargo run -p nakui-explorer
```

Estado del CHANGELOG global tras este commit: cero pendientes
fundamentados activos. Lo único que queda es `minga-vfs` (FUSE,
explícitamente diferido por el usuario) y mejoras nice-to-have
(cobertura adicional per-lenguaje, daemon-ización de nakui para
sidecar discovery).

### chore(nakui): alinear `nakui-core` con `[workspace.package]` y deps compartidas
Cleanup de drift de convenciones: `nakui-core` era el único crate del
monorepo que mantenía `version = "0.1.0"` / `edition = "2021"` /
`thiserror = "1"` hardcoded, mientras el resto heredaba del workspace
y usaba `thiserror = "2"`. Eso significaba que un bump global de versión
o de edition se olvidaba sistemáticamente de nakui.

Cambios:
- `[package]`: `version`, `edition`, `rust-version`, `license`, `authors`,
  `publish` → todos `*.workspace = true`. Agregado `description` (cumple
  convención del resto de crates).
- Deps compartidas migradas a `{ workspace = true }`: serde, serde_json,
  thiserror (v1→v2), tokio, ulid, sha2.
- `uuid` migrado a `{ workspace = true, features = ["serde"] }` — la
  feature `serde` no está en el workspace dep porque nakui es el único
  user; queda local opt-in en lugar de inflar el dep común.
- Deps específicas de nakui (sin compartición posible): rhai, petgraph,
  surrealdb permanecen inline con versión local.

Verificación: `cargo build -p nakui-core` verde tras el bump de
`thiserror` v1→v2 — el `#[derive(Error)]` de los 14+ enums de error
en nakui no requirió ajustes (la API de derive es backwards-compatible
para los patrones simples). `cargo test -p nakui-core --lib`: 27/27
verdes, sin regresión.

