rite_model/ir/ceremony.rs
1//! Resolved ceremony IR types.
2//!
3//! These types represent a fully resolved ceremony, ready for execution.
4//! All references have been validated, execution order computed, and
5//! parameter values merged with defaults.
6
7use super::SymbolTable;
8use super::ids::{ActId, ArtifactId, MaterialId, OutputId, ParamId, RoleId, SectionId, StepId};
9use crate::expression::ExprValue;
10use crate::material::MaterialSource;
11use crate::types::{ActionType, DutyType, Metadata, OutputType, ParameterType};
12use rite_sdk::BackendConfig;
13use std::collections::HashMap;
14
15/// A fully resolved ceremony, ready for execution.
16///
17/// All references have been validated, the execution order has been computed,
18/// and parameter values have been merged with external inputs.
19#[derive(Debug, Clone)]
20pub struct Ceremony {
21 /// Ceremony metadata (name, description).
22 pub metadata: Metadata,
23
24 /// Resolved roles, keyed by role ID.
25 pub roles: SymbolTable<RoleId, Role>,
26
27 /// Resolved acts, keyed by act ID (for script display).
28 pub acts: SymbolTable<ActId, Act>,
29
30 /// Resolved sections, keyed by section ID.
31 pub sections: SymbolTable<SectionId, Section>,
32
33 /// Resolved parameters with their values, keyed by parameter ID.
34 pub parameters: SymbolTable<ParamId, Parameter>,
35
36 /// Resolved materials with their sources, keyed by material ID.
37 pub materials: SymbolTable<MaterialId, Material>,
38
39 /// Pre-ceremony conditions (prose-only checklist).
40 pub prerequisites: Vec<String>,
41
42 /// Resolved outputs with their destinations, keyed by output ID.
43 pub outputs: SymbolTable<OutputId, Output>,
44
45 /// Backend configurations, keyed by backend name.
46 pub backends: HashMap<String, BackendConfig>,
47
48 /// Steps in execution order.
49 ///
50 /// Pre-computed topological order based on sections.
51 pub execution_plan: Vec<Step>,
52
53 /// Post-ceremony duties, in declaration order.
54 pub after: Vec<PostCeremonyDuty>,
55
56 /// Digest of the ceremony YAML this ceremony was resolved from. Covers
57 /// the template only: run-time inputs are merged into the fields above.
58 pub source_digest: crate::Sha256Digest,
59}
60
61/// A resolved role.
62#[derive(Debug, Clone)]
63pub struct Role {
64 /// Role identifier.
65 pub id: RoleId,
66
67 /// Human-readable name (derived or explicit).
68 pub name: String,
69
70 /// Role type: the prefix before `__` (or the whole ID if no `__`).
71 ///
72 /// E.g. `"witness__1"` → `"witness"`, `"operator"` → `"operator"`.
73 pub role_type: String,
74
75 /// The person assigned to this role slot, if any.
76 pub person: Option<String>,
77}
78
79/// A resolved act (for script display).
80#[derive(Debug, Clone)]
81pub struct Act {
82 /// Act identifier.
83 pub id: ActId,
84
85 /// Human-readable name.
86 pub name: Option<String>,
87
88 /// Preamble text displayed at the start of the act.
89 pub description: Option<String>,
90}
91
92/// A resolved section.
93#[derive(Debug, Clone)]
94pub struct Section {
95 /// Section identifier.
96 pub id: SectionId,
97
98 /// Reference to the act this section belongs to.
99 pub act: Option<ActId>,
100
101 /// Human-readable name.
102 pub name: Option<String>,
103
104 /// Section description.
105 pub description: Option<String>,
106
107 /// Default role for steps in this section.
108 pub default_role: Option<RoleId>,
109}
110
111/// A resolved step, ready for execution.
112#[derive(Debug, Clone)]
113pub struct Step {
114 /// Step identifier.
115 pub id: StepId,
116
117 /// Display label for this step (e.g., `"1.1"`, `"2.3"`).
118 ///
119 /// Uses hierarchical numbering: `act.step_within_act`.
120 /// When there's only one act, just the step number (e.g., `"1"`, `"2"`).
121 pub step_label: String,
122
123 /// Section this step belongs to.
124 pub section: SectionId,
125
126 /// Action to perform.
127 pub action: ActionType,
128
129 /// Backend to use for this step (if action requires one).
130 ///
131 /// References a backend declared in the ceremony's backends section.
132 pub backend: Option<String>,
133
134 /// Role that performs this step (resolved from step or section default).
135 pub role: Option<RoleId>,
136
137 /// Human-confirmed preconditions before this step executes.
138 pub preconditions: Vec<String>,
139
140 /// Action-specific parameters with expressions parsed (not yet evaluated).
141 ///
142 /// The runtime evaluates these to `serde_json::Value` before passing to handlers.
143 pub with: ExprValue,
144
145 /// Validated input artifact references (for ordering validation).
146 pub reads: Vec<ArtifactRef>,
147
148 /// Pre-resolved input references for handlers.
149 ///
150 /// Handlers can access artifact IDs directly without parsing.
151 pub reads_resolved: Option<StepInputs>,
152
153 /// Artifact ID this step produces.
154 pub creates: Option<ArtifactId>,
155
156 /// Human-readable description for display (may contain interpolated expressions).
157 pub description: Option<ExprValue>,
158
159 /// Skip the default pause after step completion.
160 ///
161 /// When `true`, the executor auto-advances without waiting for user acknowledgment.
162 pub silent: bool,
163
164 /// How the runtime treats a *transient* failure of this step.
165 ///
166 /// The DSL constrains the retry-by-default; see [`RetryPolicy`]. Absent in
167 /// the DSL means [`RetryPolicy::Prompt`].
168 pub retry: RetryPolicy,
169}
170
171/// How the runtime treats a transient (retriable) failure of a step.
172///
173/// Rite ceremonies always have a human at the console, so the default is to
174/// pause and let the operator decide, rather than burn a fixed retry budget.
175/// The DSL field constrains that default rather than enabling it.
176#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
177pub enum RetryPolicy {
178 /// Pause on a transient error and prompt the operator (Retry / Abort),
179 /// unlimited times. The default when the DSL omits `retry:`.
180 #[default]
181 Prompt,
182 /// Never retry: a transient error fails the step immediately. For steps
183 /// where repeated attempts are themselves security-relevant.
184 Never,
185 /// Retry up to this many total attempts, then fail. A hard cap.
186 MaxAttempts(u32),
187}
188
189/// A resolved parameter with its value.
190#[derive(Debug, Clone)]
191pub struct Parameter {
192 /// Parameter identifier.
193 pub id: ParamId,
194
195 /// Declared type.
196 pub declared_type: ParameterType,
197
198 /// Resolved value (from CLI/env/prompt or default).
199 pub value: serde_json::Value,
200
201 /// Human-readable description.
202 pub description: Option<String>,
203}
204
205/// What kind of material and its resolved source/identifier.
206#[derive(Debug, Clone)]
207pub enum MaterialKind {
208 /// Digital: loaded from file path. `None` if source not yet provided.
209 Digital {
210 /// Resolved file source, or `None` if not yet provided.
211 source: Option<MaterialSource>,
212 },
213 /// Physical: optional identifier string for display.
214 Physical {
215 /// Human-readable identifier (e.g., serial number).
216 identifier: Option<String>,
217 /// Item count for checklist rendering.
218 quantity: Option<u32>,
219 },
220}
221
222/// A resolved material with its source.
223#[derive(Debug, Clone)]
224pub struct Material {
225 /// Material identifier.
226 pub id: MaterialId,
227
228 /// What kind of material and its resolved source/identifier.
229 pub kind: MaterialKind,
230
231 /// Optional human-readable title for display (falls back to material ID).
232 pub title: Option<String>,
233
234 /// Human-readable description.
235 pub description: Option<String>,
236}
237
238impl Material {
239 /// Get the display name for this material (title or ID).
240 pub fn display_name(&self) -> &str {
241 self.title.as_deref().unwrap_or(self.id.as_str())
242 }
243
244 /// Whether this is a digital material.
245 pub fn is_digital(&self) -> bool {
246 matches!(self.kind, MaterialKind::Digital { .. })
247 }
248
249 /// Whether this is a physical material.
250 pub fn is_physical(&self) -> bool {
251 matches!(self.kind, MaterialKind::Physical { .. })
252 }
253}
254
255/// A resolved post-ceremony duty.
256#[derive(Debug, Clone)]
257pub struct PostCeremonyDuty {
258 /// Duty identifier (the dict key in the YAML `after:` block).
259 pub id: String,
260
261 /// Type of duty.
262 pub kind: DutyType,
263
264 /// Role responsible for this duty (resolved from plain role ID string).
265 pub role: Option<RoleId>,
266
267 /// Description of this duty (overrides built-in prose for the type).
268 pub description: Option<String>,
269
270 /// Checklist sub-items.
271 pub items: Vec<String>,
272
273 /// For distribute_* types: who receives.
274 pub recipient: Option<String>,
275
276 /// For storage/archive types: where.
277 pub location: Option<String>,
278}
279
280/// A resolved output declaration.
281#[derive(Debug, Clone)]
282pub struct Output {
283 /// Output identifier.
284 pub id: OutputId,
285
286 /// Type of output.
287 pub kind: OutputType,
288
289 /// Human-readable description.
290 pub description: Option<String>,
291}
292
293/// Reference to an artifact (either a material or a produced artifact).
294#[derive(Debug, Clone)]
295pub enum ArtifactRef {
296 /// References a pre-loaded material.
297 Material {
298 /// Material ID.
299 id: MaterialId,
300 /// Optional property accessor (e.g., `"public"` from a keypair).
301 property: Option<String>,
302 },
303
304 /// References an artifact produced by an earlier step.
305 Produced {
306 /// Artifact ID.
307 id: ArtifactId,
308 /// Optional property accessor.
309 property: Option<String>,
310 },
311}
312
313impl ArtifactRef {
314 /// Get a display name for this reference.
315 pub fn display_name(&self) -> String {
316 match self {
317 ArtifactRef::Material { id, property: None } => id.to_string(),
318 ArtifactRef::Material {
319 id,
320 property: Some(p),
321 } => format!("{id}.{p}"),
322 ArtifactRef::Produced { id, property: None } => id.to_string(),
323 ArtifactRef::Produced {
324 id,
325 property: Some(p),
326 } => format!("{id}.{p}"),
327 }
328 }
329
330 /// Get the artifact ID from this reference.
331 pub fn artifact_id(&self) -> ArtifactId {
332 match self {
333 ArtifactRef::Material { id, .. } => ArtifactId::new(id.as_str()),
334 ArtifactRef::Produced { id, .. } => id.clone(),
335 }
336 }
337
338 /// Get the property accessor from this reference (if any).
339 pub fn property(&self) -> Option<&str> {
340 match self {
341 ArtifactRef::Material { property, .. } | ArtifactRef::Produced { property, .. } => {
342 property.as_deref()
343 }
344 }
345 }
346}
347
348/// Pre-resolved input references for a step.
349///
350/// At resolution time, the resolver parses `${artifact.name}` strings and determines
351/// whether they reference materials or produced artifacts. This eliminates runtime
352/// string parsing in handlers.
353#[derive(Debug, Clone)]
354pub enum StepInputs {
355 /// Single artifact reference (e.g., `reads: "${artifact.keypair}"`)
356 Single(ArtifactRef),
357 /// Named artifact references (e.g., `reads: { key_to_wrap: "...", wrapping_key: "..." }`)
358 Named(HashMap<String, NamedInput>),
359}
360
361/// The value under one key of a `reads:` map: one reference, or a list of
362/// them for an action that takes as many as the ceremony gives it.
363///
364/// Which shape a key takes is the action's contract, and the resolver
365/// checks it, so a handler asks for the shape it expects and treats the
366/// other as absent.
367#[derive(Debug, Clone)]
368pub enum NamedInput {
369 /// `key: "${artifact.x}"`
370 One(ArtifactRef),
371 /// `key: ["${artifact.x}", "${artifact.y}"]`, in the order written.
372 Many(Vec<ArtifactRef>),
373}
374
375impl NamedInput {
376 /// Every reference under the key, one or many.
377 pub fn refs(&self) -> &[ArtifactRef] {
378 match self {
379 NamedInput::One(r) => std::slice::from_ref(r),
380 NamedInput::Many(refs) => refs,
381 }
382 }
383}
384
385impl StepInputs {
386 /// Get a single input reference (returns `None` if this is `Named`).
387 pub fn as_single(&self) -> Option<&ArtifactRef> {
388 match self {
389 StepInputs::Single(r) => Some(r),
390 StepInputs::Named(_) => None,
391 }
392 }
393
394 /// Get named input references (returns `None` if this is `Single`).
395 pub fn as_named(&self) -> Option<&HashMap<String, NamedInput>> {
396 match self {
397 StepInputs::Single(_) => None,
398 StepInputs::Named(m) => Some(m),
399 }
400 }
401
402 /// Get a named input by key, when it holds one reference.
403 pub fn get(&self, key: &str) -> Option<&ArtifactRef> {
404 match self.as_named()?.get(key)? {
405 NamedInput::One(r) => Some(r),
406 NamedInput::Many(_) => None,
407 }
408 }
409
410 /// Get a named input by key, when it holds a list.
411 pub fn get_many(&self, key: &str) -> Option<&[ArtifactRef]> {
412 match self.as_named()?.get(key)? {
413 NamedInput::One(_) => None,
414 NamedInput::Many(refs) => Some(refs),
415 }
416 }
417}