Skip to main content

ergo_runtime/trigger/
mod.rs

1//! trigger
2//!
3//! Purpose:
4//! - Define kernel trigger primitive types, manifests, validation errors, and
5//!   registry helpers.
6//!
7//! Owns:
8//! - `TriggerValidationError` as the typed registration failure surface for
9//!   trigger primitives.
10//! - Trigger type metadata and registry-facing trigger declarations.
11//!
12//! Does not own:
13//! - Catalog-level wrapper errors or host-facing rendering.
14//! - Trigger execution orchestration outside kernel registration.
15//!
16//! Connects to:
17//! - `catalog.rs`, which wraps trigger registration failures.
18//! - Trigger implementations under `implementations/`.
19//!
20//! Safety notes:
21//! - `Display` stays aligned with `ErrorInfo` so trigger registration meaning is
22//!   not duplicated across layers.
23
24use std::borrow::Cow;
25use std::collections::HashMap;
26use std::fmt;
27
28use crate::common::{doc_anchor_for_rule, ErrorInfo, Phase};
29
30pub mod implementations;
31pub mod registry;
32
33#[derive(Debug, Clone, PartialEq)]
34pub enum TriggerKind {
35    Trigger,
36}
37
38#[derive(Debug, Clone, PartialEq)]
39pub enum TriggerValueType {
40    Number,
41    Series,
42    Bool,
43    Event,
44}
45
46#[derive(Debug, Clone, PartialEq)]
47pub enum TriggerEvent {
48    Emitted,
49    NotEmitted,
50}
51
52#[derive(Debug, Clone, PartialEq)]
53pub enum TriggerValue {
54    Number(f64),
55    Series(Vec<f64>),
56    Bool(bool),
57    Event(TriggerEvent),
58}
59
60impl TriggerValue {
61    pub fn value_type(&self) -> TriggerValueType {
62        match self {
63            TriggerValue::Number(_) => TriggerValueType::Number,
64            TriggerValue::Series(_) => TriggerValueType::Series,
65            TriggerValue::Bool(_) => TriggerValueType::Bool,
66            TriggerValue::Event(_) => TriggerValueType::Event,
67        }
68    }
69
70    pub fn as_number(&self) -> Option<f64> {
71        match self {
72            TriggerValue::Number(n) => Some(*n),
73            _ => None,
74        }
75    }
76
77    pub fn as_bool(&self) -> Option<bool> {
78        match self {
79            TriggerValue::Bool(b) => Some(*b),
80            _ => None,
81        }
82    }
83
84    pub fn as_event(&self) -> Option<&TriggerEvent> {
85        match self {
86            TriggerValue::Event(e) => Some(e),
87            _ => None,
88        }
89    }
90}
91
92#[derive(Debug, Clone, PartialEq)]
93pub enum ParameterType {
94    Int,
95    Number,
96    Bool,
97    String,
98    Enum,
99}
100
101#[derive(Debug, Clone, PartialEq)]
102pub enum ParameterValue {
103    Int(i64),
104    Number(f64),
105    Bool(bool),
106    String(String),
107    Enum(String),
108}
109
110impl ParameterValue {
111    pub fn value_type(&self) -> ParameterType {
112        match self {
113            ParameterValue::Int(_) => ParameterType::Int,
114            ParameterValue::Number(_) => ParameterType::Number,
115            ParameterValue::Bool(_) => ParameterType::Bool,
116            ParameterValue::String(_) => ParameterType::String,
117            ParameterValue::Enum(_) => ParameterType::Enum,
118        }
119    }
120}
121
122#[derive(Debug, Clone, PartialEq)]
123pub enum Cardinality {
124    Single,
125    Multiple,
126}
127
128#[derive(Debug, Clone, PartialEq)]
129pub struct InputSpec {
130    pub name: String,
131    pub value_type: TriggerValueType,
132    pub required: bool,
133    pub cardinality: Cardinality,
134}
135
136#[derive(Debug, Clone, PartialEq)]
137pub struct OutputSpec {
138    pub name: String,
139    pub value_type: TriggerValueType,
140}
141
142#[derive(Debug, Clone, PartialEq)]
143pub struct ParameterSpec {
144    pub name: String,
145    pub value_type: ParameterType,
146    pub default: Option<ParameterValue>,
147    pub required: bool,
148    pub bounds: Option<String>,
149}
150
151#[derive(Debug, Clone, PartialEq)]
152pub enum Cadence {
153    Continuous,
154    Event,
155}
156
157#[derive(Debug, Clone, PartialEq)]
158pub struct ExecutionSpec {
159    pub deterministic: bool,
160    pub cadence: Cadence,
161}
162
163#[derive(Debug, Clone, PartialEq)]
164pub struct StateSpec {
165    pub allowed: bool,
166    pub description: Option<String>,
167}
168
169#[derive(Debug, Clone, PartialEq)]
170pub struct TriggerPrimitiveManifest {
171    pub id: String,
172    pub version: String,
173    pub kind: TriggerKind,
174    pub inputs: Vec<InputSpec>,
175    pub outputs: Vec<OutputSpec>,
176    pub parameters: Vec<ParameterSpec>,
177    pub execution: ExecutionSpec,
178    pub state: StateSpec,
179    pub side_effects: bool,
180}
181
182#[derive(Debug, Clone, PartialEq)]
183#[non_exhaustive]
184pub enum TriggerValidationError {
185    InvalidId {
186        id: String,
187    },
188    InvalidVersion {
189        version: String,
190    },
191    WrongKind {
192        expected: TriggerKind,
193        got: TriggerKind,
194    },
195    NoInputsDeclared {
196        trigger: String,
197    },
198    DuplicateInput {
199        name: String,
200        first_index: usize,
201        second_index: usize,
202    },
203    SideEffectsNotAllowed,
204    NonDeterministicExecution,
205    DuplicateId(String),
206    TriggerWrongOutputCount {
207        got: usize,
208    },
209    InvalidInputCardinality {
210        input: String,
211        got: String,
212    },
213    InvalidInputType {
214        input: String,
215        expected: TriggerValueType,
216        got: TriggerValueType,
217    },
218    InvalidOutputType {
219        output: String,
220        expected: TriggerValueType,
221        got: TriggerValueType,
222    },
223    InvalidParameterType {
224        parameter: String,
225        expected: ParameterType,
226        got: ParameterType,
227    },
228    /// TRG-STATE-1: Triggers must be stateless.
229    StatefulTriggerNotAllowed {
230        trigger_id: String,
231    },
232}
233
234impl ErrorInfo for TriggerValidationError {
235    fn rule_id(&self) -> &'static str {
236        match self {
237            Self::InvalidId { .. } => "TRG-1",
238            Self::InvalidVersion { .. } => "TRG-2",
239            Self::WrongKind { .. } => "TRG-3",
240            Self::NoInputsDeclared { .. } => "TRG-4",
241            Self::DuplicateInput { .. } => "TRG-5",
242            Self::InvalidInputType { .. } => "TRG-6",
243            Self::TriggerWrongOutputCount { .. } => "TRG-7",
244            Self::InvalidOutputType { .. } => "TRG-8",
245            Self::StatefulTriggerNotAllowed { .. } => "TRG-9",
246            Self::SideEffectsNotAllowed => "TRG-10",
247            Self::NonDeterministicExecution => "TRG-11",
248            Self::InvalidInputCardinality { .. } => "TRG-12",
249            Self::DuplicateId(_) => "TRG-13",
250            Self::InvalidParameterType { .. } => "TRG-14",
251        }
252    }
253
254    fn phase(&self) -> Phase {
255        Phase::Registration
256    }
257
258    fn doc_anchor(&self) -> &'static str {
259        doc_anchor_for_rule(self.rule_id())
260    }
261
262    fn summary(&self) -> Cow<'static, str> {
263        match self {
264            Self::InvalidId { id } => Cow::Owned(format!("Invalid trigger ID: '{}'", id)),
265            Self::InvalidVersion { version } => {
266                Cow::Owned(format!("Invalid version: '{}'", version))
267            }
268            Self::WrongKind { expected, got } => Cow::Owned(format!(
269                "Wrong kind: expected {:?}, got {:?}",
270                expected, got
271            )),
272            Self::NoInputsDeclared { .. } => Cow::Borrowed("Trigger has no inputs"),
273            Self::DuplicateInput { name, .. } => {
274                Cow::Owned(format!("Duplicate input name: '{}'", name))
275            }
276            Self::InvalidInputType {
277                input,
278                expected,
279                got,
280            } => Cow::Owned(format!(
281                "Input '{}' has invalid type: expected {:?}, got {:?}",
282                input, expected, got
283            )),
284            Self::TriggerWrongOutputCount { got } => Cow::Owned(format!(
285                "Trigger must declare exactly one output (got {})",
286                got
287            )),
288            Self::InvalidOutputType {
289                output,
290                expected,
291                got,
292            } => Cow::Owned(format!(
293                "Output '{}' has invalid type: expected {:?}, got {:?}",
294                output, expected, got
295            )),
296            Self::StatefulTriggerNotAllowed { .. } => Cow::Borrowed("Trigger state is not allowed"),
297            Self::SideEffectsNotAllowed => Cow::Borrowed("Trigger side effects are not allowed"),
298            Self::NonDeterministicExecution => {
299                Cow::Borrowed("Trigger execution must be deterministic")
300            }
301            Self::InvalidInputCardinality { input, got } => Cow::Owned(format!(
302                "Input '{}' has invalid cardinality '{}'",
303                input, got
304            )),
305            Self::DuplicateId(_) => Cow::Borrowed("Duplicate trigger ID: already registered"),
306            Self::InvalidParameterType {
307                parameter,
308                expected,
309                got,
310            } => Cow::Owned(format!(
311                "Parameter '{}' has invalid type: expected {:?}, got {:?}",
312                parameter, expected, got
313            )),
314        }
315    }
316
317    fn path(&self) -> Option<Cow<'static, str>> {
318        match self {
319            Self::InvalidId { .. } => Some(Cow::Borrowed("$.id")),
320            Self::InvalidVersion { .. } => Some(Cow::Borrowed("$.version")),
321            Self::DuplicateId(_) => Some(Cow::Borrowed("$.id")),
322            Self::WrongKind { .. } => Some(Cow::Borrowed("$.kind")),
323            Self::NoInputsDeclared { .. } => Some(Cow::Borrowed("$.inputs")),
324            Self::DuplicateInput { second_index, .. } => {
325                Some(Cow::Owned(format!("$.inputs[{}].name", second_index)))
326            }
327            Self::InvalidInputType { .. } => Some(Cow::Borrowed("$.inputs[].type")),
328            Self::InvalidInputCardinality { .. } => Some(Cow::Borrowed("$.inputs[].cardinality")),
329            Self::TriggerWrongOutputCount { .. } => Some(Cow::Borrowed("$.outputs")),
330            Self::InvalidOutputType { .. } => Some(Cow::Borrowed("$.outputs[0].type")),
331            Self::StatefulTriggerNotAllowed { .. } => Some(Cow::Borrowed("$.state.allowed")),
332            Self::SideEffectsNotAllowed => Some(Cow::Borrowed("$.side_effects")),
333            Self::NonDeterministicExecution => Some(Cow::Borrowed("$.execution.deterministic")),
334            Self::InvalidParameterType { .. } => Some(Cow::Borrowed("$.parameters[].default")),
335        }
336    }
337
338    fn fix(&self) -> Option<Cow<'static, str>> {
339        match self {
340            Self::InvalidId { .. } => Some(Cow::Borrowed(
341                "ID must start with lowercase letter and contain only lowercase letters, digits, and underscores",
342            )),
343            Self::DuplicateId(_) => Some(Cow::Borrowed("Choose a unique ID not already registered")),
344            Self::InvalidVersion { .. } => Some(Cow::Borrowed(
345                "Version must be valid semver (e.g., '1.0.0')",
346            )),
347            Self::WrongKind { .. } => Some(Cow::Borrowed("Set kind: trigger")),
348            Self::NoInputsDeclared { .. } => Some(Cow::Borrowed("Add at least one input")),
349            Self::DuplicateInput { name, .. } => Some(Cow::Owned(format!(
350                "Rename input '{}' to a unique value",
351                name
352            ))),
353            Self::InvalidInputType { .. } => Some(Cow::Borrowed(
354                "Use a valid input type: number, bool, series, or event",
355            )),
356            Self::TriggerWrongOutputCount { .. } => {
357                Some(Cow::Borrowed("Declare exactly one output"))
358            }
359            Self::InvalidOutputType { .. } => {
360                Some(Cow::Borrowed("Output type must be event"))
361            }
362            Self::StatefulTriggerNotAllowed { .. } => {
363                Some(Cow::Borrowed("Set state.allowed: false"))
364            }
365            Self::SideEffectsNotAllowed => Some(Cow::Borrowed("Set side_effects: false")),
366            Self::NonDeterministicExecution => {
367                Some(Cow::Borrowed("Set execution.deterministic: true"))
368            }
369            Self::InvalidInputCardinality { .. } => {
370                Some(Cow::Borrowed("Set input cardinality to single"))
371            }
372            Self::InvalidParameterType { .. } => Some(Cow::Borrowed(
373                "Change parameter default value to match the declared parameter type",
374            )),
375        }
376    }
377}
378
379impl fmt::Display for TriggerValidationError {
380    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
381        write!(f, "{} ({})", self.summary(), self.rule_id())
382    }
383}
384
385impl std::error::Error for TriggerValidationError {}
386
387/// A trigger primitive that evaluates inputs and emits events.
388///
389/// # TRG-STATE-1: Stateless Triggers
390///
391/// Triggers are stateless across runs. The runtime API intentionally does not
392/// support persisted trigger state. Implementations may use ephemeral
393/// evaluation-local memory only (e.g., stack variables within `evaluate()`),
394/// but no state may be preserved, observed, or depended upon between
395/// invocations.
396///
397/// Temporal patterns requiring memory (once, latch, debounce, count) must be
398/// implemented as clusters where state flows through graph structure or
399/// environment, not trigger internals.
400///
401/// Statelessness is enforced at registration by manifest validation
402/// (`TRG-9`, `state.allowed == false`) and at runtime by capture/replay.
403/// Structural enforcement on top (derive macros, marker traits, newtype
404/// wrappers) was considered and rejected; see
405/// `docs/ledger/decisions/rejected-structural-enforcement-of-statelessness.md`.
406pub trait TriggerPrimitive: Send + Sync {
407    fn manifest(&self) -> &TriggerPrimitiveManifest;
408
409    fn evaluate(
410        &self,
411        inputs: &HashMap<String, TriggerValue>,
412        parameters: &HashMap<String, ParameterValue>,
413    ) -> HashMap<String, TriggerValue>;
414}
415
416pub use implementations::emit_if_event_and_true::EmitIfEventAndTrue;
417pub use implementations::emit_if_true::EmitIfTrue;
418pub use registry::TriggerRegistry;