feat(brahman-cards): brazo unificado V1 — readers JSON + estructura canónica

Pivote arquitectónico: Brahman maneja varios formatos legítimos de
"Card" (cada uno en su crate origen, shape preservado), y un único
brazo los lee y proyecta a UNA estructura interna canónica que
consumen UI runtime / storage / DHT / wire. Agregar formato nuevo
= agregar reader, sin tocar consumers.

Crate nuevo `crates/core/brahman-cards/`:
- Card { id, schema_version, lineage, label, extensions, body }:
  wrapper común con identidad legible. PartialEq omitido porque
  MonadManifest y nakui_ui_schema::Module no lo implementan.
- CardBody enum tagged: Ente(brahman_card::Card), Monad(MonadManifest),
  UiModule(nakui_ui_schema::Module). Convención: agregar variant +
  reader; consumers hacen `match { Ente(..) => ..., _ => skip }`.
- trait CardReader { name, can_read(&Value), read(Value) }.
- 3 readers: EnteJsonReader (payload+supervision), MonadJsonReader
  (members+cardinality), UiModuleJsonReader (entities+views+menu).
- Entry points load_card / load_card_with. Errores tipados.

13 tests integration: detection x3, dispatch+projection x3,
negative cases x2, sanity de orden, e2e desde disco, unsupported
extension, custom reader set, documented invariant.

13/13 verdes. Workspace build verde.

V1 NO hace (explícito): Nickel reader, templates, migración de
consumers, yahweh refactor, KCL→Nickel — todos en commits siguientes
para mantener este aislado.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sergio
2026-05-09 23:14:56 +00:00
parent ffdfa6f8d7
commit 09501911b7
7 changed files with 817 additions and 0 deletions
+87
View File
@@ -6,6 +6,93 @@ ratio/diff ver `git show <sha>`.
## 2026-05-09
### feat(brahman-cards): brazo unificado V1 — readers JSON + estructura canónica
**Pivote arquitectónico** decidido en charla: Brahman maneja varios
formatos legítimos de "Card" (cada formato vive en su crate origen y
conserva su shape público), y un **único brazo** los lee, completa
desde templates si vienen simplificados, y los proyecta a UNA sola
estructura interna canónica que consumen UI runtime / storage / DHT /
wire. Agregar un formato nuevo = agregar un reader, sin tocar
consumers.
**V1 en este commit**: estructura canónica + readers para los 3
formatos JSON existentes en el monorepo. Sin Nickel todavía (aislado
para próximo commit).
Crate nuevo `crates/core/brahman-cards/`:
- **`Card { id, schema_version, lineage, label, extensions, body }`**:
wrapper común con identidad legible + extensiones forward-compat.
`id` como String (no `Ulid`) porque cada body variant usa un tipo
de id distinto (Ulid para Ente/Monad, slug human-friendly para
UiModule). PartialEq omitido del derive porque `MonadManifest` y
`nakui_ui_schema::Module` no lo implementan en sus crates origen.
- **`CardBody`** enum etiquetado `kind`:
- `Ente(brahman_card::Card)` — entidad runtime con
payload/soma/supervision.
- `Monad(nouser_card::MonadManifest)` — agrupación semántica de
archivos.
- `UiModule(nakui_ui_schema::Module)` — descriptor de UI con
entities/views/menu.
- Convención: agregar variant nuevo + reader; los consumers que
sólo manejen algunos hacen `match { Ente(..) => ..., _ => skip }`.
- **`trait CardReader`**: `name()` + `can_read(&Value) -> bool` +
`read(Value) -> Result<Card>`. El dispatcher prueba en orden y
delega al primero que matchee.
- **3 readers concretos** (en `readers.rs`):
- `EnteJsonReader` — heurística: `payload` Y `supervision`
presentes simultáneamente.
- `MonadJsonReader` — heurística: `members` Y `cardinality`.
- `UiModuleJsonReader` — heurística: `entities` Y `views` Y
`menu`. El más específico, va primero en `default_readers()`.
- **Entry points**:
- `load_card(path)` — abre archivo, dispatcha por extensión, dentro
de JSON prueba los readers default.
- `load_card_with(path, readers)` — variante con set custom para
apps que quieren restringir formatos.
- **Errores tipados** vía `CardLoadError`: `Io`, `JsonParse`,
`NoMatchingReader`, `ReaderFailed { reader, message }`,
`UnsupportedExtension { ext, supported }`.
13 tests integration:
- 3 detection tests (cada reader matchea sólo su shape, rechaza los
otros 2 + non-object).
- 3 dispatch+projection tests (cada formato JSON cargado produce el
variant esperado con campos del wrapper bien derivados).
- 2 negative cases (NoMatchingReader, non-object input).
- 1 sanity de orden (UiModule gana cuando el shape acepta múltiples
readers — defiende el contrato de orden documentado).
- 1 e2e desde disco con `load_card_with`.
- 1 unsupported extension.
- 1 custom reader set (restringir a sólo Ente).
- 1 documented invariant (extensions vacío en V1; si cambia, este
test se rompe como signal).
13/13 verdes. Workspace build verde tras agregar el crate al
`members[]` del workspace Cargo.toml.
**Lo que NO hace V1** (explícito):
- No carga Nickel — próximo commit. La dep `nickel-lang-core` queda
aislada para no inflar este commit.
- No define templates — los templates Nickel se diseñan junto al
reader Nickel (necesitan `merge` nativo de Nickel para fusionar
override + base).
- No migra consumers. `nakui-ui` sigue cargando `module.json` con
`nakui_ui_schema::load_modules_from_dir` directo. La migración a
`brahman_cards::load_card` viene cuando V1 + Nickel + templates
estén estables.
- No mueve los `extensions` del input a `Card.extensions` — los crates
origen ya tienen sus propios `extensions` internos (`#[serde(flatten)]`).
Documentado como decisión consciente.
**Pendientes para próximos commits** (orden):
1. Reader Nickel + template merge.
2. Migrar consumers (`nakui-ui` consume `brahman_cards::load_card`).
3. Yahweh refactor: lift del MetaUi runtime a `crates/modules/ui_engine/`
(esperando hasta que el brazo + canónico estén estables).
4. KCL → Nickel: kcl_wrapper reemplazado por evaluación de Nickel
contracts; los 3 schemas .k de nakui modules pasan a .ncl.
5. card.k eliminado (es REFERENCE ONLY documentado).
### feat(nakui-ui): validación cross-field del EntityRef (existence en store)
Cierra otro pendiente. Hasta ahora `parse_field_value(EntityRef, raw)`
sólo validaba **forma** (UUID parseable + trim de whitespace) — un