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