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:
Sergio
2026-05-09 22:15:42 +00:00
parent 613f4f299e
commit f0c0a71860
6 changed files with 438 additions and 45 deletions
+96
View File
@@ -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,