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>
This commit is contained in:
Generated
+2
@@ -306,6 +306,7 @@ dependencies = [
|
|||||||
"arje-cas",
|
"arje-cas",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bincode",
|
"bincode",
|
||||||
|
"brahman-cards",
|
||||||
"postcard",
|
"postcard",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -1720,6 +1721,7 @@ dependencies = [
|
|||||||
name = "brahman-cards"
|
name = "brahman-cards"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
"brahman-card",
|
"brahman-card",
|
||||||
"chasqui-card",
|
"chasqui-card",
|
||||||
"nahual-meta-schema",
|
"nahual-meta-schema",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ description = "Brahman — brazo unificado: lee múltiples formatos de Card (Ent
|
|||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
|
anyhow = { workspace = true }
|
||||||
ulid = { workspace = true }
|
ulid = { workspace = true }
|
||||||
brahman-card = { path = "../brahman-card" }
|
brahman-card = { path = "../brahman-card" }
|
||||||
chasqui-card = { path = "../../modules/chasqui/card" }
|
chasqui-card = { path = "../../modules/chasqui/card" }
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
}
|
||||||
@@ -183,8 +183,10 @@ pub trait CardReader: Send + Sync {
|
|||||||
fn read(&self, input: Value) -> Result<Card, CardLoadError>;
|
fn read(&self, input: Value) -> Result<Card, CardLoadError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mod entity_loader;
|
||||||
mod nickel_eval;
|
mod nickel_eval;
|
||||||
mod readers;
|
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 nickel_eval::{eval_nickel_file, NickelEvalError, BRAHMAN_CARDS_TEMPLATES_ENV};
|
||||||
pub use readers::{EnteJsonReader, MonadJsonReader, UiModuleJsonReader};
|
pub use readers::{EnteJsonReader, MonadJsonReader, UiModuleJsonReader};
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,9 @@
|
|||||||
pub mod rules;
|
pub mod rules;
|
||||||
pub mod engine;
|
pub mod engine;
|
||||||
pub mod dispatch;
|
pub mod dispatch;
|
||||||
|
pub mod loader;
|
||||||
|
|
||||||
pub use rules::{Action, EventKind, EventPattern, LogLevel, Rule, Scope, TimedEvent};
|
pub use rules::{Action, EventKind, EventPattern, LogLevel, Rule, Scope, TimedEvent};
|
||||||
pub use engine::{EventKindDiscriminant, RuleEngine, SubjectInfo};
|
pub use engine::{EventKindDiscriminant, RuleEngine, SubjectInfo};
|
||||||
pub use dispatch::{dispatch_actions, ActionSink, NullSink};
|
pub use dispatch::{dispatch_actions, ActionSink, NullSink};
|
||||||
|
pub use loader::{extract_rules_from_json, load_rules_file};
|
||||||
|
|||||||
+17
-67
@@ -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
|
//! La carga de `Rule` vive aquí, junto a su definición. La carga de
|
||||||
//! un subprocess al CLI Go `kcl` que ningún target real tenía instalado y
|
//! `EntityCard` se consolidó en `brahman-cards::entity_loader`.
|
||||||
//! 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 arje_brain_rules::rules::Rule;
|
use crate::rules::Rule;
|
||||||
use arje_card::EntityCard;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
/// Carga una `EntityCard` desde un archivo JSON. Pasa por
|
/// Carga reglas desde un archivo JSON / JSONL.
|
||||||
/// `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.
|
|
||||||
pub fn load_rules_file(path: &Path) -> anyhow::Result<Vec<Rule>> {
|
pub fn load_rules_file(path: &Path) -> anyhow::Result<Vec<Rule>> {
|
||||||
info!(path = %path.display(), "cargando reglas desde JSON");
|
info!(path = %path.display(), "cargando reglas desde JSON");
|
||||||
let raw = std::fs::read_to_string(path)?;
|
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:
|
/// 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: `[{...}, {...}]`.
|
/// 2. Array directo: `[{...}, {...}]`.
|
||||||
/// 3. Object con un campo array: `{"rules": [...]}`.
|
/// 3. Object con un campo array: `{"rules": [...]}`.
|
||||||
///
|
///
|
||||||
/// Heurística: si el primer carácter no-blanco es `[` o `{` con formato
|
/// Líneas vacías o que empiecen con `#` se ignoran (compat con archivos
|
||||||
/// "objeto-con-array", parseamos como JSON único; en otro caso intentamos
|
/// editados a mano que dejen comentarios estilo shell).
|
||||||
/// 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).
|
|
||||||
pub fn extract_rules_from_json(raw: &str) -> anyhow::Result<Vec<Rule>> {
|
pub fn extract_rules_from_json(raw: &str) -> anyhow::Result<Vec<Rule>> {
|
||||||
let trimmed_start = raw.trim_start();
|
let trimmed_start = raw.trim_start();
|
||||||
let looks_jsonl = trimmed_start.starts_with('{')
|
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)?);
|
return Ok(serde_json::from_value(arr)?);
|
||||||
}
|
}
|
||||||
// Caer a JSONL si el documento único no parsea — útil para archivos
|
// Caer a JSONL si el documento único no parsea.
|
||||||
// que mezclan comentarios `#` (no JSON válido como documento único).
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut rules = Vec::new();
|
let mut rules = Vec::new();
|
||||||
@@ -101,8 +59,7 @@ pub fn extract_rules_from_json(raw: &str) -> anyhow::Result<Vec<Rule>> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::introspect::append_rule_jsonl;
|
use crate::rules::{Action, EventKind, EventPattern, LogLevel, Rule, Scope};
|
||||||
use arje_brain_rules::rules::{Action, EventKind, EventPattern, LogLevel, Rule, Scope};
|
|
||||||
use ulid::Ulid;
|
use ulid::Ulid;
|
||||||
|
|
||||||
fn sample_rule() -> Rule {
|
fn sample_rule() -> Rule {
|
||||||
@@ -148,25 +105,18 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn append_rule_jsonl_roundtrip() {
|
fn jsonl_roundtrip_preserves_order_and_ids() {
|
||||||
let dir = tempdir_unique();
|
// Roundtrip JSONL escrito manualmente (una Rule por línea).
|
||||||
let path = dir.join("rules.jsonl");
|
|
||||||
let r1 = sample_rule();
|
let r1 = sample_rule();
|
||||||
let r2 = sample_rule();
|
let r2 = sample_rule();
|
||||||
append_rule_jsonl(&path, &r1).expect("append 1");
|
let raw = format!(
|
||||||
append_rule_jsonl(&path, &r2).expect("append 2");
|
"{}\n{}\n",
|
||||||
let raw = std::fs::read_to_string(&path).expect("read back");
|
serde_json::to_string(&r1).unwrap(),
|
||||||
|
serde_json::to_string(&r2).unwrap(),
|
||||||
|
);
|
||||||
let parsed = extract_rules_from_json(&raw).expect("roundtrip parse");
|
let parsed = extract_rules_from_json(&raw).expect("roundtrip parse");
|
||||||
assert_eq!(parsed.len(), 2);
|
assert_eq!(parsed.len(), 2);
|
||||||
assert_eq!(parsed[0].id, r1.id);
|
assert_eq!(parsed[0].id, r1.id);
|
||||||
assert_eq!(parsed[1].id, r2.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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,6 +12,7 @@ arje-brain-cognitive = { path = "../arje-brain-cognitive" }
|
|||||||
arje-brain-audit = { path = "../arje-brain-audit" }
|
arje-brain-audit = { path = "../arje-brain-audit" }
|
||||||
arje-card = { path = "../../protocol/arje-card" }
|
arje-card = { path = "../../protocol/arje-card" }
|
||||||
arje-cas = { path = "../arje-cas" }
|
arje-cas = { path = "../arje-cas" }
|
||||||
|
brahman-cards = { path = "../../protocol/brahman-cards" }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
ulid = { workspace = true }
|
ulid = { workspace = true }
|
||||||
|
|||||||
@@ -424,7 +424,7 @@ impl IntrospectServer {
|
|||||||
"ReloadRules sin path y sin rules_out configurado".into()
|
"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,
|
Ok(r) => r,
|
||||||
Err(e) => return IntrospectResponse::Error(format!("load: {e}")),
|
Err(e) => return IntrospectResponse::Error(format!("load: {e}")),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
pub mod introspect;
|
pub mod introspect;
|
||||||
pub mod autopromote;
|
pub mod autopromote;
|
||||||
pub mod metrics;
|
pub mod metrics;
|
||||||
pub mod loader;
|
|
||||||
|
|
||||||
// --- Re-export de los módulos de las 3 sub-crates ---
|
// --- Re-export de los módulos de las 3 sub-crates ---
|
||||||
pub use arje_brain_rules::{dispatch, engine, rules};
|
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 autopromote::{spawn_autopromote_loop, AutopromoteParams};
|
||||||
pub use introspect::{BrainState, IntrospectRequest, IntrospectResponse, IntrospectServer};
|
pub use introspect::{BrainState, IntrospectRequest, IntrospectResponse, IntrospectServer};
|
||||||
pub use loader::{load_card_file, load_rules_file};
|
|
||||||
pub use metrics::serve_metrics;
|
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};
|
||||||
|
|||||||
Reference in New Issue
Block a user