78fbde12b4
Módulo CRM declarativo (schema.ncl + nsmc.json + morfismos Rhai) con tres entities (Cliente, Oportunidad, Interaccion) y tres morfismos: abrir_oportunidad, mover_oportunidad (pipeline con validación de transiciones) y registrar_interaccion. crm_demo: demo realista de 18 eventos que —a diferencia de los otros demos— conserva el event log e imprime el comando de nakui-explorer, así el explorador muestra un CRM con cuerpo. tests/crm.rs: 8 tests. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1298 lines
62 KiB
Markdown
1298 lines
62 KiB
Markdown
# Changelog — nakui
|
|
|
|
ERP categórico.
|
|
|
|
### 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):
|
|
1. **Yahweh refactor**: lift del MetaUi runtime a
|
|
`crates/modules/ui_engine/` para reuso. El brazo + canónico ya
|
|
estables, ahora puede extraerse el meta-form widget genérico.
|
|
2. **KCL → Nickel**: kcl_wrapper reemplazado por Nickel contracts;
|
|
los 3 schemas .k de nakui modules pasan a .ncl.
|
|
3. **card.k eliminado** (es REFERENCE ONLY documentado).
|
|
|
|
### feat(nakui-ui): validación cross-field del EntityRef (existence en store)
|
|
Cierra otro pendiente. Hasta ahora `parse_field_value(EntityRef, raw)`
|
|
sólo validaba **forma** (UUID parseable + trim de whitespace) — un
|
|
UUID válido pero inexistente en el store pasaba silenciosamente al
|
|
log/store, dejando dangling references. Ahora validamos también
|
|
**existencia** contra la entity declarada en `FieldSpec.ref_entity`.
|
|
|
|
Cambios:
|
|
- **Nuevo helper `validate_entity_refs<S: Store>(store, refs)`**:
|
|
- `refs: &[(label, target_entity, uuid)]`.
|
|
- Loop fail-fast: primer record ausente → error con label
|
|
legible + UUID corto + target entity en el msg
|
|
`"campo 'Stock': record abc12345 de 'Stock' no existe en el store"`.
|
|
- Pure (toma `&S: Store`), totalmente testable sin GPUI.
|
|
- **Wireup en `commit_seed`**:
|
|
- Durante el parse loop, cuando un field es EntityRef + tiene
|
|
`ref_entity` declarado + value parseado a UUID, lo encolamos
|
|
en `entity_refs: Vec<(String, String, Uuid)>`.
|
|
- Después del parse loop (antes del seed/edit branch), si
|
|
`entity_refs` no está vacío, una sola toma del store lock
|
|
para validar todos via el helper.
|
|
- Falla early: ningún log entry, ningún apply.
|
|
- **Cobertura**:
|
|
- SEED path: alta nueva con EntityRef → validamos antes de
|
|
`Seed { data }`.
|
|
- EDIT path: edit con EntityRef → validamos antes de calcular
|
|
delta. Una optional empty (que iría a clear) no cuenta como
|
|
EntityRef (raw vacío skipea el push).
|
|
- Morphism inputs: NO se duplica acá. `Executor::compute` ya
|
|
valida cada input via `store.load(...).ok_or(EntityMissing)`
|
|
antes de correr el script Rhai. Documentado en el doc del
|
|
helper.
|
|
|
|
5 tests nuevos:
|
|
- `validate_entity_refs_passes_when_all_records_exist` — happy path.
|
|
- `validate_entity_refs_fails_on_first_missing` — fail-fast con
|
|
msg que incluye entity + UUID corto.
|
|
- `validate_entity_refs_uses_label_not_entity_in_msg` — el label
|
|
legible (ej: "Stock origen") aparece en el error, no la entity
|
|
desnuda.
|
|
- `validate_entity_refs_empty_list_is_ok` — lista vacía es Ok.
|
|
- `validate_entity_refs_distinguishes_target_from_other_entities` —
|
|
un UUID que existe bajo Customer pero NO Stock falla la
|
|
validación contra Stock.
|
|
|
|
45 tests verdes en nakui-ui (+5). Workspace build verde.
|
|
|
|
Comportamiento esperado:
|
|
- **Selector clickable es happy path**: el dropdown sólo lista
|
|
records existentes, así que clickearlo nunca debería disparar
|
|
el error. Sólo dispara con paste manual de UUID que no existe
|
|
o records borrados después de la selección (timing race).
|
|
- **Optional empty no se valida**: si el field es EntityRef
|
|
optional y el form lo deja vacío, lo manejamos como "no value"
|
|
(skipea el push a `entity_refs`); la lógica de Clear se
|
|
encarga del resto.
|
|
- **Lock contention**: una sola toma del store lock por
|
|
`commit_seed`, no una por field. La validación es O(refs) reads.
|
|
|
|
Pendientes restantes:
|
|
- **Validación KCL del record post-edit** antes de emitir Set/Clear
|
|
(hoy `ui.edit_record` no pasa por `Executor::compute`).
|
|
- **EntityRef cross-module** (referenciar records de OTRO módulo
|
|
por nombre, no sólo por entity local).
|
|
|
|
### feat(nakui-core,nakui-ui): FieldOp::Clear — borrar values vía form vacío
|
|
Cierra el último pendiente de UX del round. El edit no podía
|
|
"borrar" un value vaciando el input — empty optional fields
|
|
hacían `continue` en `commit_seed`, así que el current value
|
|
quedaba intacto. Para honrar el intent del usuario ("este field
|
|
ya no aplica") necesitábamos un FieldOp explícito que remueva
|
|
la key del map.
|
|
|
|
Cambios en **nakui-core** (la variante es semántica del kernel,
|
|
no específica de la UI):
|
|
|
|
- **`delta::FieldOp::Clear { path }`** — nueva variante.
|
|
Distinta de `Set { value: Null }`: Clear borra la clave; Set
|
|
Null deja la clave con valor literal `null`. Importa para
|
|
downstream que diferencia "ausente" vs "presente como null"
|
|
(ej: serde con `skip_serializing_if = "Option::is_none"`).
|
|
- **`capability_token`** — Clear devuelve `entity.field`,
|
|
mismo shape que Set. Una capability `writes: ["Customer.notes"]`
|
|
autoriza tanto Set como Clear sobre ese field.
|
|
- **`simulate_on`** — Clear remueve la key del Object si el
|
|
state es Some(Object). Skip silente si el state es None
|
|
(deleted) o no-objeto.
|
|
- **`MemoryStore::apply_dry_run`** — Set y Clear comparten
|
|
pre-condición (record padre existe + es objeto). Pattern
|
|
combinado con `|`.
|
|
- **`MemoryStore::apply`** — Clear hace `map.remove(field)`.
|
|
Field ausente = no-op silencioso (post-state idéntico).
|
|
- **`SurrealStore::apply_dry_run`** — Set/Clear combinados.
|
|
- **`SurrealStore::apply`** — Clear emite
|
|
`UPDATE type::thing UNSET <field>`. El field name viene de
|
|
un FieldSpec validado upstream; SurrealQL no soporta binding
|
|
de identifiers, así que va inline (con la advertencia
|
|
documentada en el comment).
|
|
- **`Executor` capability check** — Set/Clear comparten match
|
|
(mismo token shape, misma resolución a binding role).
|
|
- **Conservation rules** (en `check_conservation`) NO consideran
|
|
Clear — sólo Set. Documentado: morphism authors que querían
|
|
clear de un field con conservation tienen que ser cuidadosos;
|
|
KCL post-checks pueden capturar violations.
|
|
|
|
Cambios en **nakui-ui**:
|
|
|
|
- **`commit_seed` loop** acumula `to_clear: Vec<String>` con
|
|
los nombres de fields optional empty (en lugar de hacer
|
|
`continue` silencioso).
|
|
- **EDIT branch**:
|
|
- Computa `set_delta` (igual que antes) + `clear_fields` via
|
|
nuevo helper `compute_clear_fields(current, to_clear)`.
|
|
- Helper filtra a sólo los fields que actualmente tienen
|
|
valor non-null — Clear de un field ausente o ya null no
|
|
se emite (sería no-op semántico). Preserva el orden del
|
|
input para estabilidad del log entry.
|
|
- Construye `ops` combinando Set + Clear.
|
|
- NoChange ahora requiere AMBOS vacíos (set_delta y
|
|
clear_fields).
|
|
- `params` del log entry incluye `cleared: ["field1", ...]`
|
|
sólo si non-empty (preserva la shape `fields:` para
|
|
edits sin clears).
|
|
- `CommitOutcome::Updated.changed = sets + clears` para
|
|
que el toast `"actualizado X (N campo(s))"` siga siendo
|
|
preciso.
|
|
|
|
Tests nuevos:
|
|
- **delta.rs**: `simulate_clear_removes_field`,
|
|
`simulate_clear_then_set_same_field_keeps_set`,
|
|
`clear_capability_token_matches_set_shape`.
|
|
- **store.rs**: `apply_clear_removes_field_key`,
|
|
`apply_clear_on_absent_field_is_noop`,
|
|
`dry_run_rejects_clear_on_missing_record`,
|
|
`dry_run_rejects_clear_on_non_object`.
|
|
- **nakui-ui main.rs**: `clear_fields_skips_absent_and_null`,
|
|
`clear_fields_preserves_input_order`,
|
|
`clear_fields_empty_when_current_is_null`.
|
|
|
|
34 tests verdes en nakui-core (+7), 40 en nakui-ui (+3).
|
|
Workspace build verde. E2E del morphism real
|
|
`morphism_pipeline_executes_real_sales_vender` intacto — `vender`
|
|
no usa Clear.
|
|
|
|
Implicaciones:
|
|
- **El log puede crecer con entries `ui.edit_record` que sólo
|
|
tienen `cleared: [...]`** sin `fields`. Esperado y esperable.
|
|
- **Replay**: las entries con Clear se aplican en orden via
|
|
`store.apply(&ops)`. La semantic es deterministic.
|
|
- **Si un módulo tiene KCL invariants sobre la presencia de un
|
|
field**, el usuario podría romper el record vaciando ese
|
|
field via UI. Hoy esto NO se chequea — `ui.edit_record` es
|
|
un morphism manual que no pasa por `Executor::compute`. Si
|
|
esto es un problema, el camino futuro es validar contra el
|
|
KCL del entity al submit (otro pendiente).
|
|
|
|
Pendientes restantes:
|
|
- **Validación cross-field** (ej: UUID del EntityRef existe en
|
|
la entity referida).
|
|
- **Validación KCL del record post-edit** antes de emitir Set/Clear.
|
|
|
|
### feat(nakui-ui): snapshot/compaction durante runtime cada N writes
|
|
Cierra el último pending del round de persistencia. Antes el compact
|
|
sólo corría al startup — para una sesión larga con muchas escrituras,
|
|
el log crecía sin tope hasta el próximo restart, y el siguiente boot
|
|
pagaba el costo lineal del replay.
|
|
|
|
Cambios:
|
|
- **Nuevos fields en `MetaUi`**:
|
|
- `snap_path: PathBuf` — cacheado del init para que el tick no
|
|
tenga que recomputarlo.
|
|
- `snapshot_threshold: usize` — leído del env en `new()` y
|
|
cacheado. `0` desactiva runtime compact (mismo env y
|
|
semantic que el threshold de startup).
|
|
- `writes_since_compact: u64` — contador que incrementa por cada
|
|
write efectivo y se resetea cuando el threshold dispara
|
|
`maybe_compact_log`.
|
|
- **Nuevo método `tick_runtime_compact()`**:
|
|
- Early return si `threshold == 0`.
|
|
- Increment + check vs threshold.
|
|
- Si cruza: lock log + store, llama `maybe_compact_log`.
|
|
- **Si compactó OK**: counter = 0, devuelve msg.
|
|
- **Si `maybe_compact_log` returned None** (counter dijo "go"
|
|
pero entries < 2): counter = 0 (no re-entrar cada write).
|
|
- **Si error**: counter NO se resetea (próximo write reintenta),
|
|
devuelve el error.
|
|
- **Nuevo helper `append_compact_msg(base, opt)`**: concatena el
|
|
msg del compact al toast del op original con `";"` separator.
|
|
- **Wireup en 3 callsites de write efectivo**:
|
|
- `apply_action::SeedEntity`: tick si outcome != NoChange.
|
|
- `apply_action::Morphism`: tick siempre que Ok.
|
|
- Click handler `[Confirmar]` del delete modal: tick si commit_delete Ok.
|
|
- **NoChange no cuenta**: un edit que no cambia nada no escribe al
|
|
log, así que tampoco debería avanzar el counter — preserva la
|
|
semantic "1 write = 1 log entry = 1 tick".
|
|
|
|
2 tests nuevos:
|
|
- `append_compact_msg_handles_both_branches` — base solo vs base
|
|
+ compact, formato del separator.
|
|
- `runtime_compact_cycle_resets_counter_after_threshold` — E2E
|
|
estilo simulación: 7 writes con threshold=3 → 2 compacts (en
|
|
write 3 y 6), counter residual = 1, log final con 2 entries
|
|
(1 anchor + 1 write residual). Reproduce el algoritmo del tick
|
|
sin GPUI cx; si la lógica del método cambia, se rompe como signal.
|
|
|
|
37 tests verdes (+2). Workspace build verde.
|
|
|
|
Trade-offs:
|
|
- **Counter en memoria, no persistido**: si la app crashea entre
|
|
compacts, al próximo boot el counter parte de 0. El startup
|
|
compact (basado en entry_count del log file) compensa esto:
|
|
si quedó mucho post-último-compact, se compacta al boot.
|
|
- **Lock orden**: tick toma log lock primero, store lock después.
|
|
Misma orden que `commit_seed` y `commit_morphism`, no debería
|
|
haber deadlock.
|
|
- **Costo del tick**: 1 increment + 1 compare por write. Cuando
|
|
cruza threshold, 1 read del log (entries) + 1 snapshot write +
|
|
1 compact. Para threshold=50 es ~1 fsync cada 50 writes —
|
|
amortiza bien.
|
|
|
|
Pendientes restantes:
|
|
- **`FieldOp::Clear`** — para soportar borrar un value vía form vacío.
|
|
- **Validación cross-field** (UUID del EntityRef existe en la
|
|
entity referida).
|
|
|
|
### feat(nakui-ui): atajo Esc para cancelar el modal de delete
|
|
Cierra otro pendiente de UX. El banner de confirmación de delete
|
|
ya tenía botones [Cancelar] / [Confirmar], pero la acción más
|
|
natural para cancelar un dialog es Esc — y no la teníamos wireada.
|
|
|
|
Cambios:
|
|
- **`capture_key_down` en el root div** del `MetaUi::render`. Capture
|
|
phase (no bubble) para interceptar el Esc *antes* que cualquier
|
|
TextInput descendiente lo consuma. Sin pending el handler es
|
|
no-op y el evento sigue su flujo normal.
|
|
- **Match `event.keystroke.key == "escape"`** + `pending_delete.take()`
|
|
→ toast `"delete cancelado (Entity) [esc]"` (sufijo `[esc]` para
|
|
diferenciar visualmente del botón). Si no hay pending, return
|
|
temprano sin tocar nada.
|
|
- **Hint visual en el banner**: subtítulo en amber tenue debajo del
|
|
título: `"Esc para cancelar · click [Confirmar] para borrar"`.
|
|
Que el usuario descubra el atajo sin RTFM.
|
|
|
|
35 tests verdes — el handler de Esc es 8 líneas no-testeables sin
|
|
GPUI cx (la lógica de pending_delete + toast vive dentro del
|
|
listener); el wireup compila por type-check.
|
|
|
|
Pendientes restantes:
|
|
- **`FieldOp::Clear`** — para soportar borrar un value vía form vacío.
|
|
- **Snapshot durante runtime** (cada N writes, no sólo al startup).
|
|
- **Validación cross-field** (UUID del EntityRef existe en la
|
|
entity referida).
|
|
|
|
### feat(nakui-ui): EntityRef validation en parse_field_value (UUID al submit)
|
|
Cierra otro pendiente: `parse_field_value(FieldKind::EntityRef, raw)`
|
|
devolvía `Ok(json!(raw))` blindly — el value entraba al log/store
|
|
incluso si era basura. La validación de UUID sólo ocurría cuando el
|
|
field se usaba como **input** del morphism (línea ~540 de
|
|
`commit_morphism`); como **seed field** o como **param**, garbage
|
|
pasaba silenciosamente.
|
|
|
|
Cambios:
|
|
- **`parse_field_value(EntityRef, raw)`** ahora hace
|
|
`Uuid::parse_str(raw.trim())` y devuelve error claro si falla:
|
|
`"'<raw>' no es UUID válido (usá el selector de records)"`. En
|
|
caso de éxito, devuelve el UUID **trimmed** como string —
|
|
protege contra paste manual con whitespace.
|
|
- **Doble cobertura**: este path cubre seed fields (commit_seed via
|
|
obj.insert) y morphism params (resolve_param_value lo invoca por
|
|
cada FieldSpec con kind=EntityRef). El path de morphism inputs
|
|
ya validaba antes con `Uuid::parse_str` directo — sigue intacto,
|
|
no hay double-validation.
|
|
- **Selector clickable es happy path**: el dropdown setea valores
|
|
bien-formados, así que el usuario nunca debería ver el error en
|
|
uso normal. Sólo dispara con paste manual o si el usuario escribe
|
|
garbage en el input — defensivo.
|
|
|
|
5 tests nuevos (reemplazan al obsoleto `parse_field_entity_ref_returns_string`):
|
|
- `parse_field_entity_ref_accepts_valid_uuid` — happy path.
|
|
- `parse_field_entity_ref_trims_whitespace` — `" uuid\n"` → `"uuid"`.
|
|
- `parse_field_entity_ref_rejects_non_uuid` — `"abc-123"` → error
|
|
con el value y la palabra "UUID" en el mensaje.
|
|
- `parse_field_entity_ref_rejects_empty_string` — `""` → rebota.
|
|
- `resolve_param_strict_entity_ref_propagates_error` — sanity de
|
|
que el wireup en resolve_param_value hereda el strict checking,
|
|
con label del FieldSpec en el mensaje.
|
|
|
|
35 tests verdes (+4 net). El E2E del morphism real
|
|
`morphism_pipeline_executes_real_sales_vender` sigue verde — sus
|
|
inputs van por el path dedicado, no por parse_field_value.
|
|
|
|
Pendientes restantes:
|
|
- **Atajo Esc para Cancelar** del modal de delete.
|
|
- **`FieldOp::Clear`** — para soportar borrar un value vía form.
|
|
- **Snapshot durante runtime** (cada N writes, no sólo al startup).
|
|
- **Validación cross-field** (ej: el UUID del EntityRef existe en
|
|
la entity referida) — hoy sólo validamos forma; un UUID válido
|
|
pero inexistente sí pasa.
|
|
|
|
### feat(nakui-ui): snapshot/compaction automático del event log al startup
|
|
Cierra el último gran pendiente del round: el replay full cada
|
|
startup escala lineal en el log. Con 60+ entries el costo de boot
|
|
se nota; con 10k entries es prohibitivo. Wireamos el snapshot
|
|
machinery que ya estaba en `nakui-core` (`Snapshot`,
|
|
`replay_with_snapshot_into`, `EventLog::compact_through`) al
|
|
runtime de la UI.
|
|
|
|
Cambios:
|
|
- **Path del snapshot**: sibling del log, extensión `.snap.json`.
|
|
`nakui-ui-state.jsonl` ↔ `nakui-ui-state.snap.json`.
|
|
- **Nuevo helper `snapshot_path_for(log_path)`** — derivación
|
|
pura, testeable.
|
|
- **Nuevo helper `maybe_compact_log(log, snap_path, store, threshold)`**:
|
|
- Si `entry_count >= threshold` y `>= 2`, captura
|
|
`Snapshot::from_memory_store(store, next_seq - 1)`, lo escribe
|
|
atómicamente, y compacta el log dejando la última entry como
|
|
anchor.
|
|
- Anchor invariant: `EventLog::open` deriva `next_seq` del primer
|
|
entry del archivo. Si compactáramos *todo* el log file, al
|
|
reabrir el cursor volvería a 0 y el próximo append crashearía
|
|
con `NonMonotonic`. Por eso compactamos sólo hasta
|
|
`next_seq - 2` — la entry del `snap.seq` queda como anchor del
|
|
cursor; `replay_with_snapshot_into` la skipea porque snap ya
|
|
cubre hasta ese seq inclusive.
|
|
- Threshold via env `NAKUI_SNAPSHOT_THRESHOLD`, default 50.
|
|
`0` desactiva por completo.
|
|
- Devuelve `Result<Option<msg>, String>`: `Ok(Some)` si compactó,
|
|
`Ok(None)` si no había payoff, `Err` si snap o compact fallaron.
|
|
- **`MetaUi::new` reescrito**:
|
|
- Carga snapshot al inicio (Some/None según exista).
|
|
- `replay_with_snapshot_into(&log, snapshot.as_ref(), &mut store)`
|
|
en lugar de `replay_into`.
|
|
- Después del replay corre `maybe_compact_log` con el threshold.
|
|
- Toast inicial menciona snapshot loaded si aplica
|
|
("snapshot @ seq K") y la compactación si ocurrió.
|
|
- Errores de snapshot load **no son fatales**: cae a full replay
|
|
con un msg en el banner.
|
|
- Errores de auto-compact **no son fatales**: el log + snap
|
|
quedan como estaban, msg al banner.
|
|
|
|
5 tests nuevos:
|
|
- `snapshot_path_for_replaces_extension` — `.jsonl` → `.snap.json`,
|
|
edge case sin extensión.
|
|
- `maybe_compact_log_below_threshold_noops` — 5 entries vs threshold
|
|
50: no toca nada, no escribe snap.
|
|
- `maybe_compact_log_threshold_zero_noops` — threshold 0 = disabled.
|
|
- `maybe_compact_log_then_reopen_preserves_records` — E2E:
|
|
- Escribe 60 seeds (log + store en sync).
|
|
- Compacta (60 >= 50): snap escrito, log queda con 1 anchor entry,
|
|
msg reporta "59 entries dropped (1 anchor kept)".
|
|
- Reopen: `next_seq=60` se preserva via anchor, `entries.len()=1`.
|
|
- Replay con snap loadado en store fresco: los 60 records están.
|
|
- Segunda corrida del compact con threshold=1: no-op (idempotente).
|
|
|
|
31 tests verdes (+5). Workspace build verde tras la nueva firma.
|
|
|
|
Trade-offs y notas:
|
|
- **Fail-soft**: cualquier error de snap/compact no rompe el boot;
|
|
la UI sigue funcionando con full replay y el toast lo reporta.
|
|
Sólo `EventLog::open` failing es no-recoverable (pierde
|
|
persistencia).
|
|
- **Crash-safety**: WAL order preservado — escribimos snap (atómico
|
|
via tempfile + fsync + rename) ANTES de compactar el log
|
|
(atómico igual). Si crasheamos entre los dos, próximo boot ve
|
|
snap@K + log con todas las entries 0..N — replay skippea las que
|
|
snap cubre, outcome idéntico.
|
|
- **Sólo en startup**: no hay snapshot durante runtime. Para sesiones
|
|
largas con muchas escrituras, el log puede crecer arbitrariamente
|
|
hasta el próximo restart. Pendiente futuro: snapshot N writes
|
|
desde el último compact.
|
|
- **Anchor entry sobrevive sin uso útil**: el costo es 1 línea JSON
|
|
por compact. No es preocupación a menos que el threshold sea
|
|
muy chico (cada compact deja 1 línea de basura).
|
|
|
|
Pendientes restantes:
|
|
- **EntityRef validation post-submit** — validar UUID parseable al
|
|
submit en lugar de al execute del morphism.
|
|
- **Atajo Esc para Cancelar** del modal de delete.
|
|
- **`FieldOp::Clear`** — para soportar borrar un value vía form vacío.
|
|
- **Snapshot durante runtime** (cada N writes, no sólo al startup).
|
|
|
|
### feat(nakui-ui): edit delta-only — sólo campos modificados al log/store
|
|
Antes de este cambio, editar un record emitía un `FieldOp::Set` por
|
|
**cada field del form**, incluso los no tocados. Eso bloata el log
|
|
(replay tenía que aplicar N ops cuando 1 alcanzaba) y oscurece el
|
|
intent en una auditoría posterior. Con delta-only, el edit emite
|
|
sólo los Sets cuyo value nuevo difiere del actual; un edit que no
|
|
cambia nada deja el log intacto.
|
|
|
|
Cambios:
|
|
- **Nuevo helper `compute_field_delta(current, proposed)`** — toma
|
|
el record actual del store (un `Value`, posible `Null` si el
|
|
record no existe) y el `Map` propuesto desde el form, y devuelve
|
|
sólo las entries que difieren. Comparación: `PartialEq` estructural
|
|
de `serde_json::Value` (un `Null` en current = todos los proposed
|
|
son nuevos).
|
|
- **Nuevo enum `CommitOutcome`**:
|
|
- `Created(Uuid)` — alta nueva.
|
|
- `Updated { id, changed }` — edit con N campos modificados.
|
|
- `NoChange(Uuid)` — edit sin diferencias (el toast lo refleja
|
|
como "X sin cambios — no log entry").
|
|
- **`commit_seed` en path EDIT**:
|
|
- Carga current via `store.load(entity, id)` con fallback a
|
|
`Value::Null`.
|
|
- Calcula delta. Si vacío → return early sin tocar log ni store.
|
|
- Si no vacío → emite `Morphism { ui.edit_record, ops: [Set...] }`
|
|
con `params.fields` reflejando el delta (no todo el form),
|
|
haciendo la auditoría grep-able por field cambiado.
|
|
- **Toast del callsite**:
|
|
- `creado X uuid` (Created)
|
|
- `actualizado X uuid (N campo(s))` (Updated)
|
|
- `X uuid sin cambios — no log entry` (NoChange)
|
|
- **`editing` se limpia incluso en NoChange** — el modo edit cierra,
|
|
el form vuelve al state limpio.
|
|
|
|
5 tests nuevos del helper:
|
|
- delta vacío cuando todo coincide.
|
|
- delta sólo con el field cambiado.
|
|
- delta full cuando current = Null (record no existe).
|
|
- distingue int 100 de string "100".
|
|
- ignora fields del current que no están en proposed.
|
|
|
|
27 tests verdes (+5). El path SEED no cambió; el E2E del morphism
|
|
real sigue verde.
|
|
|
|
Limitación conocida (consistente con pre-delta): el form no puede
|
|
**borrar** un value vaciando el input — empty optional fields hacen
|
|
`continue` antes de llegar al delta. Para clearear un value hay que
|
|
declarar el field como required, o esperar a un `FieldOp::Clear`
|
|
futuro (no necesario hoy: ningún demo lo requiere).
|
|
|
|
Pendientes restantes:
|
|
- **Snapshot/compaction** del log (replay full cada startup escala
|
|
mal con repos grandes).
|
|
- **EntityRef validation post-submit** — validar UUID parseable al
|
|
submit en lugar de al execute del morphism.
|
|
- **Atajo Esc para Cancelar** del modal de delete.
|
|
- **`FieldOp::Clear`** — para soportar borrar un value vía form.
|
|
|
|
### feat(nakui-ui): confirmación de delete vía banner modal antes de borrar
|
|
Cierra el primer pending del último round: borrar un record pedía un
|
|
solo click en `✕` y se ejecutaba inmediatamente (irreversible —
|
|
queda en el log como `Morphism { name: "ui.delete_record" }`). Ahora
|
|
hay un paso intermedio: el click marca el record como pendiente y el
|
|
banner amber al tope de la ventana ofrece [Cancelar] o [Confirmar].
|
|
|
|
Cambios:
|
|
- **Nuevo state `MetaUi.pending_delete: Option<(String, Uuid)>`**.
|
|
Set en el click del `✕`; limpiado por:
|
|
- [Cancelar] → toast "delete cancelado (Entity)".
|
|
- [Confirmar] → llama `commit_delete` (igual que antes) y emite
|
|
el toast usual.
|
|
- Navegación a otra view (`select_view`) — el record marcado
|
|
podría no estar visible en la nueva pantalla.
|
|
- **Click handler de `✕` ya no llama `commit_delete`**: sólo setea
|
|
`pending_delete` y limpia toast. La acción destructiva ahora vive
|
|
exclusivamente en el botón [Confirmar] del banner.
|
|
- **Nuevo método `render_confirm_delete_banner`**: devuelve
|
|
`Option<Div>` (None si no hay pending). Banner amber con el texto
|
|
`¿Borrar {Entity} {short_uuid}?` + dos botones. Renderea como
|
|
sibling del row sidebar+main en `flex_col` raíz — no es overlay
|
|
flotante (GPUI no expone z-index trivialmente), pero la posición
|
|
fija al tope + color amber lo hacen imposible de ignorar.
|
|
- **Limpieza pre-commit**: `pending_delete = None` se ejecuta antes
|
|
de `commit_delete`, así un fallo del commit no deja el banner
|
|
colgado además del toast de error.
|
|
|
|
22 tests verdes — la lógica del store/log no cambió, sólo el state
|
|
machine de UI. La confirmación es puramente UX/state, no testable
|
|
sin GPUI cx, pero la compilación garantiza wireup correcto de las
|
|
closures.
|
|
|
|
Pendientes restantes:
|
|
- **Snapshot/compaction** del log para repos grandes (replay full
|
|
cada startup escala mal).
|
|
- **Edit delta-only** — sólo campos modificados, no todos.
|
|
- **EntityRef validation post-submit** — validar UUID parseable
|
|
al submit en lugar de al execute del morphism.
|
|
- **Atajo de teclado Esc para Cancelar** — requiere event
|
|
dispatcher de GPUI, fuera de scope inmediato.
|
|
|
|
### feat(nakui-ui): validación estricta de params del morphism vía FieldKind del FieldSpec
|
|
Cierra el último trade-off documentado: `infer_param_value` adivinaba
|
|
el tipo de cada param por la shape del string (i64 → f64 → bool →
|
|
string). Ahora cuando hay `FieldSpec` declarado, usamos
|
|
`parse_field_value(spec.kind, raw)` — un Boolean field con value
|
|
"abc" rebota con mensaje claro en la UI antes de llegar al morphism
|
|
Rhai (donde el error sería opaco como "Function not found: * ((), ())").
|
|
|
|
Cambios:
|
|
- **Nuevo helper `resolve_param_value(field_name, raw, spec)`**:
|
|
- Si hay `FieldSpec`: validación de `required` (rebota empty con
|
|
"param 'X' es obligatorio y está vacío") + parseo estricto via
|
|
`parse_field_value(spec.kind, raw)`. Errores incluyen el `label`
|
|
del spec para que el toast sea interpretable.
|
|
- Si NO hay spec (param declarado en `Action::Morphism.params`
|
|
que no existe en `form.fields` — módulo mal-formado): fallback
|
|
a `infer_param_value` como red de seguridad.
|
|
- Empty + opcional → `Value::Null`.
|
|
- **`commit_morphism` simplificado**: el loop de params ahora es
|
|
3 líneas (lookup spec + llamada a `resolve_param_value` +
|
|
inserción al map). La lógica vive en el helper standalone,
|
|
testable sin GPUI.
|
|
|
|
Tests: 6 nuevos en `tests` mod, todos contra `resolve_param_value`:
|
|
- `resolve_param_strict_number_parses_i64` — happy path.
|
|
- `resolve_param_strict_boolean_rejects_non_boolean` — un Boolean
|
|
con "abc" rebota con mensaje que incluye el label.
|
|
- `resolve_param_strict_number_rejects_garbage` — Number con "abc"
|
|
rebota.
|
|
- `resolve_param_required_empty_rejected` — required vacío rebota
|
|
con "obligatorio".
|
|
- `resolve_param_optional_empty_returns_null` — optional vacío
|
|
→ null.
|
|
- `resolve_param_no_spec_falls_back_to_infer` — el fallback
|
|
preserva el comportamiento anterior para back-compat.
|
|
|
|
22 tests verdes en nakui-ui (+6). E2E del morphism real
|
|
(`morphism_pipeline_executes_real_sales_vender`) sigue verde — la
|
|
validación estricta no rompe el path correcto, sólo agrega rebotes
|
|
tempranos a values mal-tipados.
|
|
|
|
Beneficio operativo:
|
|
- Mensaje de error en la UI ahora identifica el field problemático
|
|
por su label legible ("param 'Cantidad': 'abc' no es número")
|
|
en lugar del error opaco del morphism Rhai.
|
|
- Errores se ven antes de tocar el log o el store — ningún cambio
|
|
parcial.
|
|
- El módulo Nakui ya no tiene que defender contra inputs garbage
|
|
desde la UI: la metainterfaz se vuelve la primera línea de
|
|
validación tipada.
|
|
|
|
Pendientes futuros (orden de prioridad):
|
|
- **Confirmación de delete** — modal antes de borrar.
|
|
- **Snapshot/compaction** del log para repos grandes.
|
|
- **Edit delta-only** — sólo campos modificados, no todos.
|
|
- **EntityRef validation post-submit**: hoy `parse_field_value`
|
|
para EntityRef devuelve string raw; el commit_morphism luego
|
|
valida como Uuid sólo cuando es input del morphism. Para
|
|
EntityRef como param, podríamos validar UUID al submit.
|
|
|
|
### feat(nakui-ui): FieldKind::EntityRef — selector clickable de records existentes
|
|
Cierra el principal trade-off documentado del commit anterior:
|
|
"Inputs UUID a mano (no dropdown)". Los formularios pueden declarar
|
|
un campo `entity_ref` que apunta a una entity y el runtime renderea
|
|
una lista clickable de records existentes; click selecciona, el
|
|
UUID queda guardado para el submit.
|
|
|
|
Schema `nakui-ui-schema`:
|
|
- **Nueva variante `FieldKind::EntityRef`** (serializa como
|
|
`"entity_ref"` en JSON).
|
|
- **`FieldSpec.ref_entity: Option<String>`** nuevo. Indica qué
|
|
entity ofrecer en el selector. `validate()` chequea que cualquier
|
|
field con `kind=entity_ref` tenga `ref_entity` set.
|
|
- Nuevo error tipado `SchemaError::EntityRefMissingTarget`.
|
|
|
|
Runtime `nakui-ui`:
|
|
- **`render_entity_ref_selector(field_name, target_entity, ...)`** —
|
|
helper que arma la lista debajo del input. Cada item:
|
|
- Etiqueta humana via `human_label_for_record` (heurística:
|
|
`name` → `label` → `title` → `sku` → `sku_id` → fallback al
|
|
UUID corto).
|
|
- Click handler vía `cx.listener` que llama
|
|
`input.set_text(uuid_completo)` — el TextInput interno queda
|
|
como source-of-truth, así que `commit_seed` y `commit_morphism`
|
|
leen el UUID seleccionado sin saber que vino de un selector.
|
|
- Highlight en accent color cuando el item es el actualmente
|
|
seleccionado (compara contra el contenido del TextInput).
|
|
- **`parse_field_value(EntityRef, raw)`** devuelve string del raw
|
|
(la validación como Uuid ocurre downstream en `commit_morphism`).
|
|
- Mensaje "(sin {entity}: creá uno antes para referenciar)" cuando
|
|
la lista está vacía — el user sabe qué hacer en lugar de quedarse
|
|
trabado.
|
|
|
|
Demo actualizado: `examples/nakui-modules/sales_engine/module.json`:
|
|
- `vender_form.fields.stock_id_input` y `caja_id_input` cambian de
|
|
`kind: "text"` a `kind: "entity_ref"` con `ref_entity: "Stock"`
|
|
y `"Caja"` respectivamente.
|
|
- Ahora el flujo "Vender" es: (1) click en una Stock listada bajo
|
|
el input, (2) click en una Caja, (3) escribir venta_id/cantidad/
|
|
precio_unitario/timestamp, (4) submit. Sin copiar UUIDs.
|
|
|
|
Tests:
|
|
- 2 nuevos en schema: `validate_catches_entity_ref_without_target`
|
|
y `entity_ref_with_target_validates_clean`. 8 totales.
|
|
- 4 nuevos en runtime: `parse_field_entity_ref_returns_string`,
|
|
`human_label_for_record_prefers_name_over_id`,
|
|
`human_label_falls_back_through_label_title_sku`,
|
|
`human_label_falls_back_to_id_when_no_known_keys`. 16 totales.
|
|
- Integration de los 7 demos sigue verde — el demo `sales_engine`
|
|
ahora valida con EntityRef + ref_entity correctamente set.
|
|
|
|
29 tests totales nakui-ui + schema, 100% verde. El demo
|
|
`sales_engine` carga limpio con la nueva forma del schema.
|
|
|
|
Pendientes futuros:
|
|
- **Confirmación de delete** — modal antes de borrar.
|
|
- **Snapshot/compaction** del log para repos grandes.
|
|
- **Edit delta-only** (sólo campos modificados).
|
|
- **Validación de tipos en params del morphism**: `FieldKind`
|
|
declarado en el FieldSpec se podría usar para forzar parseo
|
|
estricto en `commit_morphism` en lugar de la heurística
|
|
`infer_param_value`.
|
|
|
|
### feat(nakui-ui): Action::Morphism wired al pipeline real (compute → log → apply)
|
|
Cierra el último gran TODO de la metainterfaz Nakui: las acciones
|
|
`Action::Morphism` ya no son un toast informativo; despachan al
|
|
`Executor` cargado del manifest nakui-core (`nsmc.json` + schemas
|
|
KCL + scripts Rhai), pasando por el pipeline completo de Nakui:
|
|
compute (con dry-run + KCL post-checks) → log append → store apply.
|
|
|
|
Schema `nakui-ui-schema` extendido:
|
|
- **`Module.nakui_module_dir: Option<String>`** nuevo. Path
|
|
(relativo al directorio del `module.json` o absoluto) a un módulo
|
|
nakui-core. Sin esto, las Action::Morphism del módulo quedan
|
|
no-op con toast informativo. Las Action::SeedEntity siguen
|
|
funcionando sin manifest (alta administrativa).
|
|
- **`Action::Morphism`** ganó dos campos opcionales:
|
|
- `inputs: BTreeMap<String, String>` — mapeo `role → field_name`.
|
|
Por cada input declarado en el `MorphismSpec.inputs`, indica
|
|
qué field del form contiene el UUID del record. El runtime
|
|
parsea como `Uuid` y lo pasa al `execute_and_log`.
|
|
- `params: Vec<String>` — lista de fields cuyos values van al
|
|
`params` JSON. Si vacío, todos los fields no-input van a params.
|
|
|
|
Runtime `nakui-ui`:
|
|
- **`MetaUi.executors: BTreeMap<String, Arc<Executor>>`** nuevo.
|
|
Carga `Executor::load_module(nakui_module_dir)` en `MetaUi::new`
|
|
por cada módulo UI que declare la entry. Errores de carga van al
|
|
banner; el módulo sigue cargado para SeedEntity, sólo Morphism
|
|
queda no-op.
|
|
- **`commit_morphism(mod_idx, name, inputs_map, params_fields)`** nuevo.
|
|
Resuelve inputs (parsea cada field como Uuid), arma params (Value
|
|
object con tipos inferidos via `infer_param_value` — int/float/
|
|
bool/string), llama `execute_and_log_with_recovery`. Toast con
|
|
cantidad de ops aplicadas o el error tipado.
|
|
- **`infer_param_value`** nuevo helper: heurística simple para
|
|
pasar values del form al morphism con tipo inferido (i64 → f64 →
|
|
bool → string).
|
|
|
|
Tests: 2 nuevos:
|
|
- `infer_param_value_int_then_float_then_bool_then_string` —
|
|
cobertura de la heurística.
|
|
- **E2E `morphism_pipeline_executes_real_sales_vender`** —
|
|
carga el módulo real `crates/modules/nakui/modules/sales`,
|
|
arma store + log, ejecuta el morphism `vender` con inputs
|
|
Stock+Caja y params (cantidad=5, precio_unitario=200,
|
|
venta_id, timestamp). Asserta:
|
|
- el morphism produce ops (no vacío).
|
|
- stock.cantidad bajó 100 → 95.
|
|
- caja.saldo subió 1_000_000 → 1_001_000.
|
|
|
|
12 tests verdes en nakui-ui (+1 vs commit anterior). Schema
|
|
extension no rompió nada (6 unit + 5 integration siguen verdes).
|
|
|
|
Demo nuevo: **`examples/nakui-modules/sales_engine/module.json`**
|
|
- Apunta a `crates/modules/nakui/modules/sales` vía `nakui_module_dir`.
|
|
- 6 vistas: list + form para cada Stock, Caja, Venta + form
|
|
"Vender" con `Action::Morphism { name: "vender", inputs: {stock,
|
|
caja}, params: [venta_id, cantidad, precio_unitario, timestamp] }`.
|
|
- El user crea Stocks + Cajas con seed_entity, copia los UUIDs
|
|
cortos a los inputs de "Vender", y ejecuta el morphism real:
|
|
stock baja, caja sube, Venta se persiste, todo loggeado.
|
|
- Validaciones KCL fallan limpio (toast con error) si el morphism
|
|
rebota — p. ej. cantidad > stock disponible.
|
|
|
|
Activación full:
|
|
```sh
|
|
NAKUI_EVENT_LOG=~/.nakui/state.jsonl \
|
|
NAKUI_MODULES_DIR=examples/nakui-modules \
|
|
cargo run -p nakui-ui
|
|
# Sidebar gana "Ventas (con morphism)" — los 6 menús aparecen y
|
|
# el form "Vender" dispara el pipeline nakui-core completo.
|
|
```
|
|
|
|
Trade-offs documentados:
|
|
- **Inputs UUID a mano**: el form pide que el user copie el UUID de
|
|
un Stock/Caja existente. Para UX seria habría que agregar
|
|
`FieldKind::EntityRef { entity }` que renderiza un dropdown — no
|
|
hecho por scope, queda como nice-to-have.
|
|
- **Inferencia de tipo en params**: `infer_param_value` adivina
|
|
por shape del string. Para casos sutiles (ej. "true" como string
|
|
literal vs bool), el módulo nakui-core puede explicitar tipos
|
|
via `kind` en el FieldSpec — el form lo respeta para validación
|
|
pre-submit; la inferencia final sigue siendo heurística.
|
|
|
|
### feat(nakui-ui): edit + delete de records (ciclo CRUD completo)
|
|
Cierra "no hay UI para editar/borrar records existentes" del commit
|
|
anterior. Cada fila de la lista gana dos botones (✎ edit, ✕ delete);
|
|
el form view se reusa para alta y para edit; el delete es inline.
|
|
Las mutaciones pasan por `LogEntry::Morphism` con sus ops, así el
|
|
replay restaura el estado correcto.
|
|
|
|
Cambios:
|
|
|
|
- **`MetaUi.editing: Option<(String, Uuid)>`** nuevo. Set al click
|
|
en ✎; cleared al cambiar de view o tras submit exitoso.
|
|
- **`open_edit(mod_idx, entity, id, cx)`**: setea `editing`, busca la
|
|
primera Form view del módulo cuya `entity` matchee, navega ahí. Si
|
|
el módulo no tiene Form para esa entity → toast con error
|
|
("no hay form view para entity X").
|
|
- **`select_view`** extendido: cuando carga un Form, si `editing`
|
|
matchea esa entity y el record existe en el store, pre-llena cada
|
|
input con el valor del record (vía nuevo helper
|
|
`value_to_input_text` — inverso de `parse_field_value`).
|
|
- **`commit_seed`** ramifica:
|
|
- **Edit path** (cuando `editing.is_some()` y entity matchea):
|
|
emite `LogEntry::Morphism { name: "ui.edit_record", ops:
|
|
[Set { path, value } for each field], params: { entity, id,
|
|
fields } }`. Aplica al store via `apply(&ops)`.
|
|
- **Seed path** (alta nueva): comportamiento previo.
|
|
- **`commit_delete(entity, id)`**: emite `LogEntry::Morphism {
|
|
name: "ui.delete_record", ops: [Delete { entity, id }] }` + apply.
|
|
- **Render del form**: título cambia a "Editar customer abc12345"
|
|
cuando `editing` matchea; submit label cambia a "Guardar cambios
|
|
en customer".
|
|
- **Render de la lista**: dos columnas nuevas — "id" y "acciones".
|
|
Cada fila tiene ✎ (accent color, click → open_edit) y ✕ (rojo,
|
|
click → commit_delete). Hover states.
|
|
|
|
Ramificación visible en el event log:
|
|
```
|
|
{"kind":"seed","seq":0,"entity":"customer","id":"abc...","data":{"name":"Acme"}}
|
|
{"kind":"morphism","seq":1,"morphism":"ui.edit_record","ops":[
|
|
{"op":"set","path":{"entity":"customer","id":"abc...","field":"name"},
|
|
"value":"Acme S.A."}
|
|
]}
|
|
{"kind":"morphism","seq":2,"morphism":"ui.delete_record","ops":[
|
|
{"op":"delete","entity":"customer","id":"abc..."}
|
|
]}
|
|
```
|
|
Coherente con el modelo de Nakui — todo cambio post-seed pasa por
|
|
ops dentro de Morphism. `nakui-explorer` muestra estos morphisms
|
|
con sus ops claros en su timeline.
|
|
|
|
Trade-offs documentados:
|
|
- **`schema_hash: None`** sigue para los morphism de la UI (legacy/
|
|
pre-versioning path) hasta que `Action::Morphism` cargue Manifest
|
|
schemas.
|
|
- **Delete sin confirmación**: 1 click, sin modal. Para MVP es OK
|
|
(los records son recuperables vía replay parcial), pero un futuro
|
|
iter agregaría confirmación.
|
|
- **Edit sobreescribe TODOS los campos del form**, no sólo los
|
|
cambiados — emite N ops Set, una por field. Adecuado para forms
|
|
chicos; para forms con muchos campos optimizar a delta-only.
|
|
|
|
Tests: 3 nuevos (10 totales en nakui-ui):
|
|
- `value_to_input_text_inverse_of_parse` y
|
|
`value_to_input_then_parse_round_trip` — la propiedad fundamental
|
|
del pre-llenado: text → parse devuelve el Value original.
|
|
- `event_log_replay_handles_full_crud_cycle` — E2E del log: escribe
|
|
Seed + Morphism(Set ops) + Morphism(Delete op), replay desde cero,
|
|
verifica que el store termina vacío (delete fue el último). Verifica
|
|
además que un replay parcial (sin el delete) deja los valores
|
|
editados.
|
|
|
|
Activación:
|
|
```sh
|
|
NAKUI_EVENT_LOG=~/.nakui/state.jsonl \
|
|
NAKUI_MODULES_DIR=examples/nakui-modules \
|
|
cargo run -p nakui-ui
|
|
# Crear un customer, click ✎ en su fila, modificar campos,
|
|
# "Guardar cambios". Click ✕ en otra fila para borrar.
|
|
# Cerrar y reabrir: el state persiste con todos los cambios.
|
|
```
|
|
|
|
### feat(nakui-ui): persistencia con event log + replay al startup
|
|
Cierra "sin persistencia entre runs" del commit anterior. Cada
|
|
`SeedEntity` se appendea al `nakui_core::event_log::EventLog` con
|
|
WAL semantics (log antes que store) y al re-abrir el binario el
|
|
replay reconstruye el `MemoryStore` desde cero. Cerrar y volver a
|
|
abrir ya no borra el data.
|
|
|
|
Cambios:
|
|
- **`MetaUi.event_log: Option<Arc<Mutex<EventLog>>>`** nuevo.
|
|
Compartido bajo `Mutex` para que el commit_seed pueda mutar.
|
|
- **Apertura + replay al startup** (`MetaUi::new`): path por env
|
|
`NAKUI_EVENT_LOG`, default `./nakui-ui-state.jsonl`.
|
|
`EventLog::open` + `replay_into` reconstruyen el store.
|
|
Toast informativo: "log nuevo" o "log X cargado: N evento(s)
|
|
replayed".
|
|
- **WAL en `commit_seed`**: si `event_log.is_some()`, primero
|
|
`log.append(LogEntry::Seed { ..., schema_hash: None })`, después
|
|
`store.seed`. Si el append falla, cancela toda la operación
|
|
(el user reintenta sin haber dejado state inconsistente).
|
|
- **`schema_hash: None`**: documentado como path "legacy /
|
|
pre-versioning" para seeds que no pasan por un Manifest+Executor.
|
|
Es el path correcto para alta administrativa vía la metainterfaz
|
|
hasta que `Action::Morphism` wireé el Manifest loader.
|
|
- **Degradación grácil**: si abrir log falla (permisos, disco),
|
|
toast con error pero el runtime sigue en modo in-memory.
|
|
|
|
Tests: 1 nuevo E2E `event_log_replay_restores_memory_store` que
|
|
escribe 2 seeds via `EventLog::append`, re-abre y `replay_into` un
|
|
store fresh, verifica que ambos records están con sus values
|
|
correctos. Reproduce el flujo del startup de `MetaUi::new` sin
|
|
necesitar GPUI. 7 tests verdes en nakui-ui.
|
|
|
|
Activación con persistencia explícita:
|
|
```sh
|
|
NAKUI_EVENT_LOG=~/.nakui/state.jsonl \\
|
|
NAKUI_MODULES_DIR=examples/nakui-modules \\
|
|
cargo run -p nakui-ui
|
|
# Crear varios records vía el form, cerrar el binario, abrir de
|
|
# nuevo: los records están.
|
|
```
|
|
|
|
Limitaciones que **siguen** (próximos iters):
|
|
- **`Action::Morphism`** sigue como TODO: requiere cargar el
|
|
`Manifest` de nakui-core junto al `Module` UI para conocer los
|
|
inputs/params declarados y poder llamar `execute_and_log`.
|
|
- **No hay snapshot/compaction**: el log crece append-only para
|
|
siempre. Para repos grandes habría que integrar `Snapshot` de
|
|
nakui_core (existe, no se usa todavía).
|
|
- **No hay UI para borrar/editar** records existentes — sólo alta
|
|
vía form. Edit + delete en futuras iteraciones.
|
|
- **Widget input simple** (sin selection/IME/clipboard) — heredado
|
|
de la limitación documentada de `yahweh-widget-text-input`.
|
|
|
|
### feat(nakui-ui): inputs reales con yahweh-widget-text-input + click handlers funcionales
|
|
Cierra dos limitaciones documentadas en el commit anterior de la
|
|
metainterfaz: los formularios ahora aceptan teclado real, y los
|
|
clicks en menús + botones mutan estado correctamente.
|
|
|
|
Cambios:
|
|
- **Inputs vivos**: cada `FieldSpec` del Form view materializa un
|
|
`Entity<TextInput>` (de `yahweh-widget-text-input`) al entrar a la
|
|
vista. Los entities se reemplazan al cambiar de view (drop limpio).
|
|
El widget soporta: escribir caracteres, Backspace, Enter (Confirmed
|
|
event — no usado todavía; el submit va por botón), Escape
|
|
(Cancelled). El cursor se renderea como `|` al final.
|
|
- **Click handlers wired vía `cx.listener`**: menús del sidebar
|
|
invocan `select_view`; botones de acción (header de list, submit
|
|
de form) invocan `apply_action`. Los handlers tienen acceso real
|
|
al `Context<MetaUi>` y mutan el modelo + emiten `cx.notify()`.
|
|
- **Submit lee texto de los inputs**: `commit_seed` reemplaza el
|
|
buffer ad-hoc anterior por `input.read(cx).text()` por cada
|
|
field. El value parseado va al `MemoryStore` con su tipo correcto
|
|
(text/number/boolean/date).
|
|
- **Reset de inputs tras submit**: si la acción no tiene `next_view`,
|
|
los inputs se vacían (`set_text("")`) para alta consecutiva sin
|
|
re-tipear.
|
|
- **Hover states**: items del sidebar y botones cambian de bg al
|
|
pasar el mouse, feedback visual consistente con el resto del
|
|
ecosistema yahweh.
|
|
- **Theme global**: `Theme::install_default(cx)` al inicio (lo
|
|
requiere el text_input para sus colores).
|
|
|
|
Wire en Cargo:
|
|
- Deps nuevas: `yahweh-widget-text-input`, `yahweh-theme` (paths
|
|
relativos al monorepo).
|
|
|
|
Limitaciones que **siguen abiertas** (próximos iters):
|
|
- **`Action::Morphism`** sigue como TODO: requiere cargar el
|
|
`Manifest` de nakui-core junto al `Module` UI para conocer los
|
|
inputs/params declarados.
|
|
- **Sin persistencia entre runs**: `MemoryStore` en RAM. Wire con
|
|
`EventLog` o `SurrealStore` queda para cuando exista el daemon
|
|
Nakui.
|
|
- **Inputs simples**: el widget no soporta cursor positioning,
|
|
selection, copy/paste, IME, multilínea. Para edits serios habrá
|
|
que portar `gpui::examples::input` o adoptar `gpui-input` cuando
|
|
exista upstream.
|
|
- **Enter no envía**: el `TextInputEvent::Confirmed` que emite el
|
|
widget no está suscrito todavía; el submit va por click. Trivial
|
|
de wirear si lo necesitamos.
|
|
|
|
Tests: los 6 unit del runtime siguen verdes (parse_field_value para
|
|
los 5 kinds, lookup_field nested, render_value). El comportamiento
|
|
visual requiere correr el binario con `cargo run -p nakui-ui` y
|
|
probar a mano — GPUI no provee harness de UI testing en CI hoy.
|
|
|
|
Activación full:
|
|
```sh
|
|
NAKUI_MODULES_DIR=examples/nakui-modules cargo run -p nakui-ui
|
|
# Click en un menú → carga vista. Click en "Nuevo" → form.
|
|
# Tipear en cada campo → ver el `|` al final. Click "Crear customer"
|
|
# → record aparece en la lista.
|
|
```
|
|
|
|
### feat(nakui): metainterfaz declarativa + 6 módulos ERP estándar
|
|
Salto cualitativo: Nakui pasa de "library + demos + read-only viewer
|
|
del event log" a **plataforma ERP con UI dirigida por datos**. Cada
|
|
módulo de negocio se declara como un `module.json` (sin código Rust
|
|
nuevo) y el runtime GPUI lo carga dinámicamente: sidebar de menús,
|
|
listas con columnas configurables, formularios de alta.
|
|
|
|
Tres entregables:
|
|
|
|
**1) Crate nuevo `nakui-ui-schema`** (datos puros, ~250 LOC + 200
|
|
LOC tests):
|
|
- `Module { id, label, entities, menu, views }`.
|
|
- `View::List { entity, columns, actions, search_in }` o
|
|
`View::Form { entity, fields, on_submit }`.
|
|
- `FieldSpec { name, label, kind, default, required, help }` con
|
|
`FieldKind = Text|Multiline|Number|Boolean|Date`.
|
|
- `Action::OpenView | SeedEntity | Morphism` — el runtime las
|
|
dispara desde botones / submits.
|
|
- `Module::from_path` parsea un JSON; `Module::validate` chequea que
|
|
cada `MenuItem.view` exista en `views`.
|
|
- `load_modules_from_dir(dir)` busca `dir/<modulo>/module.json`,
|
|
parsea, valida, detecta IDs duplicados, devuelve ordenado.
|
|
- 6 tests unit + 4 integration (los 6 demos cargan limpio, todos
|
|
tienen list+form, kinds reconocidos, validate pasa).
|
|
|
|
**2) Crate nuevo `nakui-ui`** (binario GPUI, ~700 LOC + 100 LOC tests):
|
|
- Carga módulos desde `NAKUI_MODULES_DIR` (default `./nakui-modules`).
|
|
- Sidebar con módulos + sus menús; click en menu cambia la vista activa.
|
|
- **List view**: tabla de instancias del entity con columnas
|
|
weighted (header de columnas + filas + id corto).
|
|
- **Form view**: campos labeled + botón submit que dispara la action
|
|
declarada (`SeedEntity` mete el record al `MemoryStore`
|
|
in-process; `Morphism` queda como TODO hasta integrar el manifest
|
|
loader nakui-core).
|
|
- `MemoryStore` compartido entre todas las vistas (Arc<Mutex>); el
|
|
cambio en un módulo se refleja en otro inmediato.
|
|
- Toast + error banner para feedback.
|
|
- 6 tests unit (parse_field_value para los 5 kinds, lookup_field
|
|
nested, render_value).
|
|
|
|
**3) 6 módulos demo** en `examples/nakui-modules/` que cubren un
|
|
ERP estándar:
|
|
- **customers**: nombre, email, teléfono, activo, límite de
|
|
crédito, notas.
|
|
- **products**: SKU, nombre, categoría, precio, stock, activo.
|
|
- **suppliers**: razón social, ID fiscal, contacto, email,
|
|
teléfono, términos de pago.
|
|
- **inventory_movements**: fecha, tipo (in/out/adjustment), SKU
|
|
producto, cantidad, costo unitario, motivo, doc. referencia.
|
|
- **sales_orders**: número, cliente, emisión, vencimiento,
|
|
estado, subtotal, impuestos, total, notas.
|
|
- **invoices**: número, cliente, emisión, vencimiento, subtotal,
|
|
impuestos, total, pagado, estado, moneda, orden referenciada.
|
|
|
|
Cada módulo tiene su `list` (catálogo) + `form` (alta), con search
|
|
field y columns weighted. Los 6 cubren un setup de ERP de ventas
|
|
chico funcional para demo.
|
|
|
|
Filosofía documentada:
|
|
- **UI como datos**: agregar un módulo = escribir un JSON, no
|
|
recompilar el binario.
|
|
- **Persistencia universal**: el runtime conecta cada vista al
|
|
`nakui_core::store::Store`; cambiar de MemoryStore a SurrealStore
|
|
no toca los module.json.
|
|
- **Schema primero, semántica después**: `nakui-ui-schema` sólo
|
|
define la forma; validación de referencias rotas (entity inexistente,
|
|
morphism faltante) vive en el runtime.
|
|
|
|
Activación:
|
|
```sh
|
|
NAKUI_MODULES_DIR=examples/nakui-modules cargo run -p nakui-ui
|
|
```
|
|
|
|
Limitaciones conocidas (próximas iteraciones):
|
|
- **Inputs sin teclado**: GPUI no incluye text input; los forms
|
|
muestran los `default` del schema y el submit usa esos. Próximo
|
|
iter: integración con `yahweh-widget-text-input`.
|
|
- **Click handlers no wired**: GPUI necesita pasar `Entity<MetaUi>`
|
|
a los handlers para mutar estado; refactor con `cx.listener` +
|
|
weak refs queda para el próximo iter. Hoy la navegación es
|
|
visual; el código de mutación sí funciona via API programática
|
|
(los tests lo cubren).
|
|
- **Acción `Morphism`**: pendiente de cargar el `Manifest` de
|
|
nakui-core junto con el `Module` UI para wirear `execute_and_log`.
|
|
- **Sin persistencia entre runs**: el `MemoryStore` se pierde al
|
|
cerrar. Wire con `EventLog` o `SurrealStore` queda para cuando
|
|
el daemon Nakui exista.
|
|
|
|
Tests: 16 totales nuevos (10 schema + 6 runtime). 100% verde.
|
|
|
|
Lo que esto desbloquea: cualquiera puede escribir un `module.json`
|
|
para su dominio (pacientes médicos, alumnos de escuela,
|
|
reservaciones de hotel) y aparece en la UI sin tocar Rust ni
|
|
recompilar. La forma de extender Nakui dejó de ser "agregar código
|
|
al ERP" y pasó a ser "escribir el contrato del módulo".
|
|
|
|
### feat(nakui-explorer): nuevo binario GPUI — Nakui visible en la interfaz
|
|
Cierra "nakui no tiene UI propia" del audit. Nuevo binario standalone
|
|
`nakui-explorer` (paralelo a `nouser-explorer`) que renderea el
|
|
event log de un repo Nakui: timeline scrollable de seeds + morphisms
|
|
con sus parámetros, breakdown por entity type, polling cada 2s para
|
|
detectar nuevos eventos appended sin restart del explorer.
|
|
|
|
Diseño:
|
|
- Lee directamente el archivo `.jsonl` del `nakui_core::event_log::EventLog`.
|
|
Path por env `NAKUI_EVENT_LOG`, default `nakui.jsonl` en pwd.
|
|
- Sin discovery vía broker brahman porque nakui hoy es CLI/library/
|
|
demos, no daemon. Cuando se daemonice, sustituir el lector de
|
|
archivo por un sidecar consumer (mismo patrón que nouser-explorer
|
|
actualmente usa).
|
|
|
|
UI:
|
|
- **Header**: path del log, count total + breakdown seeds/morphisms,
|
|
tiempo del último reload en ms.
|
|
- **Breakdown line**: top 5 buckets por frecuencia (entities + nombres
|
|
de morphisms con prefijo `→`).
|
|
- **Timeline**: tarjetas color-coded por kind (azul=seed,
|
|
verde=morphism). Cada tarjeta muestra `#seq`, kind, entity/morphism
|
|
name, id corto (8 hex), preview del data/params (80 chars), schema
|
|
hash corto (8 hex) o `(legacy)` si pre-versioning. Mostradas
|
|
más-recientes-primero, hasta 200 visibles (suficiente para
|
|
navegación; sin scroll virtualizado por ahora).
|
|
- **Error banner**: si la lectura falla (archivo inexistente o
|
|
corrupto), banner rojo con el motivo. El explorer NO crashea —
|
|
sigue intentando cada 2s.
|
|
|
|
Wire en workspace:
|
|
- Nuevo `crates/apps/nakui-explorer/` agregado a `[workspace] members`.
|
|
- Deps mínimas: `nakui-core` (para EventLog + LogEntry), `gpui`,
|
|
`serde_json`, `uuid` (con feature serde para parsear los IDs).
|
|
- Sin deps de brahman por ahora (Nakui standalone).
|
|
|
|
Tests: 7 unitarios en `tests` mod del bin:
|
|
- `load_log_returns_all_entries_in_order` — cargar un .jsonl
|
|
generado a mano, asserta que devuelve 5 entries con seqs 0..4
|
|
contiguous.
|
|
- `breakdown_counts_seeds_morphisms_and_buckets` — verifica el
|
|
conteo (3 seeds + 2 morphisms) y los buckets esperados.
|
|
- `load_missing_file_yields_empty_not_error` — archivo inexistente
|
|
devuelve `[]` sin error (delegado al contrato de `EventLog::open`).
|
|
- `preview_value_truncates_long_strings` y `_keeps_short_strings_intact`.
|
|
- `short_uuid_takes_first_8_chars` y `short_hash_takes_first_4_bytes_hex`.
|
|
|
|
Activación:
|
|
```sh
|
|
NAKUI_EVENT_LOG=/tmp/nakui_inv_xxx.jsonl cargo run -p nakui-explorer
|
|
```
|
|
|
|
Estado del CHANGELOG global tras este commit: cero pendientes
|
|
fundamentados activos. Lo único que queda es `minga-vfs` (FUSE,
|
|
explícitamente diferido por el usuario) y mejoras nice-to-have
|
|
(cobertura adicional per-lenguaje, daemon-ización de nakui para
|
|
sidecar discovery).
|
|
|
|
### chore(nakui): alinear `nakui-core` con `[workspace.package]` y deps compartidas
|
|
Cleanup de drift de convenciones: `nakui-core` era el único crate del
|
|
monorepo que mantenía `version = "0.1.0"` / `edition = "2021"` /
|
|
`thiserror = "1"` hardcoded, mientras el resto heredaba del workspace
|
|
y usaba `thiserror = "2"`. Eso significaba que un bump global de versión
|
|
o de edition se olvidaba sistemáticamente de nakui.
|
|
|
|
Cambios:
|
|
- `[package]`: `version`, `edition`, `rust-version`, `license`, `authors`,
|
|
`publish` → todos `*.workspace = true`. Agregado `description` (cumple
|
|
convención del resto de crates).
|
|
- Deps compartidas migradas a `{ workspace = true }`: serde, serde_json,
|
|
thiserror (v1→v2), tokio, ulid, sha2.
|
|
- `uuid` migrado a `{ workspace = true, features = ["serde"] }` — la
|
|
feature `serde` no está en el workspace dep porque nakui es el único
|
|
user; queda local opt-in en lugar de inflar el dep común.
|
|
- Deps específicas de nakui (sin compartición posible): rhai, petgraph,
|
|
surrealdb permanecen inline con versión local.
|
|
|
|
Verificación: `cargo build -p nakui-core` verde tras el bump de
|
|
`thiserror` v1→v2 — el `#[derive(Error)]` de los 14+ enums de error
|
|
en nakui no requirió ajustes (la API de derive es backwards-compatible
|
|
para los patrones simples). `cargo test -p nakui-core --lib`: 27/27
|
|
verdes, sin regresión.
|
|
|