From 67c0fcad114fa1b0bac09727ce41ff058d1d402d Mon Sep 17 00:00:00 2001 From: sergio Date: Wed, 20 May 2026 00:28:20 +0000 Subject: [PATCH] =?UTF-8?q?refactor(loader):=20A3=20=E2=80=94=20unificar?= =?UTF-8?q?=20loader,=20eliminar=20duplicaci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit El loader vivía partido: arje-brain/loader.rs cargaba EntityCards Y Rules, mientras brahman-cards tenía su propia infra de card-loading. Resolución por linaje: - Card-loading (load_card_file, extract_card_from_json) → brahman-cards (entity_loader.rs). Toda card-loading del ecosistema vive ahí. - Rule-loading (load_rules_file, extract_rules_from_json) → arje-brain-rules (loader.rs), junto a la definición de Rule. - arje-brain/loader.rs eliminado. arje-brain re-exporta ambos para compat de consumidores (arje-zero). cargo check --workspace verde. Tests: 13 arje-brain-rules + 31 brahman-cards. Co-Authored-By: Claude Opus 4.7 --- Cargo.lock | 2 + crates/protocol/brahman-cards/Cargo.toml | 1 + .../brahman-cards/src/entity_loader.rs | 40 +++++++++ crates/protocol/brahman-cards/src/lib.rs | 2 + crates/runtime/arje-brain-rules/src/lib.rs | 2 + .../src/loader.rs | 84 ++++--------------- crates/runtime/arje-brain/Cargo.toml | 1 + crates/runtime/arje-brain/src/introspect.rs | 2 +- crates/runtime/arje-brain/src/lib.rs | 7 +- 9 files changed, 71 insertions(+), 70 deletions(-) create mode 100644 crates/protocol/brahman-cards/src/entity_loader.rs rename crates/runtime/{arje-brain => arje-brain-rules}/src/loader.rs (51%) diff --git a/Cargo.lock b/Cargo.lock index 1a8bad8..a64c1e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -306,6 +306,7 @@ dependencies = [ "arje-cas", "base64 0.22.1", "bincode", + "brahman-cards", "postcard", "serde", "serde_json", @@ -1720,6 +1721,7 @@ dependencies = [ name = "brahman-cards" version = "0.1.0" dependencies = [ + "anyhow", "brahman-card", "chasqui-card", "nahual-meta-schema", diff --git a/crates/protocol/brahman-cards/Cargo.toml b/crates/protocol/brahman-cards/Cargo.toml index 9e955d5..8a0abc9 100644 --- a/crates/protocol/brahman-cards/Cargo.toml +++ b/crates/protocol/brahman-cards/Cargo.toml @@ -12,6 +12,7 @@ description = "Brahman — brazo unificado: lee múltiples formatos de Card (Ent serde = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } +anyhow = { workspace = true } ulid = { workspace = true } brahman-card = { path = "../brahman-card" } chasqui-card = { path = "../../modules/chasqui/card" } diff --git a/crates/protocol/brahman-cards/src/entity_loader.rs b/crates/protocol/brahman-cards/src/entity_loader.rs new file mode 100644 index 0000000..7cdbd3a --- /dev/null +++ b/crates/protocol/brahman-cards/src/entity_loader.rs @@ -0,0 +1,40 @@ +//! Loader de `EntityCard` (≡ `brahman_card::Card`) desde archivos JSON. +//! +//! Card-loading consolidado: antes vivía duplicado en `arje-brain/loader.rs`. +//! La fuente de verdad del shape es Rust + serde; en disco se guarda JSON +//! crudo. Toda card-loading del ecosistema vive ahora en `brahman-cards`. + +use brahman_card::Card as EntityCard; +use std::path::Path; + +/// 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 { + 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 { + let v: serde_json::Value = serde_json::from_str(raw)?; + let direct_err = match serde_json::from_value::(v.clone()) { + Ok(c) => return Ok(c), + Err(e) => e, + }; + if let serde_json::Value::Object(map) = v { + for (_, vv) in map { + if let Ok(c) = serde_json::from_value::(vv) { + return Ok(c); + } + } + } + // Propagamos el error del intento directo: es el caso típico (JSON + // top-level = EntityCard) y su mensaje apunta al campo que rompió. + anyhow::bail!("JSON no contiene una EntityCard válida: {direct_err}") +} diff --git a/crates/protocol/brahman-cards/src/lib.rs b/crates/protocol/brahman-cards/src/lib.rs index 3649bb8..9f2859c 100644 --- a/crates/protocol/brahman-cards/src/lib.rs +++ b/crates/protocol/brahman-cards/src/lib.rs @@ -183,8 +183,10 @@ pub trait CardReader: Send + Sync { fn read(&self, input: Value) -> Result; } +mod entity_loader; mod nickel_eval; mod readers; +pub use entity_loader::{extract_card_from_json, load_card_file}; pub use nickel_eval::{eval_nickel_file, NickelEvalError, BRAHMAN_CARDS_TEMPLATES_ENV}; pub use readers::{EnteJsonReader, MonadJsonReader, UiModuleJsonReader}; diff --git a/crates/runtime/arje-brain-rules/src/lib.rs b/crates/runtime/arje-brain-rules/src/lib.rs index bd9d698..6113fc6 100644 --- a/crates/runtime/arje-brain-rules/src/lib.rs +++ b/crates/runtime/arje-brain-rules/src/lib.rs @@ -7,7 +7,9 @@ pub mod rules; pub mod engine; pub mod dispatch; +pub mod loader; pub use rules::{Action, EventKind, EventPattern, LogLevel, Rule, Scope, TimedEvent}; pub use engine::{EventKindDiscriminant, RuleEngine, SubjectInfo}; pub use dispatch::{dispatch_actions, ActionSink, NullSink}; +pub use loader::{extract_rules_from_json, load_rules_file}; diff --git a/crates/runtime/arje-brain/src/loader.rs b/crates/runtime/arje-brain-rules/src/loader.rs similarity index 51% rename from crates/runtime/arje-brain/src/loader.rs rename to crates/runtime/arje-brain-rules/src/loader.rs index fba74f4..6ca7ef2 100644 --- a/crates/runtime/arje-brain/src/loader.rs +++ b/crates/runtime/arje-brain-rules/src/loader.rs @@ -1,52 +1,13 @@ -//! Loader de Cards y Reglas desde archivos JSON. +//! Loader de Reglas (`Rule`) desde archivos JSON / JSONL. //! -//! 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. +//! La carga de `Rule` vive aquí, junto a su definición. La carga de +//! `EntityCard` se consolidó en `brahman-cards::entity_loader`. -use arje_brain_rules::rules::Rule; -use arje_card::EntityCard; +use crate::rules::Rule; 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 { - 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 { - let v: serde_json::Value = serde_json::from_str(raw)?; - let direct_err = match serde_json::from_value::(v.clone()) { - Ok(c) => return Ok(c), - Err(e) => e, - }; - if let serde_json::Value::Object(map) = v { - for (_, vv) in map { - if let Ok(c) = serde_json::from_value::(vv) { - return Ok(c); - } - } - } - // Propagamos el error del intento directo: es el caso típico (JSON top-level - // = EntityCard) y su mensaje apunta al campo concreto que rompió. - anyhow::bail!("JSON no contiene una EntityCard válida: {direct_err}") -} - -/// Carga reglas desde un archivo JSON. +/// Carga reglas desde un archivo JSON / JSONL. pub fn load_rules_file(path: &Path) -> anyhow::Result> { info!(path = %path.display(), "cargando reglas desde JSON"); let raw = std::fs::read_to_string(path)?; @@ -54,14 +15,12 @@ pub fn load_rules_file(path: &Path) -> anyhow::Result> { } /// Extrae un `Vec` de un blob de texto. Acepta tres formas: -/// 1. JSONL: una `Rule` por línea (el formato que escribe `append_rule_jsonl`). +/// 1. JSONL: una `Rule` por línea. /// 2. Array directo: `[{...}, {...}]`. /// 3. Object con un campo array: `{"rules": [...]}`. /// -/// Heurística: si el primer carácter no-blanco es `[` o `{` con formato -/// "objeto-con-array", parseamos como JSON único; en otro caso intentamos -/// línea-por-línea. Líneas vacías o que empiecen con `#` se ignoran (compat -/// con archivos editados a mano que dejen comentarios estilo shell). +/// Líneas vacías o que empiecen con `#` se ignoran (compat con archivos +/// editados a mano que dejen comentarios estilo shell). pub fn extract_rules_from_json(raw: &str) -> anyhow::Result> { let trimmed_start = raw.trim_start(); let looks_jsonl = trimmed_start.starts_with('{') @@ -83,8 +42,7 @@ pub fn extract_rules_from_json(raw: &str) -> anyhow::Result> { }; return Ok(serde_json::from_value(arr)?); } - // Caer a JSONL si el documento único no parsea — útil para archivos - // que mezclan comentarios `#` (no JSON válido como documento único). + // Caer a JSONL si el documento único no parsea. } let mut rules = Vec::new(); @@ -101,8 +59,7 @@ pub fn extract_rules_from_json(raw: &str) -> anyhow::Result> { #[cfg(test)] mod tests { use super::*; - use crate::introspect::append_rule_jsonl; - use arje_brain_rules::rules::{Action, EventKind, EventPattern, LogLevel, Rule, Scope}; + use crate::rules::{Action, EventKind, EventPattern, LogLevel, Rule, Scope}; use ulid::Ulid; fn sample_rule() -> Rule { @@ -148,25 +105,18 @@ mod tests { } #[test] - fn append_rule_jsonl_roundtrip() { - let dir = tempdir_unique(); - let path = dir.join("rules.jsonl"); + fn jsonl_roundtrip_preserves_order_and_ids() { + // Roundtrip JSONL escrito manualmente (una Rule por línea). let r1 = sample_rule(); let r2 = sample_rule(); - append_rule_jsonl(&path, &r1).expect("append 1"); - append_rule_jsonl(&path, &r2).expect("append 2"); - let raw = std::fs::read_to_string(&path).expect("read back"); + let raw = format!( + "{}\n{}\n", + serde_json::to_string(&r1).unwrap(), + serde_json::to_string(&r2).unwrap(), + ); let parsed = extract_rules_from_json(&raw).expect("roundtrip parse"); assert_eq!(parsed.len(), 2); assert_eq!(parsed[0].id, r1.id); assert_eq!(parsed[1].id, r2.id); - let _ = std::fs::remove_dir_all(&dir); - } - - fn tempdir_unique() -> std::path::PathBuf { - let base = std::env::temp_dir(); - let p = base.join(format!("ente-brain-loader-{}", Ulid::new())); - std::fs::create_dir_all(&p).unwrap(); - p } } diff --git a/crates/runtime/arje-brain/Cargo.toml b/crates/runtime/arje-brain/Cargo.toml index bb1032f..02921d7 100644 --- a/crates/runtime/arje-brain/Cargo.toml +++ b/crates/runtime/arje-brain/Cargo.toml @@ -12,6 +12,7 @@ arje-brain-cognitive = { path = "../arje-brain-cognitive" } arje-brain-audit = { path = "../arje-brain-audit" } arje-card = { path = "../../protocol/arje-card" } arje-cas = { path = "../arje-cas" } +brahman-cards = { path = "../../protocol/brahman-cards" } serde = { workspace = true } serde_json = { workspace = true } ulid = { workspace = true } diff --git a/crates/runtime/arje-brain/src/introspect.rs b/crates/runtime/arje-brain/src/introspect.rs index 8990cf9..e6ff85c 100644 --- a/crates/runtime/arje-brain/src/introspect.rs +++ b/crates/runtime/arje-brain/src/introspect.rs @@ -424,7 +424,7 @@ impl IntrospectServer { "ReloadRules sin path y sin rules_out configurado".into() ), }; - let rules = match crate::loader::load_rules_file(&path) { + let rules = match arje_brain_rules::load_rules_file(&path) { Ok(r) => r, Err(e) => return IntrospectResponse::Error(format!("load: {e}")), }; diff --git a/crates/runtime/arje-brain/src/lib.rs b/crates/runtime/arje-brain/src/lib.rs index 372eeaf..8362446 100644 --- a/crates/runtime/arje-brain/src/lib.rs +++ b/crates/runtime/arje-brain/src/lib.rs @@ -13,7 +13,6 @@ pub mod introspect; pub mod autopromote; pub mod metrics; -pub mod loader; // --- Re-export de los módulos de las 3 sub-crates --- pub use arje_brain_rules::{dispatch, engine, rules}; @@ -30,5 +29,9 @@ pub use audit::AuditLog; pub use autopromote::{spawn_autopromote_loop, AutopromoteParams}; pub use introspect::{BrainState, IntrospectRequest, IntrospectResponse, IntrospectServer}; -pub use loader::{load_card_file, load_rules_file}; pub use metrics::serve_metrics; + +// --- Loader: card-loading vive en brahman-cards, rule-loading en +// arje-brain-rules. Re-exportados aquí por compat de consumidores. --- +pub use arje_brain_rules::{extract_rules_from_json, load_rules_file}; +pub use brahman_cards::{extract_card_from_json, load_card_file};