1use crate::applicability::{self, Derived};
2use crate::{
3 Applicability, Decision, Dependency, Endpoint, EventTime, Plan, Release, WorkItem, WorkItemId,
4 WorkKind, WorkStatus,
5};
6use chrono::{DateTime, Utc};
7use std::collections::{BTreeMap, BTreeSet};
8
9#[derive(Debug, Clone, PartialEq)]
15pub struct Timeline {
16 index: crate::graph_index::GraphIndex,
17 now: DateTime<Utc>,
18 complete: BTreeMap<WorkItemId, EventTime>,
19 applicability: BTreeMap<WorkItemId, Applicability>,
20 choice_at: BTreeMap<WorkItemId, EventTime>,
21}
22
23const APPLICABLE: Applicability = Applicability::Applicable;
24
25impl Timeline {
26 #[must_use]
36 pub fn at(plan: &Plan, now: DateTime<Utc>) -> Self {
37 let index = crate::graph_index::GraphIndex::new(plan);
38 let Derived { states, choice_at } = applicability::derive_indexed(plan, &index);
39 let mut timeline = Self {
40 index,
41 now,
42 complete: plan
43 .work_items
44 .values()
45 .filter(|w| w.is_executable())
46 .filter(|w| {
47 states
48 .get(&w.id)
49 .is_none_or(Applicability::condition_selected)
50 })
51 .filter_map(|w| task_event(w, Endpoint::Finish).map(|at| (w.id, at)))
52 .collect(),
53 applicability: states,
54 choice_at,
55 };
56 loop {
57 let reached: Vec<_> = plan
58 .work_items
59 .values()
60 .filter(|w| !timeline.complete.contains_key(&w.id))
61 .filter_map(|w| timeline.aggregate_completion(plan, w).map(|at| (w.id, at)))
62 .collect();
63 if reached.is_empty() {
64 return timeline;
65 }
66 timeline.complete.extend(reached);
67 }
68 }
69
70 pub fn incoming<'a>(
72 &'a self,
73 plan: &'a Plan,
74 work: WorkItemId,
75 ) -> impl Iterator<Item = &'a Dependency> {
76 self.index
77 .incoming
78 .get(&work)
79 .into_iter()
80 .flatten()
81 .filter_map(|i| plan.dependencies.get(*i))
82 }
83
84 #[must_use]
86 pub fn now(&self) -> DateTime<Utc> {
87 self.now
88 }
89
90 #[must_use]
92 pub fn completed_at(&self, work: WorkItemId) -> Option<EventTime> {
93 self.complete.get(&work).copied()
94 }
95
96 #[must_use]
98 pub fn completed(&self) -> BTreeSet<WorkItemId> {
99 self.complete.keys().copied().collect()
100 }
101
102 #[must_use]
104 pub fn event(&self, plan: &Plan, work: WorkItemId, endpoint: Endpoint) -> Option<EventTime> {
105 let item = plan.work_items.get(&work)?;
106 if item.is_executable() {
107 task_event(item, endpoint)
108 } else {
109 self.completed_at(work)
110 }
111 }
112
113 #[must_use]
115 pub fn applicability(&self, work: WorkItemId) -> &Applicability {
116 self.applicability.get(&work).unwrap_or(&APPLICABLE)
117 }
118
119 #[must_use]
124 pub fn edge(&self, plan: &Plan, edge: &Dependency) -> Release {
125 if self.applicability(edge.predecessor).is_not_selected() {
126 let join = plan
127 .work_items
128 .get(&edge.successor)
129 .map(|w| w.contract.join);
130 return match (join, self.choice_at.get(&edge.predecessor)) {
131 (Some(_), Some(EventTime::Recorded(at))) if *at > self.now => {
133 Release::AwaitingEvent
134 }
135 (Some(join), Some(at)) if join.skips_unselected() => {
136 Release::SkippedBranch { at: *at }
137 }
138 _ => Release::NotSelected,
139 };
140 }
141 let event = self.event(plan, edge.predecessor, edge.kind.predecessor_endpoint());
142 Release::evaluate_with(event, edge.lag_hours, self.now, |at, hours| {
143 plan.lag_end(edge, at, hours)
144 })
145 }
146
147 #[must_use]
149 pub fn decisions<'a>(&self, plan: &'a Plan, work: WorkItemId) -> Vec<(&'a Decision, Release)> {
150 let Some(scope) = ancestors(plan, work) else {
151 return Vec::new();
152 };
153 let ids: BTreeSet<_> = scope
154 .iter()
155 .flat_map(|id| self.index.decisions.get(id).into_iter().flatten())
156 .collect();
157 ids.into_iter()
158 .filter_map(|id| plan.decisions.get(id))
159 .map(|d| (d, Release::evaluate(gate_event(d), 0.0, self.now)))
160 .collect()
161 }
162
163 #[must_use]
168 pub fn selecting_choices<'a>(
169 &self,
170 plan: &'a Plan,
171 work: WorkItemId,
172 ) -> Vec<(&'a Decision, EventTime)> {
173 if !self.applicability(work).is_applicable() {
174 return Vec::new();
175 }
176 let Some(scope) = ancestors(plan, work) else {
177 return Vec::new();
178 };
179 let choices: BTreeMap<_, _> = scope
180 .iter()
181 .filter_map(|id| plan.work_items.get(id)?.contract.condition.as_ref())
182 .filter_map(|condition| applicability::effective_decision(plan, condition.decision))
183 .filter_map(|d| applicability::standing_since(plan, d).map(|at| (d.id, (d, at))))
184 .collect();
185 choices.into_values().collect()
186 }
187
188 fn aggregate_completion(&self, plan: &Plan, work: &WorkItem) -> Option<EventTime> {
189 if work.execution.status != WorkStatus::Planned
190 || !self.applicability(work.id).is_applicable()
191 {
192 return None;
193 }
194 let prerequisites: Vec<Release> = match work.kind {
195 WorkKind::Task => return None,
196 WorkKind::Milestone => self
197 .incoming(plan, work.id)
198 .map(|d| self.edge(plan, d))
199 .collect(),
200 WorkKind::WorkPackage => self
201 .index
202 .children
203 .get(&work.id)
204 .into_iter()
205 .flatten()
206 .filter_map(|id| plan.work_items.get(id))
207 .map(|w| match self.choice_at.get(&w.id) {
208 Some(at) if self.applicability(w.id).is_not_selected() => {
209 Release::SkippedBranch { at: *at }
210 }
211 _ => self
212 .completed_at(w.id)
213 .map_or(Release::AwaitingEvent, |at| Release::Released { at }),
214 })
215 .collect(),
216 };
217 let permits_empty = matches!(
219 work.contract.join,
220 crate::JoinPolicy::ActiveBranches { allow_empty: true }
221 );
222 let happened = prerequisites
223 .iter()
224 .any(|r| !matches!(r, Release::SkippedBranch { .. }));
225 if prerequisites.is_empty() || (!happened && !permits_empty) {
226 return None;
227 }
228 let gates = self.decisions(plan, work.id).into_iter().map(|(_, r)| r);
229 let chosen = self
230 .choice_at
231 .get(&work.id)
232 .map(|at| Release::Released { at: *at });
233 prerequisites
234 .into_iter()
235 .chain(gates)
236 .chain(chosen)
237 .try_fold(None, |latest: Option<EventTime>, release| {
238 let at = release.released_at()?;
239 Some(Some(latest.map_or(at, |l| l.latest(at))))
240 })
241 .flatten()
242 }
243}
244
245#[must_use]
247pub fn completion(plan: &Plan, now: DateTime<Utc>) -> BTreeSet<WorkItemId> {
248 Timeline::at(plan, now).completed()
249}
250
251fn task_event(work: &WorkItem, endpoint: Endpoint) -> Option<EventTime> {
253 match endpoint {
254 Endpoint::Start => work.start_event(),
255 Endpoint::Finish => match work.execution.events.verified_at {
256 Some(at) => Some(EventTime::Recorded(at)),
257 None => work
258 .execution
259 .status
260 .satisfies_dependency()
261 .then_some(EventTime::Unrecorded),
262 },
263 }
264}
265
266fn gate_event(decision: &Decision) -> Option<EventTime> {
268 (decision.status != crate::DecisionStatus::Open).then(|| {
269 decision
270 .resolved_at
271 .map_or(EventTime::Unrecorded, EventTime::Recorded)
272 })
273}
274
275fn ancestors(plan: &Plan, work: WorkItemId) -> Option<BTreeSet<WorkItemId>> {
276 let mut ids = BTreeSet::new();
277 let mut next = Some(work);
278 while let Some(id) = next {
279 if !ids.insert(id) {
280 return None;
281 }
282 next = plan.work_items.get(&id)?.parent;
283 }
284 Some(ids)
285}
286
287#[cfg(test)]
288mod tests;