1use std::collections::{BTreeMap, BTreeSet};
2
3use serde::{Deserialize, Serialize};
4
5use crate::{CoreError, Result, canonical_digest, content_id};
6
7pub const SEMANTIC_VERSION: &str = "cymule.semantic/1";
9pub const EVENT_VERSION: &str = "cymule.event/1";
11pub const COMMAND_VERSION: &str = "cymule.command/1";
13pub const ROOT_SCOPE_ID: &str = "scope:root";
15
16#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
18#[serde(deny_unknown_fields)]
19pub struct ArtifactRef {
20 pub artifact_id: String,
22 pub kind: String,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28#[serde(deny_unknown_fields)]
29pub struct ArtifactRecord {
30 pub reference: ArtifactRef,
32 pub bytes: Vec<u8>,
34}
35
36#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
38#[serde(deny_unknown_fields)]
39pub struct CommandEnvelope {
40 pub command_version: String,
42 pub command_id: String,
44 pub actor: String,
46 pub run_id: String,
48 #[serde(default)]
50 pub expected_precondition: Option<String>,
51 pub command: Command,
53}
54
55#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
57#[serde(tag = "type", rename_all = "snake_case")]
58pub enum Command {
59 StartRun {
61 plan_id: String,
63 binding_context: String,
65 },
66 BeginAttempt {
68 attempt_id: String,
70 continuation_id: String,
72 occurrence_binding: String,
74 epoch: u64,
76 },
77 YieldAttempt {
79 attempt_id: String,
81 epoch: u64,
83 },
84 AdvanceEpoch,
86 OpenScope {
88 scope_id: String,
90 parent_scope: String,
92 },
93 ProposeEffect {
95 scope_id: String,
97 invocation_id: String,
99 site_id: String,
101 occurrence: String,
103 operation: String,
105 args: ArtifactRef,
107 occurrence_binding: String,
109 },
110 TransitionEffect {
112 intent_id: String,
114 transition: EffectTransition,
116 },
117 CommitScope {
119 scope_id: String,
121 },
122 AbortScope {
124 scope_id: String,
126 },
127 UpdateBinding {
129 binding_context: String,
131 },
132 RecordFact {
134 key: String,
136 value: String,
138 },
139 CompleteRun {
141 #[serde(default)]
143 result: Option<ArtifactRef>,
144 },
145}
146
147#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
149#[serde(tag = "type", content = "value", rename_all = "snake_case")]
150pub enum EffectTransition {
151 Prepare,
153 AuthorizeRelease,
155 StartDispatch,
157 Observe(WorldOutcome),
159 Reconcile(ReconciliationResolution),
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
165#[serde(deny_unknown_fields)]
166pub struct Event {
167 pub event_id: String,
169 pub event_version: String,
171 pub command_id: String,
173 pub command_hash: String,
175 pub run_id: String,
177 pub parents: Vec<String>,
179 pub reads: BTreeSet<String>,
181 pub writes: BTreeSet<String>,
183 #[serde(default)]
185 pub coordination_key: Option<String>,
186 pub payload: EventPayload,
188}
189
190#[derive(Serialize)]
191struct EventPreimage<'a> {
192 event_version: &'a str,
193 command_id: &'a str,
194 command_hash: &'a str,
195 run_id: &'a str,
196 parents: &'a [String],
197 reads: &'a BTreeSet<String>,
198 writes: &'a BTreeSet<String>,
199 coordination_key: &'a Option<String>,
200 payload: &'a EventPayload,
201}
202
203impl Event {
204 #[allow(clippy::too_many_arguments)]
206 pub fn new(
207 command_id: String,
208 command_hash: String,
209 run_id: String,
210 mut parents: Vec<String>,
211 reads: BTreeSet<String>,
212 writes: BTreeSet<String>,
213 coordination_key: Option<String>,
214 payload: EventPayload,
215 ) -> Result<Self> {
216 parents.sort();
217 parents.dedup();
218 let preimage = EventPreimage {
219 event_version: EVENT_VERSION,
220 command_id: &command_id,
221 command_hash: &command_hash,
222 run_id: &run_id,
223 parents: &parents,
224 reads: &reads,
225 writes: &writes,
226 coordination_key: &coordination_key,
227 payload: &payload,
228 };
229 let event_id = content_id("cymule.event/1", &preimage)?;
230 Ok(Self {
231 event_id,
232 event_version: EVENT_VERSION.to_owned(),
233 command_id,
234 command_hash,
235 run_id,
236 parents,
237 reads,
238 writes,
239 coordination_key,
240 payload,
241 })
242 }
243
244 pub fn verify(&self) -> Result<()> {
246 if self.event_version != EVENT_VERSION {
247 return Err(CoreError::Validation(format!(
248 "unsupported event version {:?}",
249 self.event_version
250 )));
251 }
252 let preimage = EventPreimage {
253 event_version: &self.event_version,
254 command_id: &self.command_id,
255 command_hash: &self.command_hash,
256 run_id: &self.run_id,
257 parents: &self.parents,
258 reads: &self.reads,
259 writes: &self.writes,
260 coordination_key: &self.coordination_key,
261 payload: &self.payload,
262 };
263 let expected = content_id("cymule.event/1", &preimage)?;
264 if expected != self.event_id {
265 return Err(CoreError::IdentityMismatch(format!(
266 "event ID {} does not match {expected}",
267 self.event_id
268 )));
269 }
270 Ok(())
271 }
272}
273
274#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
276#[serde(tag = "type", rename_all = "snake_case")]
277pub enum EventPayload {
278 RunStarted {
280 plan_id: String,
282 binding_context: String,
284 },
285 AttemptStarted {
287 attempt_id: String,
289 continuation_id: String,
291 occurrence_binding: String,
293 epoch: u64,
295 },
296 AttemptYielded {
298 attempt_id: String,
300 epoch: u64,
302 },
303 EpochAdvanced {
305 epoch: u64,
307 },
308 ScopeOpened {
310 scope_id: String,
312 parent_scope: String,
314 },
315 EffectProposed {
317 intent_id: String,
319 scope_id: String,
321 operation: String,
323 mutating: bool,
325 args: ArtifactRef,
327 occurrence_binding: String,
329 },
330 EffectTransitioned {
332 intent_id: String,
334 transition: EffectTransition,
336 },
337 ScopeCommitted {
339 scope_id: String,
341 obligations: Vec<ObligationProjection>,
343 },
344 ScopeAborted {
346 scope_id: String,
348 },
349 BindingUpdated {
351 previous: String,
353 current: String,
355 },
356 FactRecorded {
358 key: String,
360 value: String,
362 },
363 RunCompleted {
365 result: Option<ArtifactRef>,
367 },
368}
369
370#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
372#[serde(rename_all = "snake_case")]
373pub enum CommandReceiptStatus {
374 Applied,
376 Conflict,
378}
379
380#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
382#[serde(deny_unknown_fields)]
383pub struct CommandReceipt {
384 pub command_id: String,
386 pub status: CommandReceiptStatus,
388 #[serde(default)]
390 pub event_id: Option<String>,
391 #[serde(default)]
393 pub error_code: Option<String>,
394 #[serde(default)]
396 pub message: Option<String>,
397 #[serde(default)]
399 pub observed_precondition: Option<String>,
400 #[serde(default)]
402 pub current_precondition: Option<String>,
403}
404
405#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
407#[serde(deny_unknown_fields)]
408pub struct Projection {
409 pub runs: BTreeMap<String, RunProjection>,
411 pub facts: BTreeMap<String, String>,
413}
414
415#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
417#[serde(deny_unknown_fields)]
418pub struct RunProjection {
419 pub run_id: String,
421 pub initial_plan: String,
423 pub current_plan: String,
425 pub initial_binding_context: String,
427 pub current_binding_context: String,
429 pub epoch: u64,
431 pub status: RunStatus,
433 pub scopes: BTreeMap<String, ScopeProjection>,
435 pub effects: BTreeMap<String, EffectProjection>,
437 pub obligations: BTreeMap<String, ObligationProjection>,
439 pub attempts: BTreeMap<String, AttemptProjection>,
441 pub result: Option<ArtifactRef>,
443 pub last_event: String,
445}
446
447impl RunProjection {
448 pub fn precondition_token(&self) -> String {
450 format!("pre:{}:{}", self.epoch, self.last_event)
451 }
452}
453
454#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
456#[serde(rename_all = "snake_case")]
457pub enum RunStatus {
458 Active,
460 Waiting,
462 Completed,
464 Failed,
466 Cancelled,
468}
469
470#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
472#[serde(deny_unknown_fields)]
473pub struct ScopeProjection {
474 pub scope_id: String,
476 pub parent_scope: Option<String>,
478 pub status: ScopeStatus,
480 pub intents: BTreeSet<String>,
482}
483
484#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
486#[serde(rename_all = "snake_case")]
487pub enum ScopeStatus {
488 Open,
490 ClosedCommitted,
492 ClosedAborted,
494}
495
496#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
498#[serde(rename_all = "snake_case")]
499pub enum EffectPhase {
500 Admitted,
502 Prepared,
504 ReleaseAuthorized,
506 DispatchStarted,
508 CancelledBeforeRelease,
510}
511
512#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
514#[serde(rename_all = "snake_case")]
515pub enum WorldOutcome {
516 Unobserved,
518 Applied,
520 NotApplied,
522 Unknown,
524}
525
526#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
528#[serde(rename_all = "snake_case")]
529pub enum ReconciliationResolution {
530 ResolvedApplied,
532 ResolvedNotApplied,
534 StillUnknown,
536 GovernanceRequired,
538}
539
540#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
542#[serde(rename_all = "snake_case")]
543pub enum ReconciliationState {
544 NotRequired,
546 Pending,
548 Resolved,
550 GovernanceRequired,
552}
553
554#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
556#[serde(deny_unknown_fields)]
557pub struct EffectProjection {
558 pub intent_id: String,
560 pub scope_id: String,
562 pub operation: String,
564 pub mutating: bool,
566 pub args: ArtifactRef,
568 pub occurrence_binding: String,
570 pub phase: EffectPhase,
572 pub outcome: WorldOutcome,
574 pub reconciliation: ReconciliationState,
576}
577
578#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
580#[serde(deny_unknown_fields)]
581pub struct ObligationProjection {
582 pub obligation_id: String,
584 pub intent_id: String,
586 pub blocking: bool,
588 pub resolved: bool,
590}
591
592#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
594#[serde(deny_unknown_fields)]
595pub struct AttemptProjection {
596 pub attempt_id: String,
598 pub continuation_id: String,
600 pub occurrence_binding: String,
602 pub epoch: u64,
604 pub active: bool,
606}
607
608#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
610#[serde(tag = "status", rename_all = "snake_case")]
611pub enum ReplayAvailability {
612 Exact,
614 ProjectionOnly {
616 missing: Vec<String>,
618 },
619 Unavailable {
621 reason: String,
623 },
624}
625
626#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
629#[serde(deny_unknown_fields)]
630pub struct CompactionCertificate {
631 pub source_frontier: Vec<String>,
633 pub projection_digest: String,
635 pub unresolved_obligations: Vec<String>,
637 pub occurrence_bindings: Vec<String>,
639 pub replay_availability: ReplayAvailability,
641}
642
643impl Projection {
644 pub fn apply_event(&mut self, event: &Event) -> Result<()> {
646 match &event.payload {
647 EventPayload::RunStarted {
648 plan_id,
649 binding_context,
650 } => {
651 if self.runs.contains_key(&event.run_id) {
652 return Err(CoreError::IllegalTransition(format!(
653 "Run {} already exists",
654 event.run_id
655 )));
656 }
657 let mut scopes = BTreeMap::new();
658 scopes.insert(
659 ROOT_SCOPE_ID.to_owned(),
660 ScopeProjection {
661 scope_id: ROOT_SCOPE_ID.to_owned(),
662 parent_scope: None,
663 status: ScopeStatus::Open,
664 intents: BTreeSet::new(),
665 },
666 );
667 self.runs.insert(
668 event.run_id.clone(),
669 RunProjection {
670 run_id: event.run_id.clone(),
671 initial_plan: plan_id.clone(),
672 current_plan: plan_id.clone(),
673 initial_binding_context: binding_context.clone(),
674 current_binding_context: binding_context.clone(),
675 epoch: 0,
676 status: RunStatus::Active,
677 scopes,
678 effects: BTreeMap::new(),
679 obligations: BTreeMap::new(),
680 attempts: BTreeMap::new(),
681 result: None,
682 last_event: event.event_id.clone(),
683 },
684 );
685 return Ok(());
686 }
687 EventPayload::FactRecorded { key, value } => {
688 if let Some(existing) = self.facts.get(key) {
689 if existing != value {
690 return Err(CoreError::IllegalTransition(format!(
691 "fact {key:?} already has a different value"
692 )));
693 }
694 } else {
695 self.facts.insert(key.clone(), value.clone());
696 }
697 }
698 _ => {}
699 }
700
701 let run = self
702 .runs
703 .get_mut(&event.run_id)
704 .ok_or_else(|| CoreError::NotFound(format!("Run {} does not exist", event.run_id)))?;
705 if run.status == RunStatus::Completed {
706 return Err(CoreError::IllegalTransition(format!(
707 "Run {} is already completed",
708 event.run_id
709 )));
710 }
711
712 match &event.payload {
713 EventPayload::RunStarted { .. } | EventPayload::FactRecorded { .. } => {}
714 EventPayload::AttemptStarted {
715 attempt_id,
716 continuation_id,
717 occurrence_binding,
718 epoch,
719 } => {
720 if *epoch != run.epoch {
721 return Err(CoreError::IllegalTransition(format!(
722 "attempt epoch {epoch} does not match Run epoch {}",
723 run.epoch
724 )));
725 }
726 if run.attempts.contains_key(attempt_id) {
727 return Err(CoreError::IllegalTransition(format!(
728 "attempt {attempt_id} already exists"
729 )));
730 }
731 run.attempts.insert(
732 attempt_id.clone(),
733 AttemptProjection {
734 attempt_id: attempt_id.clone(),
735 continuation_id: continuation_id.clone(),
736 occurrence_binding: occurrence_binding.clone(),
737 epoch: *epoch,
738 active: true,
739 },
740 );
741 }
742 EventPayload::AttemptYielded { attempt_id, epoch } => {
743 let attempt = run.attempts.get_mut(attempt_id).ok_or_else(|| {
744 CoreError::NotFound(format!("attempt {attempt_id} does not exist"))
745 })?;
746 if !attempt.active || attempt.epoch != *epoch {
747 return Err(CoreError::IllegalTransition(format!(
748 "attempt {attempt_id} is stale or inactive"
749 )));
750 }
751 attempt.active = false;
752 }
753 EventPayload::EpochAdvanced { epoch } => {
754 if *epoch != run.epoch + 1 {
755 return Err(CoreError::IllegalTransition(format!(
756 "epoch must advance from {} to {}; received {epoch}",
757 run.epoch,
758 run.epoch + 1
759 )));
760 }
761 run.epoch = *epoch;
762 for attempt in run.attempts.values_mut() {
763 attempt.active = false;
764 }
765 }
766 EventPayload::ScopeOpened {
767 scope_id,
768 parent_scope,
769 } => {
770 if run.scopes.contains_key(scope_id) {
771 return Err(CoreError::IllegalTransition(format!(
772 "scope {scope_id} already exists"
773 )));
774 }
775 let parent = run.scopes.get(parent_scope).ok_or_else(|| {
776 CoreError::NotFound(format!("parent scope {parent_scope} does not exist"))
777 })?;
778 if parent.status != ScopeStatus::Open {
779 return Err(CoreError::IllegalTransition(format!(
780 "parent scope {parent_scope} is not open"
781 )));
782 }
783 run.scopes.insert(
784 scope_id.clone(),
785 ScopeProjection {
786 scope_id: scope_id.clone(),
787 parent_scope: Some(parent_scope.clone()),
788 status: ScopeStatus::Open,
789 intents: BTreeSet::new(),
790 },
791 );
792 }
793 EventPayload::EffectProposed {
794 intent_id,
795 scope_id,
796 operation,
797 mutating,
798 args,
799 occurrence_binding,
800 } => {
801 if run.effects.contains_key(intent_id) {
802 return Err(CoreError::IllegalTransition(format!(
803 "effect intent {intent_id} already exists"
804 )));
805 }
806 let scope = run.scopes.get_mut(scope_id).ok_or_else(|| {
807 CoreError::NotFound(format!("scope {scope_id} does not exist"))
808 })?;
809 if scope.status != ScopeStatus::Open {
810 return Err(CoreError::IllegalTransition(format!(
811 "scope {scope_id} is not open"
812 )));
813 }
814 scope.intents.insert(intent_id.clone());
815 run.effects.insert(
816 intent_id.clone(),
817 EffectProjection {
818 intent_id: intent_id.clone(),
819 scope_id: scope_id.clone(),
820 operation: operation.clone(),
821 mutating: *mutating,
822 args: args.clone(),
823 occurrence_binding: occurrence_binding.clone(),
824 phase: EffectPhase::Admitted,
825 outcome: WorldOutcome::Unobserved,
826 reconciliation: ReconciliationState::NotRequired,
827 },
828 );
829 }
830 EventPayload::EffectTransitioned {
831 intent_id,
832 transition,
833 } => {
834 let effect = run.effects.get_mut(intent_id).ok_or_else(|| {
835 CoreError::NotFound(format!("effect {intent_id} does not exist"))
836 })?;
837 apply_effect_transition(effect, transition)?;
838 update_obligation(run, intent_id);
839 }
840 EventPayload::ScopeCommitted {
841 scope_id,
842 obligations,
843 } => {
844 let scope = run.scopes.get_mut(scope_id).ok_or_else(|| {
845 CoreError::NotFound(format!("scope {scope_id} does not exist"))
846 })?;
847 if scope.status != ScopeStatus::Open {
848 return Err(CoreError::IllegalTransition(format!(
849 "scope {scope_id} is not open"
850 )));
851 }
852 scope.status = ScopeStatus::ClosedCommitted;
853 for obligation in obligations {
854 if !scope.intents.contains(&obligation.intent_id) {
855 return Err(CoreError::IllegalTransition(format!(
856 "obligation {} does not belong to scope {scope_id}",
857 obligation.obligation_id
858 )));
859 }
860 if run
861 .obligations
862 .insert(obligation.obligation_id.clone(), obligation.clone())
863 .is_some()
864 {
865 return Err(CoreError::IllegalTransition(format!(
866 "obligation {} already exists",
867 obligation.obligation_id
868 )));
869 }
870 }
871 }
872 EventPayload::ScopeAborted { scope_id } => {
873 let scope = run.scopes.get_mut(scope_id).ok_or_else(|| {
874 CoreError::NotFound(format!("scope {scope_id} does not exist"))
875 })?;
876 if scope.status != ScopeStatus::Open {
877 return Err(CoreError::IllegalTransition(format!(
878 "scope {scope_id} is not open"
879 )));
880 }
881 for intent_id in &scope.intents {
882 let effect = run.effects.get_mut(intent_id).ok_or_else(|| {
883 CoreError::NotFound(format!("effect {intent_id} does not exist"))
884 })?;
885 if matches!(
886 effect.phase,
887 EffectPhase::ReleaseAuthorized | EffectPhase::DispatchStarted
888 ) {
889 return Err(CoreError::IllegalTransition(format!(
890 "scope {scope_id} cannot abort after effect release"
891 )));
892 }
893 effect.phase = EffectPhase::CancelledBeforeRelease;
894 }
895 scope.status = ScopeStatus::ClosedAborted;
896 }
897 EventPayload::BindingUpdated { previous, current } => {
898 if &run.current_binding_context != previous {
899 return Err(CoreError::IllegalTransition(format!(
900 "binding context changed from expected {previous}"
901 )));
902 }
903 run.current_binding_context.clone_from(current);
904 }
905 EventPayload::RunCompleted { result } => {
906 let unresolved = run
907 .obligations
908 .values()
909 .any(|obligation| obligation.blocking && !obligation.resolved);
910 if unresolved {
911 return Err(CoreError::IllegalTransition(
912 "Run has unresolved blocking effect obligations".to_owned(),
913 ));
914 }
915 if run
916 .scopes
917 .values()
918 .any(|scope| scope.status == ScopeStatus::Open)
919 {
920 return Err(CoreError::IllegalTransition(
921 "Run has an open scope".to_owned(),
922 ));
923 }
924 run.status = RunStatus::Completed;
925 run.result.clone_from(result);
926 }
927 }
928 run.last_event.clone_from(&event.event_id);
929 Ok(())
930 }
931
932 pub fn digest(&self) -> Result<String> {
934 canonical_digest(self)
935 }
936}
937
938fn apply_effect_transition(
939 effect: &mut EffectProjection,
940 transition: &EffectTransition,
941) -> Result<()> {
942 match transition {
943 EffectTransition::Prepare if effect.phase == EffectPhase::Admitted => {
944 effect.phase = EffectPhase::Prepared;
945 }
946 EffectTransition::AuthorizeRelease if effect.phase == EffectPhase::Prepared => {
947 effect.phase = EffectPhase::ReleaseAuthorized;
948 }
949 EffectTransition::StartDispatch if effect.phase == EffectPhase::ReleaseAuthorized => {
950 effect.phase = EffectPhase::DispatchStarted;
951 }
952 EffectTransition::Observe(outcome)
953 if effect.phase == EffectPhase::DispatchStarted
954 && effect.outcome == WorldOutcome::Unobserved
955 && *outcome != WorldOutcome::Unobserved =>
956 {
957 effect.outcome = *outcome;
958 effect.reconciliation = if *outcome == WorldOutcome::Unknown {
959 ReconciliationState::Pending
960 } else {
961 ReconciliationState::NotRequired
962 };
963 }
964 EffectTransition::Reconcile(resolution)
965 if effect.phase == EffectPhase::DispatchStarted
966 && effect.outcome == WorldOutcome::Unknown =>
967 {
968 match resolution {
969 ReconciliationResolution::ResolvedApplied => {
970 effect.outcome = WorldOutcome::Applied;
971 effect.reconciliation = ReconciliationState::Resolved;
972 }
973 ReconciliationResolution::ResolvedNotApplied => {
974 effect.outcome = WorldOutcome::NotApplied;
975 effect.reconciliation = ReconciliationState::Resolved;
976 }
977 ReconciliationResolution::StillUnknown => {
978 effect.reconciliation = ReconciliationState::Pending;
979 }
980 ReconciliationResolution::GovernanceRequired => {
981 effect.reconciliation = ReconciliationState::GovernanceRequired;
982 }
983 }
984 }
985 _ => {
986 return Err(CoreError::IllegalTransition(format!(
987 "illegal effect transition {transition:?} from phase {:?}, outcome {:?}, reconciliation {:?}",
988 effect.phase, effect.outcome, effect.reconciliation
989 )));
990 }
991 }
992 Ok(())
993}
994
995fn update_obligation(run: &mut RunProjection, intent_id: &str) {
996 let resolved = run.effects.get(intent_id).is_some_and(|effect| {
997 matches!(
998 effect.outcome,
999 WorldOutcome::Applied | WorldOutcome::NotApplied
1000 )
1001 });
1002 for obligation in run
1003 .obligations
1004 .values_mut()
1005 .filter(|obligation| obligation.intent_id == intent_id)
1006 {
1007 obligation.resolved = resolved;
1008 }
1009}
1010
1011#[allow(clippy::too_many_arguments)]
1013pub fn effect_intent_id(
1014 run_id: &str,
1015 invocation_id: &str,
1016 site_id: &str,
1017 scope_id: &str,
1018 scope_epoch: u64,
1019 occurrence: &str,
1020 args: &ArtifactRef,
1021 effect_schema_version: &str,
1022) -> Result<String> {
1023 #[derive(Serialize)]
1024 struct IntentPreimage<'a> {
1025 run_id: &'a str,
1026 invocation_id: &'a str,
1027 site_id: &'a str,
1028 scope_id: &'a str,
1029 scope_epoch: u64,
1030 occurrence: &'a str,
1031 args: &'a ArtifactRef,
1032 effect_schema_version: &'a str,
1033 }
1034 content_id(
1035 "cymule.effect-intent/1",
1036 &IntentPreimage {
1037 run_id,
1038 invocation_id,
1039 site_id,
1040 scope_id,
1041 scope_epoch,
1042 occurrence,
1043 args,
1044 effect_schema_version,
1045 },
1046 )
1047}
1048
1049pub fn effect_obligation_id(intent_id: &str) -> Result<String> {
1051 content_id("cymule.effect-obligation/1", &intent_id)
1052}