# Changelog — nakui ERP categórico. ### feat(nakui): Fase 5 del ERP — tablero de KPIs Quinta fase del plan maestro. El módulo CRM gana una vista «Panorama» (primera del menú): tarjetas de KPI — total de clientes y oportunidades, monto en pipeline, oportunidades ganadas y monto ganado, más breakdowns de oportunidades por etapa e interacciones por canal. Tipos nuevos en la metainterfaz: ver el changelog de `nahual` (`View::Dashboard` / `Metric` / `compute_metric`). ### feat(nakui): Fase 4 del ERP — listas profesionales (orden/búsqueda/página) Las vistas de lista de meta-form ganan orden por columna, búsqueda en vivo y paginación. Ver el changelog de `nahual`. ### feat(nakui): Fase 3 del ERP — ficha de detalle Tercera fase del plan maestro. El módulo CRM: - Las listas de Clientes y Oportunidades ganan un botón 👁 por fila que abre la **ficha** del record (`row_detail`). - `cliente_detail` muestra los campos del cliente + sus oportunidades e interacciones (back-references). `oportunidad_detail` muestra los campos de la oportunidad, con el cliente resuelto a su nombre. - Navegación lista → ficha → volver. Tipos nuevos en la metainterfaz: ver el changelog de `nahual` (`View::Detail` / `RelatedList` / `ListView.row_detail`). ### feat(nakui): Fase 2 del ERP — relaciones legibles + formato Segunda fase del plan maestro. El módulo CRM: - Las columnas `cliente_id` de las listas de Oportunidades e Interacciones muestran el **nombre del cliente**, no su UUID (`ref_entity` en la columna). - La columna `monto` se formatea como moneda (`$12,000`). - En los formularios, el campo de cliente/oportunidad muestra el record elegido por su nombre. Tipos nuevos en la metainterfaz: ver el changelog de `nahual` (`Column.ref_entity` / `Column.format`). ### feat(nakui): plan maestro del ERP + Fase 1 (captura sin fricción) Plan maestro del subproyecto en `docs/nakui-erp-masterplan.md`: 7 fases ordenadas para llevar nakui de "listas y formularios que funcionan" a "ERP profesional terminado". Fase 1 implementada: - El módulo CRM usa `select` para etapa y canal (desplegable de opciones) y `auto_id` para los ids de idempotencia — ningún formulario pide un UUID a mano. - `NakuiBackend::seed` inyecta el `id` de la entity = clave del store, así `data.id` y la clave coinciden (los schemas Nickel declaran `id | String`); el formulario de cliente ya no necesita campo `id`. - Tipos de campo nuevos en la metainterfaz: ver el changelog de `nahual` (`FieldKind::Select` / `AutoId`). ### feat(nakui-ui): CRM como ERP — listas y formularios UiModule `examples/nakui-modules/crm/module.json`: hace que el módulo `crm` se vea como un ERP de verdad en `nakui-ui` (sidebar + listas + formularios), no como el timeline del event log de `nakui-explorer`. - 3 entities, 7 vistas: lista + formulario de Clientes, Oportunidades e Interacciones, más los formularios de morfismo «Abrir», «Mover» y «Registrar». - `nakui_module_dir` engancha el módulo-kernel `crm`: el form de cliente siembra (`seed_entity`); «Abrir/Mover/Registrar» disparan los morfismos reales con la validación del kernel (etapas, transiciones, canal). - 2 tests en `nakui-ui` verifican que el UiModule parsea, valida y carga por el camino real (`brahman_cards::load_cards_from_dir`). Correr: `NAKUI_MODULES_DIR=examples/nakui-modules cargo run -p nakui-ui`. ### feat(nakui): módulo `crm` — clientes, pipeline de ventas, interacciones Módulo CRM funcional, declarativo como inventory/sales/treasury (`crates/modules/nakui/modules/crm/`): `schema.ncl` + `nsmc.json` + morfismos Rhai. Tres entities — `Cliente`, `Oportunidad`, `Interaccion` — y tres morfismos: - `abrir_oportunidad(cliente)` — crea una `Oportunidad` en etapa `prospecto`. - `mover_oportunidad(oportunidad)` — avanza el pipeline (prospecto→calificado→propuesta→negociacion→ganada/perdida). Valida: etapa destino conocida, no mover una oportunidad cerrada, no retroceder. - `registrar_interaccion(cliente)` — crea una `Interaccion` (llamada/email/reunión). - `core/src/bin/crm_demo.rs` — demo realista (3 clientes, 18 eventos). A diferencia de los otros demos **no borra el event log**: lo deja en disco e imprime el comando para abrirlo con `nakui-explorer` — así el explorador, que sólo mostraba un log vacío, muestra un CRM con cuerpo. - `core/tests/crm.rs` — 8 tests de integración (pipeline completo + rechazos: retroceso, oportunidad cerrada, etapa/canal/monto inválidos). Nota: el engine Rhai de `nakui-core` es `Engine::new_raw()` con paquetes selectos — `Array::index_of` no resuelve; los morfismos usan sólo `if`/`==`/`throw`. 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:\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 "") in (std.deserialize 'Json m%%""%%) | bundle.`. - 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//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, Vec)`** 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//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(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 `. 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` 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: `"'' 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, 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
` (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`** 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`** 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` — 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` — 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>`** 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>>`** 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` (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` 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//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); 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` 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.