Files
arje/crates/ente-brain/src/crystallize.rs
T
Sergio d6b8f18b43 Pausa: 11 crates del fractal Ente #0 con cerebro completo
PID 1 boot + bus interno autenticado + cerebro KCL/Rust:
- 6 lib crates de infra (card, bus, cas, kernel, soma, wasm, snapshot)
- ente-brain: motor de reglas O(1), observer Shannon, cristalización,
  audit hash-chain, persistencia rules.k, Prometheus /metrics
- KCL schemas card.k + rule.k como gramática autoritativa
- compat-logind D-Bus, ente-echo demo provider, ente-zero PID 1
- 22 tests OK, ~3.8k LOC Rust + ~300 LOC KCL

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 22:57:44 +00:00

170 lines
5.3 KiB
Rust

//! Cristalización: del flujo observado a reglas explícitas.
//!
//! Detecta pares (a, b) donde:
//! - support(a, b) ≥ min_support (suficientes muestras para no ser ruido)
//! - P(b|a) ≥ min_conditional_prob (a predice b con confianza)
//! - PMI(a; b) ≥ min_pmi (más correlacionados que random)
//!
//! Cada cristal puede emitirse como snippet KCL (texto humano-readable) o
//! como `Rule` ejecutable directamente por el motor.
use crate::observer::Observer;
use crate::rules::{Action, EventKind, EventPattern, LogLevel, Rule, Scope};
use serde::{Deserialize, Serialize};
use ulid::Ulid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Crystal {
pub antecedent: EventKind,
pub consequent: EventKind,
pub conditional_prob: f64,
pub pmi: f64,
pub support: u64,
}
#[derive(Debug, Clone, Copy)]
pub struct CrystallizationParams {
pub min_support: u64,
pub min_conditional_prob: f64,
pub min_pmi: f64,
}
impl Default for CrystallizationParams {
fn default() -> Self {
Self {
min_support: 5,
min_conditional_prob: 0.7,
min_pmi: 0.5,
}
}
}
pub fn detect_crystals(obs: &Observer, params: &CrystallizationParams) -> Vec<Crystal> {
let mut out = Vec::new();
for ((a, b), &count) in obs.cooccurrences() {
if count < params.min_support { continue; }
let cp = obs.conditional_prob(a, b);
if cp < params.min_conditional_prob { continue; }
let mi = obs.pmi(a, b);
if mi < params.min_pmi { continue; }
out.push(Crystal {
antecedent: a.clone(),
consequent: b.clone(),
conditional_prob: cp,
pmi: mi,
support: count,
});
}
// Orden estable: por confianza descendente para fácil inspección.
out.sort_by(|x, y| y.conditional_prob.partial_cmp(&x.conditional_prob).unwrap_or(std::cmp::Ordering::Equal));
out
}
/// Genera un snippet KCL representando la regla cristalizada. El snippet usa
/// la sintaxis tagged union del schema `rule.k` (Single + EventKind nested).
pub fn crystal_to_kcl(c: &Crystal) -> String {
let id = Ulid::new();
format!(
r#"# Auto-cristalizado:
# antecedent → consequent | P(c|a) = {cp:.3}, PMI = {pmi:.3} bits, support = {sup}
Rule {{
id = "{id}"
priority = 5
when = EventPattern {{
type = "Single"
kind = EventKind {{tag = "{ant_tag}"{ant_extra}}}
}}
scope = Scope {{}}
then = [
Action {{
kind = "Log"
level = "info"
message = "crystal: {ant_tag} → {con_tag} (auto, P={cp:.2}, PMI={pmi:.2})"
}}
]
}}
"#,
id = id,
cp = c.conditional_prob,
pmi = c.pmi,
sup = c.support,
ant_tag = kind_tag(&c.antecedent),
ant_extra = kind_extra(&c.antecedent),
con_tag = kind_tag(&c.consequent),
)
}
fn kind_tag(k: &EventKind) -> &'static str {
match k {
EventKind::EnteSpawned => "EnteSpawned",
EventKind::EnteDied => "EnteDied",
EventKind::BusAnnounce => "BusAnnounce",
EventKind::BusInvoke => "BusInvoke",
EventKind::BusInvokeOf(_) => "BusInvokeOf",
EventKind::DeviceAdded => "DeviceAdded",
EventKind::DeviceRemoved => "DeviceRemoved",
EventKind::Custom(_) => "Custom",
}
}
fn kind_extra(k: &EventKind) -> String {
match k {
EventKind::Custom(s) => format!(", custom = \"{}\"", s.replace('"', "\\\"")),
// Para BusInvokeOf el cap se omitiría por simplicidad; el snippet
// promovido es la versión "genérica BusInvoke" salvo que el operador
// edite manualmente.
_ => String::new(),
}
}
/// Convierte un cristal a una `Rule` ejecutable por el motor. Útil para
/// "auto-aprendizaje" donde cristales se promueven a reglas vivas tras
/// validar con el operador.
pub fn crystal_to_rule(c: &Crystal) -> Rule {
Rule {
id: Ulid::new(),
priority: 5,
when: EventPattern::Single { kind: c.antecedent.clone() },
scope: Scope::default(),
then: vec![Action::Log {
level: LogLevel::Info,
message: format!(
"crystal: {:?} → {:?} (P={:.2}, PMI={:.2}, n={})",
c.antecedent, c.consequent, c.conditional_prob, c.pmi, c.support
),
}],
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rules::EventKind::*;
#[test]
fn detects_perfect_correlation() {
let mut obs = Observer::new(100);
for _ in 0..10 {
obs.record(EnteSpawned);
obs.record(EnteDied);
}
let crystals = detect_crystals(&obs, &CrystallizationParams {
min_support: 3,
min_conditional_prob: 0.5,
min_pmi: 0.0,
});
assert!(crystals.iter().any(|c| matches!(c.antecedent, EnteSpawned)
&& matches!(c.consequent, EnteDied)));
}
#[test]
fn rejects_below_threshold() {
let mut obs = Observer::new(100);
// Sin co-ocurrencia significativa.
for _ in 0..3 { obs.record(EnteSpawned); }
let crystals = detect_crystals(&obs, &CrystallizationParams::default());
assert!(crystals.is_empty(), "no debería haber cristales: {:?}", crystals);
}
}