tawasuyu

tawasuyu/brahman

Rust 506TOML 198Markdown 56JSON 197212 Definiciones

⎇ eba629a806c7f861dd00a37c20b0f9e9d1ef3a52 docs/changelog/nakui.md Markdown · 1343 líneas · 63.9 KB Historia crudo

feat(nakui): Fase 2 del ERP — relaciones legibles + formato eba629a80 · sergio ·

1# Changelog — nakui
2
3ERP categórico.
4
5### feat(nakui): Fase 2 del ERP — relaciones legibles + formato
6
7Segunda fase del plan maestro. El módulo CRM:
8
9- Las columnas `cliente_id` de las listas de Oportunidades e
10  Interacciones muestran el **nombre del cliente**, no su UUID
11  (`ref_entity` en la columna).
12- La columna `monto` se formatea como moneda (`$12,000`).
13- En los formularios, el campo de cliente/oportunidad muestra el
14  record elegido por su nombre.
15
16Tipos nuevos en la metainterfaz: ver el changelog de `nahual`
17(`Column.ref_entity` / `Column.format`).
18
19### feat(nakui): plan maestro del ERP + Fase 1 (captura sin fricción)
20
21Plan maestro del subproyecto en `docs/nakui-erp-masterplan.md`: 7 fases
22ordenadas para llevar nakui de "listas y formularios que funcionan" a
23"ERP profesional terminado". Fase 1 implementada:
24
25- El módulo CRM usa `select` para etapa y canal (desplegable de
26  opciones) y `auto_id` para los ids de idempotencia — ningún
27  formulario pide un UUID a mano.
28- `NakuiBackend::seed` inyecta el `id` de la entity = clave del store,
29  así `data.id` y la clave coinciden (los schemas Nickel declaran
30  `id | String`); el formulario de cliente ya no necesita campo `id`.
31- Tipos de campo nuevos en la metainterfaz: ver el changelog de
32  `nahual` (`FieldKind::Select` / `AutoId`).
33
34### feat(nakui-ui): CRM como ERP — listas y formularios
35
36UiModule `examples/nakui-modules/crm/module.json`: hace que el módulo
37`crm` se vea como un ERP de verdad en `nakui-ui` (sidebar + listas +
38formularios), no como el timeline del event log de `nakui-explorer`.
39
40- 3 entities, 7 vistas: lista + formulario de Clientes, Oportunidades e
41  Interacciones, más los formularios de morfismo «Abrir», «Mover» y
42  «Registrar».
43- `nakui_module_dir` engancha el módulo-kernel `crm`: el form de cliente
44  siembra (`seed_entity`); «Abrir/Mover/Registrar» disparan los morfismos
45  reales con la validación del kernel (etapas, transiciones, canal).
46- 2 tests en `nakui-ui` verifican que el UiModule parsea, valida y carga
47  por el camino real (`brahman_cards::load_cards_from_dir`).
48
49Correr: `NAKUI_MODULES_DIR=examples/nakui-modules cargo run -p nakui-ui`.
50
51### feat(nakui): módulo `crm` — clientes, pipeline de ventas, interacciones
52
53Módulo CRM funcional, declarativo como inventory/sales/treasury
54(`crates/modules/nakui/modules/crm/`): `schema.ncl` + `nsmc.json` +
55morfismos Rhai. Tres entities — `Cliente`, `Oportunidad`, `Interaccion`
56— y tres morfismos:
57
58- `abrir_oportunidad(cliente)` — crea una `Oportunidad` en etapa
59  `prospecto`.
60- `mover_oportunidad(oportunidad)` — avanza el pipeline
61  (prospecto→calificado→propuesta→negociacion→ganada/perdida). Valida:
62  etapa destino conocida, no mover una oportunidad cerrada, no retroceder.
63- `registrar_interaccion(cliente)` — crea una `Interaccion`
64  (llamada/email/reunión).
65
66- `core/src/bin/crm_demo.rs` — demo realista (3 clientes, 18 eventos).
67  A diferencia de los otros demos **no borra el event log**: lo deja en
68  disco e imprime el comando para abrirlo con `nakui-explorer` — así el
69  explorador, que sólo mostraba un log vacío, muestra un CRM con cuerpo.
70- `core/tests/crm.rs` — 8 tests de integración (pipeline completo +
71  rechazos: retroceso, oportunidad cerrada, etapa/canal/monto inválidos).
72
73Nota: el engine Rhai de `nakui-core` es `Engine::new_raw()` con paquetes
74selectos — `Array::index_of` no resuelve; los morfismos usan sólo
75`if`/`==`/`throw`.
76Iter 17. Regresión surfaceada por el workspace test
77`verify_log_rejects_seed_after_schema_kcl_changes` (rebautizado a
78`verify_log_rejects_seed_after_schema_changes`).
79
80**Bug**: `compute_schema_bundle_hash` operaba sobre los bytes del
81bundle compilado, que `build_schema_bundle` arma como
82`(import "/abs/path/schema.ncl") & ...`. Los bytes del bundle nunca
83cambian cuando se edita el archivo apuntado — sólo cambian si se
84agregan/quitan schemas o se mueve el módulo de path. Resultado: el
85hash quedaba pegado y los seeds firmados bajo schema vN se aceptaban
86silenciosamente como válidos bajo schema vN+1, aunque las invariantes
87hubieran cambiado.
88
89**Fix**: nueva fn `read_schema_files_concat(module_dir, schemas)`
90que lee los bytes de cada schema declarado y los concatena con
91framing `\0NCL:<name>\0` (separador no ambiguo + nombre relativo, no
92absoluto, para que el hash sea estable entre máquinas). Esos bytes
93alimentan `compute_schema_bundle_hash` y `compute_morphism_schema_hash`
94en lugar de los bytes del bundle. El bundle compilado sigue siendo
95imports-style (necesario para que Nickel resuelva paths relativos);
96sólo la fuente del hash cambió.
97
98**Impacto en logs existentes**: como cualquier cambio al insumo del
99hash, los seeds y morphisms versados bajo el bundle hash anterior
100fallarán `SchemaMismatch` al verificarse contra un binario nuevo. No
101hay migración — esto es exactamente el comportamiento que el hash
102busca: re-seed.
103
104Tests: 10/10 en `schema_versioning` (era 9/10 con 1 FAILED).
105
106### refactor(nakui-core): KCL → Nickel — `kcl_wrapper` reemplazado por evaluación in-process
107Cierra el ciclo: el motor de validación de entities deja de
108shellear el binario externo `kcl` y pasa a evaluar **Nickel
109contracts** in-process via la dep `nickel-lang` (la misma que ya
110usa `brahman-cards` para sus templates). Los 3 schemas de los
111módulos sales/inventory/treasury migran de `.k` a `.ncl`.
112Además se borran los 2 archivos `.k` doc-only del repo
113(`ente-card/schema/card.k`, `ente-brain/schema/rule.k` — ambos
114estaban marcados "REFERENCE ONLY. NOT LOADED").
115
116Cambios en **nakui-core**:
117- **Nueva dep**: `nickel-lang = "2.0.0"` (interfaz estable).
118- **Borrado** `kcl_wrapper.rs` (43 líneas) — shellear el binario
119  desaparece.
120- **Nuevo** `nickel_validator.rs`:
121  - `pub fn vet(schema_path, state, schema_name) -> Result<(), NickelError>`
122    evalúa `let bundle = (import "<schema>") in
123    (std.deserialize 'Json m%%"<json>"%%) | bundle.<entity>`.
124  - El state JSON va dentro de un raw string Nickel
125    (`m%%"..."%%`) y se deserialize via `std.deserialize 'Json`.
126    No embebemos el state como record literal Nickel directo
127    porque la sintaxis JSON usa `:` (Nickel records usan `=`).
128  - 5 tests propios cubriendo happy path + 4 fallure modes
129    (field missing, predicate fails, cross-field invariant
130    fails, optional field present/absent).
131- **`executor.rs`**:
132  - `kcl_wrapper::vet` → `nickel_validator::vet`.
133  - `KclError` → `NickelError`.
134  - `ExecError::KclPre/KclPost/KclPostCreate` → `SchemaPre/Post/PostCreate`
135    (más neutro, ya no menciona KCL).
136  - `kcl_check` (privado) → `validate_entity`.
137  - `build_schema_bundle` ahora emite un archivo Nickel con
138    `(import "X") & (import "Y") & ...` en lugar de concatenar
139    bytes (cada `.ncl` es una expresión record completa, no
140    juntable como texto plano).
141- **`manifest.rs`**:
142  - `effective_schemas` default `"schema.k"` → `"schema.ncl"`.
143  - `extract_schema_names` reescrito: ahora detecta keys
144    CapitalCase con 2 spaces de indent (convención de los
145    `schema.ncl`), no más patrón `schema X:` de KCL.
146  - Tests del extractor actualizados (1 test reemplazado por 2:
147    `_handles_nickel_record_top_level` + `_skips_let_bindings_and_lowercase`).
148
149Cambios en **schemas de módulos**:
150- **`sales/schema.ncl`**: contracts Nickel para `Venta`. Usa
151  `std.contract.Sequence [record_contract, from_predicate]`
152  para combinar shape + invariante cross-field
153  (`total == cantidad * precio_unitario`). El patrón directo
154  `record | from_predicate` rebota con "missing definition" porque
155  el predicate evalúa el contract antes de que el value lo
156  populate; documentado en el comment.
157- **`inventory/schema.ncl`**: `Stock`, `MovimientoStock`,
158  `TransferenciaStock` (esta última con cross-field
159  `source != dest` via Sequence).
160- **`treasury/schema.ncl`**: `Caja`, `Movimiento`,
161  `Transferencia` (con cross-field via Sequence).
162- Helpers locales en cada archivo: `positive_int`,
163  `non_negative_int`, `currency_iso`, etc. via
164  `std.contract.from_predicate`.
165- Los 3 `schema.k` viejos **borrados**.
166- `sales/nsmc.json` actualizado: paths `schema.k` →
167  `schema.ncl`.
168
169Cambios en **tests**:
170- `sales.rs`, `inventory.rs`: `KclPost` → `SchemaPost`.
171- `kernel_guards.rs`: `KclPostCreate` → `SchemaPostCreate`,
172  path del schema directo `treasury/schema.k` →
173  `treasury/schema.ncl`.
174- `graph.rs`, `manifest_validation.rs`: tests que escriben
175  `schema.k` inline cambian a `schema.ncl` con sintaxis Nickel.
176- `schema_versioning.rs`: refs `schema.k` → `schema.ncl`.
177
178Cambios documentales:
179- **Borrado** `crates/core/ente-card/schema/card.k` (1 archivo,
180  REFERENCE ONLY documentado en su header).
181- **Borrado** `crates/core/ente-brain/schema/rule.k` (REFERENCE
182  ONLY documentado en su header).
183
184Tests:
185- **nakui-core**: 84 tests verdes (41 unit + 43 integration en
186  graph/event_log/manifest_validation/schema_versioning/
187  inventory/sales/kernel_guards). Suite full pasa.
188- **nakui-ui**, **brahman-cards**, **yahweh-***: sin cambios,
189  todos verdes.
190- Total cubriendo el área: 174 tests.
191
192Beneficios:
193- **Sin dep externa**: el binario `kcl` ya no es requisito de
194  runtime ni de tests. Build limpio en CI sin instalar KCL.
195- **Errores en línea**: Nickel reporta contract violations con
196  caret pointing al field exacto del schema y el value que
197  falló. KCL daba mensajes textuales menos navegables.
198- **Mismo motor que el brazo de cards**: una sola dependencia
199  Nickel para todo el repo (validación + templates de cards).
200- **Sin tempfile JSON intermedio**: el state se evalúa
201  directamente en memoria; no hay `std::fs::write` por cada
202  validate.
203
204Limitaciones / decisiones:
205- El comentario "REFERENCE ONLY" de los `.k` borrados ya estaba
206  marcado en sus headers; eran sólo notas de diseño para humanos.
207  La autoridad real (Rust validate methods) sigue intacta.
208- La sintaxis Nickel `record_contract | from_predicate` no
209  funciona — hay que envolver en `std.contract.Sequence [record,
210  from_predicate]`. Documentado en cada schema y en el doc del
211  validator.
212
213**Pendientes restantes**: ninguno del refactor original. Los
214yahweh + KCL + card.k cierran. Próximos pendientes salen de
215nuevo trabajo (no del plan que arrastrábamos).
216
217### feat(nakui-ui): migrar consumer al brazo unificado `brahman_cards::load_cards_from_dir`
218Primera consumer migration del brazo. `nakui-ui` ya no llama a
219`nakui_ui_schema::load_modules_from_dir` directamente — pasa por
220`brahman_cards::load_cards_from_dir` y extrae el variant `UiModule`
221del `CardBody` de cada Card. Beneficios concretos:
222
223- **Soporta `.ncl` además de `.json`**: el usuario puede dropear un
224  `card.ncl` (con templates Nickel + merge) en cualquier subdir y
225  el runtime lo levanta automáticamente. El layout legacy
226  `examples/nakui-modules/<id>/module.json` sigue funcionando vía
227  los filenames default `[card.ncl, card.json, module.ncl, module.json]`.
228- **Cards de otros body kinds (Ente/Monad) se skipean limpio**:
229  si el dir contiene Cards no-UiModule, se reportan en un toast
230  informativo en lugar de fallar la carga.
231
232Cambios en `brahman-cards`:
233- **Nuevo `load_cards_from_dir(dir)`** + variante con readers/filenames
234  custom. Walkea subdirs (orden lexicográfico), busca el primero de
235  `DEFAULT_CARD_FILENAMES`, dispatcha al reader. Subdirs sin ningún
236  filename matching se skipean silenciosamente (permite assets/fixtures
237  sueltos al lado de los cards). Errores per-file se propagan loud
238  (sin ocultar corrupción).
239- **`pub const DEFAULT_CARD_FILENAMES`**: lista canónica probada en
240  orden. `card.ncl` tiene prioridad sobre `card.json` y sobre los
241  legacy `module.*`.
242- **4 tests nuevos del helper**: walk + skip de subdirs sin
243  card, prioridad ncl > json, propagación loud de errores per-file,
244  custom filenames.
245
246Cambios en `nakui-ui`:
247- **Nueva dep** `brahman-cards` en `Cargo.toml`.
248- **Nuevo helper `load_ui_modules(dir) -> (Vec<Module>, Vec<String>)`**
249  que envuelve `brahman_cards::load_cards_from_dir`, filtra a
250  UiModule body, valida cada Module con su `validate()`, ordena
251  por id, y detecta duplicados. El callsite en `MetaUi::new` pasa
252  a usarlo y al ver Cards skipped emite un toast informativo.
253- **3 tests nuevos**:
254  - `load_ui_modules_via_brahman_cards_returns_ui_modules_and_skips_others`
255    — verifica que un dir con UiModule + Ente carga el primero y
256    reporta el segundo en `skipped`.
257  - `load_ui_modules_via_brahman_cards_rejects_invalid_module` —
258    `Module::validate()` se sigue aplicando (menu apuntando a view
259    inexistente rebota).
260  - `load_ui_modules_detects_duplicate_id` — dos UiModule con
261    mismo id rebotan con mensaje claro.
262
263Tests totales:
264- `brahman-cards`: 22 → 26 (+4 helper directorio).
265- `nakui-ui`: 45 → 48 (+3 e2e migración).
266- Workspace build verde.
267
268Lo que NO cambió:
269- `nakui_ui_schema::load_modules_from_dir` se mantiene intacto (sus
270  propios tests lo siguen usando, y otros consumers futuros podrían
271  preferir su error-typing más específico). La migración es opt-in:
272  `nakui-ui` usa el brazo, ui-schema sigue siendo una API válida.
273- Layout actual de `examples/nakui-modules/<id>/module.json` no
274  requiere cambio. Un usuario puede convertir cualquier módulo a
275  `card.ncl` sin tocar el dir layout.
276
277**Pendientes para próximos commits** (orden):
2781. **Yahweh refactor**: lift del MetaUi runtime a
279   `crates/modules/ui_engine/` para reuso. El brazo + canónico ya
280   estables, ahora puede extraerse el meta-form widget genérico.
2812. **KCL → Nickel**: kcl_wrapper reemplazado por Nickel contracts;
282   los 3 schemas .k de nakui modules pasan a .ncl.
2833. **card.k eliminado** (es REFERENCE ONLY documentado).
284
285### feat(nakui-ui): validación cross-field del EntityRef (existence en store)
286Cierra otro pendiente. Hasta ahora `parse_field_value(EntityRef, raw)`
287sólo validaba **forma** (UUID parseable + trim de whitespace) — un
288UUID válido pero inexistente en el store pasaba silenciosamente al
289log/store, dejando dangling references. Ahora validamos también
290**existencia** contra la entity declarada en `FieldSpec.ref_entity`.
291
292Cambios:
293- **Nuevo helper `validate_entity_refs<S: Store>(store, refs)`**:
294  - `refs: &[(label, target_entity, uuid)]`.
295  - Loop fail-fast: primer record ausente → error con label
296    legible + UUID corto + target entity en el msg
297    `"campo 'Stock': record abc12345 de 'Stock' no existe en el store"`.
298  - Pure (toma `&S: Store`), totalmente testable sin GPUI.
299- **Wireup en `commit_seed`**:
300  - Durante el parse loop, cuando un field es EntityRef + tiene
301    `ref_entity` declarado + value parseado a UUID, lo encolamos
302    en `entity_refs: Vec<(String, String, Uuid)>`.
303  - Después del parse loop (antes del seed/edit branch), si
304    `entity_refs` no está vacío, una sola toma del store lock
305    para validar todos via el helper.
306  - Falla early: ningún log entry, ningún apply.
307- **Cobertura**:
308  - SEED path: alta nueva con EntityRef → validamos antes de
309    `Seed { data }`.
310  - EDIT path: edit con EntityRef → validamos antes de calcular
311    delta. Una optional empty (que iría a clear) no cuenta como
312    EntityRef (raw vacío skipea el push).
313  - Morphism inputs: NO se duplica acá. `Executor::compute` ya
314    valida cada input via `store.load(...).ok_or(EntityMissing)`
315    antes de correr el script Rhai. Documentado en el doc del
316    helper.
317
3185 tests nuevos:
319- `validate_entity_refs_passes_when_all_records_exist` — happy path.
320- `validate_entity_refs_fails_on_first_missing` — fail-fast con
321  msg que incluye entity + UUID corto.
322- `validate_entity_refs_uses_label_not_entity_in_msg` — el label
323  legible (ej: "Stock origen") aparece en el error, no la entity
324  desnuda.
325- `validate_entity_refs_empty_list_is_ok` — lista vacía es Ok.
326- `validate_entity_refs_distinguishes_target_from_other_entities` —
327  un UUID que existe bajo Customer pero NO Stock falla la
328  validación contra Stock.
329
33045 tests verdes en nakui-ui (+5). Workspace build verde.
331
332Comportamiento esperado:
333- **Selector clickable es happy path**: el dropdown sólo lista
334  records existentes, así que clickearlo nunca debería disparar
335  el error. Sólo dispara con paste manual de UUID que no existe
336  o records borrados después de la selección (timing race).
337- **Optional empty no se valida**: si el field es EntityRef
338  optional y el form lo deja vacío, lo manejamos como "no value"
339  (skipea el push a `entity_refs`); la lógica de Clear se
340  encarga del resto.
341- **Lock contention**: una sola toma del store lock por
342  `commit_seed`, no una por field. La validación es O(refs) reads.
343
344Pendientes restantes:
345- **Validación KCL del record post-edit** antes de emitir Set/Clear
346  (hoy `ui.edit_record` no pasa por `Executor::compute`).
347- **EntityRef cross-module** (referenciar records de OTRO módulo
348  por nombre, no sólo por entity local).
349
350### feat(nakui-core,nakui-ui): FieldOp::Clear — borrar values vía form vacío
351Cierra el último pendiente de UX del round. El edit no podía
352"borrar" un value vaciando el input — empty optional fields
353hacían `continue` en `commit_seed`, así que el current value
354quedaba intacto. Para honrar el intent del usuario ("este field
355ya no aplica") necesitábamos un FieldOp explícito que remueva
356la key del map.
357
358Cambios en **nakui-core** (la variante es semántica del kernel,
359no específica de la UI):
360
361- **`delta::FieldOp::Clear { path }`** — nueva variante.
362  Distinta de `Set { value: Null }`: Clear borra la clave; Set
363  Null deja la clave con valor literal `null`. Importa para
364  downstream que diferencia "ausente" vs "presente como null"
365  (ej: serde con `skip_serializing_if = "Option::is_none"`).
366- **`capability_token`** — Clear devuelve `entity.field`,
367  mismo shape que Set. Una capability `writes: ["Customer.notes"]`
368  autoriza tanto Set como Clear sobre ese field.
369- **`simulate_on`** — Clear remueve la key del Object si el
370  state es Some(Object). Skip silente si el state es None
371  (deleted) o no-objeto.
372- **`MemoryStore::apply_dry_run`** — Set y Clear comparten
373  pre-condición (record padre existe + es objeto). Pattern
374  combinado con `|`.
375- **`MemoryStore::apply`** — Clear hace `map.remove(field)`.
376  Field ausente = no-op silencioso (post-state idéntico).
377- **`SurrealStore::apply_dry_run`** — Set/Clear combinados.
378- **`SurrealStore::apply`** — Clear emite
379  `UPDATE type::thing UNSET <field>`. El field name viene de
380  un FieldSpec validado upstream; SurrealQL no soporta binding
381  de identifiers, así que va inline (con la advertencia
382  documentada en el comment).
383- **`Executor` capability check** — Set/Clear comparten match
384  (mismo token shape, misma resolución a binding role).
385- **Conservation rules** (en `check_conservation`) NO consideran
386  Clear — sólo Set. Documentado: morphism authors que querían
387  clear de un field con conservation tienen que ser cuidadosos;
388  KCL post-checks pueden capturar violations.
389
390Cambios en **nakui-ui**:
391
392- **`commit_seed` loop** acumula `to_clear: Vec<String>` con
393  los nombres de fields optional empty (en lugar de hacer
394  `continue` silencioso).
395- **EDIT branch**:
396  - Computa `set_delta` (igual que antes) + `clear_fields` via
397    nuevo helper `compute_clear_fields(current, to_clear)`.
398  - Helper filtra a sólo los fields que actualmente tienen
399    valor non-null — Clear de un field ausente o ya null no
400    se emite (sería no-op semántico). Preserva el orden del
401    input para estabilidad del log entry.
402  - Construye `ops` combinando Set + Clear.
403  - NoChange ahora requiere AMBOS vacíos (set_delta y
404    clear_fields).
405  - `params` del log entry incluye `cleared: ["field1", ...]`
406    sólo si non-empty (preserva la shape `fields:` para
407    edits sin clears).
408  - `CommitOutcome::Updated.changed = sets + clears` para
409    que el toast `"actualizado X (N campo(s))"` siga siendo
410    preciso.
411
412Tests nuevos:
413- **delta.rs**: `simulate_clear_removes_field`,
414  `simulate_clear_then_set_same_field_keeps_set`,
415  `clear_capability_token_matches_set_shape`.
416- **store.rs**: `apply_clear_removes_field_key`,
417  `apply_clear_on_absent_field_is_noop`,
418  `dry_run_rejects_clear_on_missing_record`,
419  `dry_run_rejects_clear_on_non_object`.
420- **nakui-ui main.rs**: `clear_fields_skips_absent_and_null`,
421  `clear_fields_preserves_input_order`,
422  `clear_fields_empty_when_current_is_null`.
423
42434 tests verdes en nakui-core (+7), 40 en nakui-ui (+3).
425Workspace build verde. E2E del morphism real
426`morphism_pipeline_executes_real_sales_vender` intacto — `vender`
427no usa Clear.
428
429Implicaciones:
430- **El log puede crecer con entries `ui.edit_record` que sólo
431  tienen `cleared: [...]`** sin `fields`. Esperado y esperable.
432- **Replay**: las entries con Clear se aplican en orden via
433  `store.apply(&ops)`. La semantic es deterministic.
434- **Si un módulo tiene KCL invariants sobre la presencia de un
435  field**, el usuario podría romper el record vaciando ese
436  field via UI. Hoy esto NO se chequea — `ui.edit_record` es
437  un morphism manual que no pasa por `Executor::compute`. Si
438  esto es un problema, el camino futuro es validar contra el
439  KCL del entity al submit (otro pendiente).
440
441Pendientes restantes:
442- **Validación cross-field** (ej: UUID del EntityRef existe en
443  la entity referida).
444- **Validación KCL del record post-edit** antes de emitir Set/Clear.
445
446### feat(nakui-ui): snapshot/compaction durante runtime cada N writes
447Cierra el último pending del round de persistencia. Antes el compact
448sólo corría al startup — para una sesión larga con muchas escrituras,
449el log crecía sin tope hasta el próximo restart, y el siguiente boot
450pagaba el costo lineal del replay.
451
452Cambios:
453- **Nuevos fields en `MetaUi`**:
454  - `snap_path: PathBuf` — cacheado del init para que el tick no
455    tenga que recomputarlo.
456  - `snapshot_threshold: usize` — leído del env en `new()` y
457    cacheado. `0` desactiva runtime compact (mismo env y
458    semantic que el threshold de startup).
459  - `writes_since_compact: u64` — contador que incrementa por cada
460    write efectivo y se resetea cuando el threshold dispara
461    `maybe_compact_log`.
462- **Nuevo método `tick_runtime_compact()`**:
463  - Early return si `threshold == 0`.
464  - Increment + check vs threshold.
465  - Si cruza: lock log + store, llama `maybe_compact_log`.
466  - **Si compactó OK**: counter = 0, devuelve msg.
467  - **Si `maybe_compact_log` returned None** (counter dijo "go"
468    pero entries < 2): counter = 0 (no re-entrar cada write).
469  - **Si error**: counter NO se resetea (próximo write reintenta),
470    devuelve el error.
471- **Nuevo helper `append_compact_msg(base, opt)`**: concatena el
472  msg del compact al toast del op original con `";"` separator.
473- **Wireup en 3 callsites de write efectivo**:
474  - `apply_action::SeedEntity`: tick si outcome != NoChange.
475  - `apply_action::Morphism`: tick siempre que Ok.
476  - Click handler `[Confirmar]` del delete modal: tick si commit_delete Ok.
477- **NoChange no cuenta**: un edit que no cambia nada no escribe al
478  log, así que tampoco debería avanzar el counter — preserva la
479  semantic "1 write = 1 log entry = 1 tick".
480
4812 tests nuevos:
482- `append_compact_msg_handles_both_branches` — base solo vs base
483  + compact, formato del separator.
484- `runtime_compact_cycle_resets_counter_after_threshold` — E2E
485  estilo simulación: 7 writes con threshold=3 → 2 compacts (en
486  write 3 y 6), counter residual = 1, log final con 2 entries
487  (1 anchor + 1 write residual). Reproduce el algoritmo del tick
488  sin GPUI cx; si la lógica del método cambia, se rompe como signal.
489
49037 tests verdes (+2). Workspace build verde.
491
492Trade-offs:
493- **Counter en memoria, no persistido**: si la app crashea entre
494  compacts, al próximo boot el counter parte de 0. El startup
495  compact (basado en entry_count del log file) compensa esto:
496  si quedó mucho post-último-compact, se compacta al boot.
497- **Lock orden**: tick toma log lock primero, store lock después.
498  Misma orden que `commit_seed` y `commit_morphism`, no debería
499  haber deadlock.
500- **Costo del tick**: 1 increment + 1 compare por write. Cuando
501  cruza threshold, 1 read del log (entries) + 1 snapshot write +
502  1 compact. Para threshold=50 es ~1 fsync cada 50 writes —
503  amortiza bien.
504
505Pendientes restantes:
506- **`FieldOp::Clear`** — para soportar borrar un value vía form vacío.
507- **Validación cross-field** (UUID del EntityRef existe en la
508  entity referida).
509
510### feat(nakui-ui): atajo Esc para cancelar el modal de delete
511Cierra otro pendiente de UX. El banner de confirmación de delete
512ya tenía botones [Cancelar] / [Confirmar], pero la acción más
513natural para cancelar un dialog es Esc — y no la teníamos wireada.
514
515Cambios:
516- **`capture_key_down` en el root div** del `MetaUi::render`. Capture
517  phase (no bubble) para interceptar el Esc *antes* que cualquier
518  TextInput descendiente lo consuma. Sin pending el handler es
519  no-op y el evento sigue su flujo normal.
520- **Match `event.keystroke.key == "escape"`** + `pending_delete.take()`
521  → toast `"delete cancelado (Entity) [esc]"` (sufijo `[esc]` para
522  diferenciar visualmente del botón). Si no hay pending, return
523  temprano sin tocar nada.
524- **Hint visual en el banner**: subtítulo en amber tenue debajo del
525  título: `"Esc para cancelar · click [Confirmar] para borrar"`.
526  Que el usuario descubra el atajo sin RTFM.
527
52835 tests verdes — el handler de Esc es 8 líneas no-testeables sin
529GPUI cx (la lógica de pending_delete + toast vive dentro del
530listener); el wireup compila por type-check.
531
532Pendientes restantes:
533- **`FieldOp::Clear`** — para soportar borrar un value vía form vacío.
534- **Snapshot durante runtime** (cada N writes, no sólo al startup).
535- **Validación cross-field** (UUID del EntityRef existe en la
536  entity referida).
537
538### feat(nakui-ui): EntityRef validation en parse_field_value (UUID al submit)
539Cierra otro pendiente: `parse_field_value(FieldKind::EntityRef, raw)`
540devolvía `Ok(json!(raw))` blindly — el value entraba al log/store
541incluso si era basura. La validación de UUID sólo ocurría cuando el
542field se usaba como **input** del morphism (línea ~540 de
543`commit_morphism`); como **seed field** o como **param**, garbage
544pasaba silenciosamente.
545
546Cambios:
547- **`parse_field_value(EntityRef, raw)`** ahora hace
548  `Uuid::parse_str(raw.trim())` y devuelve error claro si falla:
549  `"'<raw>' no es UUID válido (usá el selector de records)"`. En
550  caso de éxito, devuelve el UUID **trimmed** como string —
551  protege contra paste manual con whitespace.
552- **Doble cobertura**: este path cubre seed fields (commit_seed via
553  obj.insert) y morphism params (resolve_param_value lo invoca por
554  cada FieldSpec con kind=EntityRef). El path de morphism inputs
555  ya validaba antes con `Uuid::parse_str` directo — sigue intacto,
556  no hay double-validation.
557- **Selector clickable es happy path**: el dropdown setea valores
558  bien-formados, así que el usuario nunca debería ver el error en
559  uso normal. Sólo dispara con paste manual o si el usuario escribe
560  garbage en el input — defensivo.
561
5625 tests nuevos (reemplazan al obsoleto `parse_field_entity_ref_returns_string`):
563- `parse_field_entity_ref_accepts_valid_uuid` — happy path.
564- `parse_field_entity_ref_trims_whitespace` — `"  uuid\n"` → `"uuid"`.
565- `parse_field_entity_ref_rejects_non_uuid` — `"abc-123"` → error
566  con el value y la palabra "UUID" en el mensaje.
567- `parse_field_entity_ref_rejects_empty_string` — `""` → rebota.
568- `resolve_param_strict_entity_ref_propagates_error` — sanity de
569  que el wireup en resolve_param_value hereda el strict checking,
570  con label del FieldSpec en el mensaje.
571
57235 tests verdes (+4 net). El E2E del morphism real
573`morphism_pipeline_executes_real_sales_vender` sigue verde — sus
574inputs van por el path dedicado, no por parse_field_value.
575
576Pendientes restantes:
577- **Atajo Esc para Cancelar** del modal de delete.
578- **`FieldOp::Clear`** — para soportar borrar un value vía form.
579- **Snapshot durante runtime** (cada N writes, no sólo al startup).
580- **Validación cross-field** (ej: el UUID del EntityRef existe en
581  la entity referida) — hoy sólo validamos forma; un UUID válido
582  pero inexistente sí pasa.
583
584### feat(nakui-ui): snapshot/compaction automático del event log al startup
585Cierra el último gran pendiente del round: el replay full cada
586startup escala lineal en el log. Con 60+ entries el costo de boot
587se nota; con 10k entries es prohibitivo. Wireamos el snapshot
588machinery que ya estaba en `nakui-core` (`Snapshot`,
589`replay_with_snapshot_into`, `EventLog::compact_through`) al
590runtime de la UI.
591
592Cambios:
593- **Path del snapshot**: sibling del log, extensión `.snap.json`.
594  `nakui-ui-state.jsonl` ↔ `nakui-ui-state.snap.json`.
595- **Nuevo helper `snapshot_path_for(log_path)`** — derivación
596  pura, testeable.
597- **Nuevo helper `maybe_compact_log(log, snap_path, store, threshold)`**:
598  - Si `entry_count >= threshold` y `>= 2`, captura
599    `Snapshot::from_memory_store(store, next_seq - 1)`, lo escribe
600    atómicamente, y compacta el log dejando la última entry como
601    anchor.
602  - Anchor invariant: `EventLog::open` deriva `next_seq` del primer
603    entry del archivo. Si compactáramos *todo* el log file, al
604    reabrir el cursor volvería a 0 y el próximo append crashearía
605    con `NonMonotonic`. Por eso compactamos sólo hasta
606    `next_seq - 2` — la entry del `snap.seq` queda como anchor del
607    cursor; `replay_with_snapshot_into` la skipea porque snap ya
608    cubre hasta ese seq inclusive.
609  - Threshold via env `NAKUI_SNAPSHOT_THRESHOLD`, default 50.
610    `0` desactiva por completo.
611  - Devuelve `Result<Option<msg>, String>`: `Ok(Some)` si compactó,
612    `Ok(None)` si no había payoff, `Err` si snap o compact fallaron.
613- **`MetaUi::new` reescrito**:
614  - Carga snapshot al inicio (Some/None según exista).
615  - `replay_with_snapshot_into(&log, snapshot.as_ref(), &mut store)`
616    en lugar de `replay_into`.
617  - Después del replay corre `maybe_compact_log` con el threshold.
618  - Toast inicial menciona snapshot loaded si aplica
619    ("snapshot @ seq K") y la compactación si ocurrió.
620  - Errores de snapshot load **no son fatales**: cae a full replay
621    con un msg en el banner.
622  - Errores de auto-compact **no son fatales**: el log + snap
623    quedan como estaban, msg al banner.
624
6255 tests nuevos:
626- `snapshot_path_for_replaces_extension` — `.jsonl` → `.snap.json`,
627  edge case sin extensión.
628- `maybe_compact_log_below_threshold_noops` — 5 entries vs threshold
629  50: no toca nada, no escribe snap.
630- `maybe_compact_log_threshold_zero_noops` — threshold 0 = disabled.
631- `maybe_compact_log_then_reopen_preserves_records` — E2E:
632  - Escribe 60 seeds (log + store en sync).
633  - Compacta (60 >= 50): snap escrito, log queda con 1 anchor entry,
634    msg reporta "59 entries dropped (1 anchor kept)".
635  - Reopen: `next_seq=60` se preserva via anchor, `entries.len()=1`.
636  - Replay con snap loadado en store fresco: los 60 records están.
637  - Segunda corrida del compact con threshold=1: no-op (idempotente).
638
63931 tests verdes (+5). Workspace build verde tras la nueva firma.
640
641Trade-offs y notas:
642- **Fail-soft**: cualquier error de snap/compact no rompe el boot;
643  la UI sigue funcionando con full replay y el toast lo reporta.
644  Sólo `EventLog::open` failing es no-recoverable (pierde
645  persistencia).
646- **Crash-safety**: WAL order preservado — escribimos snap (atómico
647  via tempfile + fsync + rename) ANTES de compactar el log
648  (atómico igual). Si crasheamos entre los dos, próximo boot ve
649  snap@K + log con todas las entries 0..N — replay skippea las que
650  snap cubre, outcome idéntico.
651- **Sólo en startup**: no hay snapshot durante runtime. Para sesiones
652  largas con muchas escrituras, el log puede crecer arbitrariamente
653  hasta el próximo restart. Pendiente futuro: snapshot N writes
654  desde el último compact.
655- **Anchor entry sobrevive sin uso útil**: el costo es 1 línea JSON
656  por compact. No es preocupación a menos que el threshold sea
657  muy chico (cada compact deja 1 línea de basura).
658
659Pendientes restantes:
660- **EntityRef validation post-submit** — validar UUID parseable al
661  submit en lugar de al execute del morphism.
662- **Atajo Esc para Cancelar** del modal de delete.
663- **`FieldOp::Clear`** — para soportar borrar un value vía form vacío.
664- **Snapshot durante runtime** (cada N writes, no sólo al startup).
665
666### feat(nakui-ui): edit delta-only — sólo campos modificados al log/store
667Antes de este cambio, editar un record emitía un `FieldOp::Set` por
668**cada field del form**, incluso los no tocados. Eso bloata el log
669(replay tenía que aplicar N ops cuando 1 alcanzaba) y oscurece el
670intent en una auditoría posterior. Con delta-only, el edit emite
671sólo los Sets cuyo value nuevo difiere del actual; un edit que no
672cambia nada deja el log intacto.
673
674Cambios:
675- **Nuevo helper `compute_field_delta(current, proposed)`** — toma
676  el record actual del store (un `Value`, posible `Null` si el
677  record no existe) y el `Map` propuesto desde el form, y devuelve
678  sólo las entries que difieren. Comparación: `PartialEq` estructural
679  de `serde_json::Value` (un `Null` en current = todos los proposed
680  son nuevos).
681- **Nuevo enum `CommitOutcome`**:
682  - `Created(Uuid)` — alta nueva.
683  - `Updated { id, changed }` — edit con N campos modificados.
684  - `NoChange(Uuid)` — edit sin diferencias (el toast lo refleja
685    como "X sin cambios — no log entry").
686- **`commit_seed` en path EDIT**:
687  - Carga current via `store.load(entity, id)` con fallback a
688    `Value::Null`.
689  - Calcula delta. Si vacío → return early sin tocar log ni store.
690  - Si no vacío → emite `Morphism { ui.edit_record, ops: [Set...] }`
691    con `params.fields` reflejando el delta (no todo el form),
692    haciendo la auditoría grep-able por field cambiado.
693- **Toast del callsite**:
694  - `creado X uuid` (Created)
695  - `actualizado X uuid (N campo(s))` (Updated)
696  - `X uuid sin cambios — no log entry` (NoChange)
697- **`editing` se limpia incluso en NoChange** — el modo edit cierra,
698  el form vuelve al state limpio.
699
7005 tests nuevos del helper:
701- delta vacío cuando todo coincide.
702- delta sólo con el field cambiado.
703- delta full cuando current = Null (record no existe).
704- distingue int 100 de string "100".
705- ignora fields del current que no están en proposed.
706
70727 tests verdes (+5). El path SEED no cambió; el E2E del morphism
708real sigue verde.
709
710Limitación conocida (consistente con pre-delta): el form no puede
711**borrar** un value vaciando el input — empty optional fields hacen
712`continue` antes de llegar al delta. Para clearear un value hay que
713declarar el field como required, o esperar a un `FieldOp::Clear`
714futuro (no necesario hoy: ningún demo lo requiere).
715
716Pendientes restantes:
717- **Snapshot/compaction** del log (replay full cada startup escala
718  mal con repos grandes).
719- **EntityRef validation post-submit** — validar UUID parseable al
720  submit en lugar de al execute del morphism.
721- **Atajo Esc para Cancelar** del modal de delete.
722- **`FieldOp::Clear`** — para soportar borrar un value vía form.
723
724### feat(nakui-ui): confirmación de delete vía banner modal antes de borrar
725Cierra el primer pending del último round: borrar un record pedía un
726solo click en `✕` y se ejecutaba inmediatamente (irreversible —
727queda en el log como `Morphism { name: "ui.delete_record" }`). Ahora
728hay un paso intermedio: el click marca el record como pendiente y el
729banner amber al tope de la ventana ofrece [Cancelar] o [Confirmar].
730
731Cambios:
732- **Nuevo state `MetaUi.pending_delete: Option<(String, Uuid)>`**.
733  Set en el click del `✕`; limpiado por:
734  - [Cancelar] → toast "delete cancelado (Entity)".
735  - [Confirmar] → llama `commit_delete` (igual que antes) y emite
736    el toast usual.
737  - Navegación a otra view (`select_view`) — el record marcado
738    podría no estar visible en la nueva pantalla.
739- **Click handler de `✕` ya no llama `commit_delete`**: sólo setea
740  `pending_delete` y limpia toast. La acción destructiva ahora vive
741  exclusivamente en el botón [Confirmar] del banner.
742- **Nuevo método `render_confirm_delete_banner`**: devuelve
743  `Option<Div>` (None si no hay pending). Banner amber con el texto
744  `¿Borrar {Entity} {short_uuid}?` + dos botones. Renderea como
745  sibling del row sidebar+main en `flex_col` raíz — no es overlay
746  flotante (GPUI no expone z-index trivialmente), pero la posición
747  fija al tope + color amber lo hacen imposible de ignorar.
748- **Limpieza pre-commit**: `pending_delete = None` se ejecuta antes
749  de `commit_delete`, así un fallo del commit no deja el banner
750  colgado además del toast de error.
751
75222 tests verdes — la lógica del store/log no cambió, sólo el state
753machine de UI. La confirmación es puramente UX/state, no testable
754sin GPUI cx, pero la compilación garantiza wireup correcto de las
755closures.
756
757Pendientes restantes:
758- **Snapshot/compaction** del log para repos grandes (replay full
759  cada startup escala mal).
760- **Edit delta-only** — sólo campos modificados, no todos.
761- **EntityRef validation post-submit** — validar UUID parseable
762  al submit en lugar de al execute del morphism.
763- **Atajo de teclado Esc para Cancelar** — requiere event
764  dispatcher de GPUI, fuera de scope inmediato.
765
766### feat(nakui-ui): validación estricta de params del morphism vía FieldKind del FieldSpec
767Cierra el último trade-off documentado: `infer_param_value` adivinaba
768el tipo de cada param por la shape del string (i64 → f64 → bool →
769string). Ahora cuando hay `FieldSpec` declarado, usamos
770`parse_field_value(spec.kind, raw)` — un Boolean field con value
771"abc" rebota con mensaje claro en la UI antes de llegar al morphism
772Rhai (donde el error sería opaco como "Function not found: * ((), ())").
773
774Cambios:
775- **Nuevo helper `resolve_param_value(field_name, raw, spec)`**:
776  - Si hay `FieldSpec`: validación de `required` (rebota empty con
777    "param 'X' es obligatorio y está vacío") + parseo estricto via
778    `parse_field_value(spec.kind, raw)`. Errores incluyen el `label`
779    del spec para que el toast sea interpretable.
780  - Si NO hay spec (param declarado en `Action::Morphism.params`
781    que no existe en `form.fields` — módulo mal-formado): fallback
782    a `infer_param_value` como red de seguridad.
783  - Empty + opcional → `Value::Null`.
784- **`commit_morphism` simplificado**: el loop de params ahora es
785  3 líneas (lookup spec + llamada a `resolve_param_value` +
786  inserción al map). La lógica vive en el helper standalone,
787  testable sin GPUI.
788
789Tests: 6 nuevos en `tests` mod, todos contra `resolve_param_value`:
790- `resolve_param_strict_number_parses_i64` — happy path.
791- `resolve_param_strict_boolean_rejects_non_boolean` — un Boolean
792  con "abc" rebota con mensaje que incluye el label.
793- `resolve_param_strict_number_rejects_garbage` — Number con "abc"
794  rebota.
795- `resolve_param_required_empty_rejected` — required vacío rebota
796  con "obligatorio".
797- `resolve_param_optional_empty_returns_null` — optional vacío
798  → null.
799- `resolve_param_no_spec_falls_back_to_infer` — el fallback
800  preserva el comportamiento anterior para back-compat.
801
80222 tests verdes en nakui-ui (+6). E2E del morphism real
803(`morphism_pipeline_executes_real_sales_vender`) sigue verde — la
804validación estricta no rompe el path correcto, sólo agrega rebotes
805tempranos a values mal-tipados.
806
807Beneficio operativo:
808- Mensaje de error en la UI ahora identifica el field problemático
809  por su label legible ("param 'Cantidad': 'abc' no es número")
810  en lugar del error opaco del morphism Rhai.
811- Errores se ven antes de tocar el log o el store — ningún cambio
812  parcial.
813- El módulo Nakui ya no tiene que defender contra inputs garbage
814  desde la UI: la metainterfaz se vuelve la primera línea de
815  validación tipada.
816
817Pendientes futuros (orden de prioridad):
818- **Confirmación de delete** — modal antes de borrar.
819- **Snapshot/compaction** del log para repos grandes.
820- **Edit delta-only** — sólo campos modificados, no todos.
821- **EntityRef validation post-submit**: hoy `parse_field_value`
822  para EntityRef devuelve string raw; el commit_morphism luego
823  valida como Uuid sólo cuando es input del morphism. Para
824  EntityRef como param, podríamos validar UUID al submit.
825
826### feat(nakui-ui): FieldKind::EntityRef — selector clickable de records existentes
827Cierra el principal trade-off documentado del commit anterior:
828"Inputs UUID a mano (no dropdown)". Los formularios pueden declarar
829un campo `entity_ref` que apunta a una entity y el runtime renderea
830una lista clickable de records existentes; click selecciona, el
831UUID queda guardado para el submit.
832
833Schema `nakui-ui-schema`:
834- **Nueva variante `FieldKind::EntityRef`** (serializa como
835  `"entity_ref"` en JSON).
836- **`FieldSpec.ref_entity: Option<String>`** nuevo. Indica qué
837  entity ofrecer en el selector. `validate()` chequea que cualquier
838  field con `kind=entity_ref` tenga `ref_entity` set.
839- Nuevo error tipado `SchemaError::EntityRefMissingTarget`.
840
841Runtime `nakui-ui`:
842- **`render_entity_ref_selector(field_name, target_entity, ...)`** —
843  helper que arma la lista debajo del input. Cada item:
844  - Etiqueta humana via `human_label_for_record` (heurística:
845    `name` → `label` → `title` → `sku` → `sku_id` → fallback al
846    UUID corto).
847  - Click handler vía `cx.listener` que llama
848    `input.set_text(uuid_completo)` — el TextInput interno queda
849    como source-of-truth, así que `commit_seed` y `commit_morphism`
850    leen el UUID seleccionado sin saber que vino de un selector.
851  - Highlight en accent color cuando el item es el actualmente
852    seleccionado (compara contra el contenido del TextInput).
853- **`parse_field_value(EntityRef, raw)`** devuelve string del raw
854  (la validación como Uuid ocurre downstream en `commit_morphism`).
855- Mensaje "(sin {entity}: creá uno antes para referenciar)" cuando
856  la lista está vacía — el user sabe qué hacer en lugar de quedarse
857  trabado.
858
859Demo actualizado: `examples/nakui-modules/sales_engine/module.json`:
860- `vender_form.fields.stock_id_input` y `caja_id_input` cambian de
861  `kind: "text"` a `kind: "entity_ref"` con `ref_entity: "Stock"`
862  y `"Caja"` respectivamente.
863- Ahora el flujo "Vender" es: (1) click en una Stock listada bajo
864  el input, (2) click en una Caja, (3) escribir venta_id/cantidad/
865  precio_unitario/timestamp, (4) submit. Sin copiar UUIDs.
866
867Tests:
868- 2 nuevos en schema: `validate_catches_entity_ref_without_target`
869  y `entity_ref_with_target_validates_clean`. 8 totales.
870- 4 nuevos en runtime: `parse_field_entity_ref_returns_string`,
871  `human_label_for_record_prefers_name_over_id`,
872  `human_label_falls_back_through_label_title_sku`,
873  `human_label_falls_back_to_id_when_no_known_keys`. 16 totales.
874- Integration de los 7 demos sigue verde — el demo `sales_engine`
875  ahora valida con EntityRef + ref_entity correctamente set.
876
87729 tests totales nakui-ui + schema, 100% verde. El demo
878`sales_engine` carga limpio con la nueva forma del schema.
879
880Pendientes futuros:
881- **Confirmación de delete** — modal antes de borrar.
882- **Snapshot/compaction** del log para repos grandes.
883- **Edit delta-only** (sólo campos modificados).
884- **Validación de tipos en params del morphism**: `FieldKind`
885  declarado en el FieldSpec se podría usar para forzar parseo
886  estricto en `commit_morphism` en lugar de la heurística
887  `infer_param_value`.
888
889### feat(nakui-ui): Action::Morphism wired al pipeline real (compute → log → apply)
890Cierra el último gran TODO de la metainterfaz Nakui: las acciones
891`Action::Morphism` ya no son un toast informativo; despachan al
892`Executor` cargado del manifest nakui-core (`nsmc.json` + schemas
893KCL + scripts Rhai), pasando por el pipeline completo de Nakui:
894compute (con dry-run + KCL post-checks) → log append → store apply.
895
896Schema `nakui-ui-schema` extendido:
897- **`Module.nakui_module_dir: Option<String>`** nuevo. Path
898  (relativo al directorio del `module.json` o absoluto) a un módulo
899  nakui-core. Sin esto, las Action::Morphism del módulo quedan
900  no-op con toast informativo. Las Action::SeedEntity siguen
901  funcionando sin manifest (alta administrativa).
902- **`Action::Morphism`** ganó dos campos opcionales:
903  - `inputs: BTreeMap<String, String>` — mapeo `role → field_name`.
904    Por cada input declarado en el `MorphismSpec.inputs`, indica
905    qué field del form contiene el UUID del record. El runtime
906    parsea como `Uuid` y lo pasa al `execute_and_log`.
907  - `params: Vec<String>` — lista de fields cuyos values van al
908    `params` JSON. Si vacío, todos los fields no-input van a params.
909
910Runtime `nakui-ui`:
911- **`MetaUi.executors: BTreeMap<String, Arc<Executor>>`** nuevo.
912  Carga `Executor::load_module(nakui_module_dir)` en `MetaUi::new`
913  por cada módulo UI que declare la entry. Errores de carga van al
914  banner; el módulo sigue cargado para SeedEntity, sólo Morphism
915  queda no-op.
916- **`commit_morphism(mod_idx, name, inputs_map, params_fields)`** nuevo.
917  Resuelve inputs (parsea cada field como Uuid), arma params (Value
918  object con tipos inferidos via `infer_param_value` — int/float/
919  bool/string), llama `execute_and_log_with_recovery`. Toast con
920  cantidad de ops aplicadas o el error tipado.
921- **`infer_param_value`** nuevo helper: heurística simple para
922  pasar values del form al morphism con tipo inferido (i64 → f64 →
923  bool → string).
924
925Tests: 2 nuevos:
926- `infer_param_value_int_then_float_then_bool_then_string` —
927  cobertura de la heurística.
928- **E2E `morphism_pipeline_executes_real_sales_vender`** —
929  carga el módulo real `crates/modules/nakui/modules/sales`,
930  arma store + log, ejecuta el morphism `vender` con inputs
931  Stock+Caja y params (cantidad=5, precio_unitario=200,
932  venta_id, timestamp). Asserta:
933  - el morphism produce ops (no vacío).
934  - stock.cantidad bajó 100 → 95.
935  - caja.saldo subió 1_000_000 → 1_001_000.
936
93712 tests verdes en nakui-ui (+1 vs commit anterior). Schema
938extension no rompió nada (6 unit + 5 integration siguen verdes).
939
940Demo nuevo: **`examples/nakui-modules/sales_engine/module.json`**
941- Apunta a `crates/modules/nakui/modules/sales` vía `nakui_module_dir`.
942- 6 vistas: list + form para cada Stock, Caja, Venta + form
943  "Vender" con `Action::Morphism { name: "vender", inputs: {stock,
944  caja}, params: [venta_id, cantidad, precio_unitario, timestamp] }`.
945- El user crea Stocks + Cajas con seed_entity, copia los UUIDs
946  cortos a los inputs de "Vender", y ejecuta el morphism real:
947  stock baja, caja sube, Venta se persiste, todo loggeado.
948- Validaciones KCL fallan limpio (toast con error) si el morphism
949  rebota — p. ej. cantidad > stock disponible.
950
951Activación full:
952```sh
953NAKUI_EVENT_LOG=~/.nakui/state.jsonl \
954NAKUI_MODULES_DIR=examples/nakui-modules \
955cargo run -p nakui-ui
956# Sidebar gana "Ventas (con morphism)" — los 6 menús aparecen y
957# el form "Vender" dispara el pipeline nakui-core completo.
958```
959
960Trade-offs documentados:
961- **Inputs UUID a mano**: el form pide que el user copie el UUID de
962  un Stock/Caja existente. Para UX seria habría que agregar
963  `FieldKind::EntityRef { entity }` que renderiza un dropdown — no
964  hecho por scope, queda como nice-to-have.
965- **Inferencia de tipo en params**: `infer_param_value` adivina
966  por shape del string. Para casos sutiles (ej. "true" como string
967  literal vs bool), el módulo nakui-core puede explicitar tipos
968  via `kind` en el FieldSpec — el form lo respeta para validación
969  pre-submit; la inferencia final sigue siendo heurística.
970
971### feat(nakui-ui): edit + delete de records (ciclo CRUD completo)
972Cierra "no hay UI para editar/borrar records existentes" del commit
973anterior. Cada fila de la lista gana dos botones (✎ edit, ✕ delete);
974el form view se reusa para alta y para edit; el delete es inline.
975Las mutaciones pasan por `LogEntry::Morphism` con sus ops, así el
976replay restaura el estado correcto.
977
978Cambios:
979
980- **`MetaUi.editing: Option<(String, Uuid)>`** nuevo. Set al click
981  en ✎; cleared al cambiar de view o tras submit exitoso.
982- **`open_edit(mod_idx, entity, id, cx)`**: setea `editing`, busca la
983  primera Form view del módulo cuya `entity` matchee, navega ahí. Si
984  el módulo no tiene Form para esa entity → toast con error
985  ("no hay form view para entity X").
986- **`select_view`** extendido: cuando carga un Form, si `editing`
987  matchea esa entity y el record existe en el store, pre-llena cada
988  input con el valor del record (vía nuevo helper
989  `value_to_input_text` — inverso de `parse_field_value`).
990- **`commit_seed`** ramifica:
991  - **Edit path** (cuando `editing.is_some()` y entity matchea):
992    emite `LogEntry::Morphism { name: "ui.edit_record", ops:
993    [Set { path, value } for each field], params: { entity, id,
994    fields } }`. Aplica al store via `apply(&ops)`.
995  - **Seed path** (alta nueva): comportamiento previo.
996- **`commit_delete(entity, id)`**: emite `LogEntry::Morphism {
997  name: "ui.delete_record", ops: [Delete { entity, id }] }` + apply.
998- **Render del form**: título cambia a "Editar customer abc12345"
999  cuando `editing` matchea; submit label cambia a "Guardar cambios
1000  en customer".
1001- **Render de la lista**: dos columnas nuevas — "id" y "acciones".
1002  Cada fila tiene ✎ (accent color, click → open_edit) y ✕ (rojo,
1003  click → commit_delete). Hover states.
1004
1005Ramificación visible en el event log:
1006```
1007{"kind":"seed","seq":0,"entity":"customer","id":"abc...","data":{"name":"Acme"}}
1008{"kind":"morphism","seq":1,"morphism":"ui.edit_record","ops":[
1009  {"op":"set","path":{"entity":"customer","id":"abc...","field":"name"},
1010   "value":"Acme S.A."}
1011]}
1012{"kind":"morphism","seq":2,"morphism":"ui.delete_record","ops":[
1013  {"op":"delete","entity":"customer","id":"abc..."}
1014]}
1015```
1016Coherente con el modelo de Nakui — todo cambio post-seed pasa por
1017ops dentro de Morphism. `nakui-explorer` muestra estos morphisms
1018con sus ops claros en su timeline.
1019
1020Trade-offs documentados:
1021- **`schema_hash: None`** sigue para los morphism de la UI (legacy/
1022  pre-versioning path) hasta que `Action::Morphism` cargue Manifest
1023  schemas.
1024- **Delete sin confirmación**: 1 click, sin modal. Para MVP es OK
1025  (los records son recuperables vía replay parcial), pero un futuro
1026  iter agregaría confirmación.
1027- **Edit sobreescribe TODOS los campos del form**, no sólo los
1028  cambiados — emite N ops Set, una por field. Adecuado para forms
1029  chicos; para forms con muchos campos optimizar a delta-only.
1030
1031Tests: 3 nuevos (10 totales en nakui-ui):
1032- `value_to_input_text_inverse_of_parse` y
1033  `value_to_input_then_parse_round_trip` — la propiedad fundamental
1034  del pre-llenado: text → parse devuelve el Value original.
1035- `event_log_replay_handles_full_crud_cycle` — E2E del log: escribe
1036  Seed + Morphism(Set ops) + Morphism(Delete op), replay desde cero,
1037  verifica que el store termina vacío (delete fue el último). Verifica
1038  además que un replay parcial (sin el delete) deja los valores
1039  editados.
1040
1041Activación:
1042```sh
1043NAKUI_EVENT_LOG=~/.nakui/state.jsonl \
1044NAKUI_MODULES_DIR=examples/nakui-modules \
1045cargo run -p nakui-ui
1046# Crear un customer, click ✎ en su fila, modificar campos,
1047# "Guardar cambios". Click ✕ en otra fila para borrar.
1048# Cerrar y reabrir: el state persiste con todos los cambios.
1049```
1050
1051### feat(nakui-ui): persistencia con event log + replay al startup
1052Cierra "sin persistencia entre runs" del commit anterior. Cada
1053`SeedEntity` se appendea al `nakui_core::event_log::EventLog` con
1054WAL semantics (log antes que store) y al re-abrir el binario el
1055replay reconstruye el `MemoryStore` desde cero. Cerrar y volver a
1056abrir ya no borra el data.
1057
1058Cambios:
1059- **`MetaUi.event_log: Option<Arc<Mutex<EventLog>>>`** nuevo.
1060  Compartido bajo `Mutex` para que el commit_seed pueda mutar.
1061- **Apertura + replay al startup** (`MetaUi::new`): path por env
1062  `NAKUI_EVENT_LOG`, default `./nakui-ui-state.jsonl`.
1063  `EventLog::open` + `replay_into` reconstruyen el store.
1064  Toast informativo: "log nuevo" o "log X cargado: N evento(s)
1065  replayed".
1066- **WAL en `commit_seed`**: si `event_log.is_some()`, primero
1067  `log.append(LogEntry::Seed { ..., schema_hash: None })`, después
1068  `store.seed`. Si el append falla, cancela toda la operación
1069  (el user reintenta sin haber dejado state inconsistente).
1070- **`schema_hash: None`**: documentado como path "legacy /
1071  pre-versioning" para seeds que no pasan por un Manifest+Executor.
1072  Es el path correcto para alta administrativa vía la metainterfaz
1073  hasta que `Action::Morphism` wireé el Manifest loader.
1074- **Degradación grácil**: si abrir log falla (permisos, disco),
1075  toast con error pero el runtime sigue en modo in-memory.
1076
1077Tests: 1 nuevo E2E `event_log_replay_restores_memory_store` que
1078escribe 2 seeds via `EventLog::append`, re-abre y `replay_into` un
1079store fresh, verifica que ambos records están con sus values
1080correctos. Reproduce el flujo del startup de `MetaUi::new` sin
1081necesitar GPUI. 7 tests verdes en nakui-ui.
1082
1083Activación con persistencia explícita:
1084```sh
1085NAKUI_EVENT_LOG=~/.nakui/state.jsonl \\
1086NAKUI_MODULES_DIR=examples/nakui-modules \\
1087cargo run -p nakui-ui
1088# Crear varios records vía el form, cerrar el binario, abrir de
1089# nuevo: los records están.
1090```
1091
1092Limitaciones que **siguen** (próximos iters):
1093- **`Action::Morphism`** sigue como TODO: requiere cargar el
1094  `Manifest` de nakui-core junto al `Module` UI para conocer los
1095  inputs/params declarados y poder llamar `execute_and_log`.
1096- **No hay snapshot/compaction**: el log crece append-only para
1097  siempre. Para repos grandes habría que integrar `Snapshot` de
1098  nakui_core (existe, no se usa todavía).
1099- **No hay UI para borrar/editar** records existentes — sólo alta
1100  vía form. Edit + delete en futuras iteraciones.
1101- **Widget input simple** (sin selection/IME/clipboard) — heredado
1102  de la limitación documentada de `yahweh-widget-text-input`.
1103
1104### feat(nakui-ui): inputs reales con yahweh-widget-text-input + click handlers funcionales
1105Cierra dos limitaciones documentadas en el commit anterior de la
1106metainterfaz: los formularios ahora aceptan teclado real, y los
1107clicks en menús + botones mutan estado correctamente.
1108
1109Cambios:
1110- **Inputs vivos**: cada `FieldSpec` del Form view materializa un
1111  `Entity<TextInput>` (de `yahweh-widget-text-input`) al entrar a la
1112  vista. Los entities se reemplazan al cambiar de view (drop limpio).
1113  El widget soporta: escribir caracteres, Backspace, Enter (Confirmed
1114  event — no usado todavía; el submit va por botón), Escape
1115  (Cancelled). El cursor se renderea como `|` al final.
1116- **Click handlers wired vía `cx.listener`**: menús del sidebar
1117  invocan `select_view`; botones de acción (header de list, submit
1118  de form) invocan `apply_action`. Los handlers tienen acceso real
1119  al `Context<MetaUi>` y mutan el modelo + emiten `cx.notify()`.
1120- **Submit lee texto de los inputs**: `commit_seed` reemplaza el
1121  buffer ad-hoc anterior por `input.read(cx).text()` por cada
1122  field. El value parseado va al `MemoryStore` con su tipo correcto
1123  (text/number/boolean/date).
1124- **Reset de inputs tras submit**: si la acción no tiene `next_view`,
1125  los inputs se vacían (`set_text("")`) para alta consecutiva sin
1126  re-tipear.
1127- **Hover states**: items del sidebar y botones cambian de bg al
1128  pasar el mouse, feedback visual consistente con el resto del
1129  ecosistema yahweh.
1130- **Theme global**: `Theme::install_default(cx)` al inicio (lo
1131  requiere el text_input para sus colores).
1132
1133Wire en Cargo:
1134- Deps nuevas: `yahweh-widget-text-input`, `yahweh-theme` (paths
1135  relativos al monorepo).
1136
1137Limitaciones que **siguen abiertas** (próximos iters):
1138- **`Action::Morphism`** sigue como TODO: requiere cargar el
1139  `Manifest` de nakui-core junto al `Module` UI para conocer los
1140  inputs/params declarados.
1141- **Sin persistencia entre runs**: `MemoryStore` en RAM. Wire con
1142  `EventLog` o `SurrealStore` queda para cuando exista el daemon
1143  Nakui.
1144- **Inputs simples**: el widget no soporta cursor positioning,
1145  selection, copy/paste, IME, multilínea. Para edits serios habrá
1146  que portar `gpui::examples::input` o adoptar `gpui-input` cuando
1147  exista upstream.
1148- **Enter no envía**: el `TextInputEvent::Confirmed` que emite el
1149  widget no está suscrito todavía; el submit va por click. Trivial
1150  de wirear si lo necesitamos.
1151
1152Tests: los 6 unit del runtime siguen verdes (parse_field_value para
1153los 5 kinds, lookup_field nested, render_value). El comportamiento
1154visual requiere correr el binario con `cargo run -p nakui-ui` y
1155probar a mano — GPUI no provee harness de UI testing en CI hoy.
1156
1157Activación full:
1158```sh
1159NAKUI_MODULES_DIR=examples/nakui-modules cargo run -p nakui-ui
1160# Click en un menú → carga vista. Click en "Nuevo" → form.
1161# Tipear en cada campo → ver el `|` al final. Click "Crear customer"
1162# → record aparece en la lista.
1163```
1164
1165### feat(nakui): metainterfaz declarativa + 6 módulos ERP estándar
1166Salto cualitativo: Nakui pasa de "library + demos + read-only viewer
1167del event log" a **plataforma ERP con UI dirigida por datos**. Cada
1168módulo de negocio se declara como un `module.json` (sin código Rust
1169nuevo) y el runtime GPUI lo carga dinámicamente: sidebar de menús,
1170listas con columnas configurables, formularios de alta.
1171
1172Tres entregables:
1173
1174**1) Crate nuevo `nakui-ui-schema`** (datos puros, ~250 LOC + 200
1175LOC tests):
1176- `Module { id, label, entities, menu, views }`.
1177- `View::List { entity, columns, actions, search_in }` o
1178  `View::Form { entity, fields, on_submit }`.
1179- `FieldSpec { name, label, kind, default, required, help }` con
1180  `FieldKind = Text|Multiline|Number|Boolean|Date`.
1181- `Action::OpenView | SeedEntity | Morphism` — el runtime las
1182  dispara desde botones / submits.
1183- `Module::from_path` parsea un JSON; `Module::validate` chequea que
1184  cada `MenuItem.view` exista en `views`.
1185- `load_modules_from_dir(dir)` busca `dir/<modulo>/module.json`,
1186  parsea, valida, detecta IDs duplicados, devuelve ordenado.
1187- 6 tests unit + 4 integration (los 6 demos cargan limpio, todos
1188  tienen list+form, kinds reconocidos, validate pasa).
1189
1190**2) Crate nuevo `nakui-ui`** (binario GPUI, ~700 LOC + 100 LOC tests):
1191- Carga módulos desde `NAKUI_MODULES_DIR` (default `./nakui-modules`).
1192- Sidebar con módulos + sus menús; click en menu cambia la vista activa.
1193- **List view**: tabla de instancias del entity con columnas
1194  weighted (header de columnas + filas + id corto).
1195- **Form view**: campos labeled + botón submit que dispara la action
1196  declarada (`SeedEntity` mete el record al `MemoryStore`
1197  in-process; `Morphism` queda como TODO hasta integrar el manifest
1198  loader nakui-core).
1199- `MemoryStore` compartido entre todas las vistas (Arc<Mutex>); el
1200  cambio en un módulo se refleja en otro inmediato.
1201- Toast + error banner para feedback.
1202- 6 tests unit (parse_field_value para los 5 kinds, lookup_field
1203  nested, render_value).
1204
1205**3) 6 módulos demo** en `examples/nakui-modules/` que cubren un
1206ERP estándar:
1207- **customers**: nombre, email, teléfono, activo, límite de
1208  crédito, notas.
1209- **products**: SKU, nombre, categoría, precio, stock, activo.
1210- **suppliers**: razón social, ID fiscal, contacto, email,
1211  teléfono, términos de pago.
1212- **inventory_movements**: fecha, tipo (in/out/adjustment), SKU
1213  producto, cantidad, costo unitario, motivo, doc. referencia.
1214- **sales_orders**: número, cliente, emisión, vencimiento,
1215  estado, subtotal, impuestos, total, notas.
1216- **invoices**: número, cliente, emisión, vencimiento, subtotal,
1217  impuestos, total, pagado, estado, moneda, orden referenciada.
1218
1219Cada módulo tiene su `list` (catálogo) + `form` (alta), con search
1220field y columns weighted. Los 6 cubren un setup de ERP de ventas
1221chico funcional para demo.
1222
1223Filosofía documentada:
1224- **UI como datos**: agregar un módulo = escribir un JSON, no
1225  recompilar el binario.
1226- **Persistencia universal**: el runtime conecta cada vista al
1227  `nakui_core::store::Store`; cambiar de MemoryStore a SurrealStore
1228  no toca los module.json.
1229- **Schema primero, semántica después**: `nakui-ui-schema` sólo
1230  define la forma; validación de referencias rotas (entity inexistente,
1231  morphism faltante) vive en el runtime.
1232
1233Activación:
1234```sh
1235NAKUI_MODULES_DIR=examples/nakui-modules cargo run -p nakui-ui
1236```
1237
1238Limitaciones conocidas (próximas iteraciones):
1239- **Inputs sin teclado**: GPUI no incluye text input; los forms
1240  muestran los `default` del schema y el submit usa esos. Próximo
1241  iter: integración con `yahweh-widget-text-input`.
1242- **Click handlers no wired**: GPUI necesita pasar `Entity<MetaUi>`
1243  a los handlers para mutar estado; refactor con `cx.listener` +
1244  weak refs queda para el próximo iter. Hoy la navegación es
1245  visual; el código de mutación sí funciona via API programática
1246  (los tests lo cubren).
1247- **Acción `Morphism`**: pendiente de cargar el `Manifest` de
1248  nakui-core junto con el `Module` UI para wirear `execute_and_log`.
1249- **Sin persistencia entre runs**: el `MemoryStore` se pierde al
1250  cerrar. Wire con `EventLog` o `SurrealStore` queda para cuando
1251  el daemon Nakui exista.
1252
1253Tests: 16 totales nuevos (10 schema + 6 runtime). 100% verde.
1254
1255Lo que esto desbloquea: cualquiera puede escribir un `module.json`
1256para su dominio (pacientes médicos, alumnos de escuela,
1257reservaciones de hotel) y aparece en la UI sin tocar Rust ni
1258recompilar. La forma de extender Nakui dejó de ser "agregar código
1259al ERP" y pasó a ser "escribir el contrato del módulo".
1260
1261### feat(nakui-explorer): nuevo binario GPUI — Nakui visible en la interfaz
1262Cierra "nakui no tiene UI propia" del audit. Nuevo binario standalone
1263`nakui-explorer` (paralelo a `nouser-explorer`) que renderea el
1264event log de un repo Nakui: timeline scrollable de seeds + morphisms
1265con sus parámetros, breakdown por entity type, polling cada 2s para
1266detectar nuevos eventos appended sin restart del explorer.
1267
1268Diseño:
1269- Lee directamente el archivo `.jsonl` del `nakui_core::event_log::EventLog`.
1270  Path por env `NAKUI_EVENT_LOG`, default `nakui.jsonl` en pwd.
1271- Sin discovery vía broker brahman porque nakui hoy es CLI/library/
1272  demos, no daemon. Cuando se daemonice, sustituir el lector de
1273  archivo por un sidecar consumer (mismo patrón que nouser-explorer
1274  actualmente usa).
1275
1276UI:
1277- **Header**: path del log, count total + breakdown seeds/morphisms,
1278  tiempo del último reload en ms.
1279- **Breakdown line**: top 5 buckets por frecuencia (entities + nombres
1280  de morphisms con prefijo `→`).
1281- **Timeline**: tarjetas color-coded por kind (azul=seed,
1282  verde=morphism). Cada tarjeta muestra `#seq`, kind, entity/morphism
1283  name, id corto (8 hex), preview del data/params (80 chars), schema
1284  hash corto (8 hex) o `(legacy)` si pre-versioning. Mostradas
1285  más-recientes-primero, hasta 200 visibles (suficiente para
1286  navegación; sin scroll virtualizado por ahora).
1287- **Error banner**: si la lectura falla (archivo inexistente o
1288  corrupto), banner rojo con el motivo. El explorer NO crashea —
1289  sigue intentando cada 2s.
1290
1291Wire en workspace:
1292- Nuevo `crates/apps/nakui-explorer/` agregado a `[workspace] members`.
1293- Deps mínimas: `nakui-core` (para EventLog + LogEntry), `gpui`,
1294  `serde_json`, `uuid` (con feature serde para parsear los IDs).
1295- Sin deps de brahman por ahora (Nakui standalone).
1296
1297Tests: 7 unitarios en `tests` mod del bin:
1298- `load_log_returns_all_entries_in_order` — cargar un .jsonl
1299  generado a mano, asserta que devuelve 5 entries con seqs 0..4
1300  contiguous.
1301- `breakdown_counts_seeds_morphisms_and_buckets` — verifica el
1302  conteo (3 seeds + 2 morphisms) y los buckets esperados.
1303- `load_missing_file_yields_empty_not_error` — archivo inexistente
1304  devuelve `[]` sin error (delegado al contrato de `EventLog::open`).
1305- `preview_value_truncates_long_strings` y `_keeps_short_strings_intact`.
1306- `short_uuid_takes_first_8_chars` y `short_hash_takes_first_4_bytes_hex`.
1307
1308Activación:
1309```sh
1310NAKUI_EVENT_LOG=/tmp/nakui_inv_xxx.jsonl cargo run -p nakui-explorer
1311```
1312
1313Estado del CHANGELOG global tras este commit: cero pendientes
1314fundamentados activos. Lo único que queda es `minga-vfs` (FUSE,
1315explícitamente diferido por el usuario) y mejoras nice-to-have
1316(cobertura adicional per-lenguaje, daemon-ización de nakui para
1317sidecar discovery).
1318
1319### chore(nakui): alinear `nakui-core` con `[workspace.package]` y deps compartidas
1320Cleanup de drift de convenciones: `nakui-core` era el único crate del
1321monorepo que mantenía `version = "0.1.0"` / `edition = "2021"` /
1322`thiserror = "1"` hardcoded, mientras el resto heredaba del workspace
1323y usaba `thiserror = "2"`. Eso significaba que un bump global de versión
1324o de edition se olvidaba sistemáticamente de nakui.
1325
1326Cambios:
1327- `[package]`: `version`, `edition`, `rust-version`, `license`, `authors`,
1328  `publish` → todos `*.workspace = true`. Agregado `description` (cumple
1329  convención del resto de crates).
1330- Deps compartidas migradas a `{ workspace = true }`: serde, serde_json,
1331  thiserror (v1→v2), tokio, ulid, sha2.
1332- `uuid` migrado a `{ workspace = true, features = ["serde"] }` — la
1333  feature `serde` no está en el workspace dep porque nakui es el único
1334  user; queda local opt-in en lugar de inflar el dep común.
1335- Deps específicas de nakui (sin compartición posible): rhai, petgraph,
1336  surrealdb permanecen inline con versión local.
1337
1338Verificación: `cargo build -p nakui-core` verde tras el bump de
1339`thiserror` v1→v2 — el `#[derive(Error)]` de los 14+ enums de error
1340en nakui no requirió ajustes (la API de derive es backwards-compatible
1341para los patrones simples). `cargo test -p nakui-core --lib`: 27/27
1342verdes, sin regresión.
1343

nakui.md

Changelog — nakui

ERP categórico.

feat(nakui): Fase 2 del ERP — relaciones legibles + formato

Segunda fase del plan maestro. El módulo CRM:

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:

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.

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:

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:<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:

Cambios en schemas de módulos:

Cambios en tests:

Cambios documentales:

Tests:

Beneficios:

Limitaciones / decisiones:

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:

Cambios en brahman-cards:

Cambios en nakui-ui:

Tests totales:

Lo que NO cambió:

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:

5 tests nuevos:

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

Comportamiento esperado:

Pendientes restantes:

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):

Cambios en nakui-ui:

Tests nuevos:

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:

Pendientes restantes:

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:

2 tests nuevos:

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

Trade-offs:

Pendientes restantes:

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:

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:

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:

5 tests nuevos (reemplazan al obsoleto parse_field_entity_ref_returns_string):

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:

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:

5 tests nuevos:

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

Trade-offs y notas:

Pendientes restantes:

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:

5 tests nuevos del helper:

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:

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:

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:

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:

Tests: 6 nuevos en tests mod, todos contra resolve_param_value:

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:

Pendientes futuros (orden de prioridad):

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:

Runtime nakui-ui:

Demo actualizado: examples/nakui-modules/sales_engine/module.json:

Tests:

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

Pendientes futuros:

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:

Runtime nakui-ui:

Tests: 2 nuevos:

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

Activación full:

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:

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:

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:

Tests: 3 nuevos (10 totales en nakui-ui):

Activación:

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:

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:

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):

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:

Wire en Cargo:

Limitaciones que siguen abiertas (próximos iters):

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:

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):

2) Crate nuevo nakui-ui (binario GPUI, ~700 LOC + 100 LOC tests):

3) 6 módulos demo en examples/nakui-modules/ que cubren un ERP estándar:

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:

Activación:

NAKUI_MODULES_DIR=examples/nakui-modules cargo run -p nakui-ui

Limitaciones conocidas (próximas iteraciones):

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:

UI:

Wire en workspace:

Tests: 7 unitarios en tests mod del bin:

Activación:

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:

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.