Skip to main content

effect_runtime/
compensation.rs

1//! Compensation lifecycle artifacts: plans and execution receipts for reversing or mitigating effects.
2//!
3//! When an effect fails or needs rollback, a compensation plan defines the
4//! recovery steps, and a compensation execution receipt records their outcome.
5
6use crate::error::{
7    require_id, require_non_empty, require_non_empty_slice, require_schema_version,
8    EffectRuntimeValidationError, EffectValidationResult,
9};
10use crate::v25::V25ConstitutionCitation;
11use crate::vocab::{CompensationClassV1, ExecutionStateV1};
12use schemars::JsonSchema;
13use serde::{Deserialize, Serialize};
14use stack_ids::{CompensationExecutionReceiptId, CompensationPlanId, EffectIntentId};
15
16/// Plan describing the compensation steps required to reverse or mitigate a failed effect.
17///
18/// When `compensation_required` is true, `compensation_steps` must be non-empty.
19#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
20pub struct CompensationPlanV1 {
21    pub schema_version: String,
22    pub compensation_plan_id: CompensationPlanId,
23    pub effect_intent_id: EffectIntentId,
24    /// Carries EffectiveConstitutionId and CompiledObligationSetId via the canonical v25 citation.
25    #[serde(flatten)]
26    pub citation: V25ConstitutionCitation,
27    pub compensation_required: bool,
28    pub compensation_class: CompensationClassV1,
29    pub compensation_steps: Vec<String>,
30    pub success_criteria: Vec<String>,
31    pub owner_ref: String,
32    pub latest_start: String,
33    pub advisory_only: bool,
34}
35
36/// Receipt recording the outcome of executing a compensation plan, including completed and failed steps.
37///
38/// Completed receipts must not list any failed steps.
39#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
40pub struct CompensationExecutionReceiptV1 {
41    pub schema_version: String,
42    pub compensation_execution_receipt_id: CompensationExecutionReceiptId,
43    pub compensation_plan_id: CompensationPlanId,
44    /// Carries EffectiveConstitutionId and CompiledObligationSetId via the canonical v25 citation.
45    #[serde(flatten)]
46    pub citation: V25ConstitutionCitation,
47    pub execution_state: ExecutionStateV1,
48    pub completed_steps: Vec<String>,
49    pub failed_steps: Vec<String>,
50    pub residual_risk_summary: String,
51    pub generated_at: String,
52}
53
54impl CompensationPlanV1 {
55    pub const SCHEMA_VERSION: &'static str = "CompensationPlanV1";
56
57    /// Returns a builder for a validated compensation plan artifact.
58    #[allow(clippy::too_many_arguments)]
59    pub fn builder(
60        compensation_plan_id: CompensationPlanId,
61        effect_intent_id: EffectIntentId,
62        citation: V25ConstitutionCitation,
63        compensation_required: bool,
64        compensation_class: CompensationClassV1,
65        compensation_steps: Vec<String>,
66        success_criteria: Vec<String>,
67        owner_ref: impl Into<String>,
68        latest_start: impl Into<String>,
69        advisory_only: bool,
70    ) -> CompensationPlanV1Builder {
71        CompensationPlanV1Builder::new(
72            compensation_plan_id,
73            effect_intent_id,
74            citation,
75            compensation_required,
76            compensation_class,
77            compensation_steps,
78            success_criteria,
79            owner_ref,
80            latest_start,
81            advisory_only,
82        )
83    }
84
85    /// Validates the owner-side invariants for a compensation plan.
86    pub fn validate(&self) -> EffectValidationResult {
87        require_schema_version(
88            "CompensationPlanV1",
89            Self::SCHEMA_VERSION,
90            &self.schema_version,
91        )?;
92        require_id(&self.compensation_plan_id, "compensation_plan_id")?;
93        require_id(&self.effect_intent_id, "effect_intent_id")?;
94        require_non_empty_slice(&self.success_criteria, "success_criteria")?;
95        require_non_empty(&self.owner_ref, "owner_ref")?;
96        require_non_empty(&self.latest_start, "latest_start")?;
97        if self.compensation_required {
98            require_non_empty_slice(&self.compensation_steps, "compensation_steps")?;
99        }
100        Ok(())
101    }
102}
103
104/// Builder for [`CompensationPlanV1`].
105#[derive(Debug, Clone)]
106pub struct CompensationPlanV1Builder {
107    compensation_plan_id: CompensationPlanId,
108    effect_intent_id: EffectIntentId,
109    citation: V25ConstitutionCitation,
110    compensation_required: bool,
111    compensation_class: CompensationClassV1,
112    compensation_steps: Vec<String>,
113    success_criteria: Vec<String>,
114    owner_ref: String,
115    latest_start: String,
116    advisory_only: bool,
117}
118
119impl CompensationPlanV1Builder {
120    /// Creates a builder with the required compensation plan fields.
121    #[allow(clippy::too_many_arguments)]
122    pub fn new(
123        compensation_plan_id: CompensationPlanId,
124        effect_intent_id: EffectIntentId,
125        citation: V25ConstitutionCitation,
126        compensation_required: bool,
127        compensation_class: CompensationClassV1,
128        compensation_steps: Vec<String>,
129        success_criteria: Vec<String>,
130        owner_ref: impl Into<String>,
131        latest_start: impl Into<String>,
132        advisory_only: bool,
133    ) -> Self {
134        Self {
135            compensation_plan_id,
136            effect_intent_id,
137            citation,
138            compensation_required,
139            compensation_class,
140            compensation_steps,
141            success_criteria,
142            owner_ref: owner_ref.into(),
143            latest_start: latest_start.into(),
144            advisory_only,
145        }
146    }
147
148    /// Builds a validated compensation plan artifact.
149    pub fn build(self) -> Result<CompensationPlanV1, EffectRuntimeValidationError> {
150        let value = CompensationPlanV1 {
151            schema_version: CompensationPlanV1::SCHEMA_VERSION.to_string(),
152            compensation_plan_id: self.compensation_plan_id,
153            effect_intent_id: self.effect_intent_id,
154            citation: self.citation,
155            compensation_required: self.compensation_required,
156            compensation_class: self.compensation_class,
157            compensation_steps: self.compensation_steps,
158            success_criteria: self.success_criteria,
159            owner_ref: self.owner_ref,
160            latest_start: self.latest_start,
161            advisory_only: self.advisory_only,
162        };
163        value.validate()?;
164        Ok(value)
165    }
166}
167
168impl CompensationExecutionReceiptV1 {
169    pub const SCHEMA_VERSION: &'static str = "CompensationExecutionReceiptV1";
170
171    /// Returns a builder for a validated compensation execution receipt artifact.
172    #[allow(clippy::too_many_arguments)]
173    pub fn builder(
174        compensation_execution_receipt_id: CompensationExecutionReceiptId,
175        compensation_plan_id: CompensationPlanId,
176        citation: V25ConstitutionCitation,
177        execution_state: ExecutionStateV1,
178        completed_steps: Vec<String>,
179        failed_steps: Vec<String>,
180        residual_risk_summary: impl Into<String>,
181        generated_at: impl Into<String>,
182    ) -> CompensationExecutionReceiptV1Builder {
183        CompensationExecutionReceiptV1Builder::new(
184            compensation_execution_receipt_id,
185            compensation_plan_id,
186            citation,
187            execution_state,
188            completed_steps,
189            failed_steps,
190            residual_risk_summary,
191            generated_at,
192        )
193    }
194
195    /// Validates the owner-side invariants for a compensation execution receipt.
196    pub fn validate(&self) -> EffectValidationResult {
197        require_schema_version(
198            "CompensationExecutionReceiptV1",
199            Self::SCHEMA_VERSION,
200            &self.schema_version,
201        )?;
202        require_id(
203            &self.compensation_execution_receipt_id,
204            "compensation_execution_receipt_id",
205        )?;
206        require_id(&self.compensation_plan_id, "compensation_plan_id")?;
207        require_non_empty(&self.residual_risk_summary, "residual_risk_summary")?;
208        require_non_empty(&self.generated_at, "generated_at")?;
209        if matches!(self.execution_state, ExecutionStateV1::Completed)
210            && !self.failed_steps.is_empty()
211        {
212            return Err(EffectRuntimeValidationError::InvalidState(
213                "completed compensation receipts must not list failed_steps",
214            ));
215        }
216        Ok(())
217    }
218}
219
220/// Builder for [`CompensationExecutionReceiptV1`].
221#[derive(Debug, Clone)]
222pub struct CompensationExecutionReceiptV1Builder {
223    compensation_execution_receipt_id: CompensationExecutionReceiptId,
224    compensation_plan_id: CompensationPlanId,
225    citation: V25ConstitutionCitation,
226    execution_state: ExecutionStateV1,
227    completed_steps: Vec<String>,
228    failed_steps: Vec<String>,
229    residual_risk_summary: String,
230    generated_at: String,
231}
232
233impl CompensationExecutionReceiptV1Builder {
234    /// Creates a builder with the required compensation execution receipt fields.
235    #[allow(clippy::too_many_arguments)]
236    pub fn new(
237        compensation_execution_receipt_id: CompensationExecutionReceiptId,
238        compensation_plan_id: CompensationPlanId,
239        citation: V25ConstitutionCitation,
240        execution_state: ExecutionStateV1,
241        completed_steps: Vec<String>,
242        failed_steps: Vec<String>,
243        residual_risk_summary: impl Into<String>,
244        generated_at: impl Into<String>,
245    ) -> Self {
246        Self {
247            compensation_execution_receipt_id,
248            compensation_plan_id,
249            citation,
250            execution_state,
251            completed_steps,
252            failed_steps,
253            residual_risk_summary: residual_risk_summary.into(),
254            generated_at: generated_at.into(),
255        }
256    }
257
258    /// Builds a validated compensation execution receipt artifact.
259    pub fn build(self) -> Result<CompensationExecutionReceiptV1, EffectRuntimeValidationError> {
260        let value = CompensationExecutionReceiptV1 {
261            schema_version: CompensationExecutionReceiptV1::SCHEMA_VERSION.to_string(),
262            compensation_execution_receipt_id: self.compensation_execution_receipt_id,
263            compensation_plan_id: self.compensation_plan_id,
264            citation: self.citation,
265            execution_state: self.execution_state,
266            completed_steps: self.completed_steps,
267            failed_steps: self.failed_steps,
268            residual_risk_summary: self.residual_risk_summary,
269            generated_at: self.generated_at,
270        };
271        value.validate()?;
272        Ok(value)
273    }
274}