mcp-souschef 2.1.2__py3-none-any.whl → 2.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
souschef/assessment.py CHANGED
@@ -9,7 +9,8 @@ import json
9
9
  import re
10
10
  from typing import Any
11
11
 
12
- from souschef.core import ERROR_PREFIX, METADATA_FILENAME, _normalize_path, _safe_join
12
+ from souschef.core import METADATA_FILENAME, _normalize_path, _safe_join
13
+ from souschef.core.errors import format_error_with_context
13
14
  from souschef.core.validation import (
14
15
  ValidationEngine,
15
16
  ValidationLevel,
@@ -35,116 +36,110 @@ def assess_chef_migration_complexity(
35
36
 
36
37
  """
37
38
  try:
38
- # Parse cookbook paths
39
- paths = [_normalize_path(path.strip()) for path in cookbook_paths.split(",")]
40
-
41
- # Assess each cookbook
42
- cookbook_assessments = []
43
- overall_metrics = {
44
- "total_cookbooks": 0,
45
- "total_recipes": 0,
46
- "total_resources": 0,
47
- "complexity_score": 0,
48
- "estimated_effort_days": 0,
49
- }
50
-
51
- for cookbook_path in paths:
52
- if cookbook_path.exists():
53
- # deepcode ignore PT: path normalized via _normalize_path
54
- assessment = _assess_single_cookbook(cookbook_path)
55
- cookbook_assessments.append(assessment)
56
-
57
- # Aggregate metrics
58
- overall_metrics["total_cookbooks"] += 1
59
- overall_metrics["total_recipes"] += assessment["metrics"][
60
- "recipe_count"
61
- ]
62
- overall_metrics["total_resources"] += assessment["metrics"][
63
- "resource_count"
64
- ]
65
- overall_metrics["complexity_score"] += assessment["complexity_score"]
66
- overall_metrics["estimated_effort_days"] += assessment[
67
- "estimated_effort_days"
68
- ]
69
-
70
- # Calculate averages
71
- if cookbook_assessments:
72
- overall_metrics["avg_complexity"] = int(
73
- overall_metrics["complexity_score"] / len(cookbook_assessments)
74
- )
39
+ # Validate inputs
40
+ error_msg = _validate_assessment_inputs(
41
+ cookbook_paths, migration_scope, target_platform
42
+ )
43
+ if error_msg:
44
+ return error_msg
45
+
46
+ # Parse cookbook paths (may be empty if none exist)
47
+ valid_paths = _parse_cookbook_paths(cookbook_paths)
48
+
49
+ # Analyze all cookbooks (handles empty list gracefully)
50
+ cookbook_assessments, overall_metrics = _analyze_cookbook_metrics(valid_paths)
75
51
 
76
- # Generate migration recommendations
52
+ # Generate recommendations and reports
77
53
  recommendations = _generate_migration_recommendations_from_assessment(
78
54
  cookbook_assessments, overall_metrics, target_platform
79
55
  )
80
-
81
- # Create migration roadmap
82
56
  roadmap = _create_migration_roadmap(cookbook_assessments)
83
57
 
84
- return f"""# Chef to Ansible Migration Assessment
85
- # Scope: {migration_scope}
86
- # Target Platform: {target_platform}
58
+ # Format final assessment report
59
+ return _format_assessment_report(
60
+ migration_scope,
61
+ target_platform,
62
+ overall_metrics,
63
+ cookbook_assessments,
64
+ recommendations,
65
+ roadmap,
66
+ )
67
+ except Exception as e:
68
+ return format_error_with_context(
69
+ e, "assessing Chef migration complexity", cookbook_paths
70
+ )
87
71
 
88
- ## Overall Migration Metrics:
89
- {_format_overall_metrics(overall_metrics)}
90
72
 
91
- ## Cookbook Assessments:
92
- {_format_cookbook_assessments(cookbook_assessments)}
73
+ def _validate_migration_plan_inputs(
74
+ cookbook_paths: str, migration_strategy: str, timeline_weeks: int
75
+ ) -> str | None:
76
+ """
77
+ Validate migration plan inputs.
93
78
 
94
- ## Migration Complexity Analysis:
95
- {_format_complexity_analysis(cookbook_assessments)}
79
+ Returns:
80
+ Error message if validation fails, None if valid.
96
81
 
97
- ## Migration Recommendations:
98
- {recommendations}
82
+ """
83
+ if not cookbook_paths or not cookbook_paths.strip():
84
+ return (
85
+ "Error: Cookbook paths cannot be empty\n\n"
86
+ "Suggestion: Provide comma-separated paths to Chef cookbooks"
87
+ )
99
88
 
100
- ## Migration Roadmap:
101
- {roadmap}
89
+ valid_strategies = ["big_bang", "phased", "parallel"]
90
+ if migration_strategy not in valid_strategies:
91
+ return (
92
+ f"Error: Invalid migration strategy '{migration_strategy}'\n\n"
93
+ f"Suggestion: Use one of {', '.join(valid_strategies)}"
94
+ )
102
95
 
103
- ## Risk Assessment:
104
- {_assess_migration_risks(cookbook_assessments, target_platform)}
96
+ if not (1 <= timeline_weeks <= 104): # 1 week to 2 years
97
+ return (
98
+ f"Error: Timeline must be between 1 and 104 weeks, got {timeline_weeks}\n\n"
99
+ "Suggestion: Provide a realistic timeline (4-12 weeks typical)"
100
+ )
105
101
 
106
- ## Resource Requirements:
107
- {_estimate_resource_requirements(overall_metrics, target_platform)}
108
- """
109
- except Exception as e:
110
- return f"Error assessing migration complexity: {e}"
102
+ return None
111
103
 
112
104
 
113
- def generate_migration_plan(
114
- cookbook_paths: str, migration_strategy: str = "phased", timeline_weeks: int = 12
115
- ) -> str:
105
+ def _parse_and_assess_cookbooks(cookbook_paths: str) -> tuple[list, str | None]:
116
106
  """
117
- Generate a detailed migration plan from Chef to Ansible with timeline and milestones.
118
-
119
- Args:
120
- cookbook_paths: Comma-separated paths to Chef cookbooks
121
- migration_strategy: Migration approach (big_bang, phased, parallel)
122
- timeline_weeks: Target timeline in weeks
107
+ Parse cookbook paths and assess each cookbook.
123
108
 
124
109
  Returns:
125
- Detailed migration plan with phases, milestones, and deliverables
110
+ Tuple of (cookbook_assessments, error_message).
126
111
 
127
112
  """
128
- try:
129
- # Parse and assess cookbooks
130
- paths = [_normalize_path(path.strip()) for path in cookbook_paths.split(",")]
131
- cookbook_assessments = []
113
+ paths = [_normalize_path(path.strip()) for path in cookbook_paths.split(",")]
114
+ valid_paths = [p for p in paths if p.exists()]
115
+
116
+ if not valid_paths:
117
+ return (
118
+ [],
119
+ "Error: No valid cookbook paths found\n\n"
120
+ "Suggestion: Ensure paths exist and point to cookbook directories",
121
+ )
132
122
 
133
- for cookbook_path in paths:
134
- if cookbook_path.exists():
135
- # deepcode ignore PT: path normalized via _normalize_path
136
- assessment = _assess_single_cookbook(cookbook_path)
137
- cookbook_assessments.append(assessment)
123
+ cookbook_assessments = []
124
+ for cookbook_path in valid_paths:
125
+ # deepcode ignore PT: path normalized via _normalize_path
126
+ assessment = _assess_single_cookbook(cookbook_path)
127
+ cookbook_assessments.append(assessment)
138
128
 
139
- # Generate migration plan based on strategy
140
- migration_plan = _generate_detailed_migration_plan(
141
- cookbook_assessments, migration_strategy, timeline_weeks
142
- )
129
+ return cookbook_assessments, None
143
130
 
144
- return f"""# Chef to Ansible Migration Plan
131
+
132
+ def _format_migration_plan_output(
133
+ migration_plan: dict,
134
+ migration_strategy: str,
135
+ timeline_weeks: int,
136
+ num_cookbooks: int,
137
+ ) -> str:
138
+ """Format migration plan as markdown output."""
139
+ return f"""# Chef to Ansible Migration Plan
145
140
  # Strategy: {migration_strategy}
146
141
  # Timeline: {timeline_weeks} weeks
147
- # Cookbooks: {len(cookbook_assessments)}
142
+ # Cookbooks: {num_cookbooks}
148
143
 
149
144
  ## Executive Summary:
150
145
  {migration_plan["executive_summary"]}
@@ -173,8 +168,50 @@ def generate_migration_plan(
173
168
  ## Post-Migration Tasks:
174
169
  {migration_plan["post_migration"]}
175
170
  """
171
+
172
+
173
+ def generate_migration_plan(
174
+ cookbook_paths: str, migration_strategy: str = "phased", timeline_weeks: int = 12
175
+ ) -> str:
176
+ """
177
+ Generate a detailed migration plan from Chef to Ansible with timeline and milestones.
178
+
179
+ Args:
180
+ cookbook_paths: Comma-separated paths to Chef cookbooks
181
+ migration_strategy: Migration approach (big_bang, phased, parallel)
182
+ timeline_weeks: Target timeline in weeks
183
+
184
+ Returns:
185
+ Detailed migration plan with phases, milestones, and deliverables
186
+
187
+ """
188
+ try:
189
+ # Validate inputs
190
+ error = _validate_migration_plan_inputs(
191
+ cookbook_paths, migration_strategy, timeline_weeks
192
+ )
193
+ if error:
194
+ return error
195
+
196
+ # Parse and assess cookbooks
197
+ cookbook_assessments, error = _parse_and_assess_cookbooks(cookbook_paths)
198
+ if error:
199
+ return error
200
+
201
+ # Generate migration plan based on strategy
202
+ migration_plan = _generate_detailed_migration_plan(
203
+ cookbook_assessments, migration_strategy, timeline_weeks
204
+ )
205
+
206
+ return _format_migration_plan_output(
207
+ migration_plan,
208
+ migration_strategy,
209
+ timeline_weeks,
210
+ len(cookbook_assessments),
211
+ )
212
+
176
213
  except Exception as e:
177
- return f"Error generating migration plan: {e}"
214
+ return format_error_with_context(e, "generating migration plan", cookbook_paths)
178
215
 
179
216
 
180
217
  def analyze_cookbook_dependencies(
@@ -192,9 +229,20 @@ def analyze_cookbook_dependencies(
192
229
 
193
230
  """
194
231
  try:
232
+ # Validate inputs
233
+ valid_depths = ["direct", "transitive", "full"]
234
+ if dependency_depth not in valid_depths:
235
+ return (
236
+ f"Error: Invalid dependency depth '{dependency_depth}'\n\n"
237
+ f"Suggestion: Use one of {', '.join(valid_depths)}"
238
+ )
239
+
195
240
  cookbook_path_obj = _normalize_path(cookbook_path)
196
241
  if not cookbook_path_obj.exists():
197
- return f"{ERROR_PREFIX} Cookbook path not found: {cookbook_path}"
242
+ return (
243
+ f"Error: Cookbook path not found: {cookbook_path}\n\n"
244
+ "Suggestion: Check that the path exists and points to a cookbook directory"
245
+ )
198
246
 
199
247
  # Analyze dependencies
200
248
  dependency_analysis = _analyze_cookbook_dependencies_detailed(cookbook_path_obj)
@@ -231,7 +279,9 @@ def analyze_cookbook_dependencies(
231
279
  {_analyze_dependency_migration_impact(dependency_analysis)}
232
280
  """
233
281
  except Exception as e:
234
- return f"Error analyzing cookbook dependencies: {e}"
282
+ return format_error_with_context(
283
+ e, "analyzing cookbook dependencies", cookbook_path
284
+ )
235
285
 
236
286
 
237
287
  def generate_migration_report(
@@ -300,7 +350,7 @@ def generate_migration_report(
300
350
  {report["appendices"]}
301
351
  """
302
352
  except Exception as e:
303
- return f"Error generating migration report: {e}"
353
+ return format_error_with_context(e, "generating migration report")
304
354
 
305
355
 
306
356
  def validate_conversion(
@@ -347,31 +397,188 @@ def validate_conversion(
347
397
  return _format_validation_results_text(conversion_type, results, summary)
348
398
 
349
399
  except Exception as e:
350
- return f"Error during validation: {e}"
400
+ return format_error_with_context(
401
+ e, f"validating Ansible {conversion_type} conversion"
402
+ )
351
403
 
352
404
 
353
405
  # Private helper functions for assessment
354
406
 
355
407
 
356
- def _assess_single_cookbook(cookbook_path) -> dict:
357
- """Assess complexity of a single cookbook."""
358
- cookbook = _normalize_path(cookbook_path)
359
- assessment: dict[str, Any] = {
360
- "cookbook_name": cookbook.name,
361
- "cookbook_path": str(cookbook),
362
- "metrics": {},
408
+ def _validate_assessment_inputs(
409
+ cookbook_paths: str, migration_scope: str, target_platform: str
410
+ ) -> str | None:
411
+ """
412
+ Validate inputs for migration assessment.
413
+
414
+ Args:
415
+ cookbook_paths: Paths to cookbooks
416
+ migration_scope: Scope of migration
417
+ target_platform: Target platform
418
+
419
+ Returns:
420
+ Error message if validation fails, None otherwise
421
+
422
+ """
423
+ if not cookbook_paths or not cookbook_paths.strip():
424
+ return (
425
+ "Error: Cookbook paths cannot be empty\n\n"
426
+ "Suggestion: Provide comma-separated paths to Chef cookbooks"
427
+ )
428
+
429
+ valid_scopes = ["full", "recipes_only", "infrastructure_only"]
430
+ if migration_scope not in valid_scopes:
431
+ return (
432
+ f"Error: Invalid migration scope '{migration_scope}'\n\n"
433
+ f"Suggestion: Use one of {', '.join(valid_scopes)}"
434
+ )
435
+
436
+ valid_platforms = ["ansible_awx", "ansible_core", "ansible_tower"]
437
+ if target_platform not in valid_platforms:
438
+ return (
439
+ f"Error: Invalid target platform '{target_platform}'\n\n"
440
+ f"Suggestion: Use one of {', '.join(valid_platforms)}"
441
+ )
442
+
443
+ return None
444
+
445
+
446
+ def _parse_cookbook_paths(cookbook_paths: str) -> list[Any]:
447
+ """
448
+ Parse and validate cookbook paths.
449
+
450
+ Args:
451
+ cookbook_paths: Comma-separated paths to cookbooks
452
+
453
+ Returns:
454
+ List of valid Path objects (may be empty)
455
+
456
+ """
457
+ paths = [_normalize_path(path.strip()) for path in cookbook_paths.split(",")]
458
+ valid_paths = [p for p in paths if p.exists()]
459
+ return valid_paths
460
+
461
+
462
+ def _analyze_cookbook_metrics(
463
+ valid_paths: list[Any],
464
+ ) -> tuple[list[Any], dict[str, int]]:
465
+ """
466
+ Analyze metrics for all cookbooks.
467
+
468
+ Args:
469
+ valid_paths: List of valid cookbook paths
470
+
471
+ Returns:
472
+ Tuple of (cookbook_assessments, overall_metrics)
473
+
474
+ """
475
+ cookbook_assessments = []
476
+ overall_metrics = {
477
+ "total_cookbooks": 0,
478
+ "total_recipes": 0,
479
+ "total_resources": 0,
363
480
  "complexity_score": 0,
364
481
  "estimated_effort_days": 0,
365
- "challenges": [],
366
- "migration_priority": "medium",
367
- "dependencies": [],
368
482
  }
369
483
 
370
- # Count recipes and resources
371
- recipes_dir = _safe_join(cookbook, "recipes")
484
+ for cookbook_path in valid_paths:
485
+ # deepcode ignore PT: path normalized via _normalize_path
486
+ assessment = _assess_single_cookbook(cookbook_path)
487
+ cookbook_assessments.append(assessment)
488
+
489
+ # Aggregate metrics
490
+ overall_metrics["total_cookbooks"] += 1
491
+ overall_metrics["total_recipes"] += assessment["metrics"]["recipe_count"]
492
+ overall_metrics["total_resources"] += assessment["metrics"]["resource_count"]
493
+ overall_metrics["complexity_score"] += assessment["complexity_score"]
494
+ overall_metrics["estimated_effort_days"] += assessment["estimated_effort_days"]
495
+
496
+ # Calculate averages
497
+ if cookbook_assessments:
498
+ overall_metrics["avg_complexity"] = int(
499
+ overall_metrics["complexity_score"] / len(cookbook_assessments)
500
+ )
501
+
502
+ return cookbook_assessments, overall_metrics
503
+
504
+
505
+ def _format_assessment_report(
506
+ migration_scope: str,
507
+ target_platform: str,
508
+ overall_metrics: dict[str, int],
509
+ cookbook_assessments: list[Any],
510
+ recommendations: str,
511
+ roadmap: str,
512
+ ) -> str:
513
+ """
514
+ Format the final assessment report.
515
+
516
+ Args:
517
+ migration_scope: Scope of migration
518
+ target_platform: Target platform
519
+ overall_metrics: Overall metrics dictionary
520
+ cookbook_assessments: List of cookbook assessments
521
+ recommendations: Migration recommendations
522
+ roadmap: Migration roadmap
523
+
524
+ Returns:
525
+ Formatted report string
526
+
527
+ """
528
+ return f"""# Chef to Ansible Migration Assessment
529
+ # Scope: {migration_scope}
530
+ # Target Platform: {target_platform}
531
+
532
+ ## Overall Migration Metrics:
533
+ {_format_overall_metrics(overall_metrics)}
534
+
535
+ ## Cookbook Assessments:
536
+ {_format_cookbook_assessments(cookbook_assessments)}
537
+
538
+ ## Migration Complexity Analysis:
539
+ {_format_complexity_analysis(cookbook_assessments)}
540
+
541
+ ## Migration Recommendations:
542
+ {recommendations}
543
+
544
+ ## Migration Roadmap:
545
+ {roadmap}
546
+
547
+ ## Risk Assessment:
548
+ {_assess_migration_risks(cookbook_assessments, target_platform)}
549
+
550
+ ## Resource Requirements:
551
+ {_estimate_resource_requirements(overall_metrics, target_platform)}
552
+ """
553
+
554
+
555
+ def _count_cookbook_artifacts(cookbook_path) -> dict[str, int]:
556
+ """Count basic cookbook artifacts (recipes, templates, files)."""
557
+ recipes_dir = _safe_join(cookbook_path, "recipes")
372
558
  recipe_count = len(list(recipes_dir.glob("*.rb"))) if recipes_dir.exists() else 0
373
559
 
374
- # Analyze recipe complexity
560
+ templates_count = (
561
+ len(list(_safe_join(cookbook_path, "templates").glob("*")))
562
+ if _safe_join(cookbook_path, "templates").exists()
563
+ else 0
564
+ )
565
+
566
+ files_count = (
567
+ len(list(_safe_join(cookbook_path, "files").glob("*")))
568
+ if _safe_join(cookbook_path, "files").exists()
569
+ else 0
570
+ )
571
+
572
+ return {
573
+ "recipe_count": recipe_count,
574
+ "templates": templates_count,
575
+ "files": files_count,
576
+ }
577
+
578
+
579
+ def _analyze_recipe_complexity(cookbook_path) -> dict[str, int]:
580
+ """Analyze recipe files for resource counts, Ruby blocks, and custom resources."""
581
+ recipes_dir = _safe_join(cookbook_path, "recipes")
375
582
  resource_count = 0
376
583
  custom_resources = 0
377
584
  ruby_blocks = 0
@@ -381,7 +588,6 @@ def _assess_single_cookbook(cookbook_path) -> dict:
381
588
  with recipe_file.open("r", encoding="utf-8", errors="ignore") as f:
382
589
  content = f.read()
383
590
  # Count Chef resources
384
-
385
591
  resources = len(
386
592
  re.findall(r'\w{1,100}\s+[\'"]([^\'"]{0,200})[\'"]\s+do', content)
387
593
  )
@@ -395,57 +601,85 @@ def _assess_single_cookbook(cookbook_path) -> dict:
395
601
  )
396
602
  resource_count += resources
397
603
 
398
- assessment["metrics"] = {
399
- "recipe_count": recipe_count,
604
+ return {
400
605
  "resource_count": resource_count,
401
606
  "custom_resources": custom_resources,
402
607
  "ruby_blocks": ruby_blocks,
403
- "templates": len(list(_safe_join(cookbook, "templates").glob("*")))
404
- if _safe_join(cookbook, "templates").exists()
405
- else 0,
406
- "files": len(list(_safe_join(cookbook, "files").glob("*")))
407
- if _safe_join(cookbook, "files").exists()
408
- else 0,
409
608
  }
410
609
 
411
- # Calculate complexity score (0-100)
610
+
611
+ def _calculate_complexity_score(metrics: dict[str, int]) -> int:
612
+ """Calculate complexity score (0-100) based on metrics."""
613
+ recipe_count = metrics["recipe_count"]
614
+ resource_count = metrics["resource_count"]
615
+
412
616
  complexity_factors = {
413
617
  "recipe_count": min(recipe_count * 2, 20),
414
618
  "resource_density": min(resource_count / max(recipe_count, 1) * 5, 25),
415
- "custom_resources": custom_resources * 10,
416
- "ruby_blocks": ruby_blocks * 5,
417
- "templates": min(assessment["metrics"]["templates"] * 2, 15),
418
- "files": min(assessment["metrics"]["files"] * 1, 10),
619
+ "custom_resources": metrics["custom_resources"] * 10,
620
+ "ruby_blocks": metrics["ruby_blocks"] * 5,
621
+ "templates": min(metrics["templates"] * 2, 15),
622
+ "files": min(metrics["files"] * 1, 10),
419
623
  }
420
624
 
421
- assessment["complexity_score"] = sum(complexity_factors.values())
625
+ return int(sum(complexity_factors.values()))
422
626
 
423
- # Estimate effort (person-days)
424
- base_effort = recipe_count * 0.5 # 0.5 days per recipe
425
- complexity_multiplier = 1 + (assessment["complexity_score"] / 100)
426
- assessment["estimated_effort_days"] = round(base_effort * complexity_multiplier, 1)
427
627
 
428
- # Identify challenges
429
- if custom_resources > 0:
430
- assessment["challenges"].append(
431
- f"{custom_resources} custom resources requiring manual conversion"
432
- )
433
- if ruby_blocks > 5:
434
- assessment["challenges"].append(
435
- f"{ruby_blocks} Ruby blocks needing shell script conversion"
628
+ def _identify_migration_challenges(
629
+ metrics: dict[str, int], complexity_score: int
630
+ ) -> list[str]:
631
+ """Identify migration challenges based on metrics."""
632
+ challenges = []
633
+
634
+ if metrics["custom_resources"] > 0:
635
+ challenges.append(
636
+ f"{metrics['custom_resources']} custom resources requiring manual conversion"
436
637
  )
437
- if assessment["complexity_score"] > 70:
438
- assessment["challenges"].append(
439
- "High complexity cookbook requiring expert review"
638
+ if metrics["ruby_blocks"] > 5:
639
+ challenges.append(
640
+ f"{metrics['ruby_blocks']} Ruby blocks needing shell script conversion"
440
641
  )
642
+ if complexity_score > 70:
643
+ challenges.append("High complexity cookbook requiring expert review")
644
+
645
+ return challenges
441
646
 
442
- # Set migration priority
443
- if assessment["complexity_score"] < 30:
444
- assessment["migration_priority"] = "low"
445
- elif assessment["complexity_score"] > 70:
446
- assessment["migration_priority"] = "high"
447
647
 
448
- return assessment
648
+ def _determine_migration_priority(complexity_score: int) -> str:
649
+ """Determine migration priority based on complexity score."""
650
+ if complexity_score < 30:
651
+ return "low"
652
+ elif complexity_score > 70:
653
+ return "high"
654
+ return "medium"
655
+
656
+
657
+ def _assess_single_cookbook(cookbook_path) -> dict:
658
+ """Assess complexity of a single cookbook."""
659
+ cookbook = _normalize_path(cookbook_path)
660
+
661
+ # Collect metrics
662
+ artifact_counts = _count_cookbook_artifacts(cookbook)
663
+ recipe_complexity = _analyze_recipe_complexity(cookbook)
664
+ metrics = {**artifact_counts, **recipe_complexity}
665
+
666
+ # Calculate complexity and effort
667
+ complexity_score = _calculate_complexity_score(metrics)
668
+ base_effort = metrics["recipe_count"] * 0.5 # 0.5 days per recipe
669
+ complexity_multiplier = 1 + (complexity_score / 100)
670
+ estimated_effort = round(base_effort * complexity_multiplier, 1)
671
+
672
+ # Build assessment
673
+ return {
674
+ "cookbook_name": cookbook.name,
675
+ "cookbook_path": str(cookbook),
676
+ "metrics": metrics,
677
+ "complexity_score": complexity_score,
678
+ "estimated_effort_days": estimated_effort,
679
+ "challenges": _identify_migration_challenges(metrics, complexity_score),
680
+ "migration_priority": _determine_migration_priority(complexity_score),
681
+ "dependencies": [],
682
+ }
449
683
 
450
684
 
451
685
  def _format_overall_metrics(metrics: dict) -> str:
@@ -625,17 +859,20 @@ def _create_migration_roadmap(assessments: list) -> str:
625
859
  return "\n".join(roadmap_formatted)
626
860
 
627
861
 
628
- def _assess_migration_risks(assessments: list, target_platform: str) -> str:
629
- """Assess migration risks."""
862
+ def _assess_technical_complexity_risks(assessments: list) -> list[str]:
863
+ """Assess risks related to technical complexity."""
630
864
  risks = []
631
-
632
- # Technical risks
633
865
  high_complexity_count = len([a for a in assessments if a["complexity_score"] > 70])
634
866
  if high_complexity_count > 0:
635
867
  risks.append(
636
868
  f"🔴 HIGH: {high_complexity_count} high-complexity cookbooks may cause delays"
637
869
  )
870
+ return risks
871
+
638
872
 
873
+ def _assess_custom_resource_risks(assessments: list) -> list[str]:
874
+ """Assess risks related to custom resources and Ruby blocks."""
875
+ risks = []
639
876
  custom_resource_count = sum(a["metrics"]["custom_resources"] for a in assessments)
640
877
  if custom_resource_count > 0:
641
878
  risks.append(
@@ -648,14 +885,33 @@ def _assess_migration_risks(assessments: list, target_platform: str) -> str:
648
885
  f"🟡 MEDIUM: {ruby_block_count} Ruby blocks require shell script conversion"
649
886
  )
650
887
 
651
- # Timeline risks
888
+ return risks
889
+
890
+
891
+ def _assess_timeline_risks(assessments: list) -> list[str]:
892
+ """Assess risks related to migration timeline and scope."""
893
+ risks = []
652
894
  total_effort = sum(a["estimated_effort_days"] for a in assessments)
653
895
  if total_effort > 50:
654
896
  risks.append("🟡 MEDIUM: Large migration scope may impact timeline")
897
+ return risks
655
898
 
656
- # Platform risks
899
+
900
+ def _assess_platform_risks(target_platform: str) -> list[str]:
901
+ """Assess risks related to target platform."""
657
902
  if target_platform == "ansible_awx":
658
- risks.append("🟢 LOW: AWX integration well-supported with existing tools")
903
+ return ["🟢 LOW: AWX integration well-supported with existing tools"]
904
+ return []
905
+
906
+
907
+ def _assess_migration_risks(assessments: list, target_platform: str) -> str:
908
+ """Assess migration risks."""
909
+ risks = []
910
+
911
+ risks.extend(_assess_technical_complexity_risks(assessments))
912
+ risks.extend(_assess_custom_resource_risks(assessments))
913
+ risks.extend(_assess_timeline_risks(assessments))
914
+ risks.extend(_assess_platform_risks(target_platform))
659
915
 
660
916
  if not risks:
661
917
  risks.append("🟢 LOW: No significant migration risks identified")
@@ -1146,6 +1402,46 @@ def _generate_migration_timeline(strategy: str, timeline_weeks: int) -> str:
1146
1402
  return "\n".join([f"• {milestone}" for milestone in milestones])
1147
1403
 
1148
1404
 
1405
+ def _build_validation_header(
1406
+ conversion_type: str, summary: dict[str, int]
1407
+ ) -> list[str]:
1408
+ """Build the header section of validation results."""
1409
+ return [
1410
+ f"# Validation Results for {conversion_type} Conversion",
1411
+ "",
1412
+ "## Summary",
1413
+ f"• Errors: {summary['errors']}",
1414
+ f"• Warnings: {summary['warnings']}",
1415
+ f"• Info: {summary['info']}",
1416
+ "",
1417
+ ]
1418
+
1419
+
1420
+ def _group_results_by_level(
1421
+ results: list[ValidationResult],
1422
+ ) -> tuple[list[ValidationResult], list[ValidationResult], list[ValidationResult]]:
1423
+ """Group validation results by severity level."""
1424
+ errors = [r for r in results if r.level == ValidationLevel.ERROR]
1425
+ warnings = [r for r in results if r.level == ValidationLevel.WARNING]
1426
+ infos = [r for r in results if r.level == ValidationLevel.INFO]
1427
+ return errors, warnings, infos
1428
+
1429
+
1430
+ def _format_result_section(
1431
+ title: str, icon: str, results: list[ValidationResult]
1432
+ ) -> list[str]:
1433
+ """Format a single validation results section."""
1434
+ if not results:
1435
+ return []
1436
+
1437
+ lines = [f"## {icon} {title}", ""]
1438
+ for result in results:
1439
+ lines.append(str(result))
1440
+ lines.append("")
1441
+
1442
+ return lines
1443
+
1444
+
1149
1445
  def _format_validation_results_text(
1150
1446
  conversion_type: str, results: list[ValidationResult], summary: dict[str, int]
1151
1447
  ) -> str:
@@ -1166,41 +1462,13 @@ def _format_validation_results_text(
1166
1462
 
1167
1463
  ✅ All validation checks passed! No issues found.
1168
1464
  """
1169
- output_lines = [
1170
- f"# Validation Results for {conversion_type} Conversion",
1171
- "",
1172
- "## Summary",
1173
- f"• Errors: {summary['errors']}",
1174
- f"• Warnings: {summary['warnings']}",
1175
- f"• Info: {summary['info']}",
1176
- "",
1177
- ]
1178
1465
 
1179
- # Group results by level
1180
- errors = [r for r in results if r.level == ValidationLevel.ERROR]
1181
- warnings = [r for r in results if r.level == ValidationLevel.WARNING]
1182
- infos = [r for r in results if r.level == ValidationLevel.INFO]
1466
+ output_lines = _build_validation_header(conversion_type, summary)
1467
+ errors, warnings, infos = _group_results_by_level(results)
1183
1468
 
1184
- if errors:
1185
- output_lines.append("## Errors")
1186
- output_lines.append("")
1187
- for result in errors:
1188
- output_lines.append(str(result))
1189
- output_lines.append("")
1190
-
1191
- if warnings:
1192
- output_lines.append("## ⚠️ Warnings")
1193
- output_lines.append("")
1194
- for result in warnings:
1195
- output_lines.append(str(result))
1196
- output_lines.append("")
1197
-
1198
- if infos:
1199
- output_lines.append("## ℹ️ Information")
1200
- output_lines.append("")
1201
- for result in infos:
1202
- output_lines.append(str(result))
1203
- output_lines.append("")
1469
+ output_lines.extend(_format_result_section("❌ Errors", "", errors))
1470
+ output_lines.extend(_format_result_section("⚠️ Warnings", "", warnings))
1471
+ output_lines.extend(_format_result_section("ℹ️ Information", "", infos))
1204
1472
 
1205
1473
  return "\n".join(output_lines)
1206
1474