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:
sergio
2026-05-21 18:21:09 +00:00
parent bb21c28eb1
commit 78fbde12b4
38 changed files with 1229 additions and 334 deletions
+24 -9
View File
@@ -5,9 +5,7 @@ use std::path::{Path, PathBuf};
use nakui_core::executor::Executor;
use nakui_core::graph::{DirtyTracker, GraphError, ManifestGraph};
use nakui_core::manifest::{
ConserveRule, Invariants, Manifest, MorphismInput, MorphismSpec,
};
use nakui_core::manifest::{ConserveRule, Invariants, Manifest, MorphismInput, MorphismSpec};
fn workspace_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
@@ -114,14 +112,28 @@ fn treasury_data_flow_indexes_match_manifest() {
let g = &exec.graph;
// Both register_cash_move and transfer_between_cajas write Caja.saldo.
let mut writers: Vec<&str> = g.writers_of("Caja.saldo").iter().map(|s| s.as_str()).collect();
let mut writers: Vec<&str> = g
.writers_of("Caja.saldo")
.iter()
.map(|s| s.as_str())
.collect();
writers.sort();
assert_eq!(writers, vec!["register_cash_move", "transfer_between_cajas"]);
assert_eq!(
writers,
vec!["register_cash_move", "transfer_between_cajas"]
);
// Both read Caja.saldo too.
let mut readers: Vec<&str> = g.readers_of("Caja.saldo").iter().map(|s| s.as_str()).collect();
let mut readers: Vec<&str> = g
.readers_of("Caja.saldo")
.iter()
.map(|s| s.as_str())
.collect();
readers.sort();
assert_eq!(readers, vec!["register_cash_move", "transfer_between_cajas"]);
assert_eq!(
readers,
vec!["register_cash_move", "transfer_between_cajas"]
);
// Movimiento is written only by register_cash_move.
assert_eq!(
@@ -246,8 +258,11 @@ fn executor_load_module_rejects_cyclic_manifest() {
Err(e) => e,
};
let msg = err.to_string();
assert!(msg.contains("graph") || msg.contains("cycle"),
"expected graph diagnostic, got `{}`", msg);
assert!(
msg.contains("graph") || msg.contains("cycle"),
"expected graph diagnostic, got `{}`",
msg
);
let _ = std::fs::remove_dir_all(&tmp);
}