⎇ d6b8f18b43c9bd9bf7268e48b61392a2d62bb229
crates/ente-brain/examples/brainctl.rs
Historia
Quién tocó qué
crudo
Pausa: 11 crates del fractal Ente #0 con cerebro completo
1//! brainctl: cliente CLI del introspect API.
2//!
3//! Uso:
4//! cargo run --example brainctl -p ente-brain -- list-rules
5//! cargo run --example brainctl -p ente-brain -- entropy
6//! cargo run --example brainctl -p ente-brain -- top 10
7//! cargo run --example brainctl -p ente-brain -- crystals
8//! cargo run --example brainctl -p ente-brain -- crystal-kcl 0
9//!
10//! Path del socket: $ENTE_BRAIN_SOCK o $XDG_RUNTIME_DIR/ente-brain.sock
11
12use ente_brain::introspect::{call, IntrospectRequest, IntrospectResponse};
13use std::path::PathBuf;
14
15fn socket_path() -> PathBuf {
16 if let Ok(p) = std::env::var("ENTE_BRAIN_SOCK") {
17 return p.into();
18 }
19 let runtime = std::env::var("XDG_RUNTIME_DIR")
20 .unwrap_or_else(|_| std::env::var("TMPDIR").unwrap_or_else(|_| "/tmp".into()));
21 format!("{runtime}/ente-brain.sock").into()
22}
23
24#[tokio::main(flavor = "current_thread")]
25async fn main() -> anyhow::Result<()> {
26 let args: Vec<String> = std::env::args().collect();
27 let cmd = args.get(1).map(|s| s.as_str()).unwrap_or("entropy");
28
29 let req = match cmd {
30 "list-rules" | "rules" => IntrospectRequest::ListRules,
31 "entropy" => IntrospectRequest::EntropySnapshot,
32 "top" => {
33 let n: usize = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(10);
34 IntrospectRequest::TopCorrelations { n }
35 }
36 "crystals" => IntrospectRequest::Crystals,
37 "crystal-kcl" => {
38 let i: usize = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
39 IntrospectRequest::CrystalKcl { index: i }
40 }
41 "promote" => {
42 let i: usize = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
43 IntrospectRequest::PromoteCrystal { index: i }
44 }
45 "remove" => {
46 let id_s = args.get(2).ok_or_else(|| anyhow::anyhow!("se requiere <ulid>"))?;
47 let id: ulid::Ulid = id_s.parse()?;
48 IntrospectRequest::RemoveRule { id }
49 }
50 "audit" => {
51 let limit: usize = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(20);
52 IntrospectRequest::ListAudit { limit }
53 }
54 other => {
55 eprintln!("subcomando desconocido: {other}");
56 eprintln!("válidos: list-rules | entropy | top <n> | crystals | crystal-kcl <i> | promote <i> | remove <ulid> | audit <limit>");
57 std::process::exit(2);
58 }
59 };
60
61 let path = socket_path();
62 let resp = call(&path, req).await?;
63 print_response(&resp);
64 Ok(())
65}
66
67fn print_response(r: &IntrospectResponse) {
68 match r {
69 IntrospectResponse::Rules(rs) => {
70 println!("{} reglas vivas:", rs.len());
71 for r in rs {
72 println!(" {} prio={} kind={} actions={} wildcard={}",
73 r.id, r.priority, r.event_kind_tag, r.action_count, r.scope_wildcard);
74 }
75 }
76 IntrospectResponse::Rule(rule) => match rule {
77 Some(r) => println!("{r:#?}"),
78 None => println!("regla no encontrada"),
79 },
80 IntrospectResponse::Entropy { value_bits, sample_size, distinct_kinds, window_full } => {
81 println!("Shannon entropy : {value_bits:.4} bits");
82 println!("Sample size : {sample_size}");
83 println!("Distinct kinds : {distinct_kinds}");
84 println!("Window full : {window_full}");
85 }
86 IntrospectResponse::Correlations(entries) => {
87 println!("{} pares (top, ordenado por co-ocurrencia):", entries.len());
88 for e in entries {
89 println!(" n={:>4} P(b|a)={:.3} PMI={:>6.3}b {} → {}",
90 e.joint_count, e.conditional_prob, e.pmi_bits, e.a, e.b);
91 }
92 }
93 IntrospectResponse::Crystals(cs) => {
94 println!("{} cristales detectados:", cs.len());
95 for (i, c) in cs.iter().enumerate() {
96 println!(" [{i}] {:?} → {:?} P={:.3} PMI={:.3}b n={}",
97 c.antecedent, c.consequent, c.conditional_prob, c.pmi, c.support);
98 }
99 }
100 IntrospectResponse::Kcl(s) => println!("{s}"),
101 IntrospectResponse::Promoted { rule_id, kcl_snippet } => {
102 println!("regla creada: {rule_id}");
103 println!("--- KCL para auditoría / persistencia ---");
104 println!("{kcl_snippet}");
105 }
106 IntrospectResponse::Removed(was_present) => {
107 if *was_present { println!("regla eliminada"); }
108 else { println!("regla no encontrada"); }
109 }
110 IntrospectResponse::AuditEntries(entries) => {
111 println!("{} entries de audit log:", entries.len());
112 for e in entries {
113 let prev = e.prev_sha.map(hex_short).unwrap_or_else(|| "—".into());
114 let sha = hex_short(e.sha);
115 println!(" seq={:>4} t={} prev={} sha={} {:?}",
116 e.seq, e.timestamp_ms, prev, sha, e.action);
117 }
118 }
119 IntrospectResponse::Error(e) => eprintln!("error: {e}"),
120 }
121}
122
123fn hex_short(sha: [u8; 32]) -> String {
124 sha[..4].iter().map(|b| format!("{:02x}", b)).collect::<String>() + ".."
125}