Commit Graph

161 Commits

Author SHA1 Message Date
sergio 3486949d24 feat(shipote): throughput + stats persistente + auth peer (fase P)
- FlowMeter (atomic u64 + rolling window 32 samples) en cada FlowChannel.
  flow_throughput() → (socket, bytes_total, bytes_per_sec). CLI:
  shipote flow throughput. Idle threshold 5s = rate 0.0.
- Snapshot v4 con stats_history persistente por workspace (cap 16).
  PersistedStats separado para evitar Instant. Restore hidrata el VecDeque
  con source="persisted".
- Auth SO_PEERCRED: daemon rechaza peers con uid distinto al propio.
  SHIPOTE_TRUST_ANYONE=1 = escape hatch documentado.

84 tests pasan (ente-incarnate 16, nouser-core 27, shipote-card 8,
shipote-core 25, shipote-discern 5, yahweh-provider-fs 3).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 13:58:41 +00:00
sergio 1cce50b290 feat(shipote): collision detection + stats history server-side (fase O)
- Flow socket names usan pipeline_id full (ULID 26 chars) + edge_idx.
  Cero colisiones entre pipelines (ULID es único global). Fallback con
  suffix -N si el path existe (cap 1000 retries).
- WorkspaceState.stats_history (VecDeque cap 64) — workspace_stats
  appendea cada call. API workspace_stats_history(id, tail). Protocol
  WorkspaceStatsHistory. Shell pide history al primer probe → sparkline
  hidratada al boot, sobrevive restart del shell.

84 tests pasan (ente-incarnate 16, nouser-core 27, shipote-card 8,
shipote-core 25, shipote-discern 5, yahweh-provider-fs 3).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 10:55:21 +00:00
sergio a823c40fe1 feat(shipote): drain shutdown + persist live pipelines + batched query (fase N)
- Daemon SIGTERM/SIGINT: snapshot ANTES, stop_with_grace(1s) de todos
  los workspaces DESPUÉS. Grace permite app-level cleanup.
- Snapshot v3 con live_pipelines: pipeline_supervisors se persisten;
  daemon relanza al restore con sus recursos (Incarnator+DiscernPipeline).
  RestoreOutcome separado para que core no necesite incarnator.
  Forward-compat v1/v2 via #[serde(default)].
- WorkspaceFullSummary: stats+quota+commands+flow_sockets en 1 roundtrip.
  Shell reduce N×4 requests/probe a N×1 + 4 globales.

83 tests pasan (ente-incarnate 16, nouser-core 27, shipote-card 8,
shipote-core 24, shipote-discern 5, yahweh-provider-fs 3).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 10:48:11 +00:00
sergio c3f9c9e36a feat(shipote): pipeline backoff + quota card + logs follow (fase M)
- PipelineSpec.restart_backoff_ms + restart_max_backoff_ms + restart_max:
  backoff exponencial entre relaunches (anti-thrash). take_pending_restarts
  aplica restart_max (0 = infinito); excedido = supervisor descartado con
  warning. Daemon hace tokio::sleep(backoff) antes del relaunch y escala
  current_backoff x2 hasta el cap.
- shipote-shell card "Quota breaches": probe extiende con WorkspaceQuota
  por workspace. Color rojo si hay breaches, verde si no.
- shipote logs --follow: poll cada 200ms al daemon, imprime suffix nuevo
  hasta que el comando termine. Sin cambios al protocolo. Best-effort:
  si el ring rota más rápido que el poll, se pierden bytes.

83 tests pasan (ente-incarnate 16, nouser-core 27, shipote-card 8,
shipote-core 24, shipote-discern 5, yahweh-provider-fs 3).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 10:34:27 +00:00
sergio 4c9d1b4c1d feat(shipote): quota enforce + cgroup memory.max + pipeline restart (fase L)
- WorkspaceSpec.quota_enforce: QuotaAction (None|Log|Kill) por recurso
  (mem, nproc). reap_dead aplica policy; Kill usa stop_with_grace(ZERO).
- ente_incarnate::cgroup::apply_rlimits_to_cgroup escribe memory.max y
  pids.max. WorkspaceManager::create_with_id lo invoca si soma.cgroup.path
  y delegation. Kernel hace OOM kill al exceder; falla silenciosa si no
  hay delegation.
- PipelineSpec.restart_on_failure: bool. register_pipeline_supervisor
  retiene spec; reap_dead detecta all-dead + any-failed → push a queue;
  daemon reaper drena y relanza pipeline ENTERO (los pipes intermedios
  no permiten restart parcial).

82 tests pasan (ente-incarnate 16, nouser-core 27, shipote-card 8,
shipote-core 24, shipote-discern 5, yahweh-provider-fs 3).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 10:22:46 +00:00
sergio 324a0c2d5d feat(shipote): multi-core CPU% + quota report + restart-on-failure (fase K)
- WorkspaceStats.cpu_cores via sysconf cacheado. CLI muestra
  `cpu_pct: 98.7 % (24.7% total / 4 cores)`.
- workspace_quota compara SomaSpec.rlimits contra accounting actual.
  Reporta breaches humanos. NO enforcement automático en v1.
- run_with_options(.., restart_on_failure): si exit != 0, reaper
  relaunch con backoff exponencial 200ms → 30s cap. Inner.restart_specs
  persiste el spec entre intentos.

81 tests pasan (ente-incarnate 16, nouser-core 27, shipote-card 8,
shipote-core 22, shipote-discern 5, yahweh-provider-fs 3).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 01:32:39 +00:00
sergio d8727a3038 feat(shipote): CPU% + pipeline live-tail + replay por bytes (fase J)
- CPU% derivado server-side entre samples (WorkspaceState.last_cpu_sample).
  100% = 1 core saturado. Primer sample devuelve None (sin baseline).
- shipote pipeline run --tail: tras lanzar, suscribe al primer flow_socket
  y vuelca bytes hasta EOF. Auto-implica --tap.
- DiscernPolicy.replay_bytes: cap adicional por bytes para el replay
  buffer del FlowChannel. evict_for_incoming considera el chunk entrante
  para que post-push el buffer NUNCA exceda los caps.
- shipote-shell: stats history extiende sparkline con %CPU.

80 tests pasan (ente-incarnate 16, nouser-core 27, shipote-card 8,
shipote-core 21, shipote-discern 5, yahweh-provider-fs 3).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 00:57:04 +00:00
sergio 36dac00c8d feat(shipote): data plane + DAG fan-in/out + stats + lifecycle (fases F-I)
Pipeline runtime:
- Fan-out 1→N (splitter task replica al N consumers) y fan-in N→1 (merger
  task con mpsc + reader-per-input). DAGs no lineales soportados.
- Flow channels: Unix socket + tokio broadcast con replay buffer
  configurable por pipeline (DiscernPolicy.replay_chunks). Subscribers
  externos vía `shipote flow tail <socket>`.
- Templating en specs con `${KEY}` (CLI `--var KEY=VALUE`). Walk
  recursivo sobre serde_json::Value, soporta todos los strings del schema.
- Pipelines guardados (`pipeline save/saved-list/drop/run-saved`)
  persisten con el snapshot.

Lifecycle de comandos:
- Log capture per-stream (stdout/stderr separados) via pipe O_CLOEXEC +
  AsyncFd. CLI `shipote logs <ws> <cmd> --stream {stdout,stderr,both}`.
- Stop graceful con tiempo configurable: SIGTERM → grace → SIGKILL.
  Tanto a nivel workspace como pipeline individual.
- TTL auto-stop ya existente (Fase C) sigue funcionando.

ente-incarnate:
- ChildStdio declarativo (Fase C) + ChildPreExec declarativo nuevo:
  NoNewPrivs, ParentDeathSig, Dumpable, NewSession, Chdir, Umask.
- Aplicación pre-execve async-signal-safe en ambos paths (plain via
  Command::pre_exec, namespaced via callback del clone(2)).

Observabilidad:
- WorkspaceStats: RSS + RSS peak (VmHWM o memory.peak cgroup) + CPU usec
  + uptime. Fuente per-proc o cgroup según delegation.
- shipote-shell con sparkline ASCII por workspace (history cap 24),
  card de flow channels activos, vista de comandos + saved pipelines.
- Tap → broker: cada edge enriquecido con TypeRef se anuncia como Card
  efímera vía SidecarPool (graceful si broker no corre).

Discern:
- Integrado en yahweh-provider-fs (mime_type en EntityNode).
- Integrado en nouser-core::cluster::pick_lens como fallback cuando la
  extensión cae a Lens::Grid.

79 tests pasan: ente-incarnate (16), nouser-core (27), shipote-card (8),
shipote-core (20), shipote-discern (5), yahweh-provider-fs (3).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 00:29:46 +00:00
sergio c22d2480b9 shell 2026-05-10 21:58:16 +00:00
Sergio 37e40073ef feat(yahweh-launcher): F3 — extracción del shell standard de explorers
Iter 19. Patrón con 4 consumers idénticos: cada main() repetía el mismo
~20 líneas de boot (Application::new + Theme::install_default +
cx.open_window + WindowOptions + cx.activate). Sólo varían título,
tamaño y root factory.

crates/modules/ui_engine/libs/launcher/:
- pub fn launch_app(title, size, root_factory) → 1-line boot.
- pub fn launch_app_with(config, root_factory) → variante con config
  armado afuera (env-var driven, etc).
- pub struct AppLaunchConfig::new(title, size).
- 2 tests cubren normalización del config.

Migración 4 consumers (nakui/nouser/minga/brahman-broker explorer):
- main() pasa de ~20 líneas a 1: launch_app(...).
- Imports gpui podados (no más App/Application/Bounds/WindowOpts/etc).
- Cada uno agrega dep yahweh-launcher.

Naming: yahweh-shell ya existe (bootstrap heavyweight con file/db/text
viewers en crates/apps/). Helper liviano queda como yahweh-launcher.

Ahorro ~75 líneas de boot hardcoded. Cambios de window/theme boot
ahora en un solo lugar.

2/2 tests launcher; 4 consumer suites intactas, todo verde.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:19:17 +00:00
Sergio 7fb2ad3b1e fix(nakui-core): schema_bundle_hash usa bytes reales del schema, no del bundle
Iter 17. Regresión surfaceada por verify_log_rejects_seed_after_schema_kcl_changes.

Bug: compute_schema_bundle_hash operaba sobre los bytes del bundle
compilado, que es `(import "/abs/path") & ...`. Esos bytes no cambian
cuando se edita el archivo apuntado; sólo cambian si se mueve el
módulo o se agregan/quitan schemas. El hash quedaba pegado y un seed
firmado con schema vN se verificaba ok contra schema vN+1.

Fix: nueva fn read_schema_files_concat que lee cada schema declarado
y los concatena con framing `\0NCL:<name>\0`. Esos bytes alimentan
los dos hashers (schema_bundle_hash y morphism_schema_hash). El bundle
compilado sigue siendo imports-style (Nickel necesita los paths para
resolver), sólo la fuente del hash cambia.

Impacto: logs versados con el binario anterior fallan SchemaMismatch
al verificarse — comportamiento correcto (re-seed).

Test renombrado: verify_log_rejects_seed_after_schema_kcl_changes →
_after_schema_changes (residuo de la migración KCL→Nickel).

10/10 schema_versioning verde.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:10:48 +00:00
Sergio be3a0e78fc feat(yahweh-widget-app-header): promover header standard de explorers
Iter 16. Patrón con 4 consumers idénticos: nakui-explorer,
nouser-explorer, minga-explorer, brahman-broker-explorer todos
declaraban un header flex_row + flex_grow(label) + theme_switcher
+ bg(panel) + border-bottom + text_size(14) + padding(16/12).
Ahora es 1 línea.

crates/modules/ui_engine/widgets/app-header/:
- pub fn app_header(cx, label: impl Into<SharedString>) -> impl IntoElement.
- pub fn app_header_with(cx, label_child: impl IntoElement) — variante
  para left side no-text.
- 3 tests #[gpui::test].

Migración 4 consumers:
- Cada uno: bloque de ~13 líneas → 1 línea app_header(cx, text).
- Borra dep yahweh-widget-theme-switcher (incluida vía app-header).
- Reemplaza import.

Ahorro ~50 líneas UI hardcoded. Cambios visuales del header (padding,
border, text_size) en un solo lugar.

Decisión: sidebar header del MetaApp NO se migra — es de sidebar,
no de app top, styling distinto. Diferente patrón → diferente widget.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 12:40:56 +00:00
Sergio af7417ce08 feat(yahweh-widget-stat-card): promover patrón stat card como widget
Iter 15. El patrón "tarjeta de dashboard con border-l accent +
label + value grande + descripción + listing opcional" tenía 2
consumers (minga-explorer + brahman-broker-explorer). Ahora vale
extraer al stack yahweh.

crates/modules/ui_engine/widgets/stat-card/:
- pub fn stat_card(cx, label, value: impl Into<SharedString>,
  description, accent, text, text_dim, recent_items: &[String])
  -> impl IntoElement.
- Compone yahweh-widget-card::card_themed; sin theme directo
  (caller pasa text/text_dim ya resueltos).
- 3 tests #[gpui::test] con TestAppContext + theme: smoke con/sin
  items, type-check value.

minga-explorer:
- Borra fn stat_card local (~60 líneas).
- Borra dep yahweh-widget-card.
- 3 callsites pasan value.to_string() (widget acepta
  Into<SharedString>).

brahman-broker-explorer:
- fn state_card refactorizada como wrap del stat_card compartido,
  preservando la traducción ProbeState→(accent,value,description)
  como helper local app-specific.
- Borra dep yahweh-widget-card.

Sub-header del listing: pasa de "recent (N de TOTAL):" a
"recent (N):" — el widget no conoce TOTAL; el caller lo pone en
label si quiere ("Nodos AST (5 de 247)"). Trade-off aceptable
por reusabilidad genérica.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 12:33:02 +00:00
Sergio ce424eb6af feat(yahweh-theme): persistencia de la preferencia de theme entre runs
Iter 13. El switcher ya cambiaba el chrome en runtime, pero al
cerrar/reabrir el theme volvía a Nebula default. Ahora se persiste
en $XDG_CONFIG_HOME/yahweh/theme (default ~/.config/yahweh/theme)
y se restaura al boot.

yahweh-theme:
- pub fn config_path() -> Option<PathBuf>: resuelve XDG; None en
  sandbox/CI sin HOME ni XDG_CONFIG_HOME.
- pub fn load_persisted() / persist(theme): API pública.
  Variantes load_from_path / persist_to_path con path explícito
  para tests y apps custom.
- Theme::install_default(cx) ahora load_persisted o cae a Nebula.
- Theme::set(cx, theme) ahora persist + set_global. Best-effort:
  io errors ignorados (no rebota la UX por un secundario).
- 5 tests nuevos: round-trip, missing file, unknown name, create
  parent dir, XDG_CONFIG_HOME respetado.

API pública de install_default/set sin cambio de shape — todos los
downstream compilan sin tocar nada. Smoke run verificado.

Beneficio: 4 apps yahweh-themed comparten preferencia persistente.
Usuario puede preset via `echo Aurora > ~/.config/yahweh/theme`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 11:56:48 +00:00
Sergio ea074d7d57 feat(yahweh): caret blinking + slots ornament en theme + MetaApp full themed
Iters 8-9 combinadas. Tres mejoras pequeñas que cierran la
integración del theme:

1) text-input caret blinking: caret_visible bool toggea cada 500ms
   via cx.spawn loop. _blink_task se mantiene en self para que
   drop cancele. render dibuja | sólo si focused && caret_visible.

2) yahweh-theme: 5 slots ornament secundario como methods (no
   fields) derivados de is_dark via ornament_slots() helper:
   bg_input/bg_button/bg_button_hover/accent_destructive/
   bg_destructive_hover. No requiere modificar los 6 presets.

3) MetaApp ornament cleanup: 11 rgb(0x...) hardcoded → slots del
   theme. Sidebar menu items, list row separator/buttons, icon ✕
   delete y su hover, EntityRef selector hovers, form submit
   button + fallback input bg, confirm modal hint y hovers.

Pattern: let X = theme.slot() antes de las closures + move |d|
d.bg(X) en hover/when para tomar ownership.

Antes MetaApp tenía la paleta principal themed (iter 5) pero el
ornament secundario seguía hardcoded. Ahora el theme switcher
cambia absolutamente todo el chrome.

Tests: 117 verdes. Downstream compila. Smoke nakui-ui: bootstrap
OK.

Limitación: nouser-explorer todavía hardcoded — próxima iter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 11:05:39 +00:00
Sergio fc4da751ca feat(yahweh-widget-text-input): focus-aware border + caret sólo on focus
Iter 7 (mini-iter — el text-input ya estaba themed, faltaba el
polish de focus visibility). Antes el border era siempre
accent_strong y el caret | siempre presente — imposible distinguir
cuál input está activo en un form con varios fields.

- Border focus-aware: focused → accent_strong; blur → border (slot
  tenue del theme).
- Caret | sólo on focus; sin focus se muestra texto plano (reduce
  ruido visual).
- render usa window.is_focused(focus_handle) para chequear.

Sin cambios en API pública. Tests downstream verdes.

Limitación: caret estático (no parpadea). Iter futura si emerge
la necesidad de animation timer via cx.spawn.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 10:51:00 +00:00
Sergio 8fdef818cc feat(yahweh-widget-theme-switcher): control para ciclar themes en runtime
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>
2026-05-10 10:37:49 +00:00
Sergio 5885457628 feat(yahweh-widget-meta-form): paleta del chrome migrada a Theme::global(cx)
Iter 5 de integración. MetaApp::render tenía 7 vars con colors
hardcoded (bg/panel/border/text/text_dim/accent/accent_active);
ahora salen de Theme::global(cx) que el shell instala al boot.
confirm_delete_banner usa themed_colors(Warning/Error) para sus
colors base.

render:
- 7 let X = rgb(0x...) → theme.bg_app/bg_panel/border/fg_text/
  fg_muted/accent/accent_strong.
- toast_div / error_banner: banner() → banner_themed(cx).

Firmas internas:
- render_sidebar/main/list/entity_ref_selector/form cambian
  Rgba → Hsla (Background donde aplica para panel). gpui::Div
  acepta ambos via Into, uso interno no cambia.

confirm_delete_banner:
- 6 colors hardcoded → themed_colors(Warning) para banner base,
  themed_colors(Error) para Confirm, theme.bg_panel_alt+fg_text
  para Cancel.

NO migra esta iter (ornament hardcoded para una pasada futura):
row hovers, bordes sutiles entre filas, bg de inputs custom,
bg de botones del EntityRef selector, color del icon ✕ de delete.

Smoke test del binario verificado: bootstrap completo OK, panic
esperado en open_window sin display. 115 tests verdes (sin
cambio: los tests del widget no acceden al render).

Beneficio: theme switcher (cuando llegue) cambia toda la paleta
con 1 sola llamada Theme::set(cx, ...).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 10:20:08 +00:00
Sergio 9632d8c4a7 feat(yahweh): theme integration en banner + card + nakui-explorer consume themed
Iter 4 de integración nakui↔yahweh. Los widgets banner y card
ofrecen variants _themed(cx, ...) que leen Theme::global(cx).
Versiones sin theme preservadas para apps sin theme global.

yahweh-widget-card:
- Nueva dep yahweh-theme.
- pub fn card_themed(cx: &App) -> Div: card() + bg(theme.bg_panel).

yahweh-widget-banner:
- Nueva dep yahweh-theme.
- pub fn banner_themed(cx, kind, msg) -> Div: deriva (bg, fg) según
  kind + theme.is_dark. Info usa accent del theme; Success/Warning/
  Error usan hue fijo (verde/amber/rojo) + lightness flippeada.
- pub fn themed_colors(kind, theme) -> (Background, Hsla) helper.
- 3 tests nuevos del derivation.

nakui-explorer:
- Nueva dep yahweh-theme.
- main() instala Theme::install_default antes de open_window.
- render: 5 vars rgb() locales → theme slots (bg_app/fg_text/etc).
- card() → card_themed(cx). banner() → banner_themed(cx).
- Accents semánticos del log (seed azul, morphism verde) quedan
  locales: son señales del dominio, no chrome.

Tests: 112 → 115 (+3). Stack intacto.

Beneficio: cambiar de Theme refleja en nakui-explorer automático.
Próximo candidato: migrar MetaApp (paleta hardcoded de 6 colors).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 10:11:29 +00:00
Sergio 1be7bf9b1e feat(yahweh-widget-card): container card-shape compartido para timeline entries
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>
2026-05-10 10:02:23 +00:00
Sergio 715cf6be03 feat(yahweh-widget-banner): widget compartido para toasts/errores cross-app
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>
2026-05-10 09:57:32 +00:00
Sergio 3e4278d766 feat(yahweh): MockBackend público + tests E2E del widget con TestAppContext
Cierra el ciclo de testabilidad del widget metainterfaz. Hasta
ahora los tests del MetaBackend trait vivían como impl privada en
backend.rs; el widget no podía testear handlers sin levantar
NakuiBackend (que depende de event log + Rhai).

yahweh-meta-runtime:
- Nuevo `pub mod testing` con MockBackend (renombre del MemBackend
  privado, ahora público). Constructores: new(), with_records(iter),
  with_morphism(name, handler) builder. Métodos de inspección
  total_records / records_for. Bajo `pub mod testing` (no cfg test)
  para que crates downstream lo usen en sus dev tests.
- Tests del trait en backend.rs simplificados: usan MockBackend en
  vez del MemBackend duplicado. 8 backend.rs + 9 nuevos del mock.

yahweh-widget-meta-form:
- Dev-dep nueva: gpui con feature "test-support" (TestAppContext).
- MetaApp::apply_action ahora pub (era privado). Necesario para
  invocar handlers desde tests E2E.
- Nuevo tests/widget_with_mock_backend.rs con 4 tests #[gpui::test]:
  meta_app_constructs, open_view_action_does_not_panic,
  backend_state_visible_from_widget_perspective,
  morphism_handler_can_be_registered_and_called_via_widget.

Tests: 47→56 yahweh-meta-runtime, 3→7 yahweh-widget-meta-form.
Total stack 109 verdes.

Limitación: render() no se invoca (requiere window context más
rico). Tests verifican state machine, no pixels. Snapshot tests
serían scope futuro.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 09:49:20 +00:00
Sergio d5ef7144b5 feat(yahweh-meta-runtime): promover short_hash y preview_value desde nakui-explorer
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>
2026-05-10 09:38:50 +00:00
Sergio b05de24c24 refactor(nakui-core): KCL → Nickel — kcl_wrapper reemplazado por evaluación in-process
Cierra el plan original. 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 usa el
brazo de cards). Los 3 schemas de sales/inventory/treasury migran
de .k a .ncl.

nakui-core:
- Nueva dep nickel-lang = "2.0.0".
- Borrado kcl_wrapper.rs.
- Nuevo nickel_validator.rs con vet(schema_path, state, entity)
  que evalúa `let bundle = (import "<schema>") in
  (std.deserialize 'Json m%%"<json>"%%) | bundle.<entity>`.
- executor.rs: KclError → NickelError, KclPre/Post/PostCreate →
  SchemaPre/Post/PostCreate, kcl_check → validate_entity.
  build_schema_bundle ahora emite `(import "X") & (import "Y") & ...`
  en lugar de concatenar bytes (cada .ncl es expresión completa).
- manifest.rs: default schema "schema.ncl", extract_schema_names
  reescrito para sintaxis Nickel record (CapitalCase keys con
  2-space indent).

Schemas migrados:
- sales/schema.ncl: Venta con std.contract.Sequence [record,
  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 antes de que el value populate el record;
  documentado en cada schema.
- inventory/schema.ncl, treasury/schema.ncl: idem.
- 3 schema.k viejos borrados; sales/nsmc.json paths actualizados.

Tests: refs Kcl* renombradas; paths .k → .ncl; tests inline que
escribían schema.k cambian a schema.ncl con sintaxis Nickel.
84 tests verdes en nakui-core.

Doc-only borrados:
- crates/core/ente-card/schema/card.k (REFERENCE ONLY).
- crates/core/ente-brain/schema/rule.k (REFERENCE ONLY).

Beneficios: sin dep externa al binario `kcl` (build CI limpio),
errores Nickel en línea con caret pointing al field, mismo motor
que cards (una dep para todo el repo), sin tempfile JSON
intermedio.

Cierra el plan original yahweh + KCL + card.k. Pendientes salen
de nuevo trabajo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 02:59:48 +00:00
Sergio b3a99f38dc refactor(yahweh): Fase 2c — widget render extraído a yahweh-widget-meta-form
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>
2026-05-10 02:00:34 +00:00
Sergio 2462aca444 refactor(yahweh): Fase 2b — MetaBackend trait + NakuiBackend + MetaUi consume el backend
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>
2026-05-10 01:48:49 +00:00
Sergio 6104484498 refactor(yahweh): Fase 2 — extraer helpers puros a yahweh-meta-runtime
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>
2026-05-10 01:17:17 +00:00
Sergio f5987d9cfc refactor(yahweh): Fase 1 — nakui-ui-schema → yahweh-meta-schema
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>
2026-05-09 23:38:25 +00:00
Sergio f0c0a71860 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>
2026-05-09 22:15:42 +00:00
Sergio fc72726666 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:
- 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.
2026-05-09 20:48:59 +00:00
Sergio 932e7464d7 feat(nakui-ui): Action::Morphism wired al pipeline real (compute -> log -> apply)
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.
2026-05-09 20:41:37 +00:00
Sergio 06c4fb9130 feat(nakui): metainterfaz declarativa + 6 modulos ERP estandar
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.
2026-05-09 19:54:21 +00:00
Sergio 6be50c5b73 feat(minga-core): alpha-hashing per-language para Python, TS, JS, Go
Cierra el ultimo pendiente fundamentado del CHANGELOG. Cada lenguaje
soportado por minga tiene ahora su propio profile alpha-equivalente
— refactorings tipo "rename variable" no inflan el storage del repo
en ningun dialecto.

Refactor de alpha.rs (639 LOC) a modulo alpha/:
- alpha/common.rs: primitives compartidos (TAG_*, write_kind_and_field,
  emit_*, push_identifier_name). Garantiza wire bit-equivalente.
- alpha/rust.rs: logica Rust movida sin cambios funcionales.
- alpha/python.rs, alpha/ecmascript.rs, alpha/go.rs: nuevos.
- alpha/mod.rs: re-exporta hash_node_alpha (Rust legacy) + expone
  hash_alpha_with(dialect, node) que despacha al profile correcto.

Cobertura per-language:

Python: function_definition, lambda, for_statement, list/set/dict
comprehensions, generator_expression (con scope incremental:
binders del for_in_clause viven en clauses siguientes + body),
with_statement (recursando en as_pattern_target).

ECMAScript (TS+JS): function_declaration, function_expression,
method_definition, generator_function_*, arrow_function (paren y
shorthand), statement_block (con lexical_declaration y
variable_declaration introduciendo binders al resto), for_in_statement
(cubre for-of/for-in), for_statement (initializer C-style),
catch_clause, TS typed/optional parameters.

Go: function_declaration, method_declaration, func_literal (closure),
parameter_declaration con multi-name agrupados, block (con
short_var_declaration), for_statement con range_clause y for_clause,
if_statement con initializer.

Tests: 26 nuevos en alpha_polyglot.rs cubriendo rename invariants +
sanity negatives (function name matters, type matters, operation
matters) por cada lenguaje + cross-language sanity (mismo source en
distintos lenguajes -> hashes distintos).

141 tests verdes en minga-core (115 antes; +26 polyglot). 36 alpha
tests Rust intactos (sin regresion).

Pendientes Minga: minga-vfs (FUSE, proyecto independiente).
Cobertura adicional por-lenguaje (Python class, JS destructuring,
Go type_switch) queda como nice-to-have.
2026-05-09 19:06:48 +00:00
Sergio d1888e0901 feat(minga-core): cierre del α-hashing de Rust — if let, while let, let-else, or-pattern, let-chains
Cierra los 5 pendientes documentados en alpha.rs. El hash
alpha-equivalente ahora es estable bajo renombre de TODOS los binders
de Rust, no solo los del MVP (params, let, for, match arms).

Pendientes cerrados:
- if let X = expr { ... }: if_expression detecta let_condition en
  condition, recolecta binders del pattern, los propaga al
  consequence. Alternative (else) no los ve.
- while let X = expr { ... }: simetrico al if-let, propaga al body.
- let-else: ya funcionaba por construccion (alternative procesado en
  scope antes que feed_block extienda con los binders).
- or_pattern: ambos lados introducen los mismos binders (Rust
  enforcement). Emit recorre todos, collect solo el primero para no
  duplicar.
- let-chains (if let X = a && let Y = b): collect_let_condition_binders
  recursa en el arbol del condition capturando todos los let_condition
  vivan donde vivan (binary_expression u otros).

Helper nuevo: feed_let_condition para que el pattern del let_condition
pase por feed_pattern (que distingue binders de constructors). Sin
esto, los identifiers del pattern se hasheaban como variables libres
y Some(x) != Some(y) aun teniendo el mismo significado.

Tests: 6 nuevos en alpha_invariants:
- alpha_if_let_binder_rename_invariant
- alpha_if_let_else_does_not_see_binder
- alpha_while_let_binder_rename_invariant
- alpha_let_else_binder_rename_invariant
- alpha_or_pattern_binder_rename_invariant
- alpha_let_chain_binders_propagate_to_consequence
- alpha_if_let_does_not_collide_with_unrelated_program (negativo)

36 tests alpha verdes. 115 totales en minga-core.

Refactorings del tipo "rename variable" no inflan el storage del
repo. Pendiente futuro: alpha-hashing per-language (Python, TS, JS,
Go) — cada uno requiere conocimiento profundo de su gramatica.
2026-05-09 17:21:25 +00:00
Sergio 4db168253c feat(minga): multi-lenguaje en parser — Python, TypeScript, JavaScript, Go
Minga deja de ser Rust-only. Cualquiera de los cinco dialectos
(Rust + 4 nuevos) se ingresa al CAS por su AST normalizado, hashea
estructuralmente, sincroniza por DHT como cualquier nodo. La
auto-deteccion por extension hace que minga ingest archivo.{py,ts,js,go}
"simplemente funcione".

API nueva en minga_core::parse:
- Funciones por dialecto: python, typescript, javascript, go (~6 LOC
  c/u sobre el parse_with comun). Mas la rust existente.
- Enum Dialect con parse(source) y name() para logging.
- detect_by_extension(ext) -> Option<Dialect>: rs/py/pyi/ts/js/mjs/
  cjs/go (case-insensitive). None para extensiones desconocidas.

Wire en minga-cli:
- cmd_ingest deja de hardcodear parse::rust — usa
  detect_dialect(file)?.parse(...).
- initial_scan + cmd_watch cambian is_rs_file -> is_supported_source.
- CliError::UnsupportedLanguage { path, extension } nuevo, lista las
  extensiones reconocidas en el mensaje.

Notas sobre hashing:
- Hashing estructural (cas::hash_node) funciona para todos. NO es
  alpha-equivalente.
- Hashing alpha-equivalente (alpha::hash_node_alpha) sigue siendo
  Rust-only — cada lenguaje tiene reglas distintas para binder vs
  constructor; implementacion per-language queda como work futuro
  (requiere conocimiento profundo de cada gramatica).
- Sanity test structural_hash_distinguishes_languages verifica que
  "x = 1" parseado como Python != JS — las gramaticas no comparten
  kinds, hashes salen distintos. Importante para evitar colisiones.

Deps nuevas (workspace + minga-core):
- tree-sitter-python 0.23, tree-sitter-typescript 0.23 (modo
  LANGUAGE_TYPESCRIPT, no TSX), tree-sitter-javascript 0.23,
  tree-sitter-go 0.23.

Tests: 9 nuevos en parse::tests (parse basico para 5 dialectos +
detect_by_extension canonical/case-insensitive + name() +
structural_hash_distinguishes_languages). 108 verdes en minga-core,
10 en minga-cli, sin regresion.

Pendientes: alpha-hashing per-language; alpha-Rust documentados en
alpha.rs (if let, while let, let-else, let-chains, or_pattern con
bindings).
2026-05-09 16:06:31 +00:00
Sergio ad0d475a2e feat(brahman-net): capa P2P compartida — Fase 0 (extracción del swarm)
Primer paso del plan "el encuentro entre Entes no se restringe a local".
El swarm libp2p que vivía dentro de minga-p2p::network (282 LOC) sale
a una crate compartida brahman-net para que cualquier protocolo de la
familia (handshake brahman remoto en Fase 1, sync minga, futuros) reuse
una sola malla TCP+Noise+Yamux+Kad+Identify+Stream.

BrahmanNet expone:
- new() / with_keypair() para identidad efimera o persistente
- API de comandos uniforme: dial, listen, add_dht_peer,
  find_closest_peers, start_providing, find_providers
- Publica peer_id (libp2p) y control (stream::Control) — cada
  protocolo registra su StreamProtocol sin acoplarse al swarm
- Re-exporta Stream y StreamProtocol para evitar dep directa a libp2p

minga-p2p::network reduce de 282 LOC a 22: re-export del nuevo
BrahmanNet bajo el alias historico LibP2pNode (zero churn en
MingaPeer) y la const SYNC_PROTOCOL = "/minga/sync/1.0.0" especifica
del sub-protocolo de sync Minga.

Aclaracion semantica anclada por el usuario: Arje es el init (PID 1),
Brahman es el encuentro entre Entes. El nombre brahman-net refleja
que la malla pertenece al encuentro, no al runtime — Minga es un
cliente de la malla, no su dueño.

Tests: minga-p2p completo verde (58 tests, sin regresion). Behavior
identico — solo se movio codigo, ningun cambio funcional.
2026-05-09 12:29:16 +00:00
Sergio 6f993f4268 refactor(explorer+card): independencia jerarquica enforced
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.
2026-05-09 03:32:11 +00:00
Sergio 2ae888bc8f feat(explorer+daemon): discovery dinamico via broker + query 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.
2026-05-09 03:08:20 +00:00
Sergio b23ddf2980 feat(nous-real): cache de embeddings + write-through al CAS de arje
Cierra el ciclo del feedback: el modelo real (fastembed-allMiniLML6V2,
~1-50ms por archivo) era invocado ciegamente en cada re-cluster del
watcher. Ahora se cachea por sha256(bytes-vistos) + model_id, con
write-through al CAS de arje.

Pipeline en handle_file:
1. Lee primeros 8 KiB del archivo (igual que antes).
2. file_sha = ente_cas::sha256_of(buf) — hash de los bytes que el
   modelo *realmente* verá. Garantiza que un archivo creciendo mas
   alla de la ventana sin tocar la cabeza siga sirviendo cache hits.
3. Cache lookup -> HIT: respuesta en us, sin invocar fastembed.
4. MISS: ente_cas::store(&buf) (write-through, no-fatal si falla) ->
   backend.embed_one(text) -> cache.put(...).

Backend de cache: sled local en
$XDG_CACHE_HOME/brahman/nouser-nous-real-embed-cache.sled. Tree
versionado embed_cache_v1; el MODEL_ID viaja en la key, asi que
cambiar de modelo invalida el cache implicitamente. Override por env
NOUSER_NOUS_REAL_CACHE.

Encoding compacto: cada Vec<f32> se serializa como bytes little-endian
(4B por f32, sin overhead). Para 384-d son 1.5 KiB por entry. Decode
tolera bytes corruptos (longitud no-multiplo de 4 -> None, no panic).

Por que sled y no ente-cas directo: el CAS de arje es flat
sha256-keyed; la cache necesita un mapeo (file_sha, model_id) ->
embedding, no expresable como entry CAS. El write-through a CAS queda
como registro consultable + futura GC.

Mock NO se modifica — su embedding pseudo-32d es metadata-hashing puro,
sin costo. Cachearlo seria overhead.

Tests: 5 unitarios verdes (roundtrip, miss, model collision, content
collision, corrupted value). Stub mode (sin feature) sigue compilando
sin tocar cache.
2026-05-09 02:57:55 +00:00
Sergio 79d42aba28 chore(nakui): alinear nakui-core con [workspace.package] y deps compartidas
Cleanup de drift de convenciones: nakui-core era el unico crate del
monorepo que manteia version, edition y thiserror hardcoded, mientras
el resto heredaba del workspace y usaba thiserror v2. Eso significaba
que un bump global de version o edition se olvidaba sistematicamente
de nakui.

Cambios:
- [package]: version, edition, rust-version, license, authors, publish
  -> todos *.workspace = true. Agregado description (convencion).
- 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 esta en el workspace dep porque nakui es el unico user;
  queda local opt-in en lugar de inflar el dep comun.
- Deps especificas de nakui (sin comparticion posible): rhai, petgraph,
  surrealdb permanecen inline con version local.

Verificacion: cargo build -p nakui-core verde tras el bump thiserror
v1->v2 — los 14+ enums de error de nakui no requirieron ajustes
(derive backwards-compat para patrones simples). cargo test -p
nakui-core --lib: 27/27 verdes.

Bonus en este commit: discovery.rs movio el import Ulid a #[cfg(test)]
porque el refactor a Card::new lo dejo unused en module-scope.
2026-05-09 02:49:41 +00:00
Sergio 4c9e4c3962 feat(card): Card::new(label) — alternativa segura a Default::default()
Cierra la trap documentada de Card::default() que devuelve id =
Ulid::nil(). Usar Card::default() viva colisionaba con cualquier otra
Card default-construida bajo el mismo id 00000...

La fix no es romper Default (sigue siendo determinista, requerido por
callers que lo usan como template para deserializacion y patterns de
busqueda) sino agregar un constructor explicito Card::new(label) que
asigna id = Ulid::new() + label provisto, manteniendo defaults seguros
en todo lo demas.

Pensado para struct-literals con override parcial:

    let card = Card {
        kind: CardKind::Data,
        payload: Payload::Embedded(json),
        ..Card::new("mi-modulo.algo")
    };

Refactor de call sites en codigo de produccion:
- brahman_sidecar::discovery::build_consumer_card
- nouser daemon::build_engine_card

Default queda con docstring expandida que apunta a Card::new para uso
"vivo". to_brahman_card en nouser-card NO se modifica porque asigna
el id estable de la Monada, no uno fresco.

Tests: 3 unitarios nuevos en brahman-card. 15 tests verdes (era 12).
2026-05-09 02:43:21 +00:00
Sergio 006640057a feat(sidecar): API reusable de discovery via broker
Promueve el patron ad-hoc discover_producer_socket que vivia inline en
'nouser attract --remote' a un modulo publico brahman_sidecar::discovery.
Cualquier consumer ahora puede preguntar al broker "quien provee este
TypeRef?" sin reimplementar el patron a mano.

API:
- build_consumer_card(label, flow_name, type_name) construye una Card
  minima (Ente, Oneshot, Virtual) con un input flow. Asigna Ulid::new()
  real (no nil), evitando colisiones en el broker.
- await_provider(card, timeout) async: conecta al init, espera
  MatchEvent::Available, devuelve producer_service_socket, manda
  Farewell. Ignora eventos Lost durante el await.
- await_provider_blocking(card, timeout) wrapper para mundos no-async
  (CLIs, std-thread loops). Crea su propio runtime current_thread.
- ConsumerError tipado: Connect{socket,source}, NoProvider{flow,type_ref,
  timeout}, Client(ClientError), Runtime(String). Adios al Box<dyn Error>.

Refactor en nouser daemon: discover_producer_socket inline (60 LOC) ->
5 LOC delegando en el helper. remote_embed ya no construye su propio
runtime.

Tests: 4 unitarios (id no-nil, id unico por llamada, formateo de Wit
TypeRef, fallback sin input). Build verde para sidecar y nouser-core.
2026-05-09 02:30:48 +00:00
Sergio 2725d6a297 feat(nouser+sidecar): watcher con debounce 150ms + re-publish al broker
Cierra los dos pendientes documentados en 487c457: el spam de eventos
duplicados de notify y la falta de propagación al broker cuando una
Mónada cambia composición.

SidecarPool ahora es idempotente respecto a Card.id: spawn rastrea un
HashMap<Ulid, AbortHandle> y aborta la sesión previa si el id ya
existía. Nuevo drop_session(id) para cerrar Mónadas que desaparecen y
live_sessions() para introspección.

Watcher reorganizado en dos threads: dispatcher filtra notify a un
canal de paths; coordinator agrupa con HashMap<PathBuf, Instant> y
dispara batch sólo cuando todos llevan ≥150ms quietos. Cada batch
re-scanea + re-clusteriza con hidratación + diffea contra prior:
removidas → drop_session, nuevas o con composición distinta → spawn
(que reemplaza la sesión previa). Re-scan global por batch es
deliberado y O(N archivos) — aceptable hasta que duela.
2026-05-09 01:37:39 +00:00
Sergio 487c457e5b feat(nouser): notify watcher — el sistema reacciona en tiempo real
El daemon monta notify::recommended_watcher recursivo sobre el dir
escaneado. Cada Create/Modify de archivo regular dispara:
embedding → filtro por centroid_model → ranking contra centroides →
log con 🧲 / · según supere DEFAULT_ATTRACTION_THRESHOLD.

  $ nouser daemon /tmp/x &
  $ vim /tmp/x/src/nuevo.rs
  [watcher] 🧲 /tmp/x/src/nuevo.rs  →  x/src  (0.7470)

  $ echo edit >> /tmp/x/docs/n1.md
  [watcher] 🧲 /tmp/x/docs/n1.md  →  x/docs  (0.8169)

Mecánica:
- DB pasa a Arc<Mutex<MonadDb>> para sharing con thread watcher.
- Watcher en thread dedicado nouser-watcher; reacciona sólo a
  Create/Modify, ignora Access/Metadata-only.
- react_to_change(path, metadata, db) computa embedding, filtra por
  centroid_model, busca best attraction.
- No re-publica al broker ni muta DB — sólo observa y narra. La
  invalidación selectiva (re-cluster + replace + diff publish) queda
  para futuro.

Limitación conocida: notify emite múltiples eventos por edición
(Create + Modify, etc.). Sin debounce el watcher reporta varias
veces. Aceptable para demo; producción conviene debounce ~100ms
por path.

Esto cierra la Fase C del plan post-reporte: el sistema "se siente"
vivo. Tocar un archivo en vim y ver inmediatamente la atracción
calculada cumple el meta-mensaje "Mónada Viva".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 01:06:31 +00:00
Sergio 65af98da13 feat(nouser): hidratación del daemon vía sled + path_hint
El daemon ya no recomputa ciegamente al arrancar. Si la DB tiene
Mónadas previas con centroid_model válido, las publica instantáneo
y el re-scan reusa sus IDs vía path_hint.

Schema:
- MonadManifest.path_hint: Option<String> — identidad estable
  derivada del origen (para by_directory, el parent dir canónico).
  Permite reusar ULID across re-scans.

Cluster:
- Nueva fn cluster::by_directory_hydrated(files, min_files, prior).
  Con prior, busca Mónada con mismo path_hint Y mismo centroid_model;
  si la encuentra, reusa id, lineage y created_at_ms.
- by_directory queda como wrapper sin hidratación (back-compat).

Daemon (cmd_daemon):
1. Open sled si NOUSER_DB_PATH existe.
2. Publica Mónadas previas con centroid_model válido (las inválidas
   se descartan con log explícito).
3. Re-scan + by_directory_hydrated(prior=&db).
4. Sólo spawnea sidecars para Mónadas con id NUEVO. Los path_hints
   existentes preservan identidad, evitando duplicados en el broker.
5. Persiste el set actualizado.

Validación:
  $ NOUSER_DB_PATH=/tmp/h.sled nouser daemon crates/core
  # arranque 1: re-scan 102 archivos → 5 mónadas (5 nuevas)
  $ NOUSER_DB_PATH=/tmp/h.sled nouser daemon crates/core
  # arranque 2: hidratadas 5 mónadas en O(1)
  #             re-scan → 5 mónadas (0 nuevas vs hidratación)

Costo del arranque 2: ~0.06s user CPU.

Tests: 7 (card) + 24 (core) verdes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 00:55:05 +00:00
Sergio 820a1a33bf feat(nouser): centroid_model — versionado de embeddings
Protege contra el bug silencioso de mezclar centroides de modelos
distintos (mock 32-d vs real 384-d), que daría scores sin sentido.

- MonadManifest.centroid_model: Option<String>. None = legacy.
- nouser_core::embed::MODEL_ID = "nouser-pseudo-32d". Cluster lo
  setea en cada Mónada que genera.
- nouser-nous-mock reusa la misma constante (use
  nouser_core::embed::MODEL_ID): produce vectores idénticos al
  cluster local, reportar el mismo ID es honesto.
- nouser-nous-real ya reportaba "real-fastembed-allMiniLML6V2-384d";
  el filter ahora lo descarta automáticamente cuando los centroides
  cacheados son del mock.
- cmd_attract:
  - Captura el model_id del embedding del target.
  - Filtra Mónadas cuyo centroid_model no matchee.
  - Reporta "embed: <source> (<model>)" y "skipped: N" cuando
    descarta.

Resultado: cambiar de mock a real vía BRAHMAN_BROKER_CONTEXT=prod
hace que attract filtre las Mónadas viejas con cero score en lugar
de fingir que las puede comparar.

Tests: 7 (card) + 24 (core) verdes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 00:24:38 +00:00
Sergio 9c371ee43e feat: profile.dev slim + dynamic binding del consumer Nous
Dos piezas del plan post-reporte, en un commit por estar acopladas
(ambas tocan cómo se construye y conecta el sistema):

profile.dev slim:
- debug = "line-tables-only" + split-debuginfo unpacked +
  codegen-units 256 en [profile.dev].
- Override [profile.dev.package.{gpui,ort,fastembed,tokenizers,image}]
  con opt-level=1, debug=false para los pesados que no debuggeamos.
- Resultado: binarios ~3× más livianos. ente-zero 125→47 MB;
  mock-nous ~50→22 MB. target/ futuro mucho más manejable.

dynamic binding (cierra priority_contexts):
- nouser-core Cargo.toml: deps directas brahman-handshake + tokio.
- cmd_attract refactor:
  - Si NOUSER_NOUS_SOCKET está set, atajo explícito (compat).
  - Si no, abre Client al brahman-init, anuncia consumer Card con
    flow.input = embed-result:json, espera 3s por MatchEvent::Available,
    usa producer_service_socket del evento.
- discover_producer_socket() es async; cmd_attract usa runtime tokio
  current_thread inline (block_on).
- embed_via(path, file) se separa como helper sync para la RPC.

Validación end-to-end:
  $ ente-zero & nouser-nous-mock &
  $ nouser attract --remote crates/core archivo.rs
    🧲  0.9058  ente-brain/src  ...
  (mock log: "embed_file path=archivo.rs" — discovery activo)

Con esto BRAHMAN_BROKER_CONTEXT=test/prod swappea el provider sin que
el consumer toque nada — la promesa de priority_contexts es real.

Bug colateral resuelto: la "flakiness" del cargo test --workspace era
disco lleno (24 GB en target/), no condición de carrera. Con
cargo clean + profile slim, tests deterministas.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 22:23:44 +00:00
Sergio 7831c0c827 feat(nouser): persistencia sled write-through del MonadDb
MonadDb ahora soporta backend dual:
- MonadDb::new() → memoria pura (default, back-compat).
- MonadDb::open(path) → sled-backed con cache en memoria. Carga
  contenido existente al abrir; cada insert_* hace write-through
  (cache + sled).

Diseño:
- 2 trees sled: files y monads.
- Wire format: serde_json (ergonomía + inspectability con sled-cli;
  los manifests son chicos, JSON gana sobre postcard aquí).
- Reads SIEMPRE desde la cache — sled se consulta sólo al abrir.
- replace_monads() purga el tree de sled antes de escribir.

Bin nouser: nueva env var NOUSER_DB_PATH. Si está set, persiste; si
no, in-memory:

  $ NOUSER_DB_PATH=/tmp/monads.sled nouser scan crates/core
  scan: 102 archivos, 5 mónadas
  $ ls /tmp/monads.sled
  blobs  conf

Tests nuevos en db.rs:
- persistence_roundtrip — escribe, cierra, reabre, datos están.
- replace_monads_purges_persistent_tree — replace limpia tree.

24 tests en nouser-core (era 22, +2).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 19:50:37 +00:00
Sergio d7b4886164 feat(sidecar): Phase B-3 — SidecarPool consolida sidecars en un runtime
Antes: cada spawn(card) creaba un thread + tokio runtime propio.
Para módulos con muchas sesiones (nouser daemon con 50+ Mónadas)
eso es 50 threads + 50 runtimes. Ahora: un thread + un runtime
tokio current_thread que hostea N tasks de sidecar.

API nueva (aditiva, no rompe spawn/spawn_with_handle):

  let pool = SidecarPool::new()?;
  pool.spawn(card1);
  pool.spawn(card2);
  pool.spawn_conscious(card_with_wit, wit);
  pool.spawn_with_config(custom_config);
  // pool drop = todas las sesiones cierran.

run_client se hace pública para que el pool pueda enqueuar tasks
externos al runtime con handle.spawn(run_client(config)).

nouser daemon migrado al pool. Verificación con ps -L:

  $ ps -L -p $(pidof nouser)
    LWP   CMD
    28817 nouser              # main thread
    28819 brahman-sidecar     # pool thread (todas las sesiones)

Antes serían 6+ LWP (1 main + N sesiones). Ahora 2 fijos sin
importar cuántas Mónadas se publiquen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 19:47:21 +00:00
Sergio b3feaf667c feat: Crossreferencia — Card.references como grafo del fractal
Las Cards ahora declaran sus relaciones con otras Cards. El Engine
posee Mónadas; las Mónadas declaran que son poseídas por el Engine.

- brahman-card:
  - RelationshipKind { Owns, OwnedBy, Processes, ProcessedBy, Sibling }
  - CardReference { kind, target_id: Ulid, target_label: String }.
    target_label es cache para que la UI renderee sin resolver.
  - Card.references: Vec<CardReference> + espejo en WireCard.
    Conversiones From propagan.
- brahman-broker::BrokeredCard propaga references.
- brahman-status imprime "ref OwnedBy → label (id)" por sesión.
- nouser daemon: cada Mónada publicada añade OwnedBy apuntando al
  engine. Declaración unilateral — el engine no necesita conocer
  Mónada IDs de antemano.

Validación end-to-end:
  $ ente-zero & nouser daemon crates/core
  $ brahman-status
  Sessions (6):
    [ente]  brahman.nouser_engine
    [data]  brahman-handshake/src
        ref OwnedBy  →  brahman.nouser_engine  (01K...)
    [data]  ente-brain/src
        ref OwnedBy  →  brahman.nouser_engine  (01K...)
    ...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 19:44:47 +00:00