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.
This commit is contained in:
@@ -66,6 +66,22 @@ pub struct Module {
|
||||
#[serde(default)]
|
||||
pub entities: Vec<EntitySpec>,
|
||||
|
||||
/// Path a un módulo nakui-core (directorio con `nsmc.json` +
|
||||
/// schemas KCL + scripts Rhai). Cuando está set, el runtime
|
||||
/// carga un `Executor` para ese path y permite que las acciones
|
||||
/// `Morphism { name }` despachen al pipeline real
|
||||
/// (compute → log → apply).
|
||||
///
|
||||
/// Path resuelto relativo al directorio del `module.json`
|
||||
/// o absoluto.
|
||||
///
|
||||
/// Si es `None`, las acciones `Morphism` quedan deshabilitadas
|
||||
/// (toast informativo al usuario). Las acciones `SeedEntity`
|
||||
/// siguen funcionando sin esto — son altas administrativas que
|
||||
/// no necesitan validación de manifest.
|
||||
#[serde(default)]
|
||||
pub nakui_module_dir: Option<String>,
|
||||
|
||||
/// Items del menú. Cada uno apunta a una key de `views`. Orden
|
||||
/// importa (es el orden en que se presentan en el sidebar).
|
||||
pub menu: Vec<MenuItem>,
|
||||
@@ -199,9 +215,27 @@ pub enum Action {
|
||||
next_view: Option<String>,
|
||||
},
|
||||
/// Ejecuta un morphism declarado en el manifest del módulo
|
||||
/// nakui-core. Inputs y params se mapean desde los campos del form.
|
||||
/// nakui-core (cuyo path vive en `Module.nakui_module_dir`).
|
||||
/// Inputs (records existentes) y params (valores escalares) se
|
||||
/// mapean desde los campos del form.
|
||||
Morphism {
|
||||
/// Nombre del morphism declarado en `nsmc.json` del manifest
|
||||
/// nakui apuntado por el módulo.
|
||||
name: String,
|
||||
/// Mapeo `role → field_name`: por cada input declarado en
|
||||
/// el `MorphismSpec.inputs`, indica qué field del form
|
||||
/// contiene el UUID del record. El runtime parsea el value
|
||||
/// como `Uuid` y lo pasa como input al `execute_and_log`.
|
||||
///
|
||||
/// Ej: `{ "stock": "stock_id", "caja": "caja_id" }` para un
|
||||
/// morphism `vender` que toma roles `stock` y `caja`.
|
||||
#[serde(default)]
|
||||
inputs: BTreeMap<String, String>,
|
||||
/// Lista de fields del form cuyos values van al `params`
|
||||
/// JSON object pasado al morphism. Si está vacío, todos los
|
||||
/// fields que no estén en `inputs` van a params.
|
||||
#[serde(default)]
|
||||
params: Vec<String>,
|
||||
#[serde(default)]
|
||||
next_view: Option<String>,
|
||||
},
|
||||
@@ -329,6 +363,7 @@ mod tests {
|
||||
id: "customers".into(),
|
||||
label: "Clientes".into(),
|
||||
description: Some("Gestión de clientes".into()),
|
||||
nakui_module_dir: None,
|
||||
entities: vec![EntitySpec {
|
||||
name: "customer".into(),
|
||||
label: "Cliente".into(),
|
||||
|
||||
@@ -14,7 +14,7 @@ fn examples_dir() -> std::path::PathBuf {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loads_all_six_demo_modules() {
|
||||
fn loads_all_demo_modules() {
|
||||
let dir = examples_dir();
|
||||
let mods = load_modules_from_dir(&dir).unwrap_or_else(|e| {
|
||||
panic!("load failed for {}: {e}", dir.display());
|
||||
@@ -27,10 +27,37 @@ fn loads_all_six_demo_modules() {
|
||||
"inventory_movements",
|
||||
"invoices",
|
||||
"products",
|
||||
"sales_engine",
|
||||
"sales_orders",
|
||||
"suppliers",
|
||||
],
|
||||
"expected 6 modules in alphabetical order"
|
||||
"expected 7 modules in alphabetical order \
|
||||
(sales_engine se sumó al wirear Action::Morphism)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sales_engine_declares_nakui_module_dir_and_morphism() {
|
||||
// Sanity del módulo demo de morphism: nakui_module_dir set,
|
||||
// y al menos una vista con Action::Morphism en su on_submit.
|
||||
let mods = load_modules_from_dir(examples_dir()).unwrap();
|
||||
let sales = mods
|
||||
.iter()
|
||||
.find(|m| m.id == "sales_engine")
|
||||
.expect("sales_engine debe estar");
|
||||
assert!(
|
||||
sales.nakui_module_dir.is_some(),
|
||||
"sales_engine debería declarar nakui_module_dir"
|
||||
);
|
||||
let has_morphism_view = sales.views.values().any(|v| match v {
|
||||
nakui_ui_schema::View::Form(form) => {
|
||||
matches!(form.on_submit, nakui_ui_schema::Action::Morphism { .. })
|
||||
}
|
||||
_ => false,
|
||||
});
|
||||
assert!(
|
||||
has_morphism_view,
|
||||
"sales_engine debería tener al menos una Form con Action::Morphism"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user