Skip to main content

dpm_engine/change/
applicability.rs

1use dpm_model::{Applicability, Key, Plan, WorkItemId, WorkStatus};
2use serde::{Deserialize, Serialize};
3
4/// Work whose membership in the active graph a proposal changes.
5///
6/// A reviewed change may switch a choice under started work; the work keeps its lifecycle and
7/// evidence rather than being cancelled, and this entry makes the consequence visible to review.
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
9pub struct ApplicabilityChange {
10    /// Stable identity of the affected work.
11    pub work: WorkItemId,
12    /// Human-readable key of the affected work.
13    pub key: Key,
14    /// Current lifecycle.
15    pub status: WorkStatus,
16    /// Whether the work is claimed, started, submitted, blocked or accepted.
17    pub in_flight: bool,
18    /// Applicability in the current plan; absent for new work.
19    pub before: Option<Applicability>,
20    /// Applicability in the proposed plan; absent for removed work.
21    pub after: Option<Applicability>,
22}
23
24pub(super) fn changes(current: &Plan, proposed: &Plan) -> Vec<ApplicabilityChange> {
25    let before = current.applicability();
26    let after = proposed.applicability();
27    let mut ids: Vec<_> = before.keys().chain(after.keys()).copied().collect();
28    ids.sort();
29    ids.dedup();
30    let mut changes: Vec<_> = ids
31        .into_iter()
32        .filter(|id| before.get(id) != after.get(id))
33        .filter(|id| {
34            before.contains_key(id) && after.contains_key(id)
35                || !is_plain(before.get(id).or(after.get(id)))
36        })
37        .filter_map(|id| {
38            let work = proposed
39                .work_items
40                .get(&id)
41                .or_else(|| current.work_items.get(&id))?;
42            Some(ApplicabilityChange {
43                work: id,
44                key: work.key.clone(),
45                status: work.execution.status,
46                in_flight: !matches!(
47                    work.execution.status,
48                    WorkStatus::Proposed | WorkStatus::Planned
49                ),
50                before: before.get(&id).cloned(),
51                after: after.get(&id).cloned(),
52            })
53        })
54        .collect();
55    changes.sort_by(|a, b| a.key.cmp(&b.key));
56    changes
57}
58
59/// Added or removed unconditional applicable work is already visible as an entity change.
60fn is_plain(state: Option<&Applicability>) -> bool {
61    state.is_none_or(Applicability::is_applicable)
62}