1use crate::{
2 Decision, DecisionId, DecisionStatus, DependencyId, EventTime, Key, Plan, WorkItem, WorkItemId,
3 WorkKind,
4};
5use serde::{Deserialize, Serialize};
6use std::collections::{BTreeMap, VecDeque};
7
8mod packages;
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(tag = "state", rename_all = "snake_case")]
17pub enum Applicability {
18 Applicable,
20 Undecided {
22 decision: Key,
24 option: String,
26 },
27 NotSelected {
29 decision: Key,
31 option: String,
33 selected: String,
35 },
36 AwaitingChoice {
40 predecessor: Key,
42 },
43 Stranded {
46 dependency: DependencyId,
48 predecessor: Key,
50 },
51 EmptyJoin,
53 AllChildrenExcluded {
56 decisions: Vec<Key>,
58 },
59 ChildrenStranded {
62 child: Key,
64 },
65}
66
67impl Applicability {
68 #[must_use]
70 pub fn is_applicable(&self) -> bool {
71 matches!(self, Self::Applicable)
72 }
73
74 #[must_use]
77 pub fn is_not_selected(&self) -> bool {
78 matches!(
79 self,
80 Self::NotSelected { .. } | Self::AllChildrenExcluded { .. }
81 )
82 }
83
84 #[must_use]
86 pub fn condition_selected(&self) -> bool {
87 !matches!(self, Self::NotSelected { .. } | Self::Undecided { .. })
88 }
89}
90
91pub(crate) struct Derived {
93 pub(crate) states: BTreeMap<WorkItemId, Applicability>,
94 pub(crate) choice_at: BTreeMap<WorkItemId, EventTime>,
97}
98
99pub(crate) fn derive(plan: &Plan) -> Derived {
102 derive_indexed(plan, &crate::graph_index::GraphIndex::new(plan))
103}
104
105pub(crate) fn derive_indexed(plan: &Plan, index: &crate::graph_index::GraphIndex) -> Derived {
106 let mut derived = Derived {
107 states: BTreeMap::new(),
108 choice_at: BTreeMap::new(),
109 };
110 for work in plan.work_items.values() {
111 let (state, at) = conditions(plan, work);
112 if let Some(at) = at {
113 derived.choice_at.insert(work.id, at);
114 }
115 derived.states.insert(work.id, state);
116 }
117 for id in dependency_order(plan) {
118 let Some(work) = plan.work_items.get(&id) else {
119 continue;
120 };
121 let finished = work.is_executable() && work.execution.status.satisfies_dependency();
124 if work.kind == WorkKind::WorkPackage
125 || finished
126 || !derived
127 .states
128 .get(&id)
129 .is_some_and(Applicability::is_applicable)
130 {
131 continue;
132 }
133 let state = through_constraints(plan, index, work, &derived.states);
134 derived.states.insert(id, state);
135 }
136 packages::derive(plan, index, &mut derived);
137 derived
138}
139
140impl Plan {
141 #[must_use]
143 pub fn applicability(&self) -> BTreeMap<WorkItemId, Applicability> {
144 derive(self).states
145 }
146
147 #[must_use]
149 pub fn effective_decision(&self, id: DecisionId) -> Option<&Decision> {
150 effective_decision(self, id)
151 }
152}
153
154pub(crate) fn effective_decision(plan: &Plan, id: DecisionId) -> Option<&Decision> {
156 let mut current = plan.decisions.get(&id)?;
157 let mut steps = 0_usize;
158 while let Some(next) = plan
159 .decisions
160 .values()
161 .find(|d| d.supersedes == Some(current.id))
162 {
163 current = next;
164 steps = steps.wrapping_add(1);
165 if steps > plan.decisions.len() {
166 break;
167 }
168 }
169 Some(current)
170}
171
172pub(crate) fn standing_since(plan: &Plan, decision: &Decision) -> Option<EventTime> {
181 if decision.status != DecisionStatus::Decided {
182 return None;
183 }
184 let mut since = own_time(decision);
185 let mut current = decision;
186 let mut steps = 0_usize;
187 while let Some(previous) = current.supersedes.and_then(|id| plan.decisions.get(&id)) {
188 steps = steps.wrapping_add(1);
189 if previous.outcome != decision.outcome || steps > plan.decisions.len() {
190 break;
191 }
192 since = earliest(since, own_time(previous));
193 current = previous;
194 }
195 Some(since)
196}
197
198fn own_time(decision: &Decision) -> EventTime {
199 decision
200 .resolved_at
201 .map_or(EventTime::Unrecorded, EventTime::Recorded)
202}
203
204fn earliest(a: EventTime, b: EventTime) -> EventTime {
206 match (a, b) {
207 (EventTime::Recorded(a), EventTime::Recorded(b)) => EventTime::Recorded(a.min(b)),
208 _ => EventTime::Unrecorded,
209 }
210}
211
212fn conditions(plan: &Plan, work: &WorkItem) -> (Applicability, Option<EventTime>) {
213 let mut undecided = None;
214 let mut decided: Option<EventTime> = None;
215 let mut next = Some(work);
216 let mut depth = 0_usize;
217 while let Some(item) = next {
218 if let Some(condition) = &item.contract.condition
219 && let Some(decision) = effective_decision(plan, condition.decision)
220 {
221 match (standing_since(plan, decision), decision.outcome.as_deref()) {
222 (Some(at), Some(selected)) if selected != condition.option => {
223 let state = Applicability::NotSelected {
224 decision: decision.key.clone(),
225 option: condition.option.clone(),
226 selected: selected.into(),
227 };
228 return (state, Some(at));
229 }
230 (Some(at), Some(_)) => decided = Some(decided.map_or(at, |d| d.latest(at))),
231 _ => {
232 undecided.get_or_insert_with(|| Applicability::Undecided {
233 decision: decision.key.clone(),
234 option: condition.option.clone(),
235 });
236 }
237 }
238 }
239 depth = depth.wrapping_add(1);
240 next = item
241 .parent
242 .and_then(|id| plan.work_items.get(&id))
243 .filter(|_| depth <= plan.work_items.len());
244 }
245 match undecided {
246 Some(state) => (state, None),
247 None => (Applicability::Applicable, decided),
248 }
249}
250
251fn through_constraints(
252 plan: &Plan,
253 index: &crate::graph_index::GraphIndex,
254 work: &WorkItem,
255 states: &BTreeMap<WorkItemId, Applicability>,
256) -> Applicability {
257 let mut awaiting = None;
258 let (mut incoming, mut active) = (0_usize, 0_usize);
259 for edge in index
260 .incoming
261 .get(&work.id)
262 .into_iter()
263 .flatten()
264 .filter_map(|i| plan.dependencies.get(*i))
265 {
266 incoming = incoming.wrapping_add(1);
267 let Some(predecessor) = plan.work_items.get(&edge.predecessor) else {
268 continue;
269 };
270 match states.get(&edge.predecessor) {
271 Some(state) if state.is_not_selected() && work.contract.join.skips_unselected() => {}
272 Some(
273 Applicability::NotSelected { .. }
274 | Applicability::AllChildrenExcluded { .. }
275 | Applicability::Stranded { .. }
276 | Applicability::ChildrenStranded { .. }
277 | Applicability::EmptyJoin,
278 ) => {
279 return Applicability::Stranded {
280 dependency: edge.id,
281 predecessor: predecessor.key.clone(),
282 };
283 }
284 Some(Applicability::Undecided { .. } | Applicability::AwaitingChoice { .. }) => {
285 awaiting.get_or_insert_with(|| predecessor.key.clone());
286 active = active.wrapping_add(1);
287 }
288 Some(Applicability::Applicable) | None => active = active.wrapping_add(1),
289 }
290 }
291 if let Some(predecessor) = awaiting {
292 return Applicability::AwaitingChoice { predecessor };
293 }
294 let permits_empty = matches!(
295 work.contract.join,
296 crate::JoinPolicy::ActiveBranches { allow_empty: true }
297 );
298 if work.contract.join.skips_unselected() && incoming > 0 && active == 0 && !permits_empty {
299 return Applicability::EmptyJoin;
300 }
301 Applicability::Applicable
302}
303
304fn dependency_order(plan: &Plan) -> Vec<WorkItemId> {
306 let mut indegree: BTreeMap<_, usize> = plan.work_items.keys().map(|id| (*id, 0)).collect();
307 let mut outgoing: BTreeMap<_, Vec<_>> = BTreeMap::new();
308 for edge in &plan.dependencies {
309 if let Some(count) = indegree.get_mut(&edge.successor) {
310 *count = count.wrapping_add(1);
311 outgoing
312 .entry(edge.predecessor)
313 .or_default()
314 .push(edge.successor);
315 }
316 }
317 let mut queue: VecDeque<_> = indegree
318 .iter()
319 .filter_map(|(id, n)| (*n == 0).then_some(*id))
320 .collect();
321 let mut order = Vec::with_capacity(plan.work_items.len());
322 while let Some(id) = queue.pop_front() {
323 order.push(id);
324 for child in outgoing.get(&id).into_iter().flatten() {
325 if let Some(count) = indegree.get_mut(child) {
326 *count = count.saturating_sub(1);
327 if *count == 0 {
328 queue.push_back(*child);
329 }
330 }
331 }
332 }
333 order
334}
335
336#[cfg(test)]
337mod tests;