1use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "kebab-case")]
14pub enum Requirement {
15 Advisory,
17 DecisionRequired,
19 Required,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(tag = "state", rename_all = "kebab-case")]
26pub enum Evaluation {
27 Satisfied,
29 NotObserved {
31 reason: String,
33 },
34 Unsatisfied {
36 reason: String,
38 },
39}
40
41impl Evaluation {
42 #[must_use]
44 pub const fn is_satisfied(&self) -> bool {
45 matches!(self, Self::Satisfied)
46 }
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
51#[serde(rename_all = "kebab-case")]
52pub enum Readiness {
53 Ready,
55 NeedsDecision,
57 Blocked,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
63pub struct Precondition {
64 pub id: String,
66 pub statement: String,
68 pub requirement: Requirement,
70 pub evaluation: Evaluation,
72 #[serde(default, skip_serializing_if = "Option::is_none")]
74 pub resolved_by: Option<String>,
75 #[serde(default, skip_serializing_if = "Vec::is_empty")]
77 pub evidence_refs: Vec<String>,
78}
79
80impl Precondition {
81 #[must_use]
83 pub const fn verdict(&self) -> Readiness {
84 if self.evaluation.is_satisfied() {
85 return Readiness::Ready;
86 }
87 match self.requirement {
88 Requirement::Advisory => Readiness::Ready,
89 Requirement::DecisionRequired => Readiness::NeedsDecision,
90 Requirement::Required => Readiness::Blocked,
91 }
92 }
93}
94
95#[must_use]
100pub fn readiness(preconditions: &[Precondition]) -> Readiness {
101 preconditions
102 .iter()
103 .map(Precondition::verdict)
104 .max()
105 .unwrap_or(Readiness::Ready)
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 fn precondition(requirement: Requirement, evaluation: Evaluation) -> Precondition {
113 Precondition {
114 id: "one".to_string(),
115 statement: "one thing holds".to_string(),
116 requirement,
117 evaluation,
118 resolved_by: None,
119 evidence_refs: Vec::new(),
120 }
121 }
122
123 fn unsatisfied() -> Evaluation {
124 Evaluation::Unsatisfied {
125 reason: "it does not".to_string(),
126 }
127 }
128
129 fn not_observed() -> Evaluation {
130 Evaluation::NotObserved {
131 reason: "nobody could tell".to_string(),
132 }
133 }
134
135 #[test]
136 fn readiness_is_the_worst_precondition() {
137 use Requirement::{Advisory, DecisionRequired, Required};
138
139 let cases: &[(Requirement, Evaluation, Readiness)] = &[
140 (Advisory, Evaluation::Satisfied, Readiness::Ready),
141 (Advisory, unsatisfied(), Readiness::Ready),
142 (Advisory, not_observed(), Readiness::Ready),
143 (DecisionRequired, Evaluation::Satisfied, Readiness::Ready),
144 (DecisionRequired, unsatisfied(), Readiness::NeedsDecision),
145 (DecisionRequired, not_observed(), Readiness::NeedsDecision),
146 (Required, Evaluation::Satisfied, Readiness::Ready),
147 (Required, unsatisfied(), Readiness::Blocked),
148 (Required, not_observed(), Readiness::Blocked),
149 ];
150 for (requirement, evaluation, expected) in cases {
151 let held = precondition(*requirement, evaluation.clone());
152 assert_eq!(held.verdict(), *expected, "{requirement:?} {evaluation:?}");
153 }
154 }
155
156 #[test]
157 fn one_blocked_precondition_blocks_the_plan_whatever_its_position() {
158 let ready = precondition(Requirement::Advisory, Evaluation::Satisfied);
159 let waiting = precondition(Requirement::DecisionRequired, unsatisfied());
160 let blocked = precondition(Requirement::Required, unsatisfied());
161 assert_eq!(readiness(&[]), Readiness::Ready);
162 assert_eq!(readiness(std::slice::from_ref(&ready)), Readiness::Ready);
163 assert_eq!(
164 readiness(&[ready.clone(), waiting.clone()]),
165 Readiness::NeedsDecision
166 );
167 assert_eq!(
168 readiness(&[blocked.clone(), ready.clone(), waiting.clone()]),
169 Readiness::Blocked
170 );
171 assert_eq!(readiness(&[waiting, ready, blocked]), Readiness::Blocked);
172 }
173
174 #[test]
175 fn a_gap_is_honest_and_is_not_permission() {
176 let held = precondition(Requirement::Required, not_observed());
179 assert_eq!(held.verdict(), Readiness::Blocked);
180 }
181}