Iter 10. nouser-explorer (paralela a nakui-explorer pero para ver
Mónadas via daemon nouser) tenía colors hardcoded idénticos al
patrón previo. Aplico el mismo refactor: theme global instalado,
chrome a slots, widgets compartidos.
- Nuevas deps: yahweh-theme + 3 widgets (banner, card,
theme-switcher).
- main() instala Theme::install_default.
- render: 4 vars rgb() chrome → theme slots.
- Header: flex_row + theme switcher a la derecha (mismo pattern
que nakui-explorer).
- error_banner: div hardcoded → banner_themed(Error).
- 2 cards (Engine/Monad): div().flex_col().p().bg().rounded()...
→ card_themed(cx).border_l_4().border_color(accent).
- Accents semánticos (engine cyan, data purple) quedan locales
como señales de dominio.
Las dos apps explorer del repo ahora comparten paleta themed +
switcher común.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Iter 6. Cierra el ciclo del theme: ya teníamos paleta themed +
widgets que la consumen, faltaba el control UI para rotar entre
presets en vivo. Ahora hay un botón yahweh que muestra el theme
actual y al click avanza al siguiente.
crates/modules/ui_engine/widgets/theme-switcher/:
- pub fn theme_switcher(cx: &mut App) -> impl IntoElement: botón
clickable con bg=panel_alt, hover=row_hover, label "Tema: <name> ▸".
Al click hace Theme::set(cx, Theme::next_after(current.name)).
- 2 tests #[gpui::test]: smoke + verificación de cambio de global.
- Dev-dep gpui con test-support.
Migración:
- nakui-explorer: header pasa a flex_row con label flex_grow + switcher
alineado derecha.
- yahweh-widget-meta-form (sidebar): mismo pattern en el header
"Nakui" del sidebar.
Tests stack: 115 → 117.
Beneficio: click cambia toda la paleta en vivo. 6 presets disponibles
(Nebula, Aurora, Sunset, Flat Dark, Solarized Light, High Contrast)
ciclables circularmente.
Limitación: TextInput entities tienen colors hardcoded; migrar
text_input al theme es iter futura.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Iter 3 de integración nakui↔yahweh. El card visual pattern (padding
consistente + rounded + flex_col + gap) que vivía duplicado en cada
timeline entry de nakui-explorer ahora es un widget yahweh reusable.
crates/modules/ui_engine/widgets/card/:
- pub fn card() -> Div: flex_col + px(12) + py(8) + mb(4) +
rounded(4) + gap(2). Sin colores (caller decide via builder).
- 1 test smoke.
nakui-explorer:
- Los 2 timeline entry patterns (Seed/Morphism) pasan de ~7
calls a ~3, intención "card with accent" emerge del nombre.
Tests stack: 111 → 112. App-agnostic — el widget no impone paleta,
permite themes diversos.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Patrón visual común a yahweh-widget-meta-form (toast success +
error banner) y nakui-explorer (error banner): div con bg + text
colored según severidad. Antes duplicado con colores hardcoded en
cada consumer.
Crate nuevo crates/modules/ui_engine/widgets/banner:
- pub enum Banner { Info, Success, Warning, Error } con bg()/fg()
hardcoded por variant.
- pub fn banner(kind, message) -> Div: padding/text_size defaults;
caller compone con .child()/.px()/.on_click()/etc.
- 2 tests sanity (no color collisions).
Migración:
- yahweh-widget-meta-form: 12 líneas hardcoded → 2 llamadas a
banner().
- nakui-explorer: error banner usa banner() + override de
padding custom del header.
Tests stack: 109 → 111 (+2). Cada crate compila individualmente.
Próximo natural: confirm_delete_banner (Warning + botones) puede
extraerse como modal-banner cuando emerja segundo consumer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Continúa la integración de apps nakui al stack yahweh. Los
helpers visuales que nakui-explorer tenía locales y son reusables
suben a yahweh-meta-runtime/format.
yahweh-meta-runtime:
- short_hash(h: &[u8; 32]) -> String: hex de los primeros 4 bytes.
- preview_value(v: &Value, max: usize) -> String: JSON one-liner
truncado con "..." (edge case max < 3 sin panic).
- 5 tests nuevos.
nakui-explorer:
- Nueva dep yahweh-meta-runtime.
- Borrado helpers locales (short_uuid + short_hash + preview_value)
+ 4 tests duplicados.
- Imports desde yahweh-meta-runtime.
Tests: 42→47 yahweh-meta-runtime, 7→3 nakui-explorer (los 3 que
quedan son específicos del explorer: load_log, breakdown,
missing_file). Resto intacto.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cierra el refactor de UI. El widget render (forms, lists, modal de
delete, EntityRef selector, sidebar, key handlers) deja de vivir en
nakui-ui y pasa a un crate yahweh nuevo, genérico sobre MetaBackend.
crates/modules/ui_engine/widgets/meta-form/ (yahweh-widget-meta-form):
- pub MetaApp<B: MetaBackend> con todo el state UI + impl Render
+ handlers + render_*. El bound `B: MetaBackend` se propaga.
- pub fn new(modules, backend, initial_toast, initial_error, cx):
constructor sin bootstrap. Caller pre-construye todo.
- Helpers locales del widget: lookup_field, append_compact_msg,
format_seed_toast.
- Cero deps a nakui o brahman-cards. Reusable por cualquier app.
- 3 tests funcionales puros (lookup_field, append_compact_msg,
format_seed_toast).
nakui-ui (shell):
- main.rs: 1959 → 424 líneas (78% reducción). Sólo bootstrap:
load_ui_modules + load executors + NakuiBackend::open +
cx.open_window con MetaApp::<NakuiBackend>::new como root view.
- Dep nueva yahweh-widget-meta-form.
- Tests E2E quedan: event_log_replay, morphism_pipeline real
sales, load_ui_modules x3 (4 shell). NakuiBackend tests siguen
en backend.rs (8). Widget tests en su crate.
Distribución total tests refactor yahweh:
- yahweh-meta-schema: 8
- yahweh-meta-runtime: 42
- yahweh-widget-meta-form: 3
- brahman-cards: 26
- nakui-ui: 12 (4 shell + 8 backend)
Total: 91 tests cubriendo el área.
Cada crate compila individualmente. Fase 3 (shell mínimo) era
implícita en F2c — el shell ya quedó en 424 líneas.
Pendientes restantes: KCL → Nickel, eliminar card.k.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3 steps en un commit:
A) yahweh-meta-runtime/backend.rs: trait MetaBackend con 6 métodos
(list_records, load_record, seed, update, delete, morphism) +
WriteOutcome { id, changed, post_status }. 9 tests con MemBackend.
B) nakui-ui/backend.rs: NakuiBackend struct con store/log/executors/
compaction. NakuiBackend::open() compone log+snapshot+replay+tick;
impl MetaBackend mapea cada método al pipeline nakui-core.
snapshot_path_for / maybe_compact_log se mueven acá. 7 tests del
impl.
C) MetaUi consume el backend:
- 6 fields colapsan en `backend: NakuiBackend`.
- MetaUi::new pasa de ~150 líneas a ~10 (delega a NakuiBackend::open).
- commit_seed / commit_morphism / commit_delete delegan al trait;
CommitOutcome enum eliminado, reemplazado por WriteOutcome.
- tick_runtime_compact eliminado (interno al backend; el msg sale
por WriteOutcome.post_status).
- validate_entity_refs callsite usa cierre sobre backend.load_record.
- Imports nakui_core::delta y event_log salen de main.rs (sólo
quedan en tests E2E).
Tests: 33→42 yahweh-meta-runtime (+9 trait), 14→21 nakui-ui (+7
backend impl). 97 totales en el área. Cada crate compila individualmente.
Pendiente Fase 2c: extraer widget render (form/list/modal/EntityRef)
al crate yahweh — ahora trivial porque el render solo consume
&self.modules + self.backend (via trait).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sigue de la Fase 1 (lift del schema). Ahora los helpers puros que
cualquier widget renderer o backend ejecutor consume sobre el schema
viven en yahweh-meta-runtime. Sin GPUI, sin nakui — usa cierres en
lugar de traits para decoupling máximo.
Crate nuevo crates/modules/ui_engine/libs/meta-runtime:
- parse.rs: parse_field_value, infer_param_value, resolve_param_value.
- delta.rs: compute_field_delta, compute_clear_fields.
- refs.rs: validate_entity_refs(load: F, refs) con cierre
Fn(&str, Uuid) -> Option<Value> en vez de trait Store.
- format.rs: human_label_for_record, render_value, value_to_input_text,
short_uuid.
- 33 tests propios.
nakui-ui:
- Nueva dep yahweh-meta-runtime.
- Borrado código local equivalente (~200 líneas) + 34 tests
duplicados.
- validate_entity_refs callsite usa cierre:
validate_entity_refs(|e, id| store.load(e, id), &refs).
- 14 tests runtime-específicos quedan (compact/snapshot/event-log/
morphism pipeline/load_ui_modules).
Distribución tests: 48 → 14 nakui-ui; +33 yahweh-meta-runtime.
Cada crate afectado builds + tests limpio individualmente. Workspace
build full no completó esta corrida por OOM al compilar
surrealdb-core (ambiental, no relacionado).
Fase 2b pendiente: extraer render widgets (form/list/modal/
EntityRef selector) a yahweh — requiere diseñar MetaBackend trait.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Primer paso del refactor yahweh. El schema de UI declarativa no
tiene acoplamiento real con Nakui (sólo dep en serde/thiserror) —
movemos a yahweh para que cualquier app metadata-driven lo use sin
pasar por nakui.
Mecánico:
- git mv crates/modules/nakui/ui-schema → crates/modules/ui_engine/libs/meta-schema.
- Crate name: nakui-ui-schema → yahweh-meta-schema.
- Workspace members[] actualizado (sección yahweh, no nakui).
- Consumers actualizados: brahman-cards (Cargo.toml + lib.rs +
readers.rs), nakui-ui (Cargo.toml + main.rs).
- Self-test (example_modules.rs): import + path rebase (5 niveles
arriba ahora).
Documental:
- Doc del crate ahora dice "metainterfaz (yahweh meta-schema)" +
"backend-agnostic" en filosofía.
- Module.nakui_module_dir documentado como "path opaco al backend";
se conserva el nombre por compat con módulos ya escritos +
serde alias "backend_module_dir" para futuro rename suave.
Tests: 13 yahweh-meta-schema + 26 brahman-cards + 48 nakui-ui
verdes. Workspace build verde.
NO hace Fase 1: mover widgets a yahweh (Fase 2), trait MetaBackend
(Fase 3), renombrar nakui_module_dir.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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 UiModule del CardBody.
Beneficios concretos:
- Soporta .ncl además de .json (templates Nickel + merge funcionan
en cualquier subdir de modules).
- Cards de otros body kinds (Ente/Monad) se skipean limpio con
toast informativo, no rompen la carga.
Cambios en brahman-cards:
- Nuevo load_cards_from_dir(dir) + variante con readers/filenames
custom. DEFAULT_CARD_FILENAMES = [card.ncl, card.json, module.ncl,
module.json] (orden de prioridad).
- 4 tests nuevos del helper.
Cambios en nakui-ui:
- Nueva dep brahman-cards.
- Helper load_ui_modules(dir) -> (Vec<Module>, Vec<String>) envuelve
el brazo, filtra a UiModule, aplica Module::validate(), detecta
duplicate ids.
- MetaUi::new usa el helper, emite toast con cards skipped si las hay.
- 3 tests e2e nuevos.
26/26 brahman-cards verdes (+4). 48/48 nakui-ui verdes (+3).
Workspace build verde.
nakui_ui_schema::load_modules_from_dir queda intacto (sus tests lo
usan + otros consumers futuros pueden preferirlo). Migración opt-in.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
parse_field_value(EntityRef) sólo validaba forma (UUID parseable).
Un UUID válido pero inexistente pasaba al log/store, dejando
dangling references. Ahora validamos también existencia contra la
entity declarada en FieldSpec.ref_entity.
- Nuevo helper validate_entity_refs<S: Store>(store, refs):
fail-fast loop sobre (label, target_entity, uuid) tuples; primer
record ausente → error con label legible + UUID corto.
- commit_seed: durante el parse loop encolamos cada EntityRef +
ref_entity + UUID parseado. Después del loop, una sola toma del
store lock valida todos. Falla early: ningún log entry.
- Cobertura: SEED + EDIT. Morphism inputs ya cubierto por
Executor::compute (load + EntityMissing) — documentado en el doc
del helper.
5 tests nuevos del helper: happy path, fail-fast con detalles,
label en msg, lista vacía, distingue target entity.
45 tests verdes en nakui-ui.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
El compact ya no es sólo en boot: en runtime, después de cada write
efectivo (commit_seed Created/Updated, commit_morphism, commit_delete),
incrementamos un contador en memoria y disparamos maybe_compact_log
cuando alcanza el threshold (mismo NAKUI_SNAPSHOT_THRESHOLD del
startup). El log no crece sin tope en sesiones largas.
- Nuevos fields en MetaUi: snap_path, snapshot_threshold,
writes_since_compact (los dos primeros cacheados en new()).
- Nuevo método tick_runtime_compact: increment + check + maybe
compact + reset. Si compact falla, counter NO se resetea (próximo
write reintenta). Threshold 0 desactiva.
- Helper append_compact_msg concatena el msg de compact al toast
del op original con "; " separator.
- Wireado en los 3 callsites de write. NoChange (edit sin cambios)
no cuenta — preserva "1 write = 1 log entry = 1 tick".
2 tests nuevos: format del helper, ciclo de 7 writes con
threshold=3 verifica 2 compacts + counter residual + log final con
1 anchor + 1 write.
37 tests verdes (+2). Workspace build verde.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`capture_key_down` en el root div: si event.keystroke.key=="escape" y
hay pending_delete, lo limpia y emite toast "delete cancelado (X)
[esc]". Capture phase (no bubble) intercepta el Esc antes de que
cualquier TextInput descendiente lo consuma. Sin pending el handler
es no-op, el evento sigue su flujo.
Hint visual en el banner: subtítulo amber tenue
"Esc para cancelar · click [Confirmar] para borrar" para que el
usuario descubra el atajo sin RTFM.
35 tests verdes. El handler son 8 líneas no-testeables sin GPUI cx;
type-check garantiza wireup.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Antes: parse_field_value(EntityRef, raw) devolvía Ok(json!(raw))
blindly. Garbage entraba al log/store si el field se usaba como
seed o param (sólo el path de morphism inputs validaba UUID).
Ahora: Uuid::parse_str(raw.trim()) → error claro si falla, value
trimmed si pasa. El selector clickable garantiza happy path; este
check es defensivo contra paste manual o garbage tipeado.
5 tests nuevos: happy path, trim de whitespace, rechazo de garbage,
rechazo de empty, propagación de error a resolve_param_value con
label del FieldSpec en el mensaje.
35 tests verdes. E2E del morphism real intacto (sus inputs van por
path dedicado, no por parse_field_value).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wirea el snapshot machinery existente en nakui-core (Snapshot,
replay_with_snapshot_into, EventLog::compact_through) al runtime de
la UI. Reduce el costo de boot de O(events) a O(events_post_snapshot).
- Path: sibling del log, extensión `.snap.json`.
- Threshold via `NAKUI_SNAPSHOT_THRESHOLD` env (default 50; 0 = off).
- Helper `maybe_compact_log` captura snapshot + compacta dejando la
última entry como anchor del cursor (sin anchor, EventLog::open
vería log vacío y perdería next_seq).
- WAL order: write snap antes de compactar; un crash entre los dos
da el mismo outcome al próximo boot (replay skipea entries
cubiertas por snap).
- Fail-soft: snap load/compact errors no son fatales, sólo banner.
5 tests nuevos: derivación del path, dos no-ops bajo threshold, E2E
60 entries → snapshot+compact → reopen+replay → 60 records intactos.
31 tests verdes en nakui-ui. Workspace build verde.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Antes: editar un record emitía Set por *todos* los fields del form,
incluso los no tocados. Bloataba el log y oscurecía el intent. Ahora:
- Nuevo helper `compute_field_delta(current, proposed)` — devuelve
sólo las entries que difieren (PartialEq de Value).
- Nuevo enum `CommitOutcome { Created, Updated{changed}, NoChange }`
para que el toast sea preciso ("actualizado X (2 campo(s))" vs
"sin cambios — no log entry").
- `commit_seed` en path EDIT carga current del store, calcula delta,
return early si vacío (no log entry, no apply). Si no vacío emite
`Morphism{ ui.edit_record, params.fields=delta }` con sólo los
campos modificados.
5 tests nuevos del helper: delta vacío, sólo campo cambiado, current
Null = todo nuevo, int vs string, ignora fields ausentes del proposed.
27 tests verdes. SEED path inalterado, E2E del morphism real verde.
Limitación: edit no puede *clearear* un value vaciando el input
(empty optional fields ya hacían `continue` antes del delta). Para
soportar eso haría falta `FieldOp::Clear`, no necesario hoy.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
El click en `✕` ya no borra inmediatamente: marca el record como
pendiente y abre un banner amber al tope con [Cancelar] / [Confirmar].
Sólo [Confirmar] llama `commit_delete` (la lógica del store/log no
cambia).
Cambios:
- Nuevo `MetaUi.pending_delete: Option<(String, Uuid)>`.
- Click en ✕ → set pending_delete + clear toast.
- Banner renderea como sibling del row sidebar+main en `flex_col`
raíz; None cuando no hay pending. Texto: "¿Borrar {Entity} {id}?".
- [Cancelar] → toast informativo, sin tocar el store.
- [Confirmar] → limpia pending primero (evita banner colgado en
caso de error) y llama `commit_delete`.
- `select_view` también limpia pending (record podría no ser visible
en la nueva view).
22 tests verdes — la lógica del store/log no cambió, sólo el state
machine de UI. La compilación garantiza el wireup de las closures.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reemplaza la heurística `infer_param_value` por parseo estricto basado
en el `FieldKind` declarado en el `FieldSpec` correspondiente. Un Boolean
con value "abc" ahora rebota en la UI con mensaje claro en lugar de
fallar opacamente dentro del morphism Rhai.
Cambios:
- Nuevo helper `resolve_param_value(field_name, raw, spec)`:
- Required + empty → error con label legible.
- Optional + empty → Value::Null.
- Spec presente → parse_field_value(spec.kind, raw) estricto.
- Spec ausente (módulo mal-formado) → fallback a infer_param_value.
- `commit_morphism` simplificado: el loop de params ahora delega al
helper, que es testable sin GPUI.
- 6 tests nuevos cubriendo: número estricto, boolean rechaza "abc",
required vacío, optional vacío → null, fallback a infer sin spec.
22 tests verdes (+6 nuevos). E2E del morphism real
`morphism_pipeline_executes_real_sales_vender` sigue verde — la
validación estricta no afecta el path correcto, sólo agrega rebotes
tempranos a values mal-tipados.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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:
- Nueva variante FieldKind::EntityRef (serializa como "entity_ref").
- FieldSpec.ref_entity: Option<String> nuevo. validate() chequea que
cualquier field con kind=entity_ref tenga ref_entity set.
- Nuevo SchemaError::EntityRefMissingTarget.
Runtime:
- render_entity_ref_selector helper: lista clickable debajo del input,
cada item con etiqueta humana (heuristica: name > label > title >
sku > sku_id > UUID corto) y click handler via cx.listener que
setea el TextInput con el UUID completo. Highlight en accent color
para el seleccionado.
- parse_field_value(EntityRef) devuelve string raw — validacion como
Uuid es responsabilidad de commit_morphism downstream.
- Mensaje "(sin {entity}: crea uno antes...)" cuando lista vacia —
el user sabe que hacer.
Demo actualizado sales_engine: vender_form.stock_id_input y
caja_id_input cambian a kind=entity_ref. Flujo nuevo: click en Stock
listado bajo input, click en Caja, escribir venta_id/cantidad/precio/
timestamp, submit. Sin copiar UUIDs.
Tests: 2 nuevos schema (validate detecta EntityRef sin ref_entity y
acepta con ref_entity) + 4 nuevos runtime (parse, human_label cubre
todos los key fallbacks). 29 tests totales (16 + 8 + 5).
Pendientes: confirmacion de delete, snapshot/compaction del log,
edit delta-only, validacion estricta de params del morphism via
FieldKind del FieldSpec en lugar de infer_param_value.
Cierra el ultimo 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: compute (con
dry-run + KCL post-checks) -> log append -> store apply.
Schema nakui-ui-schema extendido:
- Module.nakui_module_dir: Option<String> nuevo. Path al modulo
nakui-core. Sin esto, Action::Morphism quedan no-op con toast.
SeedEntity sigue funcionando (alta administrativa sin manifest).
- Action::Morphism gano dos campos opcionales:
- inputs: BTreeMap<String, String> — mapeo role -> field_name.
- params: Vec<String> — fields cuyos values van al params JSON.
Si vacio, 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.
- commit_morphism: resuelve inputs (parsea UUIDs), arma params
(Value object con tipos inferidos), llama
execute_and_log_with_recovery. Toast con count de ops o error.
- infer_param_value: heuristica i64 -> f64 -> bool -> string.
Tests: 2 nuevos. E2E morphism_pipeline_executes_real_sales_vender
carga el modulo real crates/modules/nakui/modules/sales, ejecuta
"vender" con inputs Stock+Caja y params (cantidad=5, precio=200,
venta_id, timestamp). Asserta:
- el morphism produce ops (no vacio).
- stock.cantidad: 100 -> 95.
- caja.saldo: 1_000_000 -> 1_001_000.
12 tests verdes en nakui-ui (+1). Schema extension no rompio nada
(6 unit + 5 integration siguen verdes).
Demo nuevo: examples/nakui-modules/sales_engine/module.json apunta
al sales real via nakui_module_dir. 6 vistas (list+form para Stock/
Caja/Venta + "Vender" con Action::Morphism). El user crea Stocks +
Cajas con seed_entity, copia los UUIDs a los inputs de "Vender", y
ejecuta el morphism real con KCL post-checks.
Activacion:
NAKUI_EVENT_LOG=~/.nakui/state.jsonl \\
NAKUI_MODULES_DIR=examples/nakui-modules \\
cargo run -p nakui-ui
Trade-offs:
- Inputs UUID a mano (no dropdown). Nice-to-have: FieldKind::EntityRef
que renderee selector.
- Inferencia de tipo en params es heuristica.
Cierra "no hay UI para editar/borrar" 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, asi el replay restaura el estado
correcto.
Cambios:
- MetaUi.editing: Option<(String, Uuid)> nuevo. Set al click ✎,
cleared al cambiar de view o tras submit.
- open_edit(mod_idx, entity, id, cx): setea editing, busca primer
Form view del modulo cuya entity matchee, navega ahi.
- select_view extendido: cuando carga un Form, si editing matchea y
el record existe, pre-llena cada input con value_to_input_text del
record (inverso de parse_field_value).
- commit_seed ramifica:
- Edit path: emite LogEntry::Morphism { name: "ui.edit_record",
ops: [Set per field] }. Aplica via store.apply.
- Seed path: alta nueva (comportamiento previo).
- commit_delete(entity, id): emite LogEntry::Morphism { name:
"ui.delete_record", ops: [Delete] } + apply.
- Render del form: titulo y submit label cambian segun editing
("Editar customer abc..." / "Guardar cambios").
- Render de la lista: dos columnas nuevas — id, acciones. Cada fila
con ✎ (edit, accent) y ✕ (delete, rojo) + hover states.
Coherencia con el modelo: todo cambio post-seed pasa por ops dentro
de Morphism. nakui-explorer muestra estos morphisms con sus ops en
la timeline.
Trade-offs:
- schema_hash: None sigue (legacy path) hasta Action::Morphism
wireé Manifest.
- Delete sin confirmacion (1 click).
- Edit sobreescribe todos los campos del form (no delta-only).
Tests: 3 nuevos. 10 totales:
- value_to_input_text_inverse_of_parse + round_trip — la propiedad
del pre-llenado.
- event_log_replay_handles_full_crud_cycle — E2E: seed + edit +
delete via log, replay desde cero deja store vacio. Replay parcial
deja valores editados.
Activacion:
NAKUI_EVENT_LOG=~/.nakui/state.jsonl \\
NAKUI_MODULES_DIR=examples/nakui-modules \\
cargo run -p nakui-ui
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 commit_seed pueda mutar.
- Apertura + replay al startup: path por env NAKUI_EVENT_LOG, default
./nakui-ui-state.jsonl. EventLog::open + replay_into reconstruyen
el store. Toast: "log nuevo" o "log X cargado: N evento(s)
replayed".
- WAL en commit_seed: log.append(LogEntry::Seed { ..., schema_hash:
None }) antes de store.seed. Si append falla, cancela operacion.
- schema_hash: None es el path "legacy / pre-versioning" documentado
para seeds que no pasan por Manifest+Executor. Correcto para alta
via metainterfaz hasta que Action::Morphism wire el Manifest.
- Degradacion gracil: si abrir log falla -> toast error + sigue
in-memory.
Tests: 1 nuevo E2E event_log_replay_restores_memory_store que escribe
2 seeds via EventLog::append, re-abre + replay_into store fresh,
verifica records con values correctos. 7 tests verdes en nakui-ui.
Activacion con persistencia:
NAKUI_EVENT_LOG=~/.nakui/state.jsonl \\
NAKUI_MODULES_DIR=examples/nakui-modules \\
cargo run -p nakui-ui
Pendientes:
- Action::Morphism (cargar Manifest junto a Module).
- Snapshot/compaction para logs grandes.
- UI para editar/borrar records existentes (hoy solo alta).
- Widget input simple sin selection/IME/clipboard.
Cierra dos limitaciones documentadas del commit anterior: los
formularios ahora aceptan teclado real, y los clicks en menus +
botones mutan estado correctamente.
Cambios:
- Cada FieldSpec del Form materializa un Entity<TextInput> de
yahweh-widget-text-input al entrar a la vista. Los entities se
reemplazan al cambiar (drop limpio). Soporta: escribir caracteres,
Backspace, Enter (Confirmed event no usado todavia), Escape.
Cursor renderea como "|" al final.
- Click handlers wired via cx.listener: menus invocan select_view,
botones invocan apply_action. Tienen acceso real al
Context<MetaUi> y mutan el modelo + cx.notify.
- commit_seed reemplaza el buffer ad-hoc por
input.read(cx).text() por cada field. El value parseado va al
MemoryStore con tipo correcto.
- Reset de inputs tras submit (set_text("")) si no hay next_view —
flujo de alta consecutiva sin re-tipear.
- Hover states en sidebar y botones.
- Theme::install_default(cx) al inicio (requerido por text_input).
Wire: deps nuevas yahweh-widget-text-input + yahweh-theme.
Limitaciones que siguen:
- Action::Morphism: requiere cargar Manifest de nakui-core.
- Sin persistencia entre runs (wire con EventLog cuando daemon Nakui
exista).
- Widget input es simple (sin cursor positioning, selection, IME,
multilinea, copy/paste).
- Enter no envia (TextInputEvent::Confirmed no suscrito; submit va
por click). Trivial de wirear si se necesita.
Tests: 6 unit verdes. Visual requiere cargo run + manual.
Activacion:
NAKUI_MODULES_DIR=examples/nakui-modules cargo run -p nakui-ui
Salto cualitativo: Nakui pasa de "library + demos + read-only viewer
del event log" a plataforma ERP con UI dirigida por datos. Cada
modulo de negocio se declara como un module.json (sin codigo Rust
nuevo) y el runtime GPUI lo carga dinamicamente: sidebar de menus,
listas con columnas configurables, formularios de alta.
3 entregables:
1. Crate nakui-ui-schema (datos puros): Module, View::List/Form,
FieldSpec con FieldKind {Text|Multiline|Number|Boolean|Date},
Action {OpenView|SeedEntity|Morphism}. Module::from_path,
Module::validate, load_modules_from_dir(dir). 6 tests unit + 4
integration.
2. Crate nakui-ui (binario GPUI): carga modulos desde
NAKUI_MODULES_DIR. Sidebar + main panel. List view con tabla
weighted; form view con campos labeled + submit que ejecuta
SeedEntity contra MemoryStore in-process compartido. Toast +
error banner. 6 tests unit.
3. 6 modulos demo en examples/nakui-modules/:
- customers (nombre, email, telefono, credito, notas)
- products (SKU, nombre, categoria, precio, stock)
- suppliers (razon social, ID fiscal, contacto, terminos pago)
- inventory_movements (fecha, tipo, SKU, cantidad, costo, motivo)
- sales_orders (numero, cliente, fechas, estado, totales)
- invoices (numero, cliente, fechas, totales, pagado, moneda)
Filosofia: UI como datos. Persistencia universal (MemoryStore hoy,
SurrealStore manana, sin tocar module.json). Schema primero, semantica
despues.
Activacion:
NAKUI_MODULES_DIR=examples/nakui-modules cargo run -p nakui-ui
Limitaciones conocidas (proximos iters):
- Inputs sin teclado (GPUI no lo trae nativo; integrar
yahweh-widget-text-input).
- Click handlers no propagan mutacion al estado (refactor con
cx.listener pendiente).
- Action::Morphism queda como TODO hasta cargar Manifest junto al
Module.
- Sin persistencia entre runs (wire con EventLog/SurrealStore para
cuando el daemon Nakui exista).
Tests: 16 totales nuevos. Lo que esto desbloquea: cualquiera puede
escribir un module.json para su dominio (pacientes, alumnos,
reservaciones) y aparece en la UI sin recompilar.
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
parametros, breakdown por entity type, polling cada 2s para detectar
nuevos eventos appended sin restart.
Diseno: lee directamente el archivo .jsonl del nakui_core::event_log::
EventLog. Path por env NAKUI_EVENT_LOG, default "nakui.jsonl" en pwd.
Sin discovery via broker brahman porque nakui hoy es CLI/library/demos,
no daemon. Cuando se daemonice, sustituir el lector de archivo por
un sidecar consumer (mismo patron que nouser-explorer).
UI:
- Header: path, count total + breakdown seeds/morphisms, reload time.
- Breakdown line: top 5 buckets por frecuencia (entities + morphisms).
- Timeline: tarjetas color-coded por kind (azul=seed, verde=morphism)
con #seq, kind, entity/morphism, id corto, preview de data/params,
schema hash o "(legacy)". Mas-recientes-primero, hasta 200 visibles.
- Error banner si lectura falla; el explorer no crashea, sigue
intentando cada 2s.
Wire: nuevo crates/apps/nakui-explorer/ agregado al workspace. Deps
minimas: nakui-core, gpui, serde_json, uuid (feature serde). Sin
deps de brahman (Nakui standalone).
Tests: 7 unitarios cubriendo load_log (in-order, missing-file),
breakdown (counts + buckets), preview_value (truncate + intact),
short_uuid + short_hash.
Activacion:
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 unico que queda es minga-vfs (FUSE,
explicitamente diferido) y mejoras nice-to-have (cobertura adicional
per-lenguaje, daemon-izacion de nakui para sidecar discovery).
Cierra el unico debt estructural detectado en el audit de
independencia: nouser-explorer ya no arrastra nouser-core (que
aportaba notify/walkdir/sled/blake3 al grafo de compilacion de una
UI que solo habla JSON contra un socket).
- Cliente movido: engine_socket::client::list_monads (~60 LOC, std +
serde_json puros) emigra de nouser_core::engine_socket a
nouser_card::query::client. Vive donde viven los wire types,
consistente con el principio "un consumer importa el contrato, no
el runtime del productor".
- Drop dep: nouser-explorer deja de depender de nouser-core.
Verificado con cargo tree: notify, sled, blake3 desaparecen del
grafo del binario.
- Fallback "falla hacia la simplicidad": nueva resolve_socket() en
el explorer intenta primero broker discovery; si el broker no
responde / no hay init vivo, fallback directo al
default_socket_path. El explorer queda funcional contra un daemon
huerfano (standalone sin init) — completa "consciente cuando hay
ecosistema, soberano cuando esta solo".
- socket_source gana tercer estado "default-path" para visibilidad.
Audit estructural confirmo que el resto del ecosistema ya respeta
el principio. Brahman es pegamento opcional, no chasis obligatorio
— y ahora el grafo de Cargo lo enforcea, no solo la convencion.
Tests: 4 + 10 + 27 verdes. Cliente movido ejercitado end-to-end
por los 3 tests integracion de engine_socket.
Cierra el "explorer encuentra al daemon de forma totalmente dinamica"
del meta-plan. La UI deja de hardcodear el socket admin: descubre al
daemon nouser via MatchEvent::Available del broker y le consulta sus
Monadas directo.
Pipeline end-to-end:
- Daemon publica engine Card con service_socket = $XDG_RUNTIME_DIR/
nouser-engine.sock y flow.output = monad-list:json.
- Daemon binda Unix socket en ese path con listener blocking que
sirve nouser_card::query::QueryRequest::ListMonads, responde
ListMonadsResponse { engine, monads: Vec<MonadView> }.
- Explorer construye consumer Card con flow.input matched,
brahman_sidecar::await_provider_blocking le devuelve el socket,
y nouser_core::engine_socket::client::list_monads lo consulta.
- Cachea el socket; cualquier fallo de query lo invalida y la
proxima iteracion re-descubre.
Wire types nuevos en nouser_card::query:
- QueryRequest::ListMonads
- ListMonadsResponse { engine: EngineInfo, monads: Vec<MonadView> }
- MonadView: proyeccion slim de MonadManifest SIN centroid ni
members (KB que no tienen por que viajar cada poll).
- transport::default_socket_path() con env override.
Listener en nouser_core::engine_socket: spawn_listener + client
blocking con QueryError tipado. 3 tests integracion verdes.
Refactor explorer:
- Drop dep brahman-admin, add brahman-sidecar/nouser-card/nouser-core.
- State: socket cache + snapshot + socket_source informativo.
- TickOutcome enum desacopla la I/O del UI.
Trade-offs: polling 2s (no streaming — broker no empuja Data cards
hoy), re-discovery full en error (discovery es barato).
Tests: 10 (nouser-card +3 query) + 27 (nouser-core +3 engine_socket)
+ 4 (sidecar) verdes. Explorer compila clean.
Bin GPUI standalone que consulta brahman-admin cada 2s y renderea
todas las sesiones del Init como cards. Cierra el círculo visual
del ecosistema brahman.
- Crate nuevo crates/apps/nouser-explorer (deps: brahman-admin,
brahman-card, gpui).
- Ventana 900x640 con header del estado del Init, banner de error
cuando no conecta, y lista de cards (una por sesión).
- Cada card muestra: kind + label + lifecycle, ULID corto, summary
(si data), keywords, lens hint, service_socket si está, y refs
(RelationshipKind → target_label). El borde izquierdo coloreado
diferencia ente (azul) de data (lavanda).
- cx.spawn(async move |this, cx| { ... }) corre el loop de refresh
en el GPUI executor; query_blocking porque GPUI no tiene runtime
tokio.
- Helper nuevo en brahman-admin: client::query_blocking(path) —
versión sync de query(), para callers con su propio executor.
Uso:
$ ente-zero & nouser daemon crates/core &
$ cargo run -p nouser-explorer
# ventana 900x640, ~6 cards en vivo, refrescando cada 2s.
cargo check --workspace: 0 errores, 0 warnings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dos cosas en una sesión, en el orden discutido:
(1) Segundo módulo brahman vivo: nakui-core
- crates/modules/nakui/core/Cargo.toml: deps brahman-card,
brahman-sidecar, ulid.
- crates/modules/nakui/core/src/bin/nakui.rs: brahman_card_for_nakui()
construye una Card como Lifecycle::Daemon, Supervision::Restart,
flow.input "command" (json) + flow.output "report" (json). El
cmd_run llama brahman_sidecar::spawn antes de levantar el server
de nakui.
(2) crates/shared/brahman-sidecar (estrena crates/shared/)
Boilerplate del sidecar extraído (DRY): el thread con tokio current
thread runtime, conexión vía Client::connect, ping loop. Yahweh y
nakui ahora consumen este crate. API:
- spawn(card) fire-and-forget
- spawn_with_handle(config) con JoinHandle
Example "presence" útil para demos: módulo dummy con label tomado
del primer arg que se queda vivo hasta SIGTERM.
(3) crates/core/brahman-admin: observabilidad del broker
Socket Unix paralelo en \$BRAHMAN_ADMIN_SOCKET (default
\$XDG_RUNTIME_DIR/brahman-admin.sock). Cada conexión recibe un
StatusSnapshot JSON line-delimited y se cierra. Compatible con nc/socat.
- StatusSnapshot { server, protocol, init_attached, sessions, matches }
- server::AdminServer
- client::query(path)
- example "brahman-status" CLI
(4) Wiring de ente-zero
En primordial_loop, junto al handshake server, ahora también levanta
AdminServer con misma política de degradación grácil.
(5) brahman-broker: BrokeredCard ahora incluye lifecycle. Endpoint y
Match derivan Serialize/Deserialize. Nuevo método cards() expone
iterador de BrokeredCard para que el admin pueda construir snapshots.
(6) brahman-card: re-export pub use ulid::* para que módulos no
necesiten depender de ulid directamente.
(7) yahweh-shell migrado al sidecar compartido. Su brahman_client.rs
pasa de 96 a 53 líneas: sólo declara la Card, delega el spawn.
Demo end-to-end:
$ ente-zero &
$ presence demo.producer &
$ presence demo.consumer &
$ brahman-status
Init: server=0.1.0 protocol=0.1.0 attached=true
Sessions (2):
01KR42TY1J... demo.producer lifecycle=Daemon priority=Normal
01KR42TY1K... demo.consumer lifecycle=Daemon priority=Normal
Matches (2):
demo.producer.in ← demo.consumer.out via Exact
demo.consumer.in ← demo.producer.out via Exact
El broker matchea bidireccional por tipo. El admin lo expone.
Tests: 27/27. cargo check --workspace: 0 errores.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
yahweh-shell se presenta al Init brahman como módulo Widget mediante un
sidecar en thread aparte. La GUI GPUI levanta normalmente; el sidecar
mantiene la sesión brahman en paralelo, desacoplado.
Cambios:
- crates/apps/yahweh-shell/Cargo.toml: deps brahman-card, brahman-handshake,
ulid.
- crates/apps/yahweh-shell/src/brahman_client.rs: thread con tokio
current_thread runtime que arma una Card, llama Client::connect, y
loop de pings cada 30s. Si el Init no está disponible, loggea y
termina — yahweh sigue funcionando standalone.
- crates/apps/yahweh-shell/src/main.rs: brahman_client::spawn() antes
de Application::new(). El spawn no bloquea.
Card declarada por yahweh:
- label: "brahman.ui_engine"
- lifecycle: Widget
- payload: Virtual (yahweh no se inicia desde el Init, se presenta)
- supervision: Delegate
- permissions: filesystem read-write (persiste layout.json), IPC wit-v1
- flow.input: render-data (json)
- flow.output: user-intent (json)
Validación end-to-end:
$ ente-zero &
$ probe → session=...8G, init_attached=true
$ yahweh → [brahman] attached: session=...Y7
Ambos clientes (probe + yahweh sidecar) se registran en el broker del
Init en sesiones distintas. yahweh es el primer módulo "real" — no un
tester — que vive como nodo del fractal mientras corre.
Tests: 27/27 verdes. cargo check --workspace: 0 errores.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>