tawasuyu
⎇ badf4257ecd5a4ef06f6f477279834893a24f87b crates/ente-brain/examples/brainctl.rs Rust · 154 siq'ikuna · 6.4 KB Ñawpa willay Pi imata llamkarqan ch'uya

audit-verify, autopromote, histogramas y hot-reload caps badf4257e · Sergio ·

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        "flush-audit" => IntrospectRequest::FlushAudit,
55        "audit-verify" | "verify" => IntrospectRequest::VerifyAudit,
56        "reload" => {
57            let path = args.get(2).cloned();
58            IntrospectRequest::ReloadRules { path }
59        }
60        other => {
61            eprintln!("subcomando desconocido: {other}");
62            eprintln!("válidos: list-rules | entropy | top <n> | crystals | crystal-kcl <i> | promote <i> | remove <ulid> | audit <limit> | flush-audit | reload [path]");
63            std::process::exit(2);
64        }
65    };
66
67    let path = socket_path();
68    let resp = call(&path, req).await?;
69    print_response(&resp);
70    Ok(())
71}
72
73fn print_response(r: &IntrospectResponse) {
74    match r {
75        IntrospectResponse::Rules(rs) => {
76            println!("{} reglas vivas:", rs.len());
77            for r in rs {
78                println!("  {} prio={} kind={} actions={} wildcard={}",
79                    r.id, r.priority, r.event_kind_tag, r.action_count, r.scope_wildcard);
80            }
81        }
82        IntrospectResponse::Rule(rule) => match rule {
83            Some(r) => println!("{r:#?}"),
84            None => println!("regla no encontrada"),
85        },
86        IntrospectResponse::Entropy { value_bits, sample_size, distinct_kinds, window_full } => {
87            println!("Shannon entropy : {value_bits:.4} bits");
88            println!("Sample size     : {sample_size}");
89            println!("Distinct kinds  : {distinct_kinds}");
90            println!("Window full     : {window_full}");
91        }
92        IntrospectResponse::Correlations(entries) => {
93            println!("{} pares (top, ordenado por co-ocurrencia):", entries.len());
94            for e in entries {
95                println!("  n={:>4}  P(b|a)={:.3}  PMI={:>6.3}b  {} → {}",
96                    e.joint_count, e.conditional_prob, e.pmi_bits, e.a, e.b);
97            }
98        }
99        IntrospectResponse::Crystals(cs) => {
100            println!("{} cristales detectados:", cs.len());
101            for (i, c) in cs.iter().enumerate() {
102                println!("  [{i}] {:?} → {:?}  P={:.3}  PMI={:.3}b  n={}",
103                    c.antecedent, c.consequent, c.conditional_prob, c.pmi, c.support);
104            }
105        }
106        IntrospectResponse::Kcl(s) => println!("{s}"),
107        IntrospectResponse::Promoted { rule_id, kcl_snippet } => {
108            println!("regla creada: {rule_id}");
109            println!("--- KCL para auditoría / persistencia ---");
110            println!("{kcl_snippet}");
111        }
112        IntrospectResponse::Removed(was_present) => {
113            if *was_present { println!("regla eliminada"); }
114            else { println!("regla no encontrada"); }
115        }
116        IntrospectResponse::AuditEntries(entries) => {
117            println!("{} entries de audit log:", entries.len());
118            for e in entries {
119                let prev = e.prev_sha.map(hex_short).unwrap_or_else(|| "—".into());
120                let sha = hex_short(e.sha);
121                println!("  seq={:>4} t={} prev={} sha={}  {:?}",
122                    e.seq, e.timestamp_ms, prev, sha, e.action);
123            }
124        }
125        IntrospectResponse::Flushed { written, head_sha, total_flushed } => {
126            println!("flushed: {written} entries esta pasada, total acumulado: {total_flushed}");
127            if let Some(sha) = head_sha {
128                println!("head sha: {}", hex_long(*sha));
129            }
130        }
131        IntrospectResponse::Reloaded { count } => {
132            println!("reload OK: {count} reglas activas tras reload");
133        }
134        IntrospectResponse::AuditVerified(rep) => {
135            if let Some(seq) = rep.broken_at_seq {
136                println!("✗ verificación FALLÓ tras seq={seq}");
137                if let Some(e) = &rep.error { println!("  motivo: {e}"); }
138                println!("  entries verificadas: {}", rep.verified);
139            } else {
140                println!("✓ chain verificada — {} entries íntegras", rep.verified);
141                if let Some(g) = rep.genesis_sha { println!("  genesis: {}", hex_long(g)); }
142            }
143        }
144        IntrospectResponse::Error(e) => eprintln!("error: {e}"),
145    }
146}
147
148fn hex_short(sha: [u8; 32]) -> String {
149    sha[..4].iter().map(|b| format!("{:02x}", b)).collect::<String>() + ".."
150}
151
152fn hex_long(sha: [u8; 32]) -> String {
153    sha.iter().map(|b| format!("{:02x}", b)).collect()
154}