konveyor_core/fix.rs
1//! Fix strategy types for the Konveyor migration pipeline.
2//!
3//! These types define the JSON schema for `fix-strategies.json`, which is
4//! written by the semver-analyzer and read by the frontend-analyzer-provider's
5//! fix engine. All types derive both `Serialize` and `Deserialize` to ensure
6//! round-trip compatibility.
7
8use serde::{Deserialize, Serialize};
9use std::collections::{BTreeMap, HashMap};
10use std::path::Path;
11
12use anyhow::{Context, Result};
13
14// ── Fix guidance types ──────────────────────────────────────────────────
15
16/// How to fix a detected issue.
17///
18/// Mirrors the frontend-analyzer-provider's fix engine: each rule is mapped
19/// to a deterministic fix strategy with confidence level.
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct FixGuidanceEntry {
22 /// The rule ID this fix corresponds to.
23 #[serde(rename = "ruleID")]
24 pub rule_id: String,
25
26 /// The fix strategy to apply.
27 pub strategy: FixStrategyKind,
28
29 /// How confident we are this fix is correct.
30 pub confidence: FixConfidence,
31
32 /// Where this fix guidance came from.
33 pub source: FixSource,
34
35 /// The affected symbol.
36 pub symbol: String,
37
38 /// Source file where the breaking change originates.
39 pub file: String,
40
41 /// Concrete instructions for fixing the issue.
42 pub fix_description: String,
43
44 /// Example of the old code pattern (when available).
45 #[serde(skip_serializing_if = "Option::is_none", default)]
46 pub before: Option<String>,
47
48 /// Example of the new code pattern (when available).
49 #[serde(skip_serializing_if = "Option::is_none", default)]
50 pub after: Option<String>,
51
52 /// Search pattern to find code that needs fixing.
53 pub search_pattern: String,
54
55 /// Suggested replacement (for mechanical fixes).
56 #[serde(skip_serializing_if = "Option::is_none", default)]
57 pub replacement: Option<String>,
58}
59
60/// What kind of fix to apply (classification label).
61///
62/// This is a classification enum used in fix guidance documents.
63/// It is distinct from the runtime `FixStrategy` in the fix engine,
64/// which carries data payloads for each variant.
65#[derive(Debug, Clone, Serialize, Deserialize)]
66#[serde(rename_all = "snake_case")]
67pub enum FixStrategyKind {
68 /// Find-and-replace: rename old symbol to new symbol.
69 Rename,
70 /// Update function call sites to match new signature.
71 UpdateSignature,
72 /// Update type annotations to match new types.
73 UpdateType,
74 /// Remove usages of a deleted symbol and find alternatives.
75 FindAlternative,
76 /// Update import paths or module system (require <-> import).
77 UpdateImport,
78 /// Ensure package.json has the correct dependency (add if missing, update if present).
79 EnsureDependency,
80 /// Requires manual review -- behavioral change or complex refactor.
81 ManualReview,
82}
83
84/// How confident the fix guidance is.
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
86#[serde(rename_all = "snake_case")]
87pub enum FixConfidence {
88 /// Mechanical rename or direct replacement -- safe to auto-apply.
89 Exact,
90 /// Pattern-based fix -- likely correct but may need review.
91 High,
92 /// Inferred fix -- needs human verification.
93 Medium,
94 /// Best-effort suggestion -- may not be applicable.
95 Low,
96}
97
98/// Where the fix guidance originates.
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
100#[serde(rename_all = "snake_case")]
101pub enum FixSource {
102 /// Deterministic -- derived from structural analysis.
103 Pattern,
104 /// AI-generated -- from LLM behavioral analysis.
105 Llm,
106 /// Flagged for manual intervention.
107 Manual,
108}
109
110/// Top-level fix guidance document written to `fix-guidance.yaml`.
111#[derive(Debug, Serialize, Deserialize)]
112pub struct FixGuidanceDoc {
113 /// Version range this guidance applies to.
114 pub migration: MigrationInfo,
115 /// Summary statistics.
116 pub summary: FixSummary,
117 /// Per-rule fix entries.
118 pub fixes: Vec<FixGuidanceEntry>,
119}
120
121/// Migration metadata.
122#[derive(Debug, Serialize, Deserialize)]
123pub struct MigrationInfo {
124 pub from_ref: String,
125 pub to_ref: String,
126 pub generated_by: String,
127}
128
129/// Summary of fix guidance.
130#[derive(Debug, Serialize, Deserialize)]
131pub struct FixSummary {
132 pub total_fixes: usize,
133 pub auto_fixable: usize,
134 pub needs_review: usize,
135 pub manual_only: usize,
136}
137
138// ── Machine-readable fix strategy types (fix-strategies.json) ───────────
139
140/// A single from/to mapping within a consolidated fix strategy.
141#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct MappingEntry {
143 #[serde(skip_serializing_if = "Option::is_none", default)]
144 pub from: Option<String>,
145 #[serde(skip_serializing_if = "Option::is_none", default)]
146 pub to: Option<String>,
147 #[serde(skip_serializing_if = "Option::is_none", default)]
148 pub component: Option<String>,
149 #[serde(skip_serializing_if = "Option::is_none", default)]
150 pub prop: Option<String>,
151}
152
153/// A member-level mapping entry for structural migration strategies.
154#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct MemberMappingEntry {
156 pub old_name: String,
157 pub new_name: String,
158}
159
160/// A single prop's migration mapping between a deprecated and v6 component.
161///
162/// Extends `MemberMappingEntry` with type information from the SD pipeline's
163/// `old_component_prop_types` and `new_component_prop_types`.
164#[derive(Debug, Clone, Default, Serialize, Deserialize)]
165pub struct PropMigrationEntry {
166 /// Prop name on the deprecated component.
167 pub old_name: String,
168 /// Prop name on the v6 replacement component.
169 pub new_name: String,
170 /// Full TypeScript type on the deprecated component (if available).
171 #[serde(skip_serializing_if = "Option::is_none", default)]
172 pub old_type: Option<String>,
173 /// Full TypeScript type on the v6 replacement component (if available).
174 #[serde(skip_serializing_if = "Option::is_none", default)]
175 pub new_type: Option<String>,
176 /// Whether the type changed between deprecated and v6.
177 #[serde(default)]
178 pub type_changed: bool,
179}
180
181/// Migration context for a deprecated component → v6 replacement.
182///
183/// Stored on family-level `FixStrategyEntry` entries to provide the LLM with
184/// the complete old→new prop mapping, including type signatures for matching
185/// props that changed type, new-only props with their types, and removed-only
186/// props with no v6 equivalent.
187#[derive(Debug, Clone, Default, Serialize, Deserialize)]
188pub struct DeprecatedMigrationContext {
189 /// Package the deprecated component was imported from.
190 pub old_package: String,
191 /// Package the v6 replacement is imported from.
192 pub new_package: String,
193 /// Props that exist on both old and new, with name mappings and types.
194 #[serde(skip_serializing_if = "Vec::is_empty", default)]
195 pub matching_props: Vec<PropMigrationEntry>,
196 /// Props that exist ONLY on the v6 component (not on deprecated).
197 /// Map of prop_name → TypeScript type string.
198 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
199 pub new_props: BTreeMap<String, String>,
200 /// Props that exist ONLY on the deprecated component (no v6 equivalent).
201 #[serde(skip_serializing_if = "Vec::is_empty", default)]
202 pub removed_props: Vec<String>,
203}
204
205/// A machine-readable fix strategy entry.
206///
207/// For non-consolidated rules, `from`/`to` hold the single mapping.
208/// For consolidated rules, `mappings` holds all individual mappings from the
209/// merged rules, allowing the fix engine to apply all renames/removals.
210/// For structural migration rules, `member_mappings` and `removed_members`
211/// describe the member-level overlap between removed and replacement interfaces.
212#[derive(Debug, Clone, Default, Serialize, Deserialize)]
213pub struct FixStrategyEntry {
214 pub strategy: String,
215 #[serde(skip_serializing_if = "Option::is_none", default)]
216 pub from: Option<String>,
217 #[serde(skip_serializing_if = "Option::is_none", default)]
218 pub to: Option<String>,
219 #[serde(skip_serializing_if = "Option::is_none", default)]
220 pub component: Option<String>,
221 #[serde(skip_serializing_if = "Option::is_none", default)]
222 pub prop: Option<String>,
223 /// All individual mappings when this strategy was merged from multiple rules.
224 #[serde(skip_serializing_if = "Vec::is_empty", default)]
225 pub mappings: Vec<MappingEntry>,
226 /// Structural migration: matching member mappings between removed and replacement.
227 #[serde(skip_serializing_if = "Vec::is_empty", default)]
228 pub member_mappings: Vec<MemberMappingEntry>,
229 /// Structural migration: member names only in the removed interface (no match).
230 #[serde(skip_serializing_if = "Vec::is_empty", default)]
231 pub removed_members: Vec<String>,
232 /// Structural migration: the replacement symbol name.
233 #[serde(skip_serializing_if = "Option::is_none", default)]
234 pub replacement: Option<String>,
235 /// Structural migration: overlap ratio between removed and replacement.
236 #[serde(skip_serializing_if = "Option::is_none", default)]
237 pub overlap_ratio: Option<f64>,
238 /// Dependency update: npm package name (e.g., "@patternfly/react-core").
239 #[serde(skip_serializing_if = "Option::is_none", default)]
240 pub package: Option<String>,
241 /// Dependency update: new version range (e.g., "^6.1.0").
242 #[serde(skip_serializing_if = "Option::is_none", default)]
243 pub new_version: Option<String>,
244
245 // ── Family migration fields ────────────────────────────────────────
246 // Used by `FamilyMigration` strategy entries (keyed `family:<Name>`)
247 // to describe the complete target component structure for a family.
248 /// Target JSX structure template showing correct composition.
249 /// Example: `<Modal ...>\n <ModalHeader .../>\n <ModalBody>...</ModalBody>\n</Modal>`
250 #[serde(skip_serializing_if = "Option::is_none", default)]
251 pub target_structure: Option<String>,
252 /// Props that remain on the root component in the new version.
253 #[serde(skip_serializing_if = "Vec::is_empty", default)]
254 pub retained_props: Vec<String>,
255 /// Map of removed prop name → child component that now owns it (as a named prop).
256 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
257 pub prop_to_child: BTreeMap<String, String>,
258 /// Props removed from the root that don't have an exact prop-name match
259 /// on any child component. Maps prop name → description of where/how to
260 /// migrate (e.g., "ModalFooter (as children)"). Unlike `prop_to_child`,
261 /// these props typically become *children* of the target component or
262 /// are removed entirely.
263 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
264 pub unmapped_removed_props: BTreeMap<String, String>,
265 /// Child component names removed from the family (flattened into parent).
266 #[serde(skip_serializing_if = "Vec::is_empty", default)]
267 pub removed_children: Vec<String>,
268 /// Map of "Child.prop" → "Parent.prop" for child-to-prop migrations.
269 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
270 pub child_props_to_parent: BTreeMap<String, String>,
271 /// Prop value changes: prop_name → list of old→new value mappings.
272 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
273 pub prop_value_changes: BTreeMap<String, Vec<MappingEntry>>,
274 /// New imports needed after restructuring (child components to add).
275 #[serde(skip_serializing_if = "Vec::is_empty", default)]
276 pub new_imports: Vec<String>,
277 /// Imports to remove after restructuring (removed child components).
278 #[serde(skip_serializing_if = "Vec::is_empty", default)]
279 pub removed_imports: Vec<String>,
280 /// Import source package (e.g., "@patternfly/react-core").
281 #[serde(skip_serializing_if = "Option::is_none", default)]
282 pub import_source: Option<String>,
283
284 // ── Deprecated migration context ──────────────────────────────────
285 // Populated for families where a deprecated component has a v6 replacement.
286 // Provides the complete old→new prop mapping with type signatures.
287 /// Deprecated → v6 migration context with prop mappings, types, and
288 /// new/removed prop lists. Present when the family includes a deprecated
289 /// component that has a detected migration target.
290 #[serde(skip_serializing_if = "Option::is_none", default)]
291 pub deprecated_migration: Option<DeprecatedMigrationContext>,
292
293 /// Patterns to exclude from this fix strategy's automated replacement.
294 ///
295 /// Used with `CssVariablePrefix` to prevent blind prefix swaps on
296 /// CSS classes that were removed in the target version. When the
297 /// matched text contains any of these patterns, the edit is skipped.
298 /// The incident is still reported (as Manual) by a separate dead-class rule.
299 #[serde(skip_serializing_if = "Vec::is_empty", default)]
300 pub exclude_patterns: Vec<String>,
301}
302
303impl FixStrategyEntry {
304 /// Create a new strategy entry with only the strategy type set.
305 pub fn new(strategy: &str) -> Self {
306 Self {
307 strategy: strategy.into(),
308 ..Default::default()
309 }
310 }
311
312 /// Create a Rename strategy with a single from/to pair.
313 pub fn rename(from: impl Into<String>, to: impl Into<String>) -> Self {
314 Self {
315 strategy: "Rename".into(),
316 from: Some(from.into()),
317 to: Some(to.into()),
318 ..Default::default()
319 }
320 }
321
322 /// Create a strategy with from/to and a named strategy type.
323 pub fn with_from_to(strategy: &str, from: impl Into<String>, to: impl Into<String>) -> Self {
324 Self {
325 strategy: strategy.into(),
326 from: Some(from.into()),
327 to: Some(to.into()),
328 ..Default::default()
329 }
330 }
331
332 /// Create a RemoveProp strategy.
333 pub fn remove_prop(component: impl Into<String>, prop: impl Into<String>) -> Self {
334 Self {
335 strategy: "RemoveProp".into(),
336 component: Some(component.into()),
337 prop: Some(prop.into()),
338 ..Default::default()
339 }
340 }
341
342 /// Create an LlmAssisted strategy enriched with structural migration data.
343 pub fn structural_migration(
344 removed_symbol: &str,
345 replacement_symbol: &str,
346 member_mappings: Vec<MemberMappingEntry>,
347 removed_members: Vec<String>,
348 overlap_ratio: f64,
349 ) -> Self {
350 Self {
351 strategy: "LlmAssisted".into(),
352 from: Some(removed_symbol.into()),
353 to: Some(replacement_symbol.into()),
354 member_mappings,
355 removed_members,
356 replacement: Some(replacement_symbol.into()),
357 overlap_ratio: Some(overlap_ratio),
358 ..Default::default()
359 }
360 }
361
362 /// Create an EnsureDependency strategy: add if missing, update version if present.
363 pub fn ensure_dependency(package: impl Into<String>, new_version: impl Into<String>) -> Self {
364 Self {
365 strategy: "EnsureDependency".into(),
366 package: Some(package.into()),
367 new_version: Some(new_version.into()),
368 ..Default::default()
369 }
370 }
371
372 /// Convert to a MappingEntry (extracting the single mapping).
373 pub fn to_mapping(&self) -> MappingEntry {
374 MappingEntry {
375 from: self.from.clone(),
376 to: self.to.clone(),
377 component: self.component.clone(),
378 prop: self.prop.clone(),
379 }
380 }
381}
382
383// ── IO helpers ──────────────────────────────────────────────────────────
384
385/// Extract fix strategies from the final (post-consolidation) rules.
386pub fn extract_fix_strategies(
387 rules: &[crate::rule::KonveyorRule],
388) -> HashMap<String, FixStrategyEntry> {
389 rules
390 .iter()
391 .filter_map(|r| {
392 r.fix_strategy
393 .as_ref()
394 .map(|s| (r.rule_id.clone(), s.clone()))
395 })
396 .collect()
397}
398
399/// Write fix strategies JSON to the fix-guidance directory.
400pub fn write_fix_strategies(
401 fix_dir: &Path,
402 strategies: &HashMap<String, FixStrategyEntry>,
403) -> Result<()> {
404 let path = fix_dir.join("fix-strategies.json");
405 let json =
406 serde_json::to_string_pretty(strategies).context("Failed to serialize fix strategies")?;
407 std::fs::write(&path, &json).with_context(|| format!("Failed to write {}", path.display()))?;
408 Ok(())
409}
410
411/// Priority for fix strategy type. Higher = more actionable.
412pub fn strategy_priority(strategy: &str) -> u8 {
413 match strategy {
414 "Rename" => 5,
415 "RemoveProp" => 4,
416 "CssVariablePrefix" => 4,
417 "ImportPathChange" => 3,
418 "PropValueChange" => 2,
419 "PropTypeChange" => 2,
420 "LlmAssisted" => 1,
421 _ => 0,
422 }
423}