⎇ badf4257ecd5a4ef06f6f477279834893a24f87b
crates/ente-brain/src/autopromote.rs
Historia
Quién tocó qué
crudo
audit-verify, autopromote, histogramas y hot-reload caps
1//! Autopromote loop. Background task que cada N segundos detecta cristales
2//! con thresholds altos y los promueve sin intervención humana.
3//!
4//! Anti-doble-promote: tras promover, registramos en un set la pareja
5//! (antecedent_kind, consequent_kind). Antes de promover, verificamos que
6//! no exista ya una regla con el mismo trigger_kind (heurística simple —
7//! evita ráfagas de duplicados de la misma estadística).
8
9use crate::audit::AuditAction;
10use crate::crystallize::{crystal_to_kcl, crystal_to_rule, detect_crystals, Crystal, CrystallizationParams};
11use crate::introspect::{append_kcl_snippet, BrainState};
12use crate::rules::EventKind;
13use std::collections::HashSet;
14use std::sync::Arc;
15use std::time::Duration;
16use tokio::sync::Mutex;
17use tracing::{info, warn};
18
19#[derive(Debug, Clone, Copy)]
20pub struct AutopromoteParams {
21 pub interval_secs: u64,
22 pub threshold: CrystallizationParams,
23}
24
25impl Default for AutopromoteParams {
26 fn default() -> Self {
27 Self {
28 interval_secs: 60,
29 // Más estrictos que el threshold default — evitar ruido.
30 threshold: CrystallizationParams {
31 min_support: 10,
32 min_conditional_prob: 0.85,
33 min_pmi: 2.0,
34 },
35 }
36 }
37}
38
39/// Spawn del bucle. El handle Mutex evita que dos pasadas concurrentes
40/// promuevan el mismo cristal (el lock garantiza serialización por brain).
41pub fn spawn_autopromote_loop(state: BrainState, params: AutopromoteParams) {
42 let promoted_keys: Arc<Mutex<HashSet<(EventKind, EventKind)>>> =
43 Arc::new(Mutex::new(HashSet::new()));
44
45 tokio::spawn(async move {
46 let mut tick = tokio::time::interval(Duration::from_secs(params.interval_secs));
47 tick.tick().await; // descartar primer tick inmediato
48 info!(?params, "autopromote loop activo");
49 loop {
50 tick.tick().await;
51 run_one_pass(&state, ¶ms, &promoted_keys).await;
52 }
53 });
54}
55
56async fn run_one_pass(
57 state: &BrainState,
58 params: &AutopromoteParams,
59 promoted_keys: &Arc<Mutex<HashSet<(EventKind, EventKind)>>>,
60) {
61 let crystals: Vec<Crystal> = {
62 let obs = state.observer.read().await;
63 detect_crystals(&obs, ¶ms.threshold)
64 };
65 if crystals.is_empty() { return; }
66
67 let mut pk = promoted_keys.lock().await;
68 for c in crystals {
69 let key = (c.antecedent.clone(), c.consequent.clone());
70 if pk.contains(&key) {
71 // Ya promovido — el observer puede seguir reportando este
72 // cristal pero no necesitamos otra regla.
73 continue;
74 }
75 promote_one(state, &c).await;
76 pk.insert(key);
77 }
78}
79
80async fn promote_one(state: &BrainState, c: &Crystal) {
81 let rule = crystal_to_rule(c);
82 let snippet = crystal_to_kcl(c);
83 let rule_id = rule.id;
84 state.engine.write().await.insert(rule);
85 if let Some(path) = state.rules_out.as_ref() {
86 if let Err(e) = append_kcl_snippet(path, &snippet) {
87 warn!(?e, "autopromote: rules_out append falló");
88 }
89 }
90 state.audit.write().await.append(AuditAction::PromoteCrystal {
91 rule_id,
92 crystal: c.clone(),
93 });
94 info!(
95 %rule_id,
96 antecedent = ?c.antecedent,
97 consequent = ?c.consequent,
98 cp = c.conditional_prob,
99 pmi = c.pmi,
100 "autopromote: cristal → regla"
101 );
102}