refactor(brain): A2 — split arje-brain en 3 sub-crates

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>
This commit is contained in:
sergio
2026-05-20 00:24:48 +00:00
parent b83d40a833
commit 848fc7a072
21 changed files with 221 additions and 89 deletions
Generated
+40
View File
@@ -299,6 +299,9 @@ name = "arje-brain"
version = "0.0.1" version = "0.0.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"arje-brain-audit",
"arje-brain-cognitive",
"arje-brain-rules",
"arje-card", "arje-card",
"arje-cas", "arje-cas",
"base64 0.22.1", "base64 0.22.1",
@@ -311,6 +314,43 @@ dependencies = [
"ulid", "ulid",
] ]
[[package]]
name = "arje-brain-audit"
version = "0.0.1"
dependencies = [
"anyhow",
"arje-brain-cognitive",
"arje-brain-rules",
"arje-cas",
"serde",
"serde_json",
"tokio",
"ulid",
]
[[package]]
name = "arje-brain-cognitive"
version = "0.0.1"
dependencies = [
"arje-brain-rules",
"serde",
"serde_json",
"ulid",
]
[[package]]
name = "arje-brain-rules"
version = "0.0.1"
dependencies = [
"anyhow",
"arje-card",
"base64 0.22.1",
"serde",
"serde_json",
"tracing",
"ulid",
]
[[package]] [[package]]
name = "arje-bus" name = "arje-bus"
version = "0.0.1" version = "0.0.1"
+3
View File
@@ -29,6 +29,9 @@ members = [
"crates/runtime/arje-bus", "crates/runtime/arje-bus",
"crates/runtime/arje-cas", "crates/runtime/arje-cas",
"crates/runtime/arje-wasm", "crates/runtime/arje-wasm",
"crates/runtime/arje-brain-rules",
"crates/runtime/arje-brain-cognitive",
"crates/runtime/arje-brain-audit",
"crates/runtime/arje-brain", "crates/runtime/arje-brain",
"crates/runtime/arje-echo", "crates/runtime/arje-echo",
+15 -11
View File
@@ -6,21 +6,25 @@ rule engine + audit log, y un ente de smoke test.
## Crates ## Crates
| crate | tipo | rol | | crate | tipo | rol |
| ------------- | ---- | ----------------------------------------------------------- | | ---------------------- | ---- | -------------------------------------------------- |
| `arje-bus` | lib | Unix SOCK_STREAM + postcard framing. Announce/Invoke/List | | `arje-bus` | lib | Unix SOCK_STREAM + postcard framing |
| `arje-cas` | lib | Content-addressed storage SHA-256: blobs Wasm + audit log | | `arje-cas` | lib | Content-addressed storage SHA-256: blobs + audit |
| `arje-wasm` | lib | Encarna `Payload::Wasm` vía `wasmi` en thread dedicado | | `arje-wasm` | lib | Encarna `Payload::Wasm` vía `wasmi` |
| `arje-brain` | lib | Rule engine + observer estadístico + audit chain con CAS | | `arje-brain-rules` | lib | Motor determinista: rules + engine O(1) + dispatch |
| `arje-echo` | bin | Ente prueba — provee `Capability::Endpoint(echo)` | | `arje-brain-cognitive` | lib | Observer estadístico + crystallize de patrones |
| `arje-brain-audit` | lib | Audit chain con hashes anclados al CAS |
| `arje-brain` | lib | Integración: introspect + autopromote + metrics |
| `arje-echo` | bin | Ente prueba — provee `Capability::Endpoint(echo)` |
## Dependencias ## Dependencias
- `arje-bus``tokio` + `postcard`. Consumido por `init/arje-zero`. - `arje-bus``tokio` + `postcard`. Consumido por `init/arje-zero`.
- `arje-cas``sha2` + `sled`. Consumido por `arje-brain` (audit log) - `arje-cas``sha2` + `sled`. Consumido por `arje-brain-audit` y `arje-wasm`.
y `arje-wasm` (blobs). - **Brain split (DAG limpio)**: `arje-brain-rules` (base) ← `arje-brain-cognitive`
- `arje-brain``arje-bus`, `arje-cas`. Consumido por Init para `arje-brain-audit``arje-brain` (umbrella de integración).
observabilidad estadística + reglas declarativas. - `arje-brain` re-exporta la API de los 3 sub-crates para los
consumidores históricos (`arje-zero`, `chasqui`).
## Invariantes ## Invariantes
@@ -0,0 +1,17 @@
[package]
name = "arje-brain-audit"
version = "0.0.1"
edition.workspace = true
license.workspace = true
publish.workspace = true
description = "Capa de accountability del brain: audit log con hash chain anclado al CAS."
[dependencies]
arje-brain-rules = { path = "../arje-brain-rules" }
arje-brain-cognitive = { path = "../arje-brain-cognitive" }
arje-cas = { path = "../arje-cas" }
serde = { workspace = true }
serde_json = { workspace = true }
ulid = { workspace = true }
tokio = { workspace = true }
anyhow = { workspace = true }
@@ -6,7 +6,7 @@
//! escribe al content-addressable store y devuelve el SHA del head, que //! escribe al content-addressable store y devuelve el SHA del head, que
//! puede guardarse en un archivo de "head pointer" (fuera de scope aquí). //! puede guardarse en un archivo de "head pointer" (fuera de scope aquí).
use crate::crystallize::Crystal; use arje_brain_cognitive::crystallize::Crystal;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::VecDeque; use std::collections::VecDeque;
use ulid::Ulid; use ulid::Ulid;
@@ -323,7 +323,7 @@ pub fn collect_chain_from_cas(start_sha: [u8; 32]) -> anyhow::Result<Vec<AuditEn
/// log informativo (los archivos pueden no existir en el ambiente actual). /// log informativo (los archivos pueden no existir en el ambiente actual).
pub fn replay_chain( pub fn replay_chain(
start_sha: [u8; 32], start_sha: [u8; 32],
engine: &mut crate::engine::RuleEngine, engine: &mut arje_brain_rules::engine::RuleEngine,
) -> ReplayReport { ) -> ReplayReport {
let entries = match collect_chain_from_cas(start_sha) { let entries = match collect_chain_from_cas(start_sha) {
Ok(es) => es, Ok(es) => es,
@@ -336,7 +336,7 @@ pub fn replay_chain(
for entry in &entries { for entry in &entries {
match &entry.action { match &entry.action {
AuditAction::PromoteCrystal { rule_id, crystal } => { AuditAction::PromoteCrystal { rule_id, crystal } => {
let mut rule = crate::crystallize::crystal_to_rule(crystal); let mut rule = arje_brain_cognitive::crystallize::crystal_to_rule(crystal);
rule.id = *rule_id; // preservar identidad histórica rule.id = *rule_id; // preservar identidad histórica
engine.insert(rule); engine.insert(rule);
} }
@@ -422,7 +422,7 @@ mod tests {
// ---------- Tests de integración con CAS real (en directorio temporal) ---------- // ---------- Tests de integración con CAS real (en directorio temporal) ----------
use crate::engine::RuleEngine; use arje_brain_rules::engine::RuleEngine;
use std::sync::Mutex; use std::sync::Mutex;
/// Lock para serializar tests que mutan ENTE_CAS_ROOT (test threads /// Lock para serializar tests que mutan ENTE_CAS_ROOT (test threads
@@ -459,7 +459,7 @@ mod tests {
} }
} }
use crate::rules::EventKind; use arje_brain_rules::rules::EventKind;
#[test] #[test]
fn flush_round_trip_preserves_chain() { fn flush_round_trip_preserves_chain() {
@@ -0,0 +1,13 @@
//! arje-brain-audit — accountability del brain.
//!
//! Audit log con cadena de hashes anclada al content-addressed storage
//! (`arje-cas`). Permite verificar la integridad de la historia de
//! decisiones del brain y reconstruir el estado vía replay.
pub mod audit;
pub use audit::{
AuditAction, AuditEntry, AuditHeadPointer, AuditLog, ReplayReport,
VerificationReport, collect_chain_from_cas, reachable_from_head,
replay_chain, verify_chain_from_cas,
};
@@ -0,0 +1,13 @@
[package]
name = "arje-brain-cognitive"
version = "0.0.1"
edition.workspace = true
license.workspace = true
publish.workspace = true
description = "Capa cognitiva del brain: observer estadístico (entropía + información mutua) + cristalización de patrones en reglas."
[dependencies]
arje-brain-rules = { path = "../arje-brain-rules" }
serde = { workspace = true }
serde_json = { workspace = true }
ulid = { workspace = true }
@@ -10,7 +10,7 @@
//! resultante con serde — sin formatos intermedios. //! resultante con serde — sin formatos intermedios.
use crate::observer::{GapStats, Observer}; use crate::observer::{GapStats, Observer};
use crate::rules::{Action, EventKind, EventPattern, LogLevel, Rule, Scope}; use arje_brain_rules::{Action, EventKind, EventPattern, LogLevel, Rule, Scope};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::time::Instant; use std::time::Instant;
use ulid::Ulid; use ulid::Ulid;
@@ -215,7 +215,7 @@ pub fn detect_pattern_crystals(obs: &Observer, params: &PatternParams) -> Vec<Pa
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::rules::EventKind::*; use arje_brain_rules::EventKind::*;
#[test] #[test]
fn detects_perfect_correlation() { fn detects_perfect_correlation() {
@@ -0,0 +1,11 @@
//! arje-brain-cognitive — capa estadística del brain.
//!
//! `Observer` con sliding window + marginales + co-ocurrencias + Shannon
//! entropy + información mutua. `crystallize` detecta patrones
//! estadísticamente significativos y los materializa como `Rule`.
pub mod observer;
pub mod crystallize;
pub use observer::Observer;
pub use crystallize::{detect_crystals, Crystal, CrystallizationParams};
@@ -8,18 +8,10 @@
//! - Sin recomputaciones globales: marginales y joint counts son state. //! - Sin recomputaciones globales: marginales y joint counts son state.
//! - El cálculo de H(X), P(B|A), I(A;B) es O(|distinct events|). //! - El cálculo de H(X), P(B|A), I(A;B) es O(|distinct events|).
use crate::rules::EventKind; use arje_brain_rules::{EventKind, TimedEvent};
use std::collections::{HashMap, VecDeque}; use std::collections::{HashMap, VecDeque};
use std::time::Instant; use std::time::Instant;
/// Evento timestamped. El timestamp se conserva para futuras políticas de
/// expiración por tiempo (no sólo por count).
#[derive(Debug, Clone)]
pub struct TimedEvent {
pub kind: EventKind,
pub at: Instant,
}
/// Histograma de gaps temporales con buckets exponenciales en segundos. /// Histograma de gaps temporales con buckets exponenciales en segundos.
/// Cubre 6 órdenes de magnitud: 1ms hasta 1000s. /// Cubre 6 órdenes de magnitud: 1ms hasta 1000s.
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
@@ -420,7 +412,7 @@ pub struct ObserverSnapshot {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::rules::EventKind::*; use arje_brain_rules::EventKind::*;
#[test] #[test]
fn entropy_zero_for_single_event() { fn entropy_zero_for_single_event() {
@@ -0,0 +1,16 @@
[package]
name = "arje-brain-rules"
version = "0.0.1"
edition.workspace = true
license.workspace = true
publish.workspace = true
description = "Motor de reglas determinista: triplets Subject+Event+Action, RuleEngine O(1), dispatch async."
[dependencies]
arje-card = { path = "../../protocol/arje-card" }
serde = { workspace = true }
serde_json = { workspace = true }
ulid = { workspace = true }
tracing = { workspace = true }
anyhow = { workspace = true }
base64 = { workspace = true }
@@ -5,7 +5,7 @@
//! Inmutabilidad fractal: `Arc<Rule>` es el unit de compartición. Clonar una //! Inmutabilidad fractal: `Arc<Rule>` es el unit de compartición. Clonar una
//! regla del motor para entregarla al dispatcher es un refcount bump, no copia. //! regla del motor para entregarla al dispatcher es un refcount bump, no copia.
use crate::observer::TimedEvent; use crate::rules::TimedEvent;
use crate::rules::{EventKind, EventPattern, Rule, Scope}; use crate::rules::{EventKind, EventPattern, Rule, Scope};
use arje_card::Capability; use arje_card::Capability;
use std::collections::HashMap; use std::collections::HashMap;
@@ -0,0 +1,13 @@
//! arje-brain-rules — motor de reglas determinista.
//!
//! Capa base del brain: tipos de regla (triplet Subject+Event+Action),
//! `RuleEngine` con dispatch O(1) por discriminante de evento, y el
//! ejecutor async de acciones. Sin dependencias estadísticas ni de UI.
pub mod rules;
pub mod engine;
pub mod dispatch;
pub use rules::{Action, EventKind, EventPattern, LogLevel, Rule, Scope, TimedEvent};
pub use engine::{EventKindDiscriminant, RuleEngine, SubjectInfo};
pub use dispatch::{dispatch_actions, ActionSink, NullSink};
@@ -7,8 +7,18 @@
use arje_card::Capability; use arje_card::Capability;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::time::Instant;
use ulid::Ulid; use ulid::Ulid;
/// Evento timestamped. El timestamp se conserva para futuras políticas de
/// expiración por tiempo (no sólo por count). Tipo base compartido entre
/// el motor de reglas (`engine`) y el observador estadístico (`cognitive`).
#[derive(Debug, Clone)]
pub struct TimedEvent {
pub kind: EventKind,
pub at: Instant,
}
/// Triplet [Sujeto + Evento + Acción]. Inmutable tras carga. /// Triplet [Sujeto + Evento + Acción]. Inmutable tras carga.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Rule { pub struct Rule {
+4
View File
@@ -4,8 +4,12 @@ version = "0.0.1"
edition.workspace = true edition.workspace = true
license.workspace = true license.workspace = true
publish.workspace = true publish.workspace = true
description = "Capa de integración del brain: introspect socket + autopromote loop + metrics HTTP + loader. Wirea arje-brain-{rules,cognitive,audit}."
[dependencies] [dependencies]
arje-brain-rules = { path = "../arje-brain-rules" }
arje-brain-cognitive = { path = "../arje-brain-cognitive" }
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" }
serde = { workspace = true } serde = { workspace = true }
+3 -3
View File
@@ -6,10 +6,10 @@
//! no exista ya una regla con el mismo trigger_kind (heurística simple — //! no exista ya una regla con el mismo trigger_kind (heurística simple —
//! evita ráfagas de duplicados de la misma estadística). //! evita ráfagas de duplicados de la misma estadística).
use crate::audit::AuditAction; use arje_brain_audit::audit::AuditAction;
use crate::crystallize::{crystal_to_rule, detect_crystals, Crystal, CrystallizationParams}; use arje_brain_cognitive::crystallize::{crystal_to_rule, detect_crystals, Crystal, CrystallizationParams};
use crate::introspect::{append_rule_jsonl, BrainState}; use crate::introspect::{append_rule_jsonl, BrainState};
use crate::rules::EventKind; use arje_brain_rules::rules::EventKind;
use std::collections::HashSet; use std::collections::HashSet;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
+23 -23
View File
@@ -4,10 +4,10 @@
//! cerebro sin tocar el bus interno del fractal. Esto separa observación de //! cerebro sin tocar el bus interno del fractal. Esto separa observación de
//! ejecución — la introspección es read-only por diseño. //! ejecución — la introspección es read-only por diseño.
use crate::crystallize::{detect_crystals, Crystal, CrystallizationParams}; use arje_brain_cognitive::crystallize::{detect_crystals, Crystal, CrystallizationParams};
use crate::engine::RuleEngine; use arje_brain_rules::engine::RuleEngine;
use crate::observer::Observer; use arje_brain_cognitive::observer::Observer;
use crate::rules::Rule; use arje_brain_rules::rules::Rule;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::io::Write; use std::io::Write;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@@ -32,7 +32,7 @@ pub struct BrainState {
/// cada PromoteCrystal añade una línea (append-only) con la Rule serializada. /// cada PromoteCrystal añade una línea (append-only) con la Rule serializada.
pub rules_out: Option<Arc<PathBuf>>, pub rules_out: Option<Arc<PathBuf>>,
/// Audit log en memoria. Cada promote/remove deja huella aquí. /// Audit log en memoria. Cada promote/remove deja huella aquí.
pub audit: Arc<RwLock<crate::audit::AuditLog>>, pub audit: Arc<RwLock<arje_brain_audit::audit::AuditLog>>,
} }
impl BrainState { impl BrainState {
@@ -46,7 +46,7 @@ impl BrainState {
observer: Arc::new(RwLock::new(Observer::new(window_size))), observer: Arc::new(RwLock::new(Observer::new(window_size))),
params, params,
rules_out: None, rules_out: None,
audit: Arc::new(RwLock::new(crate::audit::AuditLog::new())), audit: Arc::new(RwLock::new(arje_brain_audit::audit::AuditLog::new())),
} }
} }
@@ -135,21 +135,21 @@ pub enum IntrospectResponse {
/// Resultado de RemoveRule: true si existía, false si ya no. /// Resultado de RemoveRule: true si existía, false si ya no.
Removed(bool), Removed(bool),
/// Entradas del audit log (más recientes al final). /// Entradas del audit log (más recientes al final).
AuditEntries(Vec<crate::audit::AuditEntry>), AuditEntries(Vec<arje_brain_audit::audit::AuditEntry>),
/// Resultado de FlushAudit: cuántas entries se escribieron y SHA del head. /// Resultado de FlushAudit: cuántas entries se escribieron y SHA del head.
Flushed { written: usize, head_sha: Option<[u8; 32]>, total_flushed: u64 }, Flushed { written: usize, head_sha: Option<[u8; 32]>, total_flushed: u64 },
/// Resultado de ReloadRules: número total de reglas tras el reload. /// Resultado de ReloadRules: número total de reglas tras el reload.
Reloaded { count: usize }, Reloaded { count: usize },
/// Resultado de VerifyAudit. /// Resultado de VerifyAudit.
AuditVerified(crate::audit::VerificationReport), AuditVerified(arje_brain_audit::audit::VerificationReport),
/// Resultado de ReplayAudit. /// Resultado de ReplayAudit.
Replayed(crate::audit::ReplayReport), Replayed(arje_brain_audit::audit::ReplayReport),
/// Frame de streaming. El cliente lee estos en bucle hasta EOF. /// Frame de streaming. El cliente lee estos en bucle hasta EOF.
AuditStreamFrame(crate::audit::AuditEntry), AuditStreamFrame(arje_brain_audit::audit::AuditEntry),
/// Resultado de GcCas: cuántos blobs eliminados y bytes liberados. /// Resultado de GcCas: cuántos blobs eliminados y bytes liberados.
GcResult { deleted: usize, freed_bytes: u64 }, GcResult { deleted: usize, freed_bytes: u64 },
/// Cristales de Burst/Silence detectados. /// Cristales de Burst/Silence detectados.
Patterns(Vec<crate::crystallize::PatternCrystal>), Patterns(Vec<arje_brain_cognitive::crystallize::PatternCrystal>),
Error(String), Error(String),
} }
@@ -306,7 +306,7 @@ impl IntrospectServer {
let obs = self.state.observer.read().await; let obs = self.state.observer.read().await;
let crystals = detect_crystals(&obs, &self.state.params); let crystals = detect_crystals(&obs, &self.state.params);
match crystals.get(index) { match crystals.get(index) {
Some(c) => IntrospectResponse::Json(crate::crystallize::crystal_to_json_pretty(c)), Some(c) => IntrospectResponse::Json(arje_brain_cognitive::crystallize::crystal_to_json_pretty(c)),
None => IntrospectResponse::Error(format!("no crystal at index {index}")), None => IntrospectResponse::Error(format!("no crystal at index {index}")),
} }
} }
@@ -317,7 +317,7 @@ impl IntrospectServer {
}; };
match crystals.get(index) { match crystals.get(index) {
Some(c) => { Some(c) => {
let rule = crate::crystallize::crystal_to_rule(c); let rule = arje_brain_cognitive::crystallize::crystal_to_rule(c);
let rule_id = rule.id; let rule_id = rule.id;
let rule_json = serde_json::to_string_pretty(&rule) let rule_json = serde_json::to_string_pretty(&rule)
.unwrap_or_else(|_| "<serialize failed>".into()); .unwrap_or_else(|_| "<serialize failed>".into());
@@ -332,7 +332,7 @@ impl IntrospectServer {
} }
// Audit entry // Audit entry
self.state.audit.write().await.append( self.state.audit.write().await.append(
crate::audit::AuditAction::PromoteCrystal { arje_brain_audit::audit::AuditAction::PromoteCrystal {
rule_id, crystal: c.clone(), rule_id, crystal: c.clone(),
} }
); );
@@ -345,7 +345,7 @@ impl IntrospectServer {
let removed = self.state.engine.write().await.remove(id); let removed = self.state.engine.write().await.remove(id);
if removed { if removed {
self.state.audit.write().await.append( self.state.audit.write().await.append(
crate::audit::AuditAction::RemoveRule { rule_id: id } arje_brain_audit::audit::AuditAction::RemoveRule { rule_id: id }
); );
} }
IntrospectResponse::Removed(removed) IntrospectResponse::Removed(removed)
@@ -373,7 +373,7 @@ impl IntrospectServer {
"audit log sin entries flushadas — nada que verificar".into() "audit log sin entries flushadas — nada que verificar".into()
), ),
}; };
let report = crate::audit::verify_chain_from_cas(head); let report = arje_brain_audit::audit::verify_chain_from_cas(head);
IntrospectResponse::AuditVerified(report) IntrospectResponse::AuditVerified(report)
} }
IntrospectRequest::StreamAudit => { IntrospectRequest::StreamAudit => {
@@ -385,15 +385,15 @@ impl IntrospectServer {
} }
IntrospectRequest::PatternCrystals => { IntrospectRequest::PatternCrystals => {
let obs = self.state.observer.read().await; let obs = self.state.observer.read().await;
let params = crate::crystallize::PatternParams::default(); let params = arje_brain_cognitive::crystallize::PatternParams::default();
let patterns = crate::crystallize::detect_pattern_crystals(&obs, &params); let patterns = arje_brain_cognitive::crystallize::detect_pattern_crystals(&obs, &params);
IntrospectResponse::Patterns(patterns) IntrospectResponse::Patterns(patterns)
} }
IntrospectRequest::GcCas { extra_roots } => { IntrospectRequest::GcCas { extra_roots } => {
// Reachable = audit chain desde head + extra_roots provistos. // Reachable = audit chain desde head + extra_roots provistos.
let mut reachable = std::collections::HashSet::new(); let mut reachable = std::collections::HashSet::new();
if let Some(head) = self.state.audit.read().await.last_flushed_sha() { if let Some(head) = self.state.audit.read().await.last_flushed_sha() {
reachable.extend(crate::audit::reachable_from_head(head)); reachable.extend(arje_brain_audit::audit::reachable_from_head(head));
} }
reachable.extend(extra_roots); reachable.extend(extra_roots);
match arje_cas::gc(&reachable) { match arje_cas::gc(&reachable) {
@@ -410,8 +410,8 @@ impl IntrospectServer {
), ),
}; };
let mut engine = self.state.engine.write().await; let mut engine = self.state.engine.write().await;
*engine = crate::engine::RuleEngine::empty(); *engine = arje_brain_rules::engine::RuleEngine::empty();
let report = crate::audit::replay_chain(head, &mut engine); let report = arje_brain_audit::audit::replay_chain(head, &mut engine);
IntrospectResponse::Replayed(report) IntrospectResponse::Replayed(report)
} }
IntrospectRequest::ReloadRules { path } => { IntrospectRequest::ReloadRules { path } => {
@@ -430,12 +430,12 @@ impl IntrospectServer {
}; };
// Vaciamos el engine antes de re-cargar — semántica clean-slate. // Vaciamos el engine antes de re-cargar — semántica clean-slate.
let mut engine = self.state.engine.write().await; let mut engine = self.state.engine.write().await;
*engine = crate::engine::RuleEngine::empty(); *engine = arje_brain_rules::engine::RuleEngine::empty();
let count = rules.len(); let count = rules.len();
for r in rules { engine.insert(r); } for r in rules { engine.insert(r); }
drop(engine); drop(engine);
self.state.audit.write().await.append( self.state.audit.write().await.append(
crate::audit::AuditAction::LoadRulesFile { arje_brain_audit::audit::AuditAction::LoadRulesFile {
path: path.to_string_lossy().into_owned(), path: path.to_string_lossy().into_owned(),
count, count,
} }
+25 -29
View File
@@ -1,38 +1,34 @@
//! ente-brain: motor de reglas determinista + observador estadístico. //! arje-brain — capa de integración del brain.
//! //!
//! Tres capas: //! El brain se divide en tres sub-crates con un DAG de dependencias limpio:
//! 1. `rules` — tipos de regla (Triplet: Subject + Event + Action) //! - `arje-brain-rules` — motor determinista (rules + engine + dispatch)
//! 2. `engine` — RuleEngine con HashMap<EventKindDiscriminant, Vec<Arc<Rule>>> //! - `arje-brain-cognitive` — estadística (observer + crystallize)
//! para dispatch O(1) //! - `arje-brain-audit` — accountability (audit chain → CAS)
//! 3. `dispatch` — ejecutor async de Actions (vía tokio)
//! 4. `observer` — sliding window + marginales + co-ocurrencias
//! + Shannon entropy + información mutua
//! 5. `crystallize` — detección de patrones estadísticamente significativos
//! y materialización en `Rule` ejecutables
//! 6. `introspect` — Unix socket bincode API para tools externos
//! //!
//! Diseño de inmutabilidad: //! Este crate es la capa que los wirea: `introspect` (socket API),
//! - Rules son `Arc<Rule>` — clonar es zero-copy (refcount bump). //! `autopromote` (loop de promoción de cristales), `metrics` (HTTP) y
//! - El motor expone sólo lecturas; mutaciones pasan por `insert/remove`. //! `loader` (carga de cards/rules). Re-exporta la API de los tres
//! - Observer mantiene contadores incrementales — sin recomputación. //! sub-crates para compatibilidad de los consumidores históricos.
pub mod audit;
pub mod autopromote;
pub mod crystallize;
pub mod dispatch;
pub mod engine;
pub mod introspect; pub mod introspect;
pub mod loader; pub mod autopromote;
pub mod metrics; pub mod metrics;
pub mod observer; pub mod loader;
pub mod rules;
// --- Re-export de los módulos de las 3 sub-crates ---
pub use arje_brain_rules::{dispatch, engine, rules};
pub use arje_brain_cognitive::{crystallize, observer};
pub use arje_brain_audit::audit;
// --- Re-exports planos (API histórica que consumen arje-zero, chasqui) ---
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 crystallize::{detect_crystals, Crystal, CrystallizationParams};
pub use observer::Observer;
pub use audit::AuditLog;
pub use autopromote::{spawn_autopromote_loop, AutopromoteParams}; pub use autopromote::{spawn_autopromote_loop, AutopromoteParams};
pub use crystallize::{detect_crystals, Crystal, CrystallizationParams}; pub use introspect::{BrainState, IntrospectRequest, IntrospectResponse, IntrospectServer};
pub use dispatch::{dispatch_actions, ActionSink, NullSink};
pub use engine::{EventKindDiscriminant, RuleEngine, SubjectInfo};
pub use introspect::{IntrospectRequest, IntrospectResponse, IntrospectServer, BrainState};
pub use loader::{load_card_file, load_rules_file}; pub use loader::{load_card_file, load_rules_file};
pub use metrics::serve_metrics; pub use metrics::serve_metrics;
pub use observer::{Observer, TimedEvent};
pub use rules::{Action, EventKind, EventPattern, LogLevel, Rule, Scope};
+2 -2
View File
@@ -8,7 +8,7 @@
//! Ergonomía de autoría futura (RON, Dhall, etc.) se añade como ramas //! 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. //! adicionales aquí cuando duela escribir JSON a mano. Hoy: una sola rama.
use crate::rules::Rule; use arje_brain_rules::rules::Rule;
use arje_card::EntityCard; use arje_card::EntityCard;
use std::path::Path; use std::path::Path;
use tracing::info; use tracing::info;
@@ -102,7 +102,7 @@ pub fn extract_rules_from_json(raw: &str) -> anyhow::Result<Vec<Rule>> {
mod tests { mod tests {
use super::*; use super::*;
use crate::introspect::append_rule_jsonl; 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 {
+3 -3
View File
@@ -5,7 +5,7 @@
//! `prometheus` con su Registry + encoders. //! `prometheus` con su Registry + encoders.
use crate::introspect::BrainState; use crate::introspect::BrainState;
use crate::rules::EventKind; use arje_brain_rules::rules::EventKind;
use std::net::SocketAddr; use std::net::SocketAddr;
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream}; use tokio::net::{TcpListener, TcpStream};
@@ -98,7 +98,7 @@ async fn format_metrics(state: &BrainState) -> String {
} }
// ---- Cristales detectados (con params actuales) ---- // ---- Cristales detectados (con params actuales) ----
let crystals = crate::detect_crystals(&obs, &state.params); let crystals = arje_brain_cognitive::detect_crystals(&obs, &state.params);
out.push_str("# HELP ente_brain_crystals_total Number of crystals detected with current params.\n"); out.push_str("# HELP ente_brain_crystals_total Number of crystals detected with current params.\n");
out.push_str("# TYPE ente_brain_crystals_total gauge\n"); out.push_str("# TYPE ente_brain_crystals_total gauge\n");
out.push_str(&format!("ente_brain_crystals_total {}\n", crystals.len())); out.push_str(&format!("ente_brain_crystals_total {}\n", crystals.len()));
@@ -135,7 +135,7 @@ async fn format_metrics(state: &BrainState) -> String {
// ---- Histogramas de gaps temporales (top-32 pares más frecuentes) ---- // ---- Histogramas de gaps temporales (top-32 pares más frecuentes) ----
out.push_str("# HELP ente_brain_pair_gap_seconds Time gap between correlated events.\n"); out.push_str("# HELP ente_brain_pair_gap_seconds Time gap between correlated events.\n");
out.push_str("# TYPE ente_brain_pair_gap_seconds histogram\n"); out.push_str("# TYPE ente_brain_pair_gap_seconds histogram\n");
let limits = crate::observer::GapHistogram::bucket_limits(); let limits = arje_brain_cognitive::observer::GapHistogram::bucket_limits();
for ((a, b), hist) in obs.top_gap_pairs(32) { for ((a, b), hist) in obs.top_gap_pairs(32) {
let labels = format!(r#"a="{}",b="{}""#, kind_label(a), kind_label(b)); let labels = format!(r#"a="{}",b="{}""#, kind_label(a), kind_label(b));
for (i, &limit) in limits.iter().enumerate() { for (i, &limit) in limits.iter().enumerate() {