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>
This commit is contained in:
Sergio
2026-05-10 02:59:48 +00:00
parent b3a99f38dc
commit b05de24c24
23 changed files with 690 additions and 573 deletions
@@ -1,34 +0,0 @@
schema Stock:
id: str
sku_id: str
ubicacion: str
cantidad: int
check:
cantidad >= 0, "stock no puede ser negativo"
len(ubicacion) > 0, "ubicacion requerida"
len(sku_id) > 0, "sku_id requerido"
schema MovimientoStock:
id: str
stock_id: str
delta: int
razon: str
timestamp: str
check:
razon in ["recepcion", "despacho", "ajuste"], "razon invalida"
delta != 0, "delta no puede ser cero"
schema TransferenciaStock:
id: str
source_stock_id: str
dest_stock_id: str
sku_id: str
cantidad: int
timestamp: str
check:
cantidad > 0, "cantidad debe ser positiva"
source_stock_id != dest_stock_id, "source y dest no pueden ser el mismo stock"
len(sku_id) > 0, "sku_id requerido"
@@ -0,0 +1,55 @@
# Schema Nickel para `inventory` (Stock + MovimientoStock + TransferenciaStock).
let positive_int = std.contract.from_predicate (fun n =>
std.is_number n && n > 0
) in
let non_negative_int = std.contract.from_predicate (fun n =>
std.is_number n && n >= 0
) in
let non_empty_string = std.contract.from_predicate (fun s =>
std.is_string s && std.string.length s > 0
) in
let razon_movimiento = std.contract.from_predicate (fun s =>
std.is_string s && std.array.elem s ["recepcion", "despacho", "ajuste"]
) in
let non_zero_int = std.contract.from_predicate (fun n =>
std.is_number n && n != 0
) in
{
Stock = {
id | String,
sku_id | non_empty_string,
ubicacion | non_empty_string,
cantidad | non_negative_int,
},
MovimientoStock = {
id | String,
stock_id | String,
delta | non_zero_int,
razon | razon_movimiento,
timestamp | String,
},
# Patrón obligatorio para record + cross-field predicate: chain
# via Sequence (aplicar `from_predicate` directo al record rebota
# con "missing definition").
TransferenciaStock = std.contract.Sequence [
{
id | String,
source_stock_id | String,
dest_stock_id | String,
sku_id | non_empty_string,
cantidad | positive_int,
timestamp | String,
},
std.contract.from_predicate (fun r =>
r.source_stock_id != r.dest_stock_id
),
],
}
+3 -3
View File
@@ -1,9 +1,9 @@
{
"module": "sales",
"schemas": [
"schema.k",
"../treasury/schema.k",
"../inventory/schema.k"
"schema.ncl",
"../treasury/schema.ncl",
"../inventory/schema.ncl"
],
"morphisms": [
{
@@ -1,16 +0,0 @@
schema Venta:
id: str
stock_id: str
caja_id: str
sku_id: str
cantidad: int
precio_unitario: int
currency: str
total: int
timestamp: str
check:
cantidad > 0, "cantidad positiva"
precio_unitario > 0, "precio_unitario positivo"
len(currency) == 3, "currency ISO 4217"
total == cantidad * precio_unitario, "total debe ser cantidad * precio_unitario"
@@ -0,0 +1,47 @@
# Schema declarativo para entities del módulo `sales`.
# Reemplaza el `schema.k` (KCL) por contracts Nickel nativos —
# evaluables in-process por `nakui-core::nickel_validator`.
#
# El bundle del módulo (que Nakui arma juntando este archivo + los
# schemas de los módulos importados — ver `nsmc.json`) se evalúa
# como un record con un field por entity. Para validar un value V
# contra el entity `Venta` se hace en Rust:
#
# let bundle = (import "<bundle>.ncl") in V | bundle.Venta
#
# Cada entity es un record contract: cada field declara su contract
# (String, Number, predicate custom, etc.). El record entero se
# wrappea en un contract adicional para invariantes cross-field
# (ej: `total == cantidad * precio_unitario`).
let positive_int = std.contract.from_predicate (fun n =>
std.is_number n && n > 0
) in
let currency_iso = std.contract.from_predicate (fun s =>
std.is_string s && std.string.length s == 3
) in
{
# std.contract.Sequence chain-aplica los contracts en orden.
# Patrón obligatorio para combinar record contract + cross-field
# predicate: aplicar `from_predicate` directo al record (con `|`)
# rebota con "missing definition" porque el predicate evalúa antes
# de que el record esté populated desde el value.
Venta = std.contract.Sequence [
{
id | String,
stock_id | String,
caja_id | String,
sku_id | String,
cantidad | positive_int,
precio_unitario | positive_int,
currency | currency_iso,
total | Number,
timestamp | String,
},
std.contract.from_predicate (fun r =>
r.total == r.cantidad * r.precio_unitario
),
],
}
@@ -1,35 +0,0 @@
schema Caja:
id: str
name: str
saldo: int
currency: str
check:
saldo >= 0, "saldo de caja no puede ser negativo"
len(currency) == 3, "currency debe ser ISO 4217 (3 letras)"
schema Movimiento:
id: str
caja_id: str
monto: int
tipo: str
timestamp: str
memo?: str
check:
monto > 0, "monto debe ser positivo (la direccion la fija el tipo)"
tipo in ["in", "out"], "tipo debe ser 'in' u 'out'"
schema Transferencia:
id: str
source_caja_id: str
dest_caja_id: str
monto: int
currency: str
timestamp: str
memo?: str
check:
monto > 0, "monto debe ser positivo"
len(currency) == 3, "currency ISO 4217"
source_caja_id != dest_caja_id, "source y dest no pueden ser la misma caja"
@@ -0,0 +1,53 @@
# Schema Nickel para `treasury` (Caja + Movimiento + Transferencia).
let positive_int = std.contract.from_predicate (fun n =>
std.is_number n && n > 0
) in
let non_negative_int = std.contract.from_predicate (fun n =>
std.is_number n && n >= 0
) in
let currency_iso = std.contract.from_predicate (fun s =>
std.is_string s && std.string.length s == 3
) in
let movimiento_tipo = std.contract.from_predicate (fun s =>
std.is_string s && std.array.elem s ["in", "out"]
) in
{
Caja = {
id | String,
name | String,
saldo | non_negative_int,
currency | currency_iso,
},
Movimiento = {
id | String,
caja_id | String,
monto | positive_int,
tipo | movimiento_tipo,
timestamp | String,
memo | String | optional,
},
# Patrón obligatorio para record + cross-field predicate: chain
# via Sequence (aplicar `from_predicate` directo al record rebota
# con "missing definition").
Transferencia = std.contract.Sequence [
{
id | String,
source_caja_id | String,
dest_caja_id | String,
monto | positive_int,
currency | currency_iso,
timestamp | String,
memo | String | optional,
},
std.contract.from_predicate (fun r =>
r.source_caja_id != r.dest_caja_id
),
],
}