feat(nakui): módulo crm — clientes, pipeline de ventas, interacciones
Módulo CRM declarativo (schema.ncl + nsmc.json + morfismos Rhai) con tres entities (Cliente, Oportunidad, Interaccion) y tres morfismos: abrir_oportunidad, mover_oportunidad (pipeline con validación de transiciones) y registrar_interaccion. crm_demo: demo realista de 18 eventos que —a diferencia de los otros demos— conserva el event log e imprime el comando de nakui-explorer, así el explorador muestra un CRM con cuerpo. tests/crm.rs: 8 tests. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,307 @@
|
||||
//! Demo del módulo `crm`: un escenario realista — tres clientes, sus
|
||||
//! oportunidades recorriendo el pipeline de ventas, e interacciones.
|
||||
//!
|
||||
//! A diferencia de los otros demos, **no borra el event log**: lo deja
|
||||
//! en disco para que `nakui-explorer` lo muestre. Al terminar imprime
|
||||
//! el comando exacto para abrir el explorador sobre este log.
|
||||
//!
|
||||
//! ```sh
|
||||
//! cargo run -p nakui-core --bin crm_demo
|
||||
//! # …luego, con la ruta que imprime:
|
||||
//! NAKUI_EVENT_LOG=/tmp/nakui-crm.jsonl cargo run -p nakui-explorer
|
||||
//! ```
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use nakui_core::event_log::{execute_and_log, seed_and_log, EventLog, ExecuteError, LogEntry};
|
||||
use nakui_core::executor::Executor;
|
||||
use nakui_core::store::{MemoryStore, Store};
|
||||
use serde_json::json;
|
||||
use uuid::Uuid;
|
||||
|
||||
const TS: &str = "2026-05-21T12:00:00Z";
|
||||
|
||||
fn main() {
|
||||
let module_dir = std::env::var("NAKUI_MODULE")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|_| {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.expect("dir del módulo nakui sobre core/")
|
||||
.join("modules/crm")
|
||||
});
|
||||
let exec = Executor::load_module(&module_dir).expect("cargar el módulo crm");
|
||||
|
||||
let log_path = std::env::var("NAKUI_EVENT_LOG")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|_| std::env::temp_dir().join("nakui-crm.jsonl"));
|
||||
let _ = std::fs::remove_file(&log_path); // empezar de cero
|
||||
let mut log = EventLog::open(&log_path).expect("abrir el event log");
|
||||
let mut store = MemoryStore::new();
|
||||
|
||||
// --- Seed: tres clientes -------------------------------------------
|
||||
section("seed · 3 clientes");
|
||||
let acme = Uuid::new_v4();
|
||||
let beta = Uuid::new_v4();
|
||||
let gamma = Uuid::new_v4();
|
||||
seed_cliente(
|
||||
&exec,
|
||||
&mut store,
|
||||
&mut log,
|
||||
acme,
|
||||
"Acme Corp",
|
||||
"compras@acme.com",
|
||||
);
|
||||
seed_cliente(&exec, &mut store, &mut log, beta, "Beta SA", "ti@beta.com");
|
||||
seed_cliente(
|
||||
&exec,
|
||||
&mut store,
|
||||
&mut log,
|
||||
gamma,
|
||||
"Gamma Ltda",
|
||||
"ceo@gamma.com",
|
||||
);
|
||||
|
||||
// --- Acme: una oportunidad que se gana -----------------------------
|
||||
section("Acme · «Licencia anual» $12 000 — recorre el pipeline");
|
||||
let opp_acme = Uuid::new_v4();
|
||||
abrir(
|
||||
&exec,
|
||||
&mut store,
|
||||
&mut log,
|
||||
acme,
|
||||
opp_acme,
|
||||
"Licencia anual",
|
||||
12_000,
|
||||
);
|
||||
interaccion(
|
||||
&exec,
|
||||
&mut store,
|
||||
&mut log,
|
||||
acme,
|
||||
"llamada",
|
||||
"Primer contacto, interés alto",
|
||||
);
|
||||
for etapa in ["calificado", "propuesta", "negociacion", "ganada"] {
|
||||
mover(&exec, &mut store, &mut log, opp_acme, etapa);
|
||||
}
|
||||
interaccion(
|
||||
&exec,
|
||||
&mut store,
|
||||
&mut log,
|
||||
acme,
|
||||
"email",
|
||||
"Contrato firmado recibido",
|
||||
);
|
||||
|
||||
// --- Beta: una oportunidad que se pierde ---------------------------
|
||||
section("Beta · «Piloto trimestral» $3 000 — se pierde");
|
||||
let opp_beta = Uuid::new_v4();
|
||||
abrir(
|
||||
&exec,
|
||||
&mut store,
|
||||
&mut log,
|
||||
beta,
|
||||
opp_beta,
|
||||
"Piloto trimestral",
|
||||
3_000,
|
||||
);
|
||||
interaccion(
|
||||
&exec,
|
||||
&mut store,
|
||||
&mut log,
|
||||
beta,
|
||||
"reunion",
|
||||
"Demo en sus oficinas",
|
||||
);
|
||||
mover(&exec, &mut store, &mut log, opp_beta, "calificado");
|
||||
mover(&exec, &mut store, &mut log, opp_beta, "propuesta");
|
||||
mover(&exec, &mut store, &mut log, opp_beta, "perdida");
|
||||
|
||||
// --- Gamma: una oportunidad en curso -------------------------------
|
||||
section("Gamma · «Expansión regional» $25 000 — en curso");
|
||||
let opp_gamma = Uuid::new_v4();
|
||||
abrir(
|
||||
&exec,
|
||||
&mut store,
|
||||
&mut log,
|
||||
gamma,
|
||||
opp_gamma,
|
||||
"Expansión regional",
|
||||
25_000,
|
||||
);
|
||||
mover(&exec, &mut store, &mut log, opp_gamma, "calificado");
|
||||
interaccion(
|
||||
&exec,
|
||||
&mut store,
|
||||
&mut log,
|
||||
gamma,
|
||||
"llamada",
|
||||
"Pidieron referencias",
|
||||
);
|
||||
|
||||
// --- Operaciones inválidas: el kernel las rechaza, no se loguean ---
|
||||
section("validaciones · estas operaciones se rechazan");
|
||||
mover(&exec, &mut store, &mut log, opp_acme, "propuesta"); // ya cerrada
|
||||
mover(&exec, &mut store, &mut log, opp_gamma, "prospecto"); // retroceso
|
||||
abrir(
|
||||
&exec,
|
||||
&mut store,
|
||||
&mut log,
|
||||
gamma,
|
||||
Uuid::new_v4(),
|
||||
"Trato inválido",
|
||||
-500,
|
||||
);
|
||||
interaccion(
|
||||
&exec,
|
||||
&mut store,
|
||||
&mut log,
|
||||
gamma,
|
||||
"paloma",
|
||||
"canal inexistente",
|
||||
);
|
||||
|
||||
// --- Estado final --------------------------------------------------
|
||||
section("estado final · oportunidades");
|
||||
print_oportunidad(&store, "Acme ", opp_acme);
|
||||
print_oportunidad(&store, "Beta ", opp_beta);
|
||||
print_oportunidad(&store, "Gamma", opp_gamma);
|
||||
|
||||
let entries = log.entries().expect("leer el log");
|
||||
let seeds = entries
|
||||
.iter()
|
||||
.filter(|e| matches!(e, LogEntry::Seed { .. }))
|
||||
.count();
|
||||
let morphs = entries.len() - seeds;
|
||||
section(&format!(
|
||||
"log · {} eventos ({seeds} seeds, {morphs} morfismos)",
|
||||
entries.len()
|
||||
));
|
||||
println!(" archivo: {}", log_path.display());
|
||||
println!();
|
||||
println!("para ver el módulo CRM en el explorador:");
|
||||
println!(
|
||||
" NAKUI_EVENT_LOG={} cargo run -p nakui-explorer",
|
||||
log_path.display()
|
||||
);
|
||||
}
|
||||
|
||||
fn seed_cliente(
|
||||
exec: &Executor,
|
||||
store: &mut MemoryStore,
|
||||
log: &mut EventLog,
|
||||
id: Uuid,
|
||||
nombre: &str,
|
||||
email: &str,
|
||||
) {
|
||||
seed_and_log(
|
||||
exec,
|
||||
store,
|
||||
log,
|
||||
"Cliente",
|
||||
id,
|
||||
json!({
|
||||
"id": id.to_string(),
|
||||
"nombre": nombre,
|
||||
"email": email,
|
||||
"empresa": nombre,
|
||||
}),
|
||||
)
|
||||
.unwrap_or_else(|e| panic!("seed cliente {nombre}: {e}"));
|
||||
println!(" ok · cliente {nombre}");
|
||||
}
|
||||
|
||||
fn abrir(
|
||||
exec: &Executor,
|
||||
store: &mut MemoryStore,
|
||||
log: &mut EventLog,
|
||||
cliente: Uuid,
|
||||
opp: Uuid,
|
||||
titulo: &str,
|
||||
monto: i64,
|
||||
) {
|
||||
report(
|
||||
&format!("abrir_oportunidad «{titulo}»"),
|
||||
execute_and_log(
|
||||
exec,
|
||||
store,
|
||||
log,
|
||||
"abrir_oportunidad",
|
||||
&[("cliente", cliente)],
|
||||
json!({
|
||||
"oportunidad_id": opp.to_string(),
|
||||
"titulo": titulo,
|
||||
"monto": monto,
|
||||
"currency": "USD",
|
||||
"timestamp": TS,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
fn mover(exec: &Executor, store: &mut MemoryStore, log: &mut EventLog, opp: Uuid, destino: &str) {
|
||||
report(
|
||||
&format!("mover_oportunidad → {destino}"),
|
||||
execute_and_log(
|
||||
exec,
|
||||
store,
|
||||
log,
|
||||
"mover_oportunidad",
|
||||
&[("oportunidad", opp)],
|
||||
json!({ "etapa": destino, "timestamp": TS }),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
fn interaccion(
|
||||
exec: &Executor,
|
||||
store: &mut MemoryStore,
|
||||
log: &mut EventLog,
|
||||
cliente: Uuid,
|
||||
canal: &str,
|
||||
nota: &str,
|
||||
) {
|
||||
report(
|
||||
&format!("registrar_interaccion ({canal})"),
|
||||
execute_and_log(
|
||||
exec,
|
||||
store,
|
||||
log,
|
||||
"registrar_interaccion",
|
||||
&[("cliente", cliente)],
|
||||
json!({
|
||||
"interaccion_id": Uuid::new_v4().to_string(),
|
||||
"canal": canal,
|
||||
"nota": nota,
|
||||
"timestamp": TS,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Reporta el resultado de un morfismo. Genérico sobre el tipo de op
|
||||
/// para no exponer el tipo interno del executor.
|
||||
fn report<T>(label: &str, result: Result<Vec<T>, ExecuteError>) {
|
||||
match result {
|
||||
Ok(ops) => println!(" ok · {label} ({} ops)", ops.len()),
|
||||
Err(ExecuteError::PreLog(e)) => println!(" rechazado · {label}: {e}"),
|
||||
Err(e) => println!(" ERROR · {label}: {e:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn print_oportunidad(store: &MemoryStore, etiqueta: &str, id: Uuid) {
|
||||
match store.load("Oportunidad", id) {
|
||||
Some(v) => {
|
||||
let titulo = v.get("titulo").and_then(|x| x.as_str()).unwrap_or("?");
|
||||
let etapa = v.get("etapa").and_then(|x| x.as_str()).unwrap_or("?");
|
||||
let monto = v.get("monto").and_then(|x| x.as_i64()).unwrap_or(0);
|
||||
println!(" {etiqueta} · {titulo} — ${monto} — etapa: {etapa}");
|
||||
}
|
||||
None => println!(" {etiqueta} · (sin oportunidad)"),
|
||||
}
|
||||
}
|
||||
|
||||
fn section(title: &str) {
|
||||
println!("\n— {title}");
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
use nakui_core::event_log::{
|
||||
EventLog, ExecuteError, execute_and_log, replay, seed_and_log, verify_log,
|
||||
execute_and_log, replay, seed_and_log, verify_log, EventLog, ExecuteError,
|
||||
};
|
||||
use nakui_core::executor::Executor;
|
||||
use nakui_core::store::{MemoryStore, Store};
|
||||
@@ -7,8 +7,7 @@ use serde_json::json;
|
||||
use uuid::Uuid;
|
||||
|
||||
fn main() {
|
||||
let module_dir =
|
||||
std::env::var("NAKUI_MODULE").unwrap_or_else(|_| "modules/treasury".into());
|
||||
let module_dir = std::env::var("NAKUI_MODULE").unwrap_or_else(|_| "modules/treasury".into());
|
||||
let exec = Executor::load_module(&module_dir).expect("load module");
|
||||
|
||||
let log_path = std::env::temp_dir().join(format!("nakui_demo_{}.jsonl", Uuid::new_v4()));
|
||||
@@ -162,10 +161,7 @@ fn main() {
|
||||
seq, entity, id, ..
|
||||
} => println!(" #{:02} seed {} {}", seq, entity, id),
|
||||
nakui_core::event_log::LogEntry::Morphism {
|
||||
seq,
|
||||
morphism,
|
||||
ops,
|
||||
..
|
||||
seq, morphism, ops, ..
|
||||
} => println!(" #{:02} morph {} ({} ops)", seq, morphism, ops.len()),
|
||||
}
|
||||
}
|
||||
@@ -180,16 +176,17 @@ fn main() {
|
||||
|
||||
section("== determinism verification (ops) ==");
|
||||
match verify_log(&log, &exec) {
|
||||
Ok(()) => println!(
|
||||
" ok: every logged morphism reproduced its ops on re-execution"
|
||||
),
|
||||
Ok(()) => println!(" ok: every logged morphism reproduced its ops on re-execution"),
|
||||
Err(e) => println!(" nondeterminism detected: {}", e),
|
||||
}
|
||||
|
||||
if std::env::var_os("NAKUI_DEMO_KEEP").is_none() {
|
||||
let _ = std::fs::remove_file(&log_path);
|
||||
} else {
|
||||
println!("\n(NAKUI_DEMO_KEEP set — keeping log at {})", log_path.display());
|
||||
println!(
|
||||
"\n(NAKUI_DEMO_KEEP set — keeping log at {})",
|
||||
log_path.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,7 +199,11 @@ fn run_and_report(
|
||||
params: serde_json::Value,
|
||||
) {
|
||||
match execute_and_log(exec, store, log, morphism, inputs, params) {
|
||||
Ok(ops) => println!(" ok ({} ops, logged at #{})", ops.len(), log.next_seq() - 1),
|
||||
Ok(ops) => println!(
|
||||
" ok ({} ops, logged at #{})",
|
||||
ops.len(),
|
||||
log.next_seq() - 1
|
||||
),
|
||||
Err(ExecuteError::PreLog(e)) => println!(" rejected: {}", e),
|
||||
Err(ExecuteError::LogAppend(e)) => println!(" LOG APPEND FAILED: {}", e),
|
||||
Err(ExecuteError::PostLogStore(e)) => println!(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use nakui_core::event_log::{
|
||||
EventLog, ExecuteError, execute_and_log, replay, seed_and_log, verify_log,
|
||||
execute_and_log, replay, seed_and_log, verify_log, EventLog, ExecuteError,
|
||||
};
|
||||
use nakui_core::executor::Executor;
|
||||
use nakui_core::store::{MemoryStore, Store};
|
||||
@@ -7,12 +7,10 @@ use serde_json::json;
|
||||
use uuid::Uuid;
|
||||
|
||||
fn main() {
|
||||
let module_dir = std::env::var("NAKUI_MODULE")
|
||||
.unwrap_or_else(|_| "modules/inventory".into());
|
||||
let module_dir = std::env::var("NAKUI_MODULE").unwrap_or_else(|_| "modules/inventory".into());
|
||||
let exec = Executor::load_module(&module_dir).expect("load module");
|
||||
|
||||
let log_path =
|
||||
std::env::temp_dir().join(format!("nakui_inv_{}.jsonl", Uuid::new_v4()));
|
||||
let log_path = std::env::temp_dir().join(format!("nakui_inv_{}.jsonl", Uuid::new_v4()));
|
||||
let mut log = EventLog::open(&log_path).expect("open log");
|
||||
let mut store = MemoryStore::new();
|
||||
|
||||
@@ -23,34 +21,46 @@ fn main() {
|
||||
let stock_c = Uuid::new_v4();
|
||||
seed_and_log(
|
||||
&exec,
|
||||
&mut store, &mut log, "Stock", stock_a,
|
||||
&mut store,
|
||||
&mut log,
|
||||
"Stock",
|
||||
stock_a,
|
||||
json!({
|
||||
"id": stock_a.to_string(),
|
||||
"sku_id": "kg-cafe-honduras-2026",
|
||||
"ubicacion": "almacen-norte",
|
||||
"cantidad": 500_i64,
|
||||
}),
|
||||
).expect("seed A");
|
||||
)
|
||||
.expect("seed A");
|
||||
seed_and_log(
|
||||
&exec,
|
||||
&mut store, &mut log, "Stock", stock_b,
|
||||
&mut store,
|
||||
&mut log,
|
||||
"Stock",
|
||||
stock_b,
|
||||
json!({
|
||||
"id": stock_b.to_string(),
|
||||
"sku_id": "kg-cafe-honduras-2026",
|
||||
"ubicacion": "almacen-sur",
|
||||
"cantidad": 100_i64,
|
||||
}),
|
||||
).expect("seed B");
|
||||
)
|
||||
.expect("seed B");
|
||||
seed_and_log(
|
||||
&exec,
|
||||
&mut store, &mut log, "Stock", stock_c,
|
||||
&mut store,
|
||||
&mut log,
|
||||
"Stock",
|
||||
stock_c,
|
||||
json!({
|
||||
"id": stock_c.to_string(),
|
||||
"sku_id": "lt-aceite-girasol",
|
||||
"ubicacion": "almacen-sur",
|
||||
"cantidad": 200_i64,
|
||||
}),
|
||||
).expect("seed C");
|
||||
)
|
||||
.expect("seed C");
|
||||
|
||||
section("== seed ==");
|
||||
print_stock(&store, "A (cafe norte)", stock_a);
|
||||
@@ -58,7 +68,11 @@ fn main() {
|
||||
print_stock(&store, "C (aceite sur)", stock_c);
|
||||
|
||||
section("== recibir 250 kg cafe en A ==");
|
||||
run_and_report(&exec, &mut store, &mut log, "recibir_stock",
|
||||
run_and_report(
|
||||
&exec,
|
||||
&mut store,
|
||||
&mut log,
|
||||
"recibir_stock",
|
||||
&[("stock", stock_a)],
|
||||
json!({
|
||||
"cantidad": 250_i64,
|
||||
@@ -69,7 +83,11 @@ fn main() {
|
||||
print_stock(&store, "A", stock_a);
|
||||
|
||||
section("== transferir 200 kg cafe A -> B (conserva por sku_id) ==");
|
||||
run_and_report(&exec, &mut store, &mut log, "transferir_stock",
|
||||
run_and_report(
|
||||
&exec,
|
||||
&mut store,
|
||||
&mut log,
|
||||
"transferir_stock",
|
||||
&[("source", stock_a), ("dest", stock_b)],
|
||||
json!({
|
||||
"cantidad": 200_i64,
|
||||
@@ -81,7 +99,11 @@ fn main() {
|
||||
print_stock(&store, "B", stock_b);
|
||||
|
||||
section("== transferir 999_999 kg cafe A -> B (reject: stock <= 0) ==");
|
||||
run_and_report(&exec, &mut store, &mut log, "transferir_stock",
|
||||
run_and_report(
|
||||
&exec,
|
||||
&mut store,
|
||||
&mut log,
|
||||
"transferir_stock",
|
||||
&[("source", stock_a), ("dest", stock_b)],
|
||||
json!({
|
||||
"cantidad": 999_999_i64,
|
||||
@@ -91,7 +113,11 @@ fn main() {
|
||||
);
|
||||
|
||||
section("== transferir 50 cafe(A) -> aceite(C) (reject: rhai SKU mismatch) ==");
|
||||
run_and_report(&exec, &mut store, &mut log, "transferir_stock",
|
||||
run_and_report(
|
||||
&exec,
|
||||
&mut store,
|
||||
&mut log,
|
||||
"transferir_stock",
|
||||
&[("source", stock_a), ("dest", stock_c)],
|
||||
json!({
|
||||
"cantidad": 50_i64,
|
||||
@@ -113,10 +139,12 @@ fn main() {
|
||||
));
|
||||
for e in &entries {
|
||||
match e {
|
||||
nakui_core::event_log::LogEntry::Seed { seq, entity, id, .. } =>
|
||||
println!(" #{:02} seed {} {}", seq, entity, id),
|
||||
nakui_core::event_log::LogEntry::Morphism { seq, morphism, ops, .. } =>
|
||||
println!(" #{:02} morph {} ({} ops)", seq, morphism, ops.len()),
|
||||
nakui_core::event_log::LogEntry::Seed {
|
||||
seq, entity, id, ..
|
||||
} => println!(" #{:02} seed {} {}", seq, entity, id),
|
||||
nakui_core::event_log::LogEntry::Morphism {
|
||||
seq, morphism, ops, ..
|
||||
} => println!(" #{:02} morph {} ({} ops)", seq, morphism, ops.len()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,9 +158,7 @@ fn main() {
|
||||
|
||||
section("== determinism verification (ops) ==");
|
||||
match verify_log(&log, &exec) {
|
||||
Ok(()) => println!(
|
||||
" ok: every logged morphism reproduced its ops on re-execution"
|
||||
),
|
||||
Ok(()) => println!(" ok: every logged morphism reproduced its ops on re-execution"),
|
||||
Err(e) => println!(" nondeterminism detected: {}", e),
|
||||
}
|
||||
|
||||
@@ -148,11 +174,16 @@ fn run_and_report(
|
||||
params: serde_json::Value,
|
||||
) {
|
||||
match execute_and_log(exec, store, log, morphism, inputs, params) {
|
||||
Ok(ops) => println!(" ok ({} ops, logged at #{})", ops.len(), log.next_seq() - 1),
|
||||
Ok(ops) => println!(
|
||||
" ok ({} ops, logged at #{})",
|
||||
ops.len(),
|
||||
log.next_seq() - 1
|
||||
),
|
||||
Err(ExecuteError::PreLog(e)) => println!(" rejected: {}", e),
|
||||
Err(ExecuteError::LogAppend(e)) => println!(" LOG APPEND FAILED: {}", e),
|
||||
Err(ExecuteError::PostLogStore(e)) => println!(
|
||||
" POST-LOG STORE FAILED (log canonical, store stale): {}", e
|
||||
" POST-LOG STORE FAILED (log canonical, store stale): {}",
|
||||
e
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -162,7 +193,10 @@ fn print_stock(store: &MemoryStore, label: &str, id: Uuid) {
|
||||
let cantidad = v.get("cantidad").and_then(|v| v.as_i64()).unwrap_or(0);
|
||||
let sku = v.get("sku_id").and_then(|v| v.as_str()).unwrap_or("?");
|
||||
let loc = v.get("ubicacion").and_then(|v| v.as_str()).unwrap_or("?");
|
||||
println!(" {}: cantidad={} sku={} ubic={}", label, cantidad, sku, loc);
|
||||
println!(
|
||||
" {}: cantidad={} sku={} ubic={}",
|
||||
label, cantidad, sku, loc
|
||||
);
|
||||
}
|
||||
|
||||
fn section(title: &str) {
|
||||
|
||||
@@ -13,10 +13,8 @@ use std::collections::BTreeMap;
|
||||
use std::path::PathBuf;
|
||||
use std::process::ExitCode;
|
||||
|
||||
use nakui_core::drift::{DriftDiff, check_against_socket};
|
||||
use nakui_core::event_log::{
|
||||
EventLog, LogEntry, Snapshot, replay_with_snapshot_into, verify_log,
|
||||
};
|
||||
use nakui_core::drift::{check_against_socket, DriftDiff};
|
||||
use nakui_core::event_log::{replay_with_snapshot_into, verify_log, EventLog, LogEntry, Snapshot};
|
||||
use nakui_core::executor::Executor;
|
||||
use nakui_core::run::run_server;
|
||||
use nakui_core::store::MemoryStore;
|
||||
@@ -111,19 +109,16 @@ fn parse_flags(args: &[String], allowed: &[&str]) -> Result<BTreeMap<String, Str
|
||||
if !allowed.contains(&name) {
|
||||
return Err(CliError::BadArgs(format!("unknown flag `--{}`", name)));
|
||||
}
|
||||
let val = args.get(i + 1).ok_or_else(|| {
|
||||
CliError::BadArgs(format!("flag `--{}` requires a value", name))
|
||||
})?;
|
||||
let val = args
|
||||
.get(i + 1)
|
||||
.ok_or_else(|| CliError::BadArgs(format!("flag `--{}` requires a value", name)))?;
|
||||
out.insert(name.to_string(), val.clone());
|
||||
i += 2;
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn require<'a>(
|
||||
flags: &'a BTreeMap<String, String>,
|
||||
name: &str,
|
||||
) -> Result<&'a String, CliError> {
|
||||
fn require<'a>(flags: &'a BTreeMap<String, String>, name: &str) -> Result<&'a String, CliError> {
|
||||
flags
|
||||
.get(name)
|
||||
.ok_or_else(|| CliError::BadArgs(format!("missing required flag `--{}`", name)))
|
||||
@@ -141,7 +136,11 @@ fn cmd_inspect(args: &[String]) -> Result<(), CliError> {
|
||||
if entries.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
println!("seq range: {}..={}", entries[0].seq(), entries.last().unwrap().seq());
|
||||
println!(
|
||||
"seq range: {}..={}",
|
||||
entries[0].seq(),
|
||||
entries.last().unwrap().seq()
|
||||
);
|
||||
println!();
|
||||
for e in &entries {
|
||||
match e {
|
||||
@@ -195,18 +194,18 @@ fn cmd_replay(args: &[String]) -> Result<(), CliError> {
|
||||
let entries = log
|
||||
.entries()
|
||||
.map_err(|e| CliError::Op(format!("read log: {}", e)))?;
|
||||
let last_seq = entries.last().map(|e| e.seq().to_string()).unwrap_or_else(|| "<empty>".into());
|
||||
let last_seq = entries
|
||||
.last()
|
||||
.map(|e| e.seq().to_string())
|
||||
.unwrap_or_else(|| "<empty>".into());
|
||||
println!("replayed log: {}", log.path().display());
|
||||
if let Some(snap) = &snapshot {
|
||||
println!("snapshot: seq {} (covers seq <= {})", snap.seq, snap.seq);
|
||||
}
|
||||
println!("last seq: {}", last_seq);
|
||||
println!("entities:");
|
||||
let mut by_entity: Vec<(&String, usize)> = store
|
||||
.records()
|
||||
.iter()
|
||||
.map(|(k, v)| (k, v.len()))
|
||||
.collect();
|
||||
let mut by_entity: Vec<(&String, usize)> =
|
||||
store.records().iter().map(|(k, v)| (k, v.len())).collect();
|
||||
by_entity.sort_by(|a, b| a.0.cmp(b.0));
|
||||
if by_entity.is_empty() {
|
||||
println!(" (none)");
|
||||
@@ -414,7 +413,8 @@ fn cmd_compact(args: &[String]) -> Result<(), CliError> {
|
||||
let snap = Snapshot::load(&snap_path)
|
||||
.map_err(|e| CliError::Op(format!("load snapshot: {}", e)))?
|
||||
.ok_or_else(|| CliError::Op(format!("snapshot not found: {}", snap_path.display())))?;
|
||||
let mut log = EventLog::open(&log_path).map_err(|e| CliError::Op(format!("open log: {}", e)))?;
|
||||
let mut log =
|
||||
EventLog::open(&log_path).map_err(|e| CliError::Op(format!("open log: {}", e)))?;
|
||||
let before = log
|
||||
.entries()
|
||||
.map(|es| es.len())
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
//! against all three.
|
||||
|
||||
use nakui_core::event_log::{
|
||||
EventLog, ExecuteError, execute_and_log, replay, seed_and_log, verify_log,
|
||||
execute_and_log, replay, seed_and_log, verify_log, EventLog, ExecuteError,
|
||||
};
|
||||
use nakui_core::executor::Executor;
|
||||
use nakui_core::store::{MemoryStore, Store};
|
||||
@@ -13,12 +13,10 @@ use serde_json::json;
|
||||
use uuid::Uuid;
|
||||
|
||||
fn main() {
|
||||
let module_dir = std::env::var("NAKUI_MODULE")
|
||||
.unwrap_or_else(|_| "modules/sales".into());
|
||||
let module_dir = std::env::var("NAKUI_MODULE").unwrap_or_else(|_| "modules/sales".into());
|
||||
let exec = Executor::load_module(&module_dir).expect("load module");
|
||||
|
||||
let log_path =
|
||||
std::env::temp_dir().join(format!("nakui_sales_{}.jsonl", Uuid::new_v4()));
|
||||
let log_path = std::env::temp_dir().join(format!("nakui_sales_{}.jsonl", Uuid::new_v4()));
|
||||
let mut log = EventLog::open(&log_path).expect("open log");
|
||||
let mut store = MemoryStore::new();
|
||||
|
||||
@@ -26,24 +24,32 @@ fn main() {
|
||||
let caja_id = Uuid::new_v4();
|
||||
seed_and_log(
|
||||
&exec,
|
||||
&mut store, &mut log, "Stock", stock_id,
|
||||
&mut store,
|
||||
&mut log,
|
||||
"Stock",
|
||||
stock_id,
|
||||
json!({
|
||||
"id": stock_id.to_string(),
|
||||
"sku_id": "kg-cafe-honduras-2026",
|
||||
"ubicacion": "almacen-norte",
|
||||
"cantidad": 500_i64,
|
||||
}),
|
||||
).expect("seed stock");
|
||||
)
|
||||
.expect("seed stock");
|
||||
seed_and_log(
|
||||
&exec,
|
||||
&mut store, &mut log, "Caja", caja_id,
|
||||
&mut store,
|
||||
&mut log,
|
||||
"Caja",
|
||||
caja_id,
|
||||
json!({
|
||||
"id": caja_id.to_string(),
|
||||
"name": "Caja Principal",
|
||||
"saldo": 1_000_000_i64, // $10_000.00 in cents
|
||||
"currency": "USD",
|
||||
}),
|
||||
).expect("seed caja");
|
||||
)
|
||||
.expect("seed caja");
|
||||
|
||||
section("== seed ==");
|
||||
print_stock(&store, "stock", stock_id);
|
||||
@@ -51,7 +57,11 @@ fn main() {
|
||||
|
||||
// 1. Sell 100 kg cafe at $50.00 / kg = $5000.00 total.
|
||||
section("== vender 100 kg @ $50.00 c/u ==");
|
||||
run_and_report(&exec, &mut store, &mut log, "vender",
|
||||
run_and_report(
|
||||
&exec,
|
||||
&mut store,
|
||||
&mut log,
|
||||
"vender",
|
||||
&[("stock", stock_id), ("caja", caja_id)],
|
||||
json!({
|
||||
"cantidad": 100_i64,
|
||||
@@ -65,7 +75,11 @@ fn main() {
|
||||
|
||||
// 2. Try selling more than available stock — should fail Stock post-check.
|
||||
section("== vender 9999 kg (reject: stock <= 0) ==");
|
||||
run_and_report(&exec, &mut store, &mut log, "vender",
|
||||
run_and_report(
|
||||
&exec,
|
||||
&mut store,
|
||||
&mut log,
|
||||
"vender",
|
||||
&[("stock", stock_id), ("caja", caja_id)],
|
||||
json!({
|
||||
"cantidad": 9999_i64,
|
||||
@@ -77,7 +91,11 @@ fn main() {
|
||||
|
||||
// 3. Negative price — caught by Rhai.
|
||||
section("== vender con precio negativo (reject: rhai throw) ==");
|
||||
run_and_report(&exec, &mut store, &mut log, "vender",
|
||||
run_and_report(
|
||||
&exec,
|
||||
&mut store,
|
||||
&mut log,
|
||||
"vender",
|
||||
&[("stock", stock_id), ("caja", caja_id)],
|
||||
json!({
|
||||
"cantidad": 10_i64,
|
||||
@@ -89,7 +107,11 @@ fn main() {
|
||||
|
||||
// 4. Another good sale.
|
||||
section("== vender 50 kg @ $60.00 c/u ==");
|
||||
run_and_report(&exec, &mut store, &mut log, "vender",
|
||||
run_and_report(
|
||||
&exec,
|
||||
&mut store,
|
||||
&mut log,
|
||||
"vender",
|
||||
&[("stock", stock_id), ("caja", caja_id)],
|
||||
json!({
|
||||
"cantidad": 50_i64,
|
||||
@@ -113,10 +135,12 @@ fn main() {
|
||||
));
|
||||
for e in &entries {
|
||||
match e {
|
||||
nakui_core::event_log::LogEntry::Seed { seq, entity, id, .. } =>
|
||||
println!(" #{:02} seed {} {}", seq, entity, id),
|
||||
nakui_core::event_log::LogEntry::Morphism { seq, morphism, ops, .. } =>
|
||||
println!(" #{:02} morph {} ({} ops)", seq, morphism, ops.len()),
|
||||
nakui_core::event_log::LogEntry::Seed {
|
||||
seq, entity, id, ..
|
||||
} => println!(" #{:02} seed {} {}", seq, entity, id),
|
||||
nakui_core::event_log::LogEntry::Morphism {
|
||||
seq, morphism, ops, ..
|
||||
} => println!(" #{:02} morph {} ({} ops)", seq, morphism, ops.len()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,9 +154,7 @@ fn main() {
|
||||
|
||||
section("== determinism verification (ops) ==");
|
||||
match verify_log(&log, &exec) {
|
||||
Ok(()) => println!(
|
||||
" ok: every logged morphism reproduced its ops on re-execution"
|
||||
),
|
||||
Ok(()) => println!(" ok: every logged morphism reproduced its ops on re-execution"),
|
||||
Err(e) => println!(" nondeterminism detected: {}", e),
|
||||
}
|
||||
|
||||
@@ -148,11 +170,16 @@ fn run_and_report(
|
||||
params: serde_json::Value,
|
||||
) {
|
||||
match execute_and_log(exec, store, log, morphism, inputs, params) {
|
||||
Ok(ops) => println!(" ok ({} ops, logged at #{})", ops.len(), log.next_seq() - 1),
|
||||
Ok(ops) => println!(
|
||||
" ok ({} ops, logged at #{})",
|
||||
ops.len(),
|
||||
log.next_seq() - 1
|
||||
),
|
||||
Err(ExecuteError::PreLog(e)) => println!(" rejected: {}", e),
|
||||
Err(ExecuteError::LogAppend(e)) => println!(" LOG APPEND FAILED: {}", e),
|
||||
Err(ExecuteError::PostLogStore(e)) => println!(
|
||||
" POST-LOG STORE FAILED (log canonical, store stale): {}", e
|
||||
" POST-LOG STORE FAILED (log canonical, store stale): {}",
|
||||
e
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user