⎇ 848fc7a0727d88eb9b45522acc083c95d480407f
crates/runtime/arje-brain-rules/src/rules.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
refactor(brain): A2 — split arje-brain en 3 sub-crates
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}