dpm_engine/gates/
transition.rs1use dpm_model::{Applicability, DependencyKind, DependencyPolicy, Endpoint, Release, WorkStatus};
2use serde::{Deserialize, Serialize};
3use std::fmt;
4
5#[derive(
18 Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
19)]
20#[serde(rename_all = "snake_case")]
21pub enum Transition {
22 #[default]
24 Claim,
25 Start,
27 Submit,
29 Verify,
31}
32
33impl Transition {
34 pub const ALL: [Self; 4] = [Self::Claim, Self::Start, Self::Submit, Self::Verify];
36
37 #[must_use]
39 pub fn lifecycle(self) -> WorkStatus {
40 match self {
41 Self::Claim => WorkStatus::Planned,
42 Self::Start => WorkStatus::Claimed,
43 Self::Submit => WorkStatus::InProgress,
44 Self::Verify => WorkStatus::Submitted,
45 }
46 }
47
48 #[must_use]
50 pub fn governs(self, kind: DependencyKind) -> bool {
51 match self {
52 Self::Claim | Self::Start => kind.successor_endpoint() == Endpoint::Start,
53 Self::Submit => kind.successor_endpoint() == Endpoint::Finish,
54 Self::Verify => true,
55 }
56 }
57
58 #[must_use]
61 pub fn starts(self) -> bool {
62 matches!(self, Self::Claim | Self::Start)
63 }
64
65 #[must_use]
68 pub fn checks_basis(self) -> bool {
69 matches!(self, Self::Submit | Self::Verify)
70 }
71
72 #[must_use]
74 pub fn checks_decisions(self) -> bool {
75 !matches!(self, Self::Submit)
76 }
77}
78
79impl fmt::Display for Transition {
80 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81 f.write_str(match self {
82 Self::Claim => "claim",
83 Self::Start => "start",
84 Self::Submit => "submit",
85 Self::Verify => "verify",
86 })
87 }
88}
89
90pub(super) fn describe_release(
91 key: &str,
92 requires: Endpoint,
93 release: Release,
94 edge: &str,
95 policy: DependencyPolicy,
96) -> String {
97 let event = match requires {
98 Endpoint::Start => "start",
99 Endpoint::Finish => "verified finish",
100 };
101 match release {
102 Release::AwaitingEvent => format!("awaits the {event} of {key} ({edge})"),
103 Release::Elapsing { event_at, opens_at } => {
104 format!("{key} {event} recorded at {event_at}; lag elapses at {opens_at} ({edge})")
105 }
106 Release::UnrecordedEventTime => {
107 let remedy = match policy {
108 DependencyPolicy::Soft => "a human or service may waive this Soft edge",
109 DependencyPolicy::Hard => {
110 "a Hard edge cannot be waived; a reviewed plan change may alter its lag only while neither the successor nor any work depending on it has been claimed, and otherwise the gate stays closed"
111 }
112 };
113 format!(
114 "the {event} time of {key} was not recorded, so its positive lag cannot be shown to have elapsed ({edge}); {remedy}"
115 )
116 }
117 Release::LagOutOfRange { event_at } => format!(
118 "{key} {event} recorded at {event_at}, but the lag exceeds the supported time range ({edge})"
119 ),
120 Release::Released { .. } => format!("{key} {event} released ({edge})"),
121 Release::SkippedBranch { .. } => {
122 format!(
123 "{key} was not selected; this active-branch join treats it as an absent branch ({edge})"
124 )
125 }
126 Release::NotSelected => format!(
127 "{key} was not selected, and an ordinary dependency on skipped work never releases ({edge}); declare an active-branch join or change the plan"
128 ),
129 }
130}
131
132#[must_use]
134pub fn describe_applicability(applicability: &Applicability) -> String {
135 match applicability {
136 Applicability::Applicable => "work is applicable".into(),
137 Applicability::Undecided { decision, option } => {
138 format!("work applies only if {decision} selects {option}, and it is undecided")
139 }
140 Applicability::NotSelected {
141 decision,
142 option,
143 selected,
144 } => format!(
145 "work applies only if {decision} selects {option}, but it selected {selected}; it cannot be claimed or completed"
146 ),
147 Applicability::AwaitingChoice { predecessor } => format!(
148 "prerequisite {predecessor} depends on an undecided choice, so this work is not yet committed"
149 ),
150 Applicability::Stranded { predecessor, .. } => format!(
151 "prerequisite {predecessor} was not selected or can never proceed, and an ordinary dependency never releases from it; only a reviewed plan change that rewires the dependency, or a waiver of a Soft dependency, lets this work proceed"
152 ),
153 Applicability::EmptyJoin => "every branch into this join was not selected, and the join does not permit an empty result".into(),
154 Applicability::AllChildrenExcluded { decisions } => format!(
155 "every child of this work package was excluded by {}; the package is excluded with them",
156 join_keys(decisions)
157 ),
158 Applicability::ChildrenStranded { child } => format!(
159 "no child of this work package is applicable and {child} can never proceed, so the package cannot complete without a reviewed plan change"
160 ),
161 }
162}
163
164fn join_keys(keys: &[dpm_model::Key]) -> String {
165 keys.iter()
166 .map(|k| k.0.as_str())
167 .collect::<Vec<_>>()
168 .join(", ")
169}