chore: absorbe nakui (ERP matemático) en modules/nakui

- crates/modules/nakui/core/: el crate nakui-core (4 bins, tests).
  Deps directas (serde, rhai, surrealdb, petgraph, sha2, uuid, tokio,
  thiserror v1) — no convertidas a workspace = true en esta pasada.
- crates/modules/nakui/modules/{inventory,sales,treasury}/: datos
  declarativos del dominio (nsmc.json, schema.k, morphisms/) que el
  crate consume — no son crates.

cargo check -p nakui-core: 0 errores.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sergio
2026-05-08 05:49:58 +00:00
parent 53dbdf0f1d
commit 4d50bfc587
49 changed files with 11953 additions and 40 deletions
@@ -0,0 +1,36 @@
// recibir_stock
// Inflow: external supplier delivers `cantidad` units into a Stock record.
// NOT conservation-bound: the units enter the system from outside.
//
// states.stock: the current Stock record.
// ids.stock: its UUID (string).
// params: { cantidad:i64, timestamp:str, movimiento_id:str }
let cantidad = input.params.cantidad;
if cantidad <= 0 {
throw "cantidad debe ser positiva (la dirección la fija el morfismo)"
}
let mov_id = input.params.movimiento_id;
if type_of(mov_id) == "()" {
throw "params.movimiento_id es obligatorio (idempotencia)"
}
[
#{
op: "set",
path: #{ entity: "Stock", id: input.ids.stock, field: "cantidad" },
value: input.states.stock.cantidad + cantidad,
},
#{
op: "create",
entity: "MovimientoStock",
id: mov_id,
data: #{
id: mov_id,
stock_id: input.ids.stock,
delta: cantidad,
razon: "recepcion",
timestamp: input.params.timestamp,
},
},
]
@@ -0,0 +1,50 @@
// transferir_stock
// Two-input morphism. Conservation rule (Stock.cantidad grouped by sku_id)
// is enforced by the kernel against the produced ops. Same-SKU is also
// asserted in-script for an explicit error message; without that check
// the kernel would still catch the violation via per-sku grouping.
//
// states.source / states.dest: the two Stock records.
// ids.source / ids.dest: their UUIDs.
// params: { cantidad:i64, timestamp:str, transfer_id:str }
let cantidad = input.params.cantidad;
let source = input.states.source;
let dest = input.states.dest;
if cantidad <= 0 {
throw "cantidad debe ser positiva"
}
if source.sku_id != dest.sku_id {
throw "transferencia exige mismo SKU; source=" + source.sku_id + " dest=" + dest.sku_id
}
let xfr_id = input.params.transfer_id;
if type_of(xfr_id) == "()" {
throw "params.transfer_id es obligatorio (idempotencia)"
}
[
#{
op: "set",
path: #{ entity: "Stock", id: input.ids.source, field: "cantidad" },
value: source.cantidad - cantidad,
},
#{
op: "set",
path: #{ entity: "Stock", id: input.ids.dest, field: "cantidad" },
value: dest.cantidad + cantidad,
},
#{
op: "create",
entity: "TransferenciaStock",
id: xfr_id,
data: #{
id: xfr_id,
source_stock_id: input.ids.source,
dest_stock_id: input.ids.dest,
sku_id: source.sku_id,
cantidad: cantidad,
timestamp: input.params.timestamp,
},
},
]
@@ -0,0 +1,31 @@
{
"module": "inventory",
"morphisms": [
{
"name": "recibir_stock",
"inputs": [
{ "role": "stock", "entity": "Stock" }
],
"reads": ["stock.cantidad", "stock.sku_id"],
"writes": ["stock.cantidad", "MovimientoStock"],
"depends_on": [],
"script": "morphisms/recibir_stock.rhai"
},
{
"name": "transferir_stock",
"inputs": [
{ "role": "source", "entity": "Stock" },
{ "role": "dest", "entity": "Stock" }
],
"reads": ["source.cantidad", "source.sku_id", "dest.cantidad", "dest.sku_id"],
"writes": ["source.cantidad", "dest.cantidad", "TransferenciaStock"],
"invariants": {
"conserve": [
{ "entity": "Stock", "field": "cantidad", "group_by": "sku_id" }
]
},
"depends_on": [],
"script": "morphisms/transferir_stock.rhai"
}
]
}
@@ -0,0 +1,34 @@
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,52 @@
// vender
// Cross-module morphism: decreases Stock (inventory) and increases Caja
// (treasury). NOT conservation-bound — a sale is asymmetric: units leave
// the system, money enters. The kernel validates each entity against its
// own schema (Stock from inventory, Caja from treasury, Venta from sales).
//
// states.stock: the Stock record (inventory module).
// states.caja: the Caja record (treasury module).
// params: { cantidad:i64, precio_unitario:i64 (en centavos),
// timestamp:str, venta_id:str }
let cantidad = input.params.cantidad;
let precio = input.params.precio_unitario;
let venta_id = input.params.venta_id;
if cantidad <= 0 { throw "cantidad debe ser positiva" }
if precio <= 0 { throw "precio_unitario debe ser positivo" }
if type_of(venta_id) == "()" { throw "params.venta_id es obligatorio (idempotencia)" }
let stock = input.states.stock;
let caja = input.states.caja;
let total = cantidad * precio;
[
#{
op: "set",
path: #{ entity: "Stock", id: input.ids.stock, field: "cantidad" },
value: stock.cantidad - cantidad,
},
#{
op: "set",
path: #{ entity: "Caja", id: input.ids.caja, field: "saldo" },
value: caja.saldo + total,
},
#{
op: "create",
entity: "Venta",
id: venta_id,
data: #{
id: venta_id,
stock_id: input.ids.stock,
caja_id: input.ids.caja,
sku_id: stock.sku_id,
cantidad: cantidad,
precio_unitario: precio,
currency: caja.currency,
total: total,
timestamp: input.params.timestamp,
},
},
]
@@ -0,0 +1,21 @@
{
"module": "sales",
"schemas": [
"schema.k",
"../treasury/schema.k",
"../inventory/schema.k"
],
"morphisms": [
{
"name": "vender",
"inputs": [
{ "role": "stock", "entity": "Stock" },
{ "role": "caja", "entity": "Caja" }
],
"reads": ["stock.cantidad", "stock.sku_id", "caja.saldo", "caja.currency"],
"writes": ["stock.cantidad", "caja.saldo", "Venta"],
"depends_on": [],
"script": "morphisms/vender.rhai"
}
]
}
@@ -0,0 +1,16 @@
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,46 @@
// register_cash_move
// Pure transition. Receives `input` as { states, ids, params }; returns
// an array of FieldOp objects. No clock, no random, no IO.
//
// states.caja: the current Caja record loaded from the store.
// ids.caja: the Caja's UUID (string form).
// params: { monto:i64, tipo:"in"|"out", timestamp:str, memo:str, movimiento_id:str }
let saldo = input.states.caja.saldo;
let monto = input.params.monto;
let tipo = input.params.tipo;
let caja_id = input.ids.caja;
let nuevo_saldo = if tipo == "in" {
saldo + monto
} else if tipo == "out" {
saldo - monto
} else {
throw "tipo desconocido: " + tipo
};
let mov_id = input.params.movimiento_id;
if type_of(mov_id) == "()" {
throw "params.movimiento_id es obligatorio (idempotencia)"
}
[
#{
op: "set",
path: #{ entity: "Caja", id: caja_id, field: "saldo" },
value: nuevo_saldo,
},
#{
op: "create",
entity: "Movimiento",
id: mov_id,
data: #{
id: mov_id,
caja_id: caja_id,
monto: monto,
tipo: tipo,
timestamp: input.params.timestamp,
memo: input.params.memo,
},
},
]
@@ -0,0 +1,53 @@
// transfer_between_cajas
// Two-input morphism. Conservation rule (Caja.saldo grouped by currency)
// is checked by the kernel against the produced ops; the script just emits
// the deltas.
//
// states.source / states.dest: the two Caja records.
// ids.source / ids.dest: their UUIDs (string form).
// params: { monto:i64, timestamp:str, memo:str, transfer_id:str }
let source = input.states.source;
let dest = input.states.dest;
let monto = input.params.monto;
if monto <= 0 {
throw "monto debe ser positivo"
}
if source.currency != dest.currency {
throw "transferencia exige misma moneda; source=" + source.currency + " dest=" + dest.currency
}
let transfer_id = input.params.transfer_id;
if type_of(transfer_id) == "()" {
throw "params.transfer_id es obligatorio (idempotencia)"
}
let new_source_saldo = source.saldo - monto;
let new_dest_saldo = dest.saldo + monto;
[
#{
op: "set",
path: #{ entity: "Caja", id: input.ids.source, field: "saldo" },
value: new_source_saldo,
},
#{
op: "set",
path: #{ entity: "Caja", id: input.ids.dest, field: "saldo" },
value: new_dest_saldo,
},
#{
op: "create",
entity: "Transferencia",
id: transfer_id,
data: #{
id: transfer_id,
source_caja_id: input.ids.source,
dest_caja_id: input.ids.dest,
monto: monto,
currency: source.currency,
timestamp: input.params.timestamp,
memo: input.params.memo,
},
},
]
@@ -0,0 +1,31 @@
{
"module": "treasury",
"morphisms": [
{
"name": "register_cash_move",
"inputs": [
{ "role": "caja", "entity": "Caja" }
],
"reads": ["caja.saldo", "caja.currency"],
"writes": ["caja.saldo", "Movimiento"],
"depends_on": [],
"script": "morphisms/register_cash_move.rhai"
},
{
"name": "transfer_between_cajas",
"inputs": [
{ "role": "source", "entity": "Caja" },
{ "role": "dest", "entity": "Caja" }
],
"reads": ["source.saldo", "source.currency", "dest.saldo", "dest.currency"],
"writes": ["source.saldo", "dest.saldo", "Transferencia"],
"invariants": {
"conserve": [
{ "entity": "Caja", "field": "saldo", "group_by": "currency" }
]
},
"depends_on": [],
"script": "morphisms/transfer_between_cajas.rhai"
}
]
}
@@ -0,0 +1,35 @@
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"