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:
@@ -219,8 +219,9 @@ fn executor_load_module_rejects_cyclic_manifest() {
|
||||
let tmp = std::env::temp_dir().join(format!("nakui_cycle_{}", uuid::Uuid::new_v4()));
|
||||
std::fs::create_dir_all(tmp.join("morphisms")).unwrap();
|
||||
std::fs::write(
|
||||
tmp.join("schema.k"),
|
||||
"schema Caja:\n saldo: int\n check:\n saldo >= 0\n",
|
||||
tmp.join("schema.ncl"),
|
||||
// Schema Nickel mínimo (top-level Caja con saldo >= 0).
|
||||
"{\n Caja = {\n saldo | std.contract.from_predicate (fun n => std.is_number n && n >= 0),\n },\n}\n",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(tmp.join("morphisms/op.rhai"), "[]").unwrap();
|
||||
|
||||
@@ -132,11 +132,11 @@ fn overdraw_transfer_blocked_by_kcl_post_check() {
|
||||
);
|
||||
|
||||
match result {
|
||||
Err(ExecError::KclPost { role, entity, .. }) => {
|
||||
Err(ExecError::SchemaPost { role, entity, .. }) => {
|
||||
assert_eq!(role, "source");
|
||||
assert_eq!(entity, "Stock");
|
||||
}
|
||||
other => panic!("expected KclPost on source, got {:?}", other),
|
||||
other => panic!("expected SchemaPost on source, got {:?}", other),
|
||||
}
|
||||
assert_eq!(cantidad(&store, a), 100);
|
||||
assert_eq!(cantidad(&store, b), 0);
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
//! Layers exercised (in pipeline order):
|
||||
//! 1. CapabilityViolation (untracked write)
|
||||
//! 2. ConservationViolation (delta sum != 0)
|
||||
//! 3. KclPostCreate (created record fails its schema)
|
||||
//! 3. SchemaPostCreate (created record fails its schema)
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
@@ -47,7 +47,7 @@ fn build_executor(spec: MorphismSpec) -> Executor {
|
||||
// schema_path stays on the real treasury schema so we exercise the
|
||||
// production check blocks. `owned_bundle: false` so Drop leaves it
|
||||
// alone — it belongs to the source tree.
|
||||
schema_path: workspace_root().join("modules/treasury/schema.k"),
|
||||
schema_path: workspace_root().join("modules/treasury/schema.ncl"),
|
||||
rhai: RhaiExecutor::new_sandboxed(),
|
||||
owned_bundle: false,
|
||||
// Inline-built executors don't go through `load_module`, so they
|
||||
@@ -282,10 +282,10 @@ fn bad_created_record_blocks_negative_movimiento() {
|
||||
let result = exec.run(&mut store, "evil_create", &[("caja", caja_id)], params);
|
||||
|
||||
match result {
|
||||
Err(ExecError::KclPostCreate { entity, .. }) => {
|
||||
Err(ExecError::SchemaPostCreate { entity, .. }) => {
|
||||
assert_eq!(entity, "Movimiento");
|
||||
}
|
||||
other => panic!("expected KclPostCreate, got {:?}", other),
|
||||
other => panic!("expected SchemaPostCreate, got {:?}", other),
|
||||
}
|
||||
|
||||
// Caja unchanged, Movimiento never landed.
|
||||
|
||||
@@ -195,25 +195,26 @@ fn rejects_missing_schema_file() {
|
||||
|
||||
#[test]
|
||||
fn rejects_duplicate_schema_across_files() {
|
||||
// Synthesize a tempdir with two .k files that both declare `schema X`.
|
||||
// Synthesize a tempdir with two .ncl files that both declare
|
||||
// `Caja` en el record top-level.
|
||||
let tmp = std::env::temp_dir().join(format!("nakui_dup_{}", Uuid::new_v4()));
|
||||
fs::create_dir_all(&tmp).unwrap();
|
||||
fs::create_dir_all(tmp.join("morphisms")).unwrap();
|
||||
fs::write(
|
||||
tmp.join("a.k"),
|
||||
"schema Caja:\n saldo: int\n check:\n saldo >= 0\n",
|
||||
tmp.join("a.ncl"),
|
||||
"{\n Caja = { saldo | Number },\n}\n",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
tmp.join("b.k"),
|
||||
"schema Caja:\n monto: int\n check:\n monto >= 0\n",
|
||||
tmp.join("b.ncl"),
|
||||
"{\n Caja = { monto | Number },\n}\n",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(tmp.join("morphisms/op.rhai"), "[]").unwrap();
|
||||
|
||||
let m = Manifest {
|
||||
module: "dup".into(),
|
||||
schemas: vec!["a.k".into(), "b.k".into()],
|
||||
schemas: vec!["a.ncl".into(), "b.ncl".into()],
|
||||
morphisms: vec![MorphismSpec {
|
||||
name: "op".into(),
|
||||
inputs: vec![MorphismInput {
|
||||
@@ -231,8 +232,8 @@ fn rejects_duplicate_schema_across_files() {
|
||||
match m.validate(&tmp) {
|
||||
Err(ValidationError::DuplicateSchema { name, files }) => {
|
||||
assert_eq!(name, "Caja");
|
||||
assert!(files.contains(&"a.k".to_string()));
|
||||
assert!(files.contains(&"b.k".to_string()));
|
||||
assert!(files.contains(&"a.ncl".to_string()));
|
||||
assert!(files.contains(&"b.ncl".to_string()));
|
||||
}
|
||||
other => panic!("expected DuplicateSchema, got {:?}", other),
|
||||
}
|
||||
@@ -247,8 +248,8 @@ fn executor_load_module_runs_validation() {
|
||||
let tmp = std::env::temp_dir().join(format!("nakui_bad_{}", Uuid::new_v4()));
|
||||
fs::create_dir_all(&tmp).unwrap();
|
||||
fs::write(
|
||||
tmp.join("schema.k"),
|
||||
"schema Caja:\n saldo: int\n check:\n saldo >= 0\n",
|
||||
tmp.join("schema.ncl"),
|
||||
"{\n Caja = {\n saldo | std.contract.from_predicate (fun n => std.is_number n && n >= 0),\n },\n}\n",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
|
||||
@@ -120,11 +120,11 @@ fn overdraw_stock_rejected_by_inventory_post_check() {
|
||||
);
|
||||
|
||||
match result {
|
||||
Err(ExecError::KclPost { role, entity, .. }) => {
|
||||
Err(ExecError::SchemaPost { role, entity, .. }) => {
|
||||
assert_eq!(role, "stock");
|
||||
assert_eq!(entity, "Stock");
|
||||
}
|
||||
other => panic!("expected KclPost on stock, got {:?}", other),
|
||||
other => panic!("expected SchemaPost on stock, got {:?}", other),
|
||||
}
|
||||
assert_eq!(stock_cantidad(&store, stock), 500);
|
||||
assert_eq!(caja_saldo(&store, caja), 1_000_000);
|
||||
|
||||
@@ -348,10 +348,10 @@ fn verify_log_rejects_seed_after_schema_kcl_changes() {
|
||||
seed_caja(&exec, &mut store, &mut log, id);
|
||||
}
|
||||
|
||||
// Mutate schema.k. Even a comment is enough — bundle hash is byte-
|
||||
// Mutate schema.ncl. Even a comment is enough — bundle hash is byte-
|
||||
// level for the same false-positive-over-false-negative reason as
|
||||
// morphism hashes.
|
||||
let schema_path = temp.path.join("schema.k");
|
||||
let schema_path = temp.path.join("schema.ncl");
|
||||
let original = std::fs::read_to_string(&schema_path).expect("read schema");
|
||||
std::fs::write(
|
||||
&schema_path,
|
||||
@@ -361,7 +361,7 @@ fn verify_log_rejects_seed_after_schema_kcl_changes() {
|
||||
|
||||
let exec2 = Executor::load_module(&temp.path).expect("reload v2");
|
||||
let new_hash = exec2.schema_bundle_hash;
|
||||
assert_ne!(original_hash, new_hash, "schema.k byte change must move the bundle hash");
|
||||
assert_ne!(original_hash, new_hash, "schema.ncl byte change must move the bundle hash");
|
||||
|
||||
let log = EventLog::open(&log_path).unwrap();
|
||||
match verify_log(&log, &exec2) {
|
||||
@@ -410,13 +410,13 @@ fn comment_only_edits_do_not_invalidate_the_hash() {
|
||||
"comment-only and whitespace-only edits must not move the hash"
|
||||
);
|
||||
|
||||
// Sanity: the bundle hash also stays intact (we didn't touch schema.k).
|
||||
// Sanity: the bundle hash also stays intact (we didn't touch schema.ncl).
|
||||
assert_eq!(exec1.schema_bundle_hash, exec2.schema_bundle_hash);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn morphism_script_change_does_not_flag_unrelated_seeds() {
|
||||
// Bundle hash covers schema.k only — a .rhai edit moves the
|
||||
// Bundle hash covers schema.ncl only — a .rhai edit moves the
|
||||
// morphism hash but leaves the bundle hash alone. So existing
|
||||
// seeds verify cleanly even when a morphism's behaviour changed.
|
||||
let temp = TempModule::from(&treasury_module());
|
||||
|
||||
Reference in New Issue
Block a user