gioser

tawasuyu/brahman

Rust 582TOML 222Markdown 83JSON 198635 Definiciones

refactor(loader): A3 — unificar loader, eliminar duplicación

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 <noreply@anthropic.com>

67c0fcad114fa1b0bac09727ce41ff058d1d402d · por sergio · (hace 5 meses) · padre 848fc7a07 · ver el árbol en este commit

Delta semántico · 25

crates/protocol/brahman-cards/src/entity_loader.rs

crates/protocol/brahman-cards/src/lib.rs

crates/runtime/arje-brain-rules/src/lib.rs

crates/runtime/arje-brain-rules/src/loader.rs

crates/runtime/arje-brain/src/introspect.rs

crates/runtime/arje-brain/src/lib.rs

crates/runtime/arje-brain/src/loader.rs

10 archivos · +176 −175

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<EntityCard> {
+    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)?;
+    let direct_err = match serde_json::from_value::<EntityCard>(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::<EntityCard>(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<Card, CardLoadError>;
 }
 
+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<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)?;
-    let direct_err = match serde_json::from_value::<EntityCard>(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::<EntityCard>(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<Vec<Rule>> {
     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<Vec<Rule>> {
 }
 
 /// Extrae un `Vec<Rule>` 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<Vec<Rule>> {
     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<Vec<Rule>> {
             };
             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<Vec<Rule>> {
 #[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};