Skip to main content

ergo_runtime/action/
mod.rs

1//! action
2//!
3//! Purpose:
4//! - Define kernel action primitive types, manifests, validation errors, and
5//!   registry helpers.
6//!
7//! Owns:
8//! - `ActionValidationError` as the typed registration failure surface for
9//!   action primitives.
10//! - Action type metadata and registry-facing action declarations.
11//!
12//! Does not own:
13//! - Catalog-level wrapper errors or host-facing diagnostics.
14//! - Runtime execution/orchestration semantics outside action registration.
15//!
16//! Connects to:
17//! - `catalog.rs`, which wraps action registration failures.
18//! - Action implementations under `implementations/`.
19//!
20//! Safety notes:
21//! - `Display` uses the `ErrorInfo` authority so action rule ids and summaries do
22//!   not drift from the kernel meaning they already own.
23
24use std::borrow::Cow;
25use std::collections::HashMap;
26use std::fmt;
27
28use crate::common::{doc_anchor_for_rule, ErrorInfo, Phase, ValueType};
29
30pub mod implementations;
31pub mod registry;
32
33#[derive(Debug, Clone, PartialEq)]
34pub enum ActionKind {
35    Action,
36}
37
38#[derive(Debug, Clone, PartialEq)]
39pub enum ActionValueType {
40    Event,
41    Number,
42    Series,
43    Bool,
44    String,
45}
46
47#[derive(Debug, Clone, PartialEq)]
48pub enum ActionOutcome {
49    Attempted,
50    /// Action executed successfully and completed.
51    /// NOTE: If serialization is introduced, add backward-compat serde alias for legacy "Filled".
52    Completed,
53    Rejected,
54    Cancelled,
55    Failed,
56    /// Action was never attempted because gating trigger emitted NotEmitted.
57    Skipped,
58}
59
60#[derive(Debug, Clone, PartialEq)]
61pub enum ActionValue {
62    Event(ActionOutcome),
63    Number(f64),
64    Series(Vec<f64>),
65    Bool(bool),
66    String(String),
67}
68
69impl ActionValue {
70    pub fn value_type(&self) -> ActionValueType {
71        match self {
72            ActionValue::Event(_) => ActionValueType::Event,
73            ActionValue::Number(_) => ActionValueType::Number,
74            ActionValue::Series(_) => ActionValueType::Series,
75            ActionValue::Bool(_) => ActionValueType::Bool,
76            ActionValue::String(_) => ActionValueType::String,
77        }
78    }
79
80    pub fn as_event(&self) -> Option<&ActionOutcome> {
81        match self {
82            ActionValue::Event(e) => Some(e),
83            _ => None,
84        }
85    }
86
87    pub fn as_series(&self) -> Option<&Vec<f64>> {
88        match self {
89            ActionValue::Series(series) => Some(series),
90            _ => None,
91        }
92    }
93}
94
95#[derive(Debug, Clone, PartialEq)]
96pub enum ParameterType {
97    Int,
98    Number,
99    Bool,
100    String,
101    Enum,
102}
103
104#[derive(Debug, Clone, PartialEq)]
105pub enum ParameterValue {
106    Int(i64),
107    Number(f64),
108    Bool(bool),
109    String(String),
110    Enum(String),
111}
112
113impl ParameterValue {
114    pub fn value_type(&self) -> ParameterType {
115        match self {
116            ParameterValue::Int(_) => ParameterType::Int,
117            ParameterValue::Number(_) => ParameterType::Number,
118            ParameterValue::Bool(_) => ParameterType::Bool,
119            ParameterValue::String(_) => ParameterType::String,
120            ParameterValue::Enum(_) => ParameterType::Enum,
121        }
122    }
123}
124
125#[derive(Debug, Clone, PartialEq)]
126pub enum Cardinality {
127    Single,
128}
129
130#[derive(Debug, Clone, PartialEq)]
131pub struct InputSpec {
132    pub name: String,
133    pub value_type: ActionValueType,
134    pub required: bool,
135    pub cardinality: Cardinality,
136}
137
138#[derive(Debug, Clone, PartialEq)]
139pub struct OutputSpec {
140    pub name: String,
141    pub value_type: ActionValueType,
142}
143
144#[derive(Debug, Clone, PartialEq)]
145pub struct ParameterSpec {
146    pub name: String,
147    pub value_type: ParameterType,
148    pub default: Option<ParameterValue>,
149    pub required: bool,
150    pub bounds: Option<String>,
151}
152
153#[derive(Debug, Clone, PartialEq)]
154pub struct ActionWriteSpec {
155    pub name: String,
156    pub value_type: ValueType,
157    pub from_input: String,
158}
159
160#[derive(Debug, Clone, PartialEq)]
161pub struct IntentFieldSpec {
162    pub name: String,
163    pub value_type: ValueType,
164    pub from_input: Option<String>,
165    pub from_param: Option<String>,
166}
167
168#[derive(Debug, Clone, PartialEq)]
169pub struct IntentMirrorWriteSpec {
170    pub name: String,
171    pub value_type: ValueType,
172    pub from_field: String,
173}
174
175#[derive(Debug, Clone, PartialEq)]
176pub struct IntentSpec {
177    pub name: String,
178    pub fields: Vec<IntentFieldSpec>,
179    pub mirror_writes: Vec<IntentMirrorWriteSpec>,
180}
181
182#[derive(Debug, Clone, PartialEq, Default)]
183pub struct ActionEffects {
184    pub writes: Vec<ActionWriteSpec>,
185    pub intents: Vec<IntentSpec>,
186}
187
188#[derive(Debug, Clone, PartialEq)]
189pub struct ExecutionSpec {
190    pub deterministic: bool,
191    pub retryable: bool,
192}
193
194#[derive(Debug, Clone, PartialEq)]
195pub struct StateSpec {
196    pub allowed: bool,
197}
198
199#[derive(Debug, Clone, PartialEq)]
200pub struct ActionPrimitiveManifest {
201    pub id: String,
202    pub version: String,
203    pub kind: ActionKind,
204    pub inputs: Vec<InputSpec>,
205    pub outputs: Vec<OutputSpec>,
206    pub parameters: Vec<ParameterSpec>,
207    pub effects: ActionEffects,
208    pub execution: ExecutionSpec,
209    pub state: StateSpec,
210    pub side_effects: bool,
211}
212
213#[derive(Debug, Clone, Default)]
214pub struct ActionState {
215    pub data: HashMap<String, ActionValue>,
216}
217
218#[derive(Debug, Clone, PartialEq)]
219#[non_exhaustive]
220pub enum ActionValidationError {
221    InvalidId {
222        id: String,
223    },
224    InvalidVersion {
225        version: String,
226    },
227    WrongKind {
228        expected: ActionKind,
229        got: ActionKind,
230    },
231    SideEffectsRequired,
232    NonDeterministicExecution,
233    RetryNotAllowed,
234    StateNotAllowed,
235    DuplicateId(String),
236    DuplicateInput {
237        name: String,
238        first_index: usize,
239        second_index: usize,
240    },
241    EventInputRequired,
242    DuplicateWriteName {
243        name: String,
244        first_index: usize,
245        second_index: usize,
246    },
247    InvalidWriteType {
248        name: String,
249        got: ValueType,
250    },
251    InvalidInputType {
252        input: String,
253        expected: ActionValueType,
254        got: ActionValueType,
255    },
256    OutputNotOutcome {
257        name: String,
258        index: usize,
259    },
260    InvalidOutputType {
261        output: String,
262        expected: ActionValueType,
263        got: ActionValueType,
264    },
265    UndeclaredOutput {
266        primitive: String,
267        output: String,
268    },
269    InvalidParameterType {
270        parameter: String,
271        expected: ParameterType,
272        got: ParameterType,
273    },
274    UnboundWriteKeyReference {
275        name: String,
276        referenced_param: String,
277    },
278    WriteKeyReferenceNotString {
279        name: String,
280        referenced_param: String,
281    },
282    WriteFromInputNotFound {
283        write_name: String,
284        from_input: String,
285    },
286    WriteFromInputTypeMismatch {
287        write_name: String,
288        from_input: String,
289        expected: ValueType,
290        found: ActionValueType,
291    },
292    DuplicateIntentName {
293        name: String,
294        first_index: usize,
295        second_index: usize,
296    },
297    DuplicateIntentFieldName {
298        intent_name: String,
299        field_name: String,
300        first_index: usize,
301        second_index: usize,
302    },
303    IntentFieldMissingSource {
304        intent_name: String,
305        field_name: String,
306    },
307    IntentFieldMultipleSources {
308        intent_name: String,
309        field_name: String,
310    },
311    IntentFieldFromInputNotFound {
312        intent_name: String,
313        field_name: String,
314        from_input: String,
315    },
316    IntentFieldFromInputTypeMismatch {
317        intent_name: String,
318        field_name: String,
319        from_input: String,
320        expected: ValueType,
321        found: ActionValueType,
322    },
323    IntentFieldFromParamNotFound {
324        intent_name: String,
325        field_name: String,
326        from_param: String,
327    },
328    IntentFieldFromParamTypeMismatch {
329        intent_name: String,
330        field_name: String,
331        from_param: String,
332        expected: ValueType,
333        found: ParameterType,
334    },
335    MirrorWriteFromFieldNotFound {
336        intent_name: String,
337        write_name: String,
338        from_field: String,
339    },
340    MirrorWriteTypeMismatch {
341        intent_name: String,
342        write_name: String,
343        from_field: String,
344        expected: ValueType,
345        found: ValueType,
346    },
347}
348
349impl ErrorInfo for ActionValidationError {
350    fn rule_id(&self) -> &'static str {
351        match self {
352            Self::InvalidId { .. } => "ACT-1",
353            Self::InvalidVersion { .. } => "ACT-2",
354            Self::WrongKind { .. } => "ACT-3",
355            Self::EventInputRequired => "ACT-4",
356            Self::DuplicateInput { .. } => "ACT-5",
357            Self::InvalidInputType { .. } => "ACT-6",
358            Self::UndeclaredOutput { .. } => "ACT-7",
359            Self::OutputNotOutcome { .. } => "ACT-8",
360            Self::InvalidOutputType { .. } => "ACT-9",
361            Self::StateNotAllowed => "ACT-10",
362            Self::SideEffectsRequired => "ACT-11",
363            Self::DuplicateWriteName { .. } => "ACT-14",
364            Self::InvalidWriteType { .. } => "ACT-15",
365            Self::RetryNotAllowed => "ACT-16",
366            Self::NonDeterministicExecution => "ACT-17",
367            Self::DuplicateId(_) => "ACT-18",
368            Self::InvalidParameterType { .. } => "ACT-19",
369            Self::UnboundWriteKeyReference { .. } => "ACT-20",
370            Self::WriteKeyReferenceNotString { .. } => "ACT-21",
371            Self::WriteFromInputNotFound { .. } => "ACT-22",
372            Self::WriteFromInputTypeMismatch { .. } => "ACT-23",
373            Self::DuplicateIntentName { .. } => "ACT-24",
374            Self::DuplicateIntentFieldName { .. } => "ACT-25",
375            Self::IntentFieldMissingSource { .. } => "ACT-26",
376            Self::IntentFieldMultipleSources { .. } => "ACT-27",
377            Self::IntentFieldFromInputNotFound { .. } => "ACT-28",
378            Self::IntentFieldFromInputTypeMismatch { .. } => "ACT-29",
379            Self::IntentFieldFromParamNotFound { .. } => "ACT-30",
380            Self::IntentFieldFromParamTypeMismatch { .. } => "ACT-31",
381            Self::MirrorWriteFromFieldNotFound { .. } => "ACT-32",
382            Self::MirrorWriteTypeMismatch { .. } => "ACT-33",
383        }
384    }
385
386    fn phase(&self) -> Phase {
387        Phase::Registration
388    }
389
390    fn doc_anchor(&self) -> &'static str {
391        doc_anchor_for_rule(self.rule_id())
392    }
393
394    fn summary(&self) -> Cow<'static, str> {
395        match self {
396            Self::InvalidId { id } => Cow::Owned(format!("Invalid action ID: '{}'", id)),
397            Self::InvalidVersion { version } => {
398                Cow::Owned(format!("Invalid version: '{}'", version))
399            }
400            Self::WrongKind { expected, got } => Cow::Owned(format!(
401                "Wrong kind: expected {:?}, got {:?}",
402                expected, got
403            )),
404            Self::SideEffectsRequired => Cow::Borrowed("Actions must declare side effects"),
405            Self::NonDeterministicExecution => {
406                Cow::Borrowed("Action execution must be deterministic")
407            }
408            Self::RetryNotAllowed => Cow::Borrowed("Action retryable must be false"),
409            Self::StateNotAllowed => Cow::Borrowed("Action state is not allowed"),
410            Self::DuplicateId(_) => Cow::Borrowed("Duplicate action ID: already registered"),
411            Self::DuplicateInput { name, .. } => {
412                Cow::Owned(format!("Duplicate input name: '{}'", name))
413            }
414            Self::EventInputRequired => Cow::Borrowed("Action requires at least one event input"),
415            Self::DuplicateWriteName { name, .. } => {
416                Cow::Owned(format!("Duplicate write name: '{}'", name))
417            }
418            Self::InvalidWriteType { name, got } => {
419                Cow::Owned(format!("Write '{}' has invalid type {:?}", name, got))
420            }
421            Self::InvalidInputType {
422                input,
423                expected,
424                got,
425            } => Cow::Owned(format!(
426                "Input '{}' has invalid type: expected {:?}, got {:?}",
427                input, expected, got
428            )),
429            Self::OutputNotOutcome { name, .. } => Cow::Owned(format!(
430                "Action output must be named 'outcome', got '{}'",
431                name
432            )),
433            Self::InvalidOutputType {
434                output,
435                expected,
436                got,
437            } => Cow::Owned(format!(
438                "Output '{}' has invalid type: expected {:?}, got {:?}",
439                output, expected, got
440            )),
441            Self::UndeclaredOutput { primitive, output } => Cow::Owned(format!(
442                "Undeclared output '{}' on primitive '{}'",
443                output, primitive
444            )),
445            Self::InvalidParameterType {
446                parameter,
447                expected,
448                got,
449            } => Cow::Owned(format!(
450                "Parameter '{}' has invalid type: expected {:?}, got {:?}",
451                parameter, expected, got
452            )),
453            Self::UnboundWriteKeyReference {
454                name,
455                referenced_param,
456            } => Cow::Owned(format!(
457                "Write key '{}' references undefined parameter '{}'",
458                name, referenced_param
459            )),
460            Self::WriteKeyReferenceNotString {
461                name,
462                referenced_param,
463            } => Cow::Owned(format!(
464                "Write key '{}' references parameter '{}' which is not String type",
465                name, referenced_param
466            )),
467            Self::WriteFromInputNotFound {
468                write_name,
469                from_input,
470            } => Cow::Owned(format!(
471                "Write '{}' references undeclared input '{}'",
472                write_name, from_input
473            )),
474            Self::WriteFromInputTypeMismatch {
475                write_name,
476                from_input,
477                expected,
478                found,
479            } => Cow::Owned(format!(
480                "Write '{}' type {:?} does not match input '{}' type {:?}",
481                write_name, expected, from_input, found
482            )),
483            Self::DuplicateIntentName { name, .. } => {
484                Cow::Owned(format!("Duplicate intent name: '{}'", name))
485            }
486            Self::DuplicateIntentFieldName {
487                intent_name,
488                field_name,
489                ..
490            } => Cow::Owned(format!(
491                "Intent '{}' has duplicate field name '{}'",
492                intent_name, field_name
493            )),
494            Self::IntentFieldMissingSource {
495                intent_name,
496                field_name,
497            } => Cow::Owned(format!(
498                "Intent '{}' field '{}' must set exactly one source (from_input or from_param)",
499                intent_name, field_name
500            )),
501            Self::IntentFieldMultipleSources {
502                intent_name,
503                field_name,
504            } => Cow::Owned(format!(
505                "Intent '{}' field '{}' sets both from_input and from_param",
506                intent_name, field_name
507            )),
508            Self::IntentFieldFromInputNotFound {
509                intent_name,
510                field_name,
511                from_input,
512            } => Cow::Owned(format!(
513                "Intent '{}' field '{}' references undeclared input '{}'",
514                intent_name, field_name, from_input
515            )),
516            Self::IntentFieldFromInputTypeMismatch {
517                intent_name,
518                field_name,
519                from_input,
520                expected,
521                found,
522            } => Cow::Owned(format!(
523                "Intent '{}' field '{}' expects {:?} but input '{}' is {:?}",
524                intent_name, field_name, expected, from_input, found
525            )),
526            Self::IntentFieldFromParamNotFound {
527                intent_name,
528                field_name,
529                from_param,
530            } => Cow::Owned(format!(
531                "Intent '{}' field '{}' references undeclared parameter '{}'",
532                intent_name, field_name, from_param
533            )),
534            Self::IntentFieldFromParamTypeMismatch {
535                intent_name,
536                field_name,
537                from_param,
538                expected,
539                found,
540            } => Cow::Owned(format!(
541                "Intent '{}' field '{}' expects {:?} but parameter '{}' is {:?}",
542                intent_name, field_name, expected, from_param, found
543            )),
544            Self::MirrorWriteFromFieldNotFound {
545                intent_name,
546                write_name,
547                from_field,
548            } => Cow::Owned(format!(
549                "Intent '{}' mirror write '{}' references undeclared field '{}'",
550                intent_name, write_name, from_field
551            )),
552            Self::MirrorWriteTypeMismatch {
553                intent_name,
554                write_name,
555                from_field,
556                expected,
557                found,
558            } => Cow::Owned(format!(
559                "Intent '{}' mirror write '{}' type {:?} does not match field '{}' type {:?}",
560                intent_name, write_name, expected, from_field, found
561            )),
562        }
563    }
564
565    fn path(&self) -> Option<Cow<'static, str>> {
566        match self {
567            Self::InvalidId { .. } => Some(Cow::Borrowed("$.id")),
568            Self::InvalidVersion { .. } => Some(Cow::Borrowed("$.version")),
569            Self::DuplicateId(_) => Some(Cow::Borrowed("$.id")),
570            Self::WrongKind { .. } => Some(Cow::Borrowed("$.kind")),
571            Self::EventInputRequired => Some(Cow::Borrowed("$.inputs")),
572            Self::DuplicateInput { second_index, .. } => {
573                Some(Cow::Owned(format!("$.inputs[{}].name", second_index)))
574            }
575            Self::InvalidInputType { .. } => Some(Cow::Borrowed("$.inputs[].type")),
576            Self::UndeclaredOutput { .. } => Some(Cow::Borrowed("$.outputs")),
577            Self::OutputNotOutcome { index, .. } => {
578                Some(Cow::Owned(format!("$.outputs[{}].name", index)))
579            }
580            Self::InvalidOutputType { .. } => Some(Cow::Borrowed("$.outputs[0].type")),
581            Self::StateNotAllowed => Some(Cow::Borrowed("$.state.allowed")),
582            Self::SideEffectsRequired => Some(Cow::Borrowed("$.side_effects")),
583            Self::DuplicateWriteName { second_index, .. } => Some(Cow::Owned(format!(
584                "$.effects.writes[{}].name",
585                second_index
586            ))),
587            Self::InvalidWriteType { .. } => Some(Cow::Borrowed("$.effects.writes[].type")),
588            Self::RetryNotAllowed => Some(Cow::Borrowed("$.execution.retryable")),
589            Self::NonDeterministicExecution => Some(Cow::Borrowed("$.execution.deterministic")),
590            Self::InvalidParameterType { .. } => Some(Cow::Borrowed("$.parameters[].default")),
591            Self::UnboundWriteKeyReference { .. } => Some(Cow::Borrowed("$.effects.writes[].name")),
592            Self::WriteKeyReferenceNotString { .. } => {
593                Some(Cow::Borrowed("$.effects.writes[].name"))
594            }
595            Self::WriteFromInputNotFound { .. } => {
596                Some(Cow::Borrowed("$.effects.writes[].from_input"))
597            }
598            Self::WriteFromInputTypeMismatch { .. } => {
599                Some(Cow::Borrowed("$.effects.writes[].from_input"))
600            }
601            Self::DuplicateIntentName { second_index, .. } => Some(Cow::Owned(format!(
602                "$.effects.intents[{}].name",
603                second_index
604            ))),
605            Self::DuplicateIntentFieldName {
606                intent_name,
607                second_index,
608                ..
609            } => Some(Cow::Owned(format!(
610                "$.effects.intents[?(@.name==\"{}\")].fields[{}].name",
611                intent_name, second_index
612            ))),
613            Self::IntentFieldMissingSource { intent_name, .. }
614            | Self::IntentFieldMultipleSources { intent_name, .. } => Some(Cow::Owned(format!(
615                "$.effects.intents[?(@.name==\"{}\")].fields[]",
616                intent_name
617            ))),
618            Self::IntentFieldFromInputNotFound { intent_name, .. }
619            | Self::IntentFieldFromInputTypeMismatch { intent_name, .. } => {
620                Some(Cow::Owned(format!(
621                    "$.effects.intents[?(@.name==\"{}\")].fields[].from_input",
622                    intent_name
623                )))
624            }
625            Self::IntentFieldFromParamNotFound { intent_name, .. }
626            | Self::IntentFieldFromParamTypeMismatch { intent_name, .. } => {
627                Some(Cow::Owned(format!(
628                    "$.effects.intents[?(@.name==\"{}\")].fields[].from_param",
629                    intent_name
630                )))
631            }
632            Self::MirrorWriteFromFieldNotFound { intent_name, .. }
633            | Self::MirrorWriteTypeMismatch { intent_name, .. } => Some(Cow::Owned(format!(
634                "$.effects.intents[?(@.name==\"{}\")].mirror_writes[]",
635                intent_name
636            ))),
637        }
638    }
639
640    fn fix(&self) -> Option<Cow<'static, str>> {
641        match self {
642            Self::InvalidId { .. } => Some(Cow::Borrowed(
643                "ID must start with lowercase letter and contain only lowercase letters, digits, and underscores",
644            )),
645            Self::DuplicateId(_) => Some(Cow::Borrowed("Choose a unique ID not already registered")),
646            Self::InvalidVersion { .. } => Some(Cow::Borrowed(
647                "Version must be valid semver (e.g., '1.0.0')",
648            )),
649            Self::WrongKind { .. } => Some(Cow::Borrowed("Set kind: action")),
650            Self::EventInputRequired => Some(Cow::Borrowed("Add at least one event input")),
651            Self::DuplicateInput { name, .. } => Some(Cow::Owned(format!(
652                "Rename input '{}' to a unique value",
653                name
654            ))),
655            Self::InvalidInputType { .. } => Some(Cow::Borrowed(
656                "Use a valid input type: event, number, series, bool, or string",
657            )),
658            Self::UndeclaredOutput { .. } => Some(Cow::Borrowed("Declare a single outcome output")),
659            Self::OutputNotOutcome { .. } => Some(Cow::Borrowed("Rename output to 'outcome'")),
660            Self::InvalidOutputType { .. } => Some(Cow::Borrowed("Output type must be event")),
661            Self::StateNotAllowed => Some(Cow::Borrowed("Set state.allowed: false")),
662            Self::SideEffectsRequired => Some(Cow::Borrowed("Set side_effects: true")),
663            Self::DuplicateWriteName { name, .. } => Some(Cow::Owned(format!(
664                "Rename write '{}' to a unique value",
665                name
666            ))),
667            Self::InvalidWriteType { .. } => Some(Cow::Borrowed(
668                "Write types must be Number, Series, Bool, or String",
669            )),
670            Self::RetryNotAllowed => Some(Cow::Borrowed("Set execution.retryable: false")),
671            Self::NonDeterministicExecution => {
672                Some(Cow::Borrowed("Set execution.deterministic: true"))
673            }
674            Self::InvalidParameterType { .. } => Some(Cow::Borrowed(
675                "Change parameter default value to match the declared parameter type",
676            )),
677            Self::UnboundWriteKeyReference {
678                referenced_param, ..
679            } => Some(Cow::Owned(format!(
680                "Add parameter '{}' to the action manifest",
681                referenced_param
682            ))),
683            Self::WriteKeyReferenceNotString {
684                referenced_param, ..
685            } => Some(Cow::Owned(format!(
686                "Change parameter '{}' type to String",
687                referenced_param
688            ))),
689            Self::WriteFromInputNotFound { from_input, .. } => Some(Cow::Owned(format!(
690                "Declare input '{}' in the action manifest inputs",
691                from_input
692            ))),
693            Self::WriteFromInputTypeMismatch {
694                from_input,
695                expected,
696                ..
697            } => Some(Cow::Owned(format!(
698                "Change input '{}' type to match write type {:?}, or use a scalar-typed input",
699                from_input, expected
700            ))),
701            Self::DuplicateIntentName { name, .. } => Some(Cow::Owned(format!(
702                "Rename intent '{}' to a unique value",
703                name
704            ))),
705            Self::DuplicateIntentFieldName { field_name, .. } => Some(Cow::Owned(format!(
706                "Rename intent field '{}' to a unique value within its intent",
707                field_name
708            ))),
709            Self::IntentFieldMissingSource { .. } => Some(Cow::Borrowed(
710                "Set exactly one source on each intent field: from_input or from_param",
711            )),
712            Self::IntentFieldMultipleSources { .. } => Some(Cow::Borrowed(
713                "Set only one source on each intent field: from_input or from_param",
714            )),
715            Self::IntentFieldFromInputNotFound { from_input, .. } => Some(Cow::Owned(format!(
716                "Declare input '{}' in the action manifest inputs",
717                from_input
718            ))),
719            Self::IntentFieldFromInputTypeMismatch {
720                from_input,
721                expected,
722                ..
723            } => Some(Cow::Owned(format!(
724                "Change input '{}' type to match intent field type {:?}",
725                from_input, expected
726            ))),
727            Self::IntentFieldFromParamNotFound { from_param, .. } => Some(Cow::Owned(format!(
728                "Declare parameter '{}' in the action manifest parameters",
729                from_param
730            ))),
731            Self::IntentFieldFromParamTypeMismatch {
732                from_param,
733                expected,
734                ..
735            } => Some(Cow::Owned(format!(
736                "Change parameter '{}' type to match intent field type {:?}",
737                from_param, expected
738            ))),
739            Self::MirrorWriteFromFieldNotFound { from_field, .. } => Some(Cow::Owned(format!(
740                "Declare intent field '{}' before referencing it from mirror_writes",
741                from_field
742            ))),
743            Self::MirrorWriteTypeMismatch {
744                from_field,
745                expected,
746                ..
747            } => Some(Cow::Owned(format!(
748                "Change mirror write type to match field '{}' type {:?}",
749                from_field, expected
750            ))),
751        }
752    }
753}
754
755impl fmt::Display for ActionValidationError {
756    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
757        write!(f, "{} ({})", self.summary(), self.rule_id())
758    }
759}
760
761impl std::error::Error for ActionValidationError {}
762
763/// An action primitive that performs effects keyed by inputs and parameters.
764///
765/// Statelessness is enforced at registration by manifest validation
766/// (`ACT-10`, `state.allowed == false`) and at runtime by capture/replay.
767/// Structural enforcement on top (derive macros, marker traits, newtype
768/// wrappers) was considered and rejected; see
769/// `docs/ledger/decisions/rejected-structural-enforcement-of-statelessness.md`.
770pub trait ActionPrimitive {
771    fn manifest(&self) -> &ActionPrimitiveManifest;
772
773    fn execute(
774        &self,
775        inputs: &HashMap<String, ActionValue>,
776        parameters: &HashMap<String, ParameterValue>,
777    ) -> HashMap<String, ActionValue>;
778}
779
780pub use implementations::{
781    AckAction, AnnotateAction, ContextSetBoolAction, ContextSetNumberAction,
782    ContextSetSeriesAction, ContextSetStringAction,
783};
784pub use registry::ActionRegistry;
785
786#[cfg(test)]
787mod tests;