848fc7a072
DAG de dependencias limpio (modularidad horizontal):
- arje-brain-rules — rules + engine + dispatch (motor determinista)
- arje-brain-cognitive — observer + crystallize (estadística)
- arje-brain-audit — audit chain → CAS (accountability)
- arje-brain — umbrella de integración (introspect +
autopromote + metrics + loader)
Habilitador clave: TimedEvent movido de observer.rs a rules.rs
(engine lo necesitaba, era el único acoplo que rompía el DAG).
arje-brain re-exporta la API de los 3 sub-crates: arje-zero y chasqui
(consumidores) no requieren cambios. cargo check --workspace verde.
24 tests del brain pasan (4 rules + 6 cognitive + 5 audit + 9 umbrella).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
74 lines
3.2 KiB
Rust
74 lines
3.2 KiB
Rust
//! Despacho asíncrono de Actions. El motor entrega `Vec<Arc<Rule>>` matched;
|
|
//! este módulo las traduce a efectos del fractal vía un `ActionSink` trait.
|
|
//!
|
|
//! Esto invierte la dependencia: ente-brain no conoce a ente-zero. El init
|
|
//! implementa `ActionSink` y wirea spawn/invoke/log a sus propias estructuras.
|
|
|
|
use crate::rules::{Action, LogLevel, Rule};
|
|
use std::sync::Arc;
|
|
use tracing::{debug, error, info, trace, warn};
|
|
|
|
/// Backend de ejecución de Actions. ente-zero implementa esto delegando a
|
|
/// graph_tx (Spawn → SpawnRequest, Invoke → bus call, etc.).
|
|
pub trait ActionSink: Send + Sync {
|
|
/// Spawn una Card decodificada. Implementación: GraphEvent::SpawnRequest.
|
|
fn spawn(&self, card_blob: &str);
|
|
/// Invoke por bus. blob crudo; el sink lo enruta vía bus_mediator.
|
|
fn invoke(&self, target_cap: arje_card::Capability, blob: Vec<u8>);
|
|
/// Notifica a un Ente específico (target_id). Implementación: forward por bus.
|
|
fn notify(&self, target_id: ulid::Ulid, message: &str);
|
|
/// Inhibe un comportamiento (placeholder; semántica depende del sink).
|
|
fn inhibit(&self, reason: &str);
|
|
}
|
|
|
|
/// Sink por defecto que sólo logea. Útil para tests y dev sin runtime.
|
|
pub struct NullSink;
|
|
|
|
impl ActionSink for NullSink {
|
|
fn spawn(&self, card_blob: &str) {
|
|
info!(blob_len = card_blob.len(), "NullSink::spawn (no-op)");
|
|
}
|
|
fn invoke(&self, target_cap: arje_card::Capability, blob: Vec<u8>) {
|
|
info!(?target_cap, blob_len = blob.len(), "NullSink::invoke (no-op)");
|
|
}
|
|
fn notify(&self, target_id: ulid::Ulid, message: &str) {
|
|
info!(%target_id, %message, "NullSink::notify (no-op)");
|
|
}
|
|
fn inhibit(&self, reason: &str) {
|
|
info!(%reason, "NullSink::inhibit (no-op)");
|
|
}
|
|
}
|
|
|
|
/// Ejecuta las reglas matched. Cada Rule puede tener N Actions; ejecutamos
|
|
/// todas. Las acciones de Log se evalúan inline (tracing es async-safe).
|
|
/// Las acciones de Spawn/Invoke/Notify se delegan al sink — el sink decide
|
|
/// si procesarlas sincrónica o asincrónicamente.
|
|
pub async fn dispatch_actions(rules: &[Arc<Rule>], sink: &dyn ActionSink) {
|
|
for rule in rules {
|
|
trace!(id = %rule.id, priority = rule.priority, n = rule.then.len(), "dispatching rule");
|
|
for action in &rule.then {
|
|
execute_action(action, sink, rule.id).await;
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn execute_action(action: &Action, sink: &dyn ActionSink, rule_id: ulid::Ulid) {
|
|
match action {
|
|
Action::Log { level, message } => emit_log(level, message, rule_id),
|
|
Action::Notify { target_id, message } => sink.notify(*target_id, message),
|
|
Action::Spawn { card_blob } => sink.spawn(card_blob),
|
|
Action::Invoke { target_cap, blob } => sink.invoke(target_cap.clone(), blob.clone()),
|
|
Action::Inhibit { reason } => sink.inhibit(reason),
|
|
}
|
|
}
|
|
|
|
fn emit_log(level: &LogLevel, message: &str, rule_id: ulid::Ulid) {
|
|
match level {
|
|
LogLevel::Trace => trace!(rule = %rule_id, "{}", message),
|
|
LogLevel::Debug => debug!(rule = %rule_id, "{}", message),
|
|
LogLevel::Info => info! (rule = %rule_id, "{}", message),
|
|
LogLevel::Warn => warn! (rule = %rule_id, "{}", message),
|
|
LogLevel::Error => error!(rule = %rule_id, "{}", message),
|
|
}
|
|
}
|