1use 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::{
12 ClosureRecommendationV1, ExecutionStateV1, ExternalObservationStateV1, ObservationStateV1,
13};
14use schemars::JsonSchema;
15use serde::{Deserialize, Serialize};
16use stack_ids::{
17 EffectCommitDecisionId, EffectExecutionReceiptId, EffectObservationBundleId,
18 ExternalEffectLedgerEntryId,
19};
20
21#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
26pub struct EffectExecutionReceiptV1 {
27 pub schema_version: String,
28 pub effect_execution_receipt_id: EffectExecutionReceiptId,
29 pub effect_commit_decision_id: EffectCommitDecisionId,
30 #[serde(flatten)]
32 pub citation: V25ConstitutionCitation,
33 pub provider_route: Vec<String>,
34 pub execution_state: ExecutionStateV1,
35 pub partial_execution: bool,
36 pub cancellation_reason: String,
37 pub timeout_exhausted: bool,
38 pub generated_effect_refs: Vec<String>,
39 pub observed_external_handle: String,
40 pub generated_at: String,
41}
42
43#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
47pub struct EffectObservationBundleV1 {
48 pub schema_version: String,
49 pub effect_observation_bundle_id: EffectObservationBundleId,
50 pub effect_execution_receipt_id: EffectExecutionReceiptId,
51 #[serde(flatten)]
53 pub citation: V25ConstitutionCitation,
54 pub observation_window: String,
55 pub observation_state: ObservationStateV1,
56 pub observed_outcome: String,
57 pub drift_summary: String,
58 pub external_evidence_refs: Vec<String>,
59 pub closure_recommendation: ClosureRecommendationV1,
60 pub generated_at: String,
61}
62
63#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
65pub struct ExternalEffectLedgerEntryV1 {
66 pub schema_version: String,
67 pub external_effect_ledger_entry_id: ExternalEffectLedgerEntryId,
68 pub effect_execution_receipt_id: EffectExecutionReceiptId,
69 #[serde(flatten)]
71 pub citation: V25ConstitutionCitation,
72 pub target_system: String,
73 pub external_handle: String,
74 pub externally_observed_state: ExternalObservationStateV1,
75 pub entered_at: String,
76 pub immutable: bool,
77}
78
79impl EffectExecutionReceiptV1 {
80 pub const SCHEMA_VERSION: &'static str = "EffectExecutionReceiptV1";
81
82 #[allow(clippy::too_many_arguments)]
84 pub fn builder(
85 effect_execution_receipt_id: EffectExecutionReceiptId,
86 effect_commit_decision_id: EffectCommitDecisionId,
87 citation: V25ConstitutionCitation,
88 provider_route: Vec<String>,
89 execution_state: ExecutionStateV1,
90 partial_execution: bool,
91 cancellation_reason: impl Into<String>,
92 timeout_exhausted: bool,
93 generated_effect_refs: Vec<String>,
94 observed_external_handle: impl Into<String>,
95 generated_at: impl Into<String>,
96 ) -> EffectExecutionReceiptV1Builder {
97 EffectExecutionReceiptV1Builder::new(
98 effect_execution_receipt_id,
99 effect_commit_decision_id,
100 citation,
101 provider_route,
102 execution_state,
103 partial_execution,
104 cancellation_reason,
105 timeout_exhausted,
106 generated_effect_refs,
107 observed_external_handle,
108 generated_at,
109 )
110 }
111
112 pub fn validate(&self) -> EffectValidationResult {
114 require_schema_version(
115 "EffectExecutionReceiptV1",
116 Self::SCHEMA_VERSION,
117 &self.schema_version,
118 )?;
119 require_id(
120 &self.effect_execution_receipt_id,
121 "effect_execution_receipt_id",
122 )?;
123 require_id(&self.effect_commit_decision_id, "effect_commit_decision_id")?;
124 require_non_empty_slice(&self.provider_route, "provider_route")?;
125 require_non_empty(&self.generated_at, "generated_at")?;
126 require_non_empty(&self.observed_external_handle, "observed_external_handle")?;
127 if self.partial_execution != matches!(self.execution_state, ExecutionStateV1::Partial) {
128 return Err(EffectRuntimeValidationError::InvalidState(
129 "partial_execution must match execution_state=partial",
130 ));
131 }
132 if self.timeout_exhausted
133 && !matches!(
134 self.execution_state,
135 ExecutionStateV1::TimedOut | ExecutionStateV1::Partial
136 )
137 {
138 return Err(EffectRuntimeValidationError::InvalidState(
139 "timeout_exhausted requires execution_state=timed_out or partial",
140 ));
141 }
142 if matches!(self.execution_state, ExecutionStateV1::Completed)
143 && !self.cancellation_reason.trim().is_empty()
144 {
145 return Err(EffectRuntimeValidationError::InvalidState(
146 "completed execution receipts must not carry cancellation_reason",
147 ));
148 }
149 if matches!(
150 self.execution_state,
151 ExecutionStateV1::Completed | ExecutionStateV1::Partial
152 ) {
153 require_non_empty_slice(&self.generated_effect_refs, "generated_effect_refs")?;
154 }
155 Ok(())
156 }
157}
158
159#[derive(Debug, Clone)]
161pub struct EffectExecutionReceiptV1Builder {
162 effect_execution_receipt_id: EffectExecutionReceiptId,
163 effect_commit_decision_id: EffectCommitDecisionId,
164 citation: V25ConstitutionCitation,
165 provider_route: Vec<String>,
166 execution_state: ExecutionStateV1,
167 partial_execution: bool,
168 cancellation_reason: String,
169 timeout_exhausted: bool,
170 generated_effect_refs: Vec<String>,
171 observed_external_handle: String,
172 generated_at: String,
173}
174
175impl EffectExecutionReceiptV1Builder {
176 #[allow(clippy::too_many_arguments)]
178 pub fn new(
179 effect_execution_receipt_id: EffectExecutionReceiptId,
180 effect_commit_decision_id: EffectCommitDecisionId,
181 citation: V25ConstitutionCitation,
182 provider_route: Vec<String>,
183 execution_state: ExecutionStateV1,
184 partial_execution: bool,
185 cancellation_reason: impl Into<String>,
186 timeout_exhausted: bool,
187 generated_effect_refs: Vec<String>,
188 observed_external_handle: impl Into<String>,
189 generated_at: impl Into<String>,
190 ) -> Self {
191 Self {
192 effect_execution_receipt_id,
193 effect_commit_decision_id,
194 citation,
195 provider_route,
196 execution_state,
197 partial_execution,
198 cancellation_reason: cancellation_reason.into(),
199 timeout_exhausted,
200 generated_effect_refs,
201 observed_external_handle: observed_external_handle.into(),
202 generated_at: generated_at.into(),
203 }
204 }
205
206 pub fn build(self) -> Result<EffectExecutionReceiptV1, EffectRuntimeValidationError> {
208 let value = EffectExecutionReceiptV1 {
209 schema_version: EffectExecutionReceiptV1::SCHEMA_VERSION.to_string(),
210 effect_execution_receipt_id: self.effect_execution_receipt_id,
211 effect_commit_decision_id: self.effect_commit_decision_id,
212 citation: self.citation,
213 provider_route: self.provider_route,
214 execution_state: self.execution_state,
215 partial_execution: self.partial_execution,
216 cancellation_reason: self.cancellation_reason,
217 timeout_exhausted: self.timeout_exhausted,
218 generated_effect_refs: self.generated_effect_refs,
219 observed_external_handle: self.observed_external_handle,
220 generated_at: self.generated_at,
221 };
222 value.validate()?;
223 Ok(value)
224 }
225}
226
227impl EffectObservationBundleV1 {
228 pub const SCHEMA_VERSION: &'static str = "EffectObservationBundleV1";
229
230 #[allow(clippy::too_many_arguments)]
232 pub fn builder(
233 effect_observation_bundle_id: EffectObservationBundleId,
234 effect_execution_receipt_id: EffectExecutionReceiptId,
235 citation: V25ConstitutionCitation,
236 observation_window: impl Into<String>,
237 observation_state: ObservationStateV1,
238 observed_outcome: impl Into<String>,
239 drift_summary: impl Into<String>,
240 external_evidence_refs: Vec<String>,
241 closure_recommendation: ClosureRecommendationV1,
242 generated_at: impl Into<String>,
243 ) -> EffectObservationBundleV1Builder {
244 EffectObservationBundleV1Builder::new(
245 effect_observation_bundle_id,
246 effect_execution_receipt_id,
247 citation,
248 observation_window,
249 observation_state,
250 observed_outcome,
251 drift_summary,
252 external_evidence_refs,
253 closure_recommendation,
254 generated_at,
255 )
256 }
257
258 pub fn validate(&self) -> EffectValidationResult {
260 require_schema_version(
261 "EffectObservationBundleV1",
262 Self::SCHEMA_VERSION,
263 &self.schema_version,
264 )?;
265 require_id(
266 &self.effect_observation_bundle_id,
267 "effect_observation_bundle_id",
268 )?;
269 require_id(
270 &self.effect_execution_receipt_id,
271 "effect_execution_receipt_id",
272 )?;
273 require_non_empty(&self.observation_window, "observation_window")?;
274 require_non_empty(&self.observed_outcome, "observed_outcome")?;
275 require_non_empty(&self.generated_at, "generated_at")?;
276 if matches!(self.observation_state, ObservationStateV1::Complete) {
277 require_non_empty_slice(&self.external_evidence_refs, "external_evidence_refs")?;
278 }
279 Ok(())
280 }
281}
282
283#[derive(Debug, Clone)]
285pub struct EffectObservationBundleV1Builder {
286 effect_observation_bundle_id: EffectObservationBundleId,
287 effect_execution_receipt_id: EffectExecutionReceiptId,
288 citation: V25ConstitutionCitation,
289 observation_window: String,
290 observation_state: ObservationStateV1,
291 observed_outcome: String,
292 drift_summary: String,
293 external_evidence_refs: Vec<String>,
294 closure_recommendation: ClosureRecommendationV1,
295 generated_at: String,
296}
297
298impl EffectObservationBundleV1Builder {
299 #[allow(clippy::too_many_arguments)]
301 pub fn new(
302 effect_observation_bundle_id: EffectObservationBundleId,
303 effect_execution_receipt_id: EffectExecutionReceiptId,
304 citation: V25ConstitutionCitation,
305 observation_window: impl Into<String>,
306 observation_state: ObservationStateV1,
307 observed_outcome: impl Into<String>,
308 drift_summary: impl Into<String>,
309 external_evidence_refs: Vec<String>,
310 closure_recommendation: ClosureRecommendationV1,
311 generated_at: impl Into<String>,
312 ) -> Self {
313 Self {
314 effect_observation_bundle_id,
315 effect_execution_receipt_id,
316 citation,
317 observation_window: observation_window.into(),
318 observation_state,
319 observed_outcome: observed_outcome.into(),
320 drift_summary: drift_summary.into(),
321 external_evidence_refs,
322 closure_recommendation,
323 generated_at: generated_at.into(),
324 }
325 }
326
327 pub fn build(self) -> Result<EffectObservationBundleV1, EffectRuntimeValidationError> {
329 let value = EffectObservationBundleV1 {
330 schema_version: EffectObservationBundleV1::SCHEMA_VERSION.to_string(),
331 effect_observation_bundle_id: self.effect_observation_bundle_id,
332 effect_execution_receipt_id: self.effect_execution_receipt_id,
333 citation: self.citation,
334 observation_window: self.observation_window,
335 observation_state: self.observation_state,
336 observed_outcome: self.observed_outcome,
337 drift_summary: self.drift_summary,
338 external_evidence_refs: self.external_evidence_refs,
339 closure_recommendation: self.closure_recommendation,
340 generated_at: self.generated_at,
341 };
342 value.validate()?;
343 Ok(value)
344 }
345}
346
347impl ExternalEffectLedgerEntryV1 {
348 pub const SCHEMA_VERSION: &'static str = "ExternalEffectLedgerEntryV1";
349
350 #[allow(clippy::too_many_arguments)]
352 pub fn builder(
353 external_effect_ledger_entry_id: ExternalEffectLedgerEntryId,
354 effect_execution_receipt_id: EffectExecutionReceiptId,
355 citation: V25ConstitutionCitation,
356 target_system: impl Into<String>,
357 external_handle: impl Into<String>,
358 externally_observed_state: ExternalObservationStateV1,
359 entered_at: impl Into<String>,
360 immutable: bool,
361 ) -> ExternalEffectLedgerEntryV1Builder {
362 ExternalEffectLedgerEntryV1Builder::new(
363 external_effect_ledger_entry_id,
364 effect_execution_receipt_id,
365 citation,
366 target_system,
367 external_handle,
368 externally_observed_state,
369 entered_at,
370 immutable,
371 )
372 }
373
374 pub fn validate(&self) -> EffectValidationResult {
376 require_schema_version(
377 "ExternalEffectLedgerEntryV1",
378 Self::SCHEMA_VERSION,
379 &self.schema_version,
380 )?;
381 require_id(
382 &self.external_effect_ledger_entry_id,
383 "external_effect_ledger_entry_id",
384 )?;
385 require_id(
386 &self.effect_execution_receipt_id,
387 "effect_execution_receipt_id",
388 )?;
389 require_non_empty(&self.target_system, "target_system")?;
390 require_non_empty(&self.external_handle, "external_handle")?;
391 require_non_empty(&self.entered_at, "entered_at")?;
392 Ok(())
393 }
394}
395
396#[derive(Debug, Clone)]
398pub struct ExternalEffectLedgerEntryV1Builder {
399 external_effect_ledger_entry_id: ExternalEffectLedgerEntryId,
400 effect_execution_receipt_id: EffectExecutionReceiptId,
401 citation: V25ConstitutionCitation,
402 target_system: String,
403 external_handle: String,
404 externally_observed_state: ExternalObservationStateV1,
405 entered_at: String,
406 immutable: bool,
407}
408
409impl ExternalEffectLedgerEntryV1Builder {
410 #[allow(clippy::too_many_arguments)]
412 pub fn new(
413 external_effect_ledger_entry_id: ExternalEffectLedgerEntryId,
414 effect_execution_receipt_id: EffectExecutionReceiptId,
415 citation: V25ConstitutionCitation,
416 target_system: impl Into<String>,
417 external_handle: impl Into<String>,
418 externally_observed_state: ExternalObservationStateV1,
419 entered_at: impl Into<String>,
420 immutable: bool,
421 ) -> Self {
422 Self {
423 external_effect_ledger_entry_id,
424 effect_execution_receipt_id,
425 citation,
426 target_system: target_system.into(),
427 external_handle: external_handle.into(),
428 externally_observed_state,
429 entered_at: entered_at.into(),
430 immutable,
431 }
432 }
433
434 pub fn build(self) -> Result<ExternalEffectLedgerEntryV1, EffectRuntimeValidationError> {
436 let value = ExternalEffectLedgerEntryV1 {
437 schema_version: ExternalEffectLedgerEntryV1::SCHEMA_VERSION.to_string(),
438 external_effect_ledger_entry_id: self.external_effect_ledger_entry_id,
439 effect_execution_receipt_id: self.effect_execution_receipt_id,
440 citation: self.citation,
441 target_system: self.target_system,
442 external_handle: self.external_handle,
443 externally_observed_state: self.externally_observed_state,
444 entered_at: self.entered_at,
445 immutable: self.immutable,
446 };
447 value.validate()?;
448 Ok(value)
449 }
450}