feat(nakui-core,nakui-ui): FieldOp::Clear — borrar values vía form vacío
Nueva variante semántica del kernel: Clear { path } remueve la key
del record, distinta de Set { value: Null } (que deja la key con
valor literal null). Habilita "limpiar" un field optional vaciando
el input en la UI.
nakui-core:
- delta::FieldOp::Clear + simulate_on + capability_token (mismo
shape que Set: "entity.field").
- MemoryStore::apply_dry_run y apply: Set/Clear comparten
pre-condition (record existe + es objeto). Clear de field
ausente = no-op silencioso.
- SurrealStore: equivalente con `UPDATE ... UNSET <field>`.
- Executor capability check: Set/Clear comparten match.
- Conservation rules NO consideran Clear (sólo Set) — documentado
como morphism-author responsibility.
nakui-ui:
- commit_seed acumula `to_clear: Vec<String>` con optional empties
en lugar de `continue` silencioso.
- EDIT branch: nuevo helper compute_clear_fields filtra a sólo los
fields con current value non-null. Combina Set + Clear ops.
NoChange ahora requiere ambos vacíos. Log entry incluye
`cleared: [...]` sólo si non-empty. Updated.changed cuenta
sets+clears.
Tests: +7 en nakui-core (4 store + 3 delta), +3 en nakui-ui.
Suites: 34/34 nakui-core, 40/40 nakui-ui. Workspace build verde.
E2E del morphism real intacto.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,102 @@ ratio/diff ver `git show <sha>`.
|
||||
|
||||
## 2026-05-09
|
||||
|
||||
### 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,
|
||||
|
||||
Reference in New Issue
Block a user