This commit is contained in:
Sergio
2026-05-06 20:40:47 +00:00
parent b5d8400fdf
commit d270c5e674
8 changed files with 93 additions and 354 deletions
+1 -1
View File
@@ -423,7 +423,7 @@ impl IntrospectServer {
"ReloadRules sin path y sin rules_out configurado".into()
),
};
let rules = match crate::kcl_loader::load_rules_file(&path) {
let rules = match crate::loader::load_rules_file(&path) {
Ok(r) => r,
Err(e) => return IntrospectResponse::Error(format!("load: {e}")),
};
-143
View File
@@ -1,143 +0,0 @@
//! Loader de reglas desde archivos `.k` vía subprocess al CLI de KCL.
//!
//! ## ¿Por qué subprocess y no SDK Rust?
//!
//! El SDK Rust de KusionStack KCL (en el monorepo `kcl-lang/kcl`) no se
//! publica como crate independiente en crates.io. Los crates `kcl-*` que
//! sí están publicados (kcl-lib, kcl-api, etc.) pertenecen al proyecto
//! KittyCAD — un lenguaje CAD distinto pese al nombre. Verificado 2026-05.
//!
//! Subprocess al CLI `kcl` (instalable vía `go install kcl-lang.io/cli/cmd/kcl@latest`
//! o desde el release de GitHub) es funcionalmente equivalente al SDK:
//! produce JSON validado contra el schema KCL declarado, sin dependencia
//! de Go runtime en el binario final del fractal.
//!
//! Si `kcl` no está en PATH, el caller decide: cargar JSON crudo (skip KCL),
//! o fallar el boot.
//!
//! ## Formato esperado del .k file
//!
//! ```kcl
//! import .rule # schema/rule.k
//!
//! rules: [Rule] = [
//! Rule { id = "...", priority = 5, when = ..., then = [...] },
//! ...
//! ]
//! ```
//!
//! Salida tras `kcl run --format json`: `{"rules": [...]}`. El loader busca
//! la primera array en el JSON (top-level o anidada un nivel) y la deserializa.
use crate::rules::Rule;
use ente_card::EntityCard;
use std::path::Path;
use std::process::Command;
use tracing::{debug, info};
/// Detecta si `kcl` está disponible en PATH. Útil para degradar a JSON-only
/// en entornos sin la toolchain.
pub fn kcl_available() -> bool {
Command::new("kcl")
.arg("version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
/// Ejecuta `kcl run <path> --format=json` y devuelve el JSON crudo.
pub fn run_kcl(path: &Path) -> anyhow::Result<String> {
let output = Command::new("kcl")
.arg("run")
.arg(path)
.arg("--format=json")
.output()
.map_err(|e| anyhow::anyhow!("invocando `kcl`: {e}. ¿Instalado en PATH?"))?;
if !output.status.success() {
anyhow::bail!(
"kcl run {} falló: {}",
path.display(),
String::from_utf8_lossy(&output.stderr)
);
}
debug!(path = %path.display(), out_bytes = output.stdout.len(), "kcl run ok");
Ok(String::from_utf8(output.stdout)?)
}
/// Carga reglas desde un archivo `.k` o JSON. Discrimina por extensión:
/// `.k` → invoca KCL, `.json` → directo.
pub fn load_rules_file(path: &Path) -> anyhow::Result<Vec<Rule>> {
let raw = match path.extension().and_then(|e| e.to_str()) {
Some("k") => {
info!(path = %path.display(), "cargando reglas vía kcl");
run_kcl(path)?
}
_ => {
info!(path = %path.display(), "cargando reglas como JSON crudo");
std::fs::read_to_string(path)?
}
};
extract_rules_from_json(&raw)
}
/// Extrae un `Vec<Rule>` de JSON que puede ser:
/// 1. Array directo: `[{...}, {...}]`
/// 2. Object con un campo array: `{"rules": [...]}`
pub fn extract_rules_from_json(raw: &str) -> anyhow::Result<Vec<Rule>> {
let v: serde_json::Value = serde_json::from_str(raw)?;
let arr = match v {
serde_json::Value::Array(_) => v,
serde_json::Value::Object(map) => {
map.into_values()
.find(|x| x.is_array())
.ok_or_else(|| anyhow::anyhow!("JSON no contiene ningún array"))?
}
_ => anyhow::bail!("JSON debe ser array o object con campo array"),
};
let rules: Vec<Rule> = serde_json::from_value(arr)?;
Ok(rules)
}
// ============================================================================
// Carga de Cards desde KCL/JSON. Cierra la "puerta genética": ninguna Card
// se acepta sin pasar `validate()` extendido en ente-card.
// ============================================================================
/// Carga una `EntityCard` desde un archivo `.k` (vía kcl run) o `.json`.
/// Pasa por `EntityCard::validate()` antes de devolver — falla rápida.
pub fn load_card_file(path: &Path) -> anyhow::Result<EntityCard> {
let raw = match path.extension().and_then(|e| e.to_str()) {
Some("k") => {
info!(path = %path.display(), "cargando Card vía kcl");
run_kcl(path)?
}
_ => {
info!(path = %path.display(), "cargando Card como JSON crudo");
std::fs::read_to_string(path)?
}
};
let card = extract_card_from_json(&raw)?;
card.validate()
.map_err(|e| anyhow::anyhow!("Card inválida ({}): {e}", path.display()))?;
Ok(card)
}
/// Extrae una `EntityCard` de JSON. Acepta:
/// 1. Object directamente serializable como EntityCard
/// 2. Object dict con un único valor que sea EntityCard (KCL output típico)
pub fn extract_card_from_json(raw: &str) -> anyhow::Result<EntityCard> {
let v: serde_json::Value = serde_json::from_str(raw)?;
// Intento 1: deserializar el value directamente.
if let Ok(c) = serde_json::from_value::<EntityCard>(v.clone()) {
return Ok(c);
}
// Intento 2: si es dict, buscar el primer value que parsee como Card.
if let serde_json::Value::Object(map) = v {
for (_, vv) in map {
if let Ok(c) = serde_json::from_value::<EntityCard>(vv) {
return Ok(c);
}
}
}
anyhow::bail!("JSON no contiene una EntityCard válida")
}
+2 -2
View File
@@ -22,7 +22,7 @@ pub mod crystallize;
pub mod dispatch;
pub mod engine;
pub mod introspect;
pub mod kcl_loader;
pub mod loader;
pub mod metrics;
pub mod observer;
pub mod rules;
@@ -32,7 +32,7 @@ pub use crystallize::{detect_crystals, Crystal, CrystallizationParams};
pub use dispatch::{dispatch_actions, ActionSink, NullSink};
pub use engine::{EventKindDiscriminant, RuleEngine, SubjectInfo};
pub use introspect::{IntrospectRequest, IntrospectResponse, IntrospectServer, BrainState};
pub use kcl_loader::{kcl_available, load_card_file, load_rules_file};
pub use loader::{load_card_file, load_rules_file};
pub use metrics::serve_metrics;
pub use observer::{Observer, TimedEvent};
pub use rules::{Action, EventKind, EventPattern, LogLevel, Rule, Scope};
+68
View File
@@ -0,0 +1,68 @@
//! Loader de Cards y Reglas desde archivos JSON.
//!
//! Sustituye al antiguo `kcl_loader.rs` (eliminado): la rama KCL invocaba
//! un subprocess al CLI Go `kcl` que ningún target real tenía instalado y
//! cuya validación duplicaba `EntityCard::validate()`. La fuente de verdad
//! del shape de la Card es Rust + serde; en disco se guarda JSON crudo.
//!
//! Ergonomía de autoría futura (RON, Dhall, etc.) se añade como ramas
//! adicionales aquí cuando duela escribir JSON a mano. Hoy: una sola rama.
use crate::rules::Rule;
use ente_card::EntityCard;
use std::path::Path;
use tracing::info;
/// Carga una `EntityCard` desde un archivo JSON. Pasa por
/// `EntityCard::validate()` antes de devolver — falla rápida.
pub fn load_card_file(path: &Path) -> anyhow::Result<EntityCard> {
info!(path = %path.display(), "cargando Card desde JSON");
let raw = std::fs::read_to_string(path)?;
let card = extract_card_from_json(&raw)?;
card.validate()
.map_err(|e| anyhow::anyhow!("Card inválida ({}): {e}", path.display()))?;
Ok(card)
}
/// Extrae una `EntityCard` de JSON. Acepta:
/// 1. Object directamente serializable como EntityCard.
/// 2. Object dict con un único valor que sea EntityCard (compat con
/// salidas de generadores que envuelven en `{"seed": {...}}`).
pub fn extract_card_from_json(raw: &str) -> anyhow::Result<EntityCard> {
let v: serde_json::Value = serde_json::from_str(raw)?;
if let Ok(c) = serde_json::from_value::<EntityCard>(v.clone()) {
return Ok(c);
}
if let serde_json::Value::Object(map) = v {
for (_, vv) in map {
if let Ok(c) = serde_json::from_value::<EntityCard>(vv) {
return Ok(c);
}
}
}
anyhow::bail!("JSON no contiene una EntityCard válida")
}
/// Carga reglas desde un archivo JSON.
pub fn load_rules_file(path: &Path) -> anyhow::Result<Vec<Rule>> {
info!(path = %path.display(), "cargando reglas desde JSON");
let raw = std::fs::read_to_string(path)?;
extract_rules_from_json(&raw)
}
/// Extrae un `Vec<Rule>` de JSON que puede ser:
/// 1. Array directo: `[{...}, {...}]`
/// 2. Object con un campo array: `{"rules": [...]}`
pub fn extract_rules_from_json(raw: &str) -> anyhow::Result<Vec<Rule>> {
let v: serde_json::Value = serde_json::from_str(raw)?;
let arr = match v {
serde_json::Value::Array(_) => v,
serde_json::Value::Object(map) => map
.into_values()
.find(|x| x.is_array())
.ok_or_else(|| anyhow::anyhow!("JSON no contiene ningún array"))?,
_ => anyhow::bail!("JSON debe ser array o object con campo array"),
};
let rules: Vec<Rule> = serde_json::from_value(arr)?;
Ok(rules)
}
+8 -7
View File
@@ -3,7 +3,7 @@
//! Tres caminos:
//! 1. `--restore <path>`: leer `FractalSnapshot` y reconstruir Semilla
//! con seed_id preservado + entes anteriores como genesis.
//! 2. `seed.card` en disco: deserialize directo (prod o dev).
//! 2. `seed.card.json` en disco: deserialize directo (prod o dev).
//! 3. Fallback dev: sintetizar Semilla + 6 genesis Entes que ejercitan
//! todas las capacidades del fractal.
@@ -63,13 +63,14 @@ fn load_from_snapshot(path: &Path) -> anyhow::Result<EntityCard> {
}
fn load_or_synthesize(dev_mode: bool) -> anyhow::Result<EntityCard> {
// Buscamos primero `.k` (KCL canónico, validado por su schema), luego
// `.json` para compatibilidad. La puerta genética se cruza vía
// `ente_brain::load_card_file` que pasa por `validate()` extendido.
// Buscamos primero `.json` (canónico), luego sin extensión por
// compatibilidad con instalaciones que dejan el archivo crudo. La puerta
// genética se cruza vía `ente_brain::load_card_file` que pasa por
// `validate()` extendido.
let candidates: &[&str] = if dev_mode {
&["seed.card.k", SEED_PATH_DEV]
&["seed.card.json", SEED_PATH_DEV]
} else {
&["/ente/seed.card.k", SEED_PATH_PROD]
&["/ente/seed.card.json", SEED_PATH_PROD]
};
for cand in candidates {
let path = PathBuf::from(cand);
@@ -83,7 +84,7 @@ fn load_or_synthesize(dev_mode: bool) -> anyhow::Result<EntityCard> {
info!("sin seed.card — sintetizando semilla mínima (dev)");
return Ok(synthesize_dev_seed());
}
anyhow::bail!("seed.card no encontrada en /ente/seed.card[.k]")
anyhow::bail!("seed.card no encontrada en /ente/seed.card.json ni /ente/seed.card")
}
fn synthesize_dev_seed() -> EntityCard {