1use dpm_model::{
2 Artifact, Decision, DecisionId, Dependency, Plan, Project, Requirement, Risk, WorkItem,
3 WorkItemId, WorkLink,
4};
5use serde::{Deserialize, Serialize};
6use std::collections::BTreeSet;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct ExecutionContext {
11 pub assets: Vec<dpm_model::WorkspaceAsset>,
13 pub project: Project,
15 pub parents: Vec<WorkItem>,
17 pub requirements: Vec<Requirement>,
19 pub artifacts: Vec<Artifact>,
21 pub decisions: Vec<Decision>,
23 pub risks: Vec<Risk>,
25 pub dependencies: Vec<Dependency>,
27 pub links: Vec<WorkLink>,
29 pub successors: Vec<WorkItem>,
31 #[serde(default)]
33 pub external_references: Vec<dpm_model::ExternalReference>,
34}
35
36pub(crate) fn lineage<'a>(plan: &'a Plan, work: &'a WorkItem) -> Vec<&'a WorkItem> {
38 let mut items = vec![work];
39 let mut parent = work.parent;
40 while let Some(item) = parent.and_then(|id| plan.work_items.get(&id)) {
41 items.push(item);
42 parent = item.parent;
43 }
44 items
45}
46
47pub(crate) fn decision_applies(
50 plan: &Plan,
51 decision: &Decision,
52 lineage: &BTreeSet<WorkItemId>,
53) -> bool {
54 !decision.blocks.is_disjoint(lineage)
55 || !decision.related_work.is_disjoint(lineage)
56 || lineage.iter().any(|id| {
57 plan.work_items
58 .get(id)
59 .and_then(|w| w.contract.condition.as_ref())
60 .is_some_and(|c| c.decision == decision.id)
61 })
62}
63
64pub(crate) fn applicable_decisions(
69 plan: &Plan,
70 lineage: &BTreeSet<WorkItemId>,
71) -> BTreeSet<DecisionId> {
72 let mut selected: BTreeSet<_> = plan
73 .decisions
74 .values()
75 .filter(|d| decision_applies(plan, d, lineage))
76 .map(|d| d.id)
77 .collect();
78 loop {
79 let before = selected.len();
80 let replacements: Vec<_> = plan
81 .decisions
82 .values()
83 .filter(|d| d.supersedes.is_some_and(|old| selected.contains(&old)))
84 .map(|d| d.id)
85 .collect();
86 selected.extend(replacements);
87 if selected.len() == before {
88 return selected;
89 }
90 }
91}
92
93pub(crate) fn execution_context(
94 plan: &Plan,
95 work: &WorkItem,
96) -> Result<ExecutionContext, crate::EngineError> {
97 let lineage = lineage(plan, work);
98 let ancestors: BTreeSet<_> = lineage.iter().map(|w| w.id).collect();
99 let parents: Vec<_> = lineage.iter().skip(1).map(|w| (*w).clone()).collect();
100 let decisions: Vec<_> = applicable_decisions(plan, &ancestors)
101 .into_iter()
102 .filter_map(|id| plan.decisions.get(&id).cloned())
103 .collect();
104 let mut artifact_ids = work.execution.artifact_ids.clone();
105 for decision in &decisions {
106 artifact_ids.extend(&decision.artifact_ids);
107 }
108 for dependency in &plan.dependencies {
109 if dependency.successor == work.id
110 && let Some(predecessor) = plan.work_items.get(&dependency.predecessor)
111 {
112 artifact_ids.extend(&predecessor.execution.artifact_ids);
113 }
114 }
115 let project = plan.projects.get(&work.project).cloned().ok_or_else(|| {
116 crate::EngineError::InvalidCommand {
117 entity: work.key.to_string(),
118 reason: format!(
119 "work names project {} the plan does not contain",
120 work.project
121 ),
122 }
123 })?;
124 Ok(ExecutionContext {
125 assets: work
126 .contract
127 .assets
128 .iter()
129 .filter_map(|r| plan.assets.get(&r.asset).cloned())
130 .collect(),
131 project,
132 parents,
133 requirements: work
134 .contract
135 .requirement_ids
136 .iter()
137 .filter_map(|id| plan.requirements.get(id).cloned())
138 .collect(),
139 artifacts: artifact_ids
140 .iter()
141 .filter_map(|id| plan.artifacts.get(id).cloned())
142 .collect(),
143 decisions,
144 risks: plan
145 .risks
146 .values()
147 .filter(|r| !r.related_work.is_disjoint(&ancestors))
148 .cloned()
149 .collect(),
150 dependencies: plan
151 .dependencies
152 .iter()
153 .filter(|d| d.predecessor == work.id || d.successor == work.id)
154 .cloned()
155 .collect(),
156 links: plan
157 .links
158 .iter()
159 .filter(|l| l.source == work.id || l.target == work.id)
160 .cloned()
161 .collect(),
162 successors: plan
163 .dependencies
164 .iter()
165 .filter(|d| d.predecessor == work.id)
166 .filter_map(|d| plan.work_items.get(&d.successor).cloned())
167 .collect(),
168 external_references: plan
169 .external_references
170 .values()
171 .filter(|r| r.links.iter().any(|l| ancestors.contains(&l.work)))
172 .cloned()
173 .collect(),
174 })
175}
176
177#[cfg(test)]
178mod tests;