1use crate::errors::{CoreError, CoreResult};
7use crate::orchestrate::gates::{
8 GATE_IMPLEMENTATION_READINESS, default_gate_order, ensure_implementation_readiness_first,
9};
10use crate::orchestrate::types::{
11 FailurePolicy, GatePolicy, OrchestrateRunConfig, parse_max_parallel,
12};
13use ito_domain::changes::ChangeOrchestrateMetadata;
14use serde::{Deserialize, Serialize};
15use std::collections::{BTreeMap, BTreeSet, VecDeque};
16
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
18pub struct PlannedGate {
20 pub name: String,
22 pub policy: GatePolicy,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
27pub struct PlannedChange {
29 pub id: String,
31 #[serde(default)]
33 pub depends_on: Vec<String>,
34 pub gates: Vec<PlannedGate>,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
39pub struct RunPlan {
41 pub run_id: String,
43 pub preset: String,
45 pub max_parallel: usize,
47 pub failure_policy: FailurePolicy,
49 pub changes: Vec<PlannedChange>,
51}
52
53#[derive(Debug, Clone)]
54pub struct ChangePlanInput {
56 pub id: String,
58 pub orchestrate: ChangeOrchestrateMetadata,
60}
61
62pub fn build_run_plan(
66 run_id: &str,
67 preset: &str,
68 config: OrchestrateRunConfig,
69 changes: Vec<ChangePlanInput>,
70) -> CoreResult<RunPlan> {
71 let max_parallel = parse_max_parallel(config.max_parallel, config.max_parallel_cap)?;
72 let failure_policy = config.failure_policy.unwrap_or(FailurePolicy::Remediate);
73
74 let change_ids = collect_change_ids(&changes);
75 let deps = build_dependency_map(&changes, &change_ids);
76
77 let ordered_ids = topo_sort(&deps)?;
78 let mut changes_by_id: BTreeMap<String, ChangePlanInput> = BTreeMap::new();
79 for c in changes {
80 changes_by_id.insert(c.id.clone(), c);
81 }
82
83 let default_order = if config.gate_order.is_empty() {
84 default_gate_order()
85 } else {
86 config.gate_order.clone()
87 };
88 let skip_gates = config.skip_gates;
89
90 let mut planned = Vec::new();
91 for id in ordered_ids {
92 let Some(input) = changes_by_id.remove(&id) else {
93 continue;
94 };
95
96 let gate_names =
97 ensure_implementation_readiness_first(&resolve_gate_names(&input, &default_order));
98 let gates = build_planned_gates(gate_names, &skip_gates);
99
100 let depends_on = deps
101 .get(&input.id)
102 .map(|s| s.iter().cloned().collect())
103 .unwrap_or_default();
104
105 planned.push(PlannedChange {
106 id: input.id,
107 depends_on,
108 gates,
109 });
110 }
111
112 Ok(RunPlan {
113 run_id: run_id.to_string(),
114 preset: preset.to_string(),
115 max_parallel,
116 failure_policy,
117 changes: planned,
118 })
119}
120
121fn collect_change_ids(changes: &[ChangePlanInput]) -> BTreeSet<String> {
122 let mut change_ids = BTreeSet::new();
123 for change in changes {
124 change_ids.insert(change.id.clone());
125 }
126 change_ids
127}
128
129fn build_dependency_map(
130 changes: &[ChangePlanInput],
131 change_ids: &BTreeSet<String>,
132) -> BTreeMap<String, BTreeSet<String>> {
133 let mut deps = BTreeMap::new();
134 for change in changes {
135 let depends_on = change
136 .orchestrate
137 .depends_on
138 .iter()
139 .filter(|dep| change_ids.contains(dep.as_str()))
140 .cloned()
141 .collect();
142 deps.insert(change.id.clone(), depends_on);
143 }
144 deps
145}
146
147fn resolve_gate_names(input: &ChangePlanInput, default_order: &[String]) -> Vec<String> {
148 if !input.orchestrate.preferred_gates.is_empty() {
149 return input.orchestrate.preferred_gates.clone();
150 }
151
152 default_order.to_vec()
153}
154
155fn build_planned_gates(gate_names: Vec<String>, skip_gates: &BTreeSet<String>) -> Vec<PlannedGate> {
156 gate_names
157 .into_iter()
158 .map(|name| PlannedGate {
159 policy: gate_policy_for_name(skip_gates, &name),
160 name,
161 })
162 .collect()
163}
164
165fn gate_policy_for_name(skip_gates: &BTreeSet<String>, gate_name: &str) -> GatePolicy {
166 if gate_name != GATE_IMPLEMENTATION_READINESS && skip_gates.contains(gate_name) {
167 return GatePolicy::Skip;
168 }
169
170 GatePolicy::Run
171}
172
173fn topo_sort(deps: &BTreeMap<String, BTreeSet<String>>) -> CoreResult<Vec<String>> {
174 let mut indegree: BTreeMap<String, usize> = BTreeMap::new();
175 let mut reverse: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
176
177 for (node, node_deps) in deps {
178 indegree.insert(node.clone(), node_deps.len());
179 for dep in node_deps {
180 reverse.entry(dep.clone()).or_default().insert(node.clone());
181 }
182 }
183
184 let mut q: VecDeque<String> = VecDeque::new();
185 for (node, d) in &indegree {
186 if *d == 0 {
187 q.push_back(node.clone());
188 }
189 }
190
191 let mut out = Vec::new();
192 while let Some(n) = q.pop_front() {
193 out.push(n.clone());
194 let Some(children) = reverse.get(&n) else {
195 continue;
196 };
197 for child in children {
198 if let Some(d) = indegree.get_mut(child) {
199 *d = d.saturating_sub(1);
200 if *d == 0 {
201 q.push_back(child.clone());
202 }
203 }
204 }
205 }
206
207 if out.len() == deps.len() {
208 return Ok(out);
209 }
210
211 let cycle = find_cycle(deps).unwrap_or_else(|| vec!["<unknown>".to_string()]);
213 Err(CoreError::Validation(format!(
214 "circular depends_on cycle detected: {}",
215 cycle.join(" -> ")
216 )))
217}
218
219fn find_cycle(deps: &BTreeMap<String, BTreeSet<String>>) -> Option<Vec<String>> {
220 #[derive(Clone, Copy, PartialEq, Eq)]
221 enum Mark {
222 Temp,
223 Perm,
224 }
225
226 fn dfs(
227 node: &str,
228 deps: &BTreeMap<String, BTreeSet<String>>,
229 marks: &mut BTreeMap<String, Mark>,
230 stack: &mut Vec<String>,
231 ) -> Option<Vec<String>> {
232 match marks.get(node) {
233 Some(Mark::Temp) => {
234 return Some(build_cycle(stack, node));
235 }
236 Some(Mark::Perm) => {
237 return None;
238 }
239 None => {}
240 }
241
242 marks.insert(node.to_string(), Mark::Temp);
243 stack.push(node.to_string());
244 if let Some(next) = deps.get(node) {
245 for dep in next {
246 if let Some(cycle) = dfs(dep, deps, marks, stack) {
247 return Some(cycle);
248 }
249 }
250 }
251 stack.pop();
252 marks.insert(node.to_string(), Mark::Perm);
253 None
254 }
255
256 let mut marks: BTreeMap<String, Mark> = BTreeMap::new();
257 for node in deps.keys() {
258 let mut stack = Vec::new();
259 if let Some(cycle) = dfs(node, deps, &mut marks, &mut stack) {
260 return Some(cycle);
261 }
262 }
263 None
264}
265
266fn build_cycle(stack: &[String], node: &str) -> Vec<String> {
267 let start = stack
268 .iter()
269 .position(|candidate| candidate == node)
270 .unwrap_or(0);
271 let mut cycle = stack[start..].to_vec();
272 cycle.push(node.to_string());
273 cycle
274}