1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3
4use crate::core::recipe::{DetectionReport, FileAnalysis, Recipe, TransformReport};
5
6#[derive(Debug, Clone)]
7#[allow(dead_code)]
8pub struct PipelineContext {
9 pub project_root: PathBuf,
10 pub files: HashMap<PathBuf, FilePipelineState>,
11}
12
13#[derive(Debug, Clone)]
14#[allow(dead_code)]
15pub struct FilePipelineState {
16 pub original_path: PathBuf,
17 pub current_content: Option<String>,
18 pub stages_passed: Vec<String>,
19 pub stages_failed: Vec<FailedStage>,
20}
21
22#[derive(Debug, Clone)]
23#[allow(dead_code)]
24pub struct FailedStage {
25 pub recipe: String,
26 pub error: String,
27}
28
29impl PipelineContext {
30 pub fn new(project_root: PathBuf) -> Self {
31 Self {
32 project_root,
33 files: HashMap::new(),
34 }
35 }
36
37 pub fn add_file(&mut self, path: PathBuf, _analysis: FileAnalysis) {
38 self.files.insert(
39 path.clone(),
40 FilePipelineState {
41 original_path: path,
42 current_content: None,
43 stages_passed: Vec::new(),
44 stages_failed: Vec::new(),
45 },
46 );
47 }
48
49 pub fn mark_stage_passed(&mut self, path: &Path, recipe: &str) {
50 if let Some(state) = self.files.get_mut(path) {
51 state.stages_passed.push(recipe.to_string());
52 }
53 }
54
55 #[allow(dead_code)]
56 pub fn mark_stage_failed(&mut self, path: &Path, recipe: &str, error: &str) {
57 if let Some(state) = self.files.get_mut(path) {
58 state.stages_failed.push(FailedStage {
59 recipe: recipe.to_string(),
60 error: error.to_string(),
61 });
62 }
63 }
64
65 #[allow(dead_code)]
66 pub fn get_files_at_stage(&self, stage: &str) -> Vec<&PathBuf> {
67 self.files
68 .iter()
69 .filter(|(_, state)| state.stages_passed.contains(&stage.to_string()))
70 .map(|(path, _)| path)
71 .collect()
72 }
73
74 #[allow(dead_code)]
75 pub fn get_files_without_stage(&self, stage: &str) -> Vec<&PathBuf> {
76 self.files
77 .iter()
78 .filter(|(_, state)| !state.stages_passed.contains(&stage.to_string()))
79 .map(|(path, _)| path)
80 .collect()
81 }
82}
83
84#[derive(Debug, Clone)]
85#[allow(dead_code)]
86pub struct PipelineStage {
87 pub recipe_name: &'static str,
88 pub detect_result: Option<DetectionReport>,
89 pub transform_result: Option<TransformReport>,
90 pub timing: StageTiming,
91}
92
93#[derive(Debug, Clone, Default)]
94pub struct StageTiming {
95 pub detect_ms: u64,
96 pub transform_ms: u64,
97 pub total_ms: u64,
98}
99
100#[derive(Debug, Clone)]
101pub struct RecipeDependency {
102 pub after: String,
104 pub before: String,
106}
107
108pub struct RecipeOrderer {
109 pub(crate) dependencies: Vec<RecipeDependency>,
110}
111
112impl RecipeOrderer {
113 pub fn new() -> Self {
114 Self {
115 dependencies: Vec::new(),
116 }
117 }
118
119 pub fn from_metadata(recipes: &[&dyn Recipe]) -> Self {
122 let mut orderer = Self::new();
123 for recipe in recipes {
124 let meta = recipe.metadata();
125 for before in meta.should_run_before {
127 orderer.dependencies.push(RecipeDependency {
128 after: meta.name.to_string(),
129 before: (*before).to_string(),
130 });
131 }
132 for after in meta.should_run_after {
134 orderer.dependencies.push(RecipeDependency {
135 after: meta.name.to_string(),
136 before: (*after).to_string(),
137 });
138 }
139 }
140 orderer
141 }
142
143 #[allow(dead_code)]
144 pub fn add_dependency_str(mut self, after: impl Into<String>, before: impl Into<String>) -> Self {
145 self.dependencies.push(RecipeDependency {
146 after: after.into(),
147 before: before.into(),
148 });
149 self
150 }
151
152 #[allow(dead_code)]
154 pub fn add_dependency(mut self, after: &'static str, before: &'static str) -> Self {
155 self.dependencies.push(RecipeDependency {
156 after: after.to_string(),
157 before: before.to_string(),
158 });
159 self
160 }
161
162 pub fn order<'a>(&self, recipes: &'a [&'a dyn Recipe]) -> Vec<&'a dyn Recipe> {
163 let mut ordered = Vec::from(recipes);
164
165 for dep in &self.dependencies {
166 let after_idx = ordered.iter().position(|r| r.metadata().name == dep.after);
167 let before_idx = ordered.iter().position(|r| r.metadata().name == dep.before);
168 if let (Some(ai), Some(bi)) = (after_idx, before_idx) {
169 if ai > bi {
170 } else if ai < bi {
172 ordered.swap(ai, bi);
174 }
175 }
176 }
177
178 ordered
179 }
180}
181
182impl Default for RecipeOrderer {
183 fn default() -> Self {
184 Self::new()
185 }
186}
187
188
189#[derive(Debug, Clone, PartialEq, Eq)]
190#[allow(dead_code)]
191pub enum IncompatibilityReason {
192 DuplicateRecipe,
193 IncompatibleRecipe,
194 MissingRequiredRecipe,
195 RequiredRecipeOutOfOrder,
196 OrderingHintViolation,
198}
199
200#[derive(Debug, Clone)]
201#[allow(dead_code)]
202pub struct IncompatibleRecipe {
203 pub recipe_a: String,
204 pub recipe_b: String,
205 pub reason: IncompatibilityReason,
206}
207
208pub fn validate_recipe_order(recipes: &[&dyn Recipe]) -> Vec<IncompatibleRecipe> {
209 let mut incompatibilities = Vec::new();
210 let recipe_names: Vec<_> = recipes
211 .iter()
212 .map(|recipe| recipe.metadata().name)
213 .collect();
214
215 for (index, recipe) in recipes.iter().enumerate() {
216 let metadata = recipe.metadata();
217
218 if recipe_names[..index].contains(&metadata.name) {
219 incompatibilities.push(IncompatibleRecipe {
220 recipe_a: metadata.name.to_string(),
221 recipe_b: metadata.name.to_string(),
222 reason: IncompatibilityReason::DuplicateRecipe,
223 });
224 }
225
226 for required in metadata.required_recipes {
227 match recipe_names.iter().position(|name| name == required) {
228 Some(required_index) if required_index < index => {}
229 Some(_) => incompatibilities.push(IncompatibleRecipe {
230 recipe_a: metadata.name.to_string(),
231 recipe_b: (*required).to_string(),
232 reason: IncompatibilityReason::RequiredRecipeOutOfOrder,
233 }),
234 None => incompatibilities.push(IncompatibleRecipe {
235 recipe_a: metadata.name.to_string(),
236 recipe_b: (*required).to_string(),
237 reason: IncompatibilityReason::MissingRequiredRecipe,
238 }),
239 }
240 }
241
242 for incompatible in metadata.incompatible_recipes {
243 if recipe_names.contains(incompatible) {
244 incompatibilities.push(IncompatibleRecipe {
245 recipe_a: metadata.name.to_string(),
246 recipe_b: (*incompatible).to_string(),
247 reason: IncompatibilityReason::IncompatibleRecipe,
248 });
249 }
250 }
251
252 for before_recipe in metadata.should_run_before {
254 if let Some(before_idx) = recipe_names.iter().position(|n| n == before_recipe) {
255 if before_idx < index {
256 incompatibilities.push(IncompatibleRecipe {
258 recipe_a: metadata.name.to_string(),
259 recipe_b: (*before_recipe).to_string(),
260 reason: IncompatibilityReason::OrderingHintViolation,
261 });
262 }
263 }
264 }
265
266 for after_recipe in metadata.should_run_after {
268 if let Some(after_idx) = recipe_names.iter().position(|n| n == after_recipe) {
269 if after_idx > index {
270 incompatibilities.push(IncompatibleRecipe {
272 recipe_a: metadata.name.to_string(),
273 recipe_b: (*after_recipe).to_string(),
274 reason: IncompatibilityReason::OrderingHintViolation,
275 });
276 }
277 }
278 }
279 }
280
281 incompatibilities
282}
283
284#[derive(Debug, Clone)]
285#[allow(dead_code)]
286pub enum ExecutionEvent {
287 StageStarted {
288 recipe: String,
289 },
290 StageCompleted {
291 recipe: String,
292 duration_ms: u64,
293 },
294 StageFailed {
295 recipe: String,
296 error: String,
297 },
298 FileProcessed {
299 path: PathBuf,
300 recipe: String,
301 success: bool,
302 },
303 StageSkipped {
304 recipe: String,
305 reason: String,
306 },
307}