gioser

tawasuyu/brahman

Rust 581TOML 221Markdown 73JSON 198578 Sutichaykuna

⎇ 848fc7a0727d88eb9b45522acc083c95d480407f crates/runtime/arje-brain-rules/src/rules.rs Rust · 216 siq'ikuna · 6.7 KB Ñawpa willay Pi imata llamkarqan ch'uya

refactor(brain): A2 — split arje-brain en 3 sub-crates 848fc7a07 · sergio ·

1//! Tipos de regla. La fuente de verdad del shape es esta definición Rust;
2//! `schema/rule.k` queda como referencia de diseño no cargada.
3//!
4//! Cargables desde JSON (array, objeto-con-array, o JSONL). El motor no
5//! acepta Rules construidas a mano sin pasar por validate() — ver
6//! `engine::insert`.
7
8use arje_card::Capability;
9use serde::{Deserialize, Serialize};
10use std::time::Instant;
11use ulid::Ulid;
12
13/// Evento timestamped. El timestamp se conserva para futuras políticas de
14/// expiración por tiempo (no sólo por count). Tipo base compartido entre
15/// el motor de reglas (`engine`) y el observador estadístico (`cognitive`).
16#[derive(Debug, Clone)]
17pub struct TimedEvent {
18    pub kind: EventKind,
19    pub at: Instant,
20}
21
22/// Triplet [Sujeto + Evento + Acción]. Inmutable tras carga.
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct Rule {
25    pub id: Ulid,
26    #[serde(default = "default_priority")]
27    pub priority: u8,
28    pub when: EventPattern,
29    pub then: Vec<Action>,
30    #[serde(default)]
31    pub scope: Scope,
32}
33
34fn default_priority() -> u8 { 5 }
35
36impl Rule {
37    pub fn validate(&self) -> Result<(), RuleError> {
38        if self.then.is_empty() {
39            return Err(RuleError::EmptyActions);
40        }
41        self.when.validate_recursive()
42    }
43}
44
45#[derive(Debug)]
46pub enum RuleError {
47    EmptyActions,
48    EmptySequence,
49    EmptyCompound,
50    InvalidPriority,
51}
52
53impl std::fmt::Display for RuleError {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        match self {
56            Self::EmptyActions => write!(f, "regla sin acciones"),
57            Self::EmptySequence => write!(f, "Sequence pattern con kinds vacío"),
58            Self::EmptyCompound => write!(f, "Either/All con patterns vacío"),
59            Self::InvalidPriority => write!(f, "prioridad fuera de rango"),
60        }
61    }
62}
63
64impl std::error::Error for RuleError {}
65
66/// Match del sujeto. Vacío en todos los campos = match cualquier Ente.
67#[derive(Debug, Clone, Default, Serialize, Deserialize)]
68pub struct Scope {
69    #[serde(default, skip_serializing_if = "Option::is_none")]
70    pub subject_id: Option<Ulid>,
71    #[serde(default, skip_serializing_if = "Option::is_none")]
72    pub subject_label: Option<String>,
73    #[serde(default, skip_serializing_if = "Option::is_none")]
74    pub subject_has_cap: Option<Capability>,
75}
76
77impl Scope {
78    pub fn is_wildcard(&self) -> bool {
79        self.subject_id.is_none()
80            && self.subject_label.is_none()
81            && self.subject_has_cap.is_none()
82    }
83}
84
85/// Patrón de evento que dispara una regla. Tagged union — JSON con campo
86/// `type`. Soporta composición recursiva (Either/All) sobre Single y
87/// Sequence atómicos.
88#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
89#[serde(tag = "type")]
90pub enum EventPattern {
91    /// Match un único evento por kind.
92    Single { kind: EventKind },
93    /// Match si los últimos N eventos del history coinciden en orden con
94    /// `kinds`, todos dentro de `within_ms` (0 = sin límite temporal).
95    Sequence {
96        kinds: Vec<EventKind>,
97        #[serde(default)]
98        within_ms: u64,
99    },
100    /// OR: match si cualquier sub-pattern matchea.
101    Either { patterns: Vec<EventPattern> },
102    /// AND: match si todos los sub-patterns matchean simultáneamente
103    /// contra el mismo (event, history).
104    All { patterns: Vec<EventPattern> },
105}
106
107impl EventPattern {
108    /// `true` si el pattern es atómico (no recursivo) y puede ser indexado
109    /// por discriminante de `EventKind` para dispatch O(1). Compuestos
110    /// (Either/All) se evalúan en un bucket de fallback.
111    pub fn is_simple(&self) -> bool {
112        matches!(self, Self::Single { .. } | Self::Sequence { .. })
113    }
114
115    /// Última `EventKind` que dispara la evaluación de un pattern atómico.
116    /// Devuelve None para compuestos.
117    pub fn trigger_kind(&self) -> Option<&EventKind> {
118        match self {
119            Self::Single { kind } => Some(kind),
120            Self::Sequence { kinds, .. } => kinds.last(),
121            Self::Either { .. } | Self::All { .. } => None,
122        }
123    }
124
125    /// Validación recursiva. Compuestos vacíos se rechazan al cargar.
126    pub fn validate_recursive(&self) -> Result<(), RuleError> {
127        match self {
128            Self::Single { .. } => Ok(()),
129            Self::Sequence { kinds, .. } => {
130                if kinds.is_empty() { Err(RuleError::EmptySequence) } else { Ok(()) }
131            }
132            Self::Either { patterns } | Self::All { patterns } => {
133                if patterns.is_empty() {
134                    return Err(RuleError::EmptyCompound);
135                }
136                for p in patterns { p.validate_recursive()?; }
137                Ok(())
138            }
139        }
140    }
141}
142
143/// Tipo de evento que dispara reglas. Indexado por discriminante en el motor.
144/// Diseñado para que `EventKindDiscriminant::from(&kind)` sea barato (no hash
145/// del payload, sólo del tag).
146#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
147pub enum EventKind {
148    EnteSpawned,
149    EnteDied,
150    BusAnnounce,
151    BusInvoke,
152    BusInvokeOf(Capability),
153    DeviceAdded,
154    DeviceRemoved,
155    Custom(String),
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
159#[serde(rename_all = "lowercase")]
160pub enum LogLevel { Trace, Debug, Info, Warn, Error }
161
162impl LogLevel {
163    pub fn as_str(&self) -> &'static str {
164        match self {
165            Self::Trace => "trace",
166            Self::Debug => "debug",
167            Self::Info => "info",
168            Self::Warn => "warn",
169            Self::Error => "error",
170        }
171    }
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
175#[serde(tag = "kind", rename_all = "PascalCase")]
176pub enum Action {
177    Log {
178        #[serde(default = "default_log_level")]
179        level: LogLevel,
180        message: String,
181    },
182    Notify {
183        target_id: Ulid,
184        message: String,
185    },
186    /// `card_blob` es JSON del EntityCard codificado en base64. El motor lo
187    /// decodifica y forwarda como SpawnRequest al graph.
188    Spawn {
189        card_blob: String,
190    },
191    Invoke {
192        target_cap: Capability,
193        /// blob crudo (en JSON viaja como base64 vía `blob_b64`).
194        #[serde(with = "blob_b64")]
195        blob: Vec<u8>,
196    },
197    Inhibit {
198        reason: String,
199    },
200}
201
202fn default_log_level() -> LogLevel { LogLevel::Info }
203
204mod blob_b64 {
205    use base64::{engine::general_purpose::STANDARD, Engine};
206    use serde::{Deserialize, Deserializer, Serializer};
207
208    pub fn serialize<S: Serializer>(bytes: &[u8], s: S) -> Result<S::Ok, S::Error> {
209        s.serialize_str(&STANDARD.encode(bytes))
210    }
211
212    pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> {
213        let s = String::deserialize(d)?;
214        STANDARD.decode(&s).map_err(serde::de::Error::custom)
215    }
216}