feat(brahman-cards): Nickel reader + templates con merge nativo (V2)

El brazo ahora acepta `.ncl`: evalúa via nickel-lang 2.0, exporta a
JSON, dispatcha por los readers JSON estándar. Templates funcionan
con import + & merge nativos de Nickel — el brazo no inventa
mecánica paralela.

- Dep nickel-lang = "2.0.0" (interfaz estable).
- Nuevo módulo nickel_eval con eval_nickel_file(path) -> Value y
  errores tipados (Io/Eval/Export/JsonReparse). Mensaje de Nickel
  como texto plano sin ANSI.
- load_card_with añade arm "ncl" simétrico al "json".
- CardLoadError::Nickel propaga el error limpio.
- Imports resueltos: parent dir del input + env
  BRAHMAN_CARDS_TEMPLATES_DIR (registry global, opcional).
- Convención obligatoria documentada: fields override-ables del
  template usan `| default` (sin eso Nickel rechaza el merge).

9 tests nuevos: eval directo, dispatch a UiModule/Ente, template
merge con id+label override, registry via env, error wrapping,
contract violation en eval-time (`id | String = 42`), shape
desconocida.

22 tests totales en brahman-cards (13 JSON V1 + 9 Nickel V2).
Workspace build verde.

NO hace: migrar consumers, set canonical de templates, KCL→Nickel
— todos para commits siguientes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sergio
2026-05-09 23:25:57 +00:00
parent 09501911b7
commit 2a4443790c
6 changed files with 1185 additions and 27 deletions
+95
View File
@@ -6,6 +6,101 @@ ratio/diff ver `git show <sha>`.
## 2026-05-09
### feat(brahman-cards): Nickel reader + templates con merge nativo (V2)
Sigue al V1 (readers JSON). Ahora el brazo acepta inputs `.ncl`:
los evalúa via `nickel-lang` 2.0, exporta a JSON, y dispatcha por
los mismos readers JSON estándar. Un `.ncl` puede producir
cualquier `CardBody` siempre que su shape sea reconocida. Los
templates funcionan con los `import` + `&` merge nativos de
Nickel — el brazo no inventa una mecánica paralela.
Cambios:
- **Dep `nickel-lang = "2.0.0"`** (interfaz estable, no
`nickel-lang-core` que es internal/inestable). Compila clean
pero suma ~1 min al build cold del crate.
- **Nuevo módulo `nickel_eval.rs`** con `eval_nickel_file(path) ->
Result<Value, NickelEvalError>`. Errores tipados:
`Io`, `Eval`, `Export`, `JsonReparse` — el mensaje de Nickel se
formatea como texto plano (sin ANSI) para que sea legible en
logs y toasts.
- **`load_card_with` añade `"ncl"`**: lee archivo → eval Nickel →
exporta a JSON → parsea de vuelta a Value → dispatch a los
readers JSON. Pipeline simétrico a `"json"`.
- **`CardLoadError::Nickel(NickelEvalError)`**: el error de
Nickel se propaga limpio al error público del brazo.
- **Resolución de imports**:
- El parent dir del input se agrega como import path → `import
"./template.ncl"` resuelve sin config.
- El env `BRAHMAN_CARDS_TEMPLATES_DIR` (constante exportada
`BRAHMAN_CARDS_TEMPLATES_ENV`) agrega un registry global →
`import "ui_module_minimal.ncl"` desde cualquier ubicación.
- No hay magic resolución por kind. El autor del Card decide
qué template importa.
**Convención obligatoria de templates** (documentada en
`nickel_eval.rs`): las fields que el usuario va a sobrescribir
deben marcarse `| default` (o `| optional`). Sin ese marker
Nickel rechaza el merge de strings/numbers no-iguales con la
misma prioridad. Patrón canónico:
```nickel
# template ui_module_basic.ncl
{
id | String | default = "TEMPLATE_ID",
label | String | default = "TEMPLATE_LABEL",
...
}
# uso concreto
let base = import "ui_module_basic.ncl" in
base & { id = "my_id", label = "Mi Label" }
```
9 tests nuevos en `tests/nickel.rs`:
- `eval_nickel_file_returns_value_for_valid_input` — happy path.
- `eval_nickel_file_surfaces_evaluation_error` — variant `Eval`
con path + message.
- `load_card_dispatches_ncl_to_ui_module_variant` — pipeline
e2e a UiModule.
- `load_card_dispatches_ncl_to_ente_variant` — pipeline e2e a
Ente.
- `template_merge_overrides_id_and_label_only` — el caso del
user: template + override de id+label, resto del template
intacto.
- `template_resolves_via_env_registry` — uso del env como
registry global.
- `load_card_wraps_nickel_error_in_card_load_error` — wrap
limpio del error.
- `nickel_contract_violation_caught_at_eval_time` — value-add
concreto: `id | String = 42` falla en eval, no en deserialize
ni aguas abajo.
- `ncl_evaluating_to_unknown_shape_returns_no_matching_reader`
— sanity de coherencia con dispatcher JSON.
22 tests en total en `brahman-cards` (13 JSON V1 + 9 Nickel V2).
Workspace build verde tras la dep nueva.
**Lo que NO hace V2** (sigue pendiente):
- No migra consumers — `nakui-ui` sigue cargando con
`nakui_ui_schema::load_modules_from_dir`. La migración a
`brahman_cards::load_card` queda para después.
- No define un set canonical de templates en el repo (algo
como `templates/ente_basic.ncl`, `templates/ui_module_minimal.ncl`).
Eso emerge cuando aparezcan los primeros casos de uso reales
donde dos cards comparten estructura.
- No hace cross-validation entre template + override (ej:
detectar que un override saca un campo required del template).
Nickel ya lo hace via contracts si el template tiene un schema.
- No expone una API streaming (load N cards en paralelo). El
use case actual es one-shot al boot.
**Pendientes para próximos commits** (orden):
1. Migrar consumers (`nakui-ui` consume `brahman_cards::load_card`).
2. Yahweh refactor: lift del MetaUi runtime a `crates/modules/ui_engine/`.
3. KCL → Nickel: kcl_wrapper reemplazado por evaluación de Nickel
contracts; los 3 schemas .k de nakui modules pasan a .ncl.
4. card.k eliminado (es REFERENCE ONLY documentado).
### 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