TradingAgents/.claude/lib/migration_planner.py

584 lines
19 KiB
Python

"""Migration planning for brownfield retrofit.
This module generates step-by-step migration plans to align brownfield projects
with autonomous-dev standards. It analyzes alignment gaps, estimates effort,
detects dependencies, and optimizes execution order.
Classes:
EffortSize: Effort size categories (XS/S/M/L/XL)
ImpactLevel: Impact level categories (LOW/MEDIUM/HIGH)
MigrationStep: Represents a single migration step
MigrationPlan: Complete migration plan with steps and estimates
MigrationPlanner: Main planning coordinator
Security:
- CWE-22: Path validation via security_utils
- CWE-117: Audit logging with sanitization
- CWE-20: Input validation for all user inputs
Related:
- GitHub Issue #59: Brownfield retrofit command implementation
Relevant Skills:
- project-alignment-validation: Gap assessment methodology, prioritization patterns
- error-handling-patterns: Exception hierarchy and error handling best practices
- library-design-patterns: Standardized design patterns
- state-management-patterns: Standardized design patterns
"""
import json
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from typing import Dict, List
from .security_utils import audit_log, validate_path
from .alignment_assessor import AlignmentGap, AssessmentResult, Severity
class EffortSize(Enum):
"""Effort size categories."""
XS = "XS" # 1 hour
S = "S" # 2 hours
M = "M" # 4 hours
L = "L" # 8 hours
XL = "XL" # 16 hours
class ImpactLevel(Enum):
"""Impact level categories."""
LOW = "LOW"
MEDIUM = "MEDIUM"
HIGH = "HIGH"
@dataclass
class MigrationStep:
"""Represents a single migration step.
Attributes:
step_id: Unique step identifier (e.g., "STEP-001")
title: Human-readable step title
description: Detailed step description
tasks: List of specific tasks to complete
effort_size: T-shirt size estimate
effort_hours: Estimated hours (derived from effort_size)
impact_level: Impact on project (LOW/MEDIUM/HIGH)
dependencies: List of step_ids that must complete first
verification_criteria: List of criteria to verify completion
"""
step_id: str
title: str
description: str
tasks: List[str]
effort_size: EffortSize
effort_hours: float
impact_level: ImpactLevel
dependencies: List[str] = field(default_factory=list)
verification_criteria: List[str] = field(default_factory=list)
def to_dict(self) -> dict:
"""Convert to dictionary representation.
Returns:
Dictionary with all step data
"""
return {
"step_id": self.step_id,
"title": self.title,
"description": self.description,
"tasks": self.tasks,
"effort_size": self.effort_size.value,
"effort_hours": self.effort_hours,
"impact_level": self.impact_level.value,
"dependencies": self.dependencies,
"verification_criteria": self.verification_criteria
}
@dataclass
class MigrationPlan:
"""Complete migration plan with steps and estimates.
Attributes:
steps: List of migration steps in execution order
total_effort_hours: Total estimated effort
critical_path_hours: Critical path duration (accounting for parallelism)
"""
steps: List[MigrationStep] = field(default_factory=list)
total_effort_hours: float = 0.0
critical_path_hours: float = 0.0
def to_dict(self) -> dict:
"""Convert to dictionary representation.
Returns:
Dictionary with all plan data
"""
return {
"steps": [step.to_dict() for step in self.steps],
"total_effort_hours": self.total_effort_hours,
"critical_path_hours": self.critical_path_hours,
"step_count": len(self.steps)
}
def to_json(self, indent: int = 2) -> str:
"""Convert to JSON string.
Args:
indent: JSON indentation level
Returns:
JSON string representation
"""
return json.dumps(self.to_dict(), indent=indent)
def to_markdown(self) -> str:
"""Convert to markdown format.
Returns:
Markdown-formatted migration plan
"""
lines = [
"# Migration Plan\n",
f"**Total Steps**: {len(self.steps)}",
f"**Total Effort**: {self.total_effort_hours:.1f} hours",
f"**Critical Path**: {self.critical_path_hours:.1f} hours\n",
"---\n"
]
for i, step in enumerate(self.steps, 1):
lines.append(f"## {i}. {step.title}\n")
lines.append(f"**ID**: {step.step_id}")
lines.append(f"**Effort**: {step.effort_size.value} ({step.effort_hours:.1f}h)")
lines.append(f"**Impact**: {step.impact_level.value}\n")
lines.append(f"**Description**: {step.description}\n")
if step.dependencies:
lines.append("**Dependencies**:")
for dep in step.dependencies:
lines.append(f"- {dep}")
lines.append("")
lines.append("**Tasks**:")
for task in step.tasks:
lines.append(f"- {task}")
lines.append("")
if step.verification_criteria:
lines.append("**Verification**:")
for criterion in step.verification_criteria:
lines.append(f"- {criterion}")
lines.append("")
lines.append("---\n")
return "\n".join(lines)
class MigrationPlanner:
"""Main migration planning coordinator.
Analyzes alignment assessment results and generates optimized migration
plans with effort estimates, dependency tracking, and execution ordering.
"""
# Effort size to hours mapping
EFFORT_HOURS = {
EffortSize.XS: 1.0,
EffortSize.S: 2.0,
EffortSize.M: 4.0,
EffortSize.L: 8.0,
EffortSize.XL: 16.0
}
def __init__(self, project_root: Path):
"""Initialize migration planner.
Args:
project_root: Path to project root directory
Raises:
ValueError: If project_root invalid
"""
# Security: Validate project root path (CWE-22)
validated_root = validate_path(
project_root,
"project_root",
allow_missing=False,
)
self.project_root = Path(validated_root)
# Audit log initialization
audit_log(
"migration_planner_init",
project_root=str(self.project_root),
success=True
)
def plan(self, assessment: AssessmentResult) -> MigrationPlan:
"""Generate complete migration plan.
Args:
assessment: Alignment assessment results
Returns:
Migration plan with optimized execution order
Raises:
ValueError: If assessment invalid
"""
if not assessment:
raise ValueError("Assessment result required")
audit_log(
"migration_planning_start",
project_root=str(self.project_root),
gap_count=len(assessment.priority_list)
)
try:
# Generate migration steps from prioritized gaps
steps = self.generate_migration_steps(assessment.priority_list)
# Detect dependencies between steps
dependencies = self.detect_dependencies(steps)
for step in steps:
if step.step_id in dependencies:
step.dependencies = dependencies[step.step_id]
# Optimize execution order
optimized_steps = self.optimize_execution_order(steps)
# Calculate totals
total_effort = sum(step.effort_hours for step in optimized_steps)
critical_path = self._calculate_critical_path(optimized_steps)
plan = MigrationPlan(
steps=optimized_steps,
total_effort_hours=total_effort,
critical_path_hours=critical_path
)
audit_log(
"migration_planning_complete",
project_root=str(self.project_root),
step_count=len(optimized_steps),
total_effort_hours=total_effort,
critical_path_hours=critical_path,
success=True
)
return plan
except Exception as e:
audit_log(
"migration_planning_failed",
project_root=str(self.project_root),
error=str(e),
success=False
)
raise
def generate_migration_steps(self, gaps: List[AlignmentGap]) -> List[MigrationStep]:
"""Generate migration steps from alignment gaps.
Args:
gaps: List of prioritized alignment gaps
Returns:
List of migration steps
"""
steps = []
for i, gap in enumerate(gaps, 1):
step_id = f"STEP-{i:03d}"
# Estimate effort size
effort_size = self.estimate_effort(gap)
effort_hours = self.EFFORT_HOURS[effort_size]
# Determine impact level
impact_level = self._map_severity_to_impact(gap.severity)
# Generate verification criteria
verification_criteria = self._generate_verification_criteria(gap)
step = MigrationStep(
step_id=step_id,
title=gap.description,
description=f"**Current**: {gap.current_state}\n**Target**: {gap.desired_state}",
tasks=gap.fix_steps,
effort_size=effort_size,
effort_hours=effort_hours,
impact_level=impact_level,
verification_criteria=verification_criteria
)
steps.append(step)
return steps
def estimate_effort(self, gap: AlignmentGap) -> EffortSize:
"""Estimate effort size for a gap.
Args:
gap: Alignment gap
Returns:
Effort size category
"""
hours = gap.effort_hours
if hours <= 1.5:
return EffortSize.XS
elif hours <= 3.0:
return EffortSize.S
elif hours <= 6.0:
return EffortSize.M
elif hours <= 12.0:
return EffortSize.L
else:
return EffortSize.XL
def analyze_impact(self, step: MigrationStep) -> str:
"""Analyze impact of a migration step.
Args:
step: Migration step
Returns:
Impact analysis description
"""
impact_descriptions = {
ImpactLevel.LOW: "Minimal impact - localized changes, low risk",
ImpactLevel.MEDIUM: "Moderate impact - affects multiple areas, moderate risk",
ImpactLevel.HIGH: "High impact - fundamental changes, high risk"
}
return impact_descriptions[step.impact_level]
def detect_dependencies(self, steps: List[MigrationStep]) -> Dict[str, List[str]]:
"""Detect dependencies between migration steps.
Args:
steps: List of migration steps
Returns:
Dict mapping step_id to list of dependency step_ids
"""
dependencies = {}
# Build category index
category_steps = {}
for step in steps:
# Extract category from description (simplified heuristic)
category = self._extract_category(step)
if category not in category_steps:
category_steps[category] = []
category_steps[category].append(step.step_id)
# Define dependency rules
dependency_rules = {
"documentation": [], # No dependencies
"file-organization": [], # No dependencies
"testing": ["file-organization"], # Tests depend on organization
"automation": ["testing"], # CI/CD depends on tests
"twelve-factor": ["file-organization", "documentation"] # Cleanup depends on basics
}
# Apply rules
for step in steps:
category = self._extract_category(step)
step_deps = []
if category in dependency_rules:
for dep_category in dependency_rules[category]:
if dep_category in category_steps:
# Depend on all steps in that category
for dep_step_id in category_steps[dep_category]:
if dep_step_id != step.step_id:
step_deps.append(dep_step_id)
if step_deps:
dependencies[step.step_id] = step_deps
return dependencies
def optimize_execution_order(self, steps: List[MigrationStep]) -> List[MigrationStep]:
"""Optimize execution order using topological sort.
Args:
steps: List of migration steps
Returns:
Steps sorted by optimal execution order
"""
# Build adjacency list
graph = {step.step_id: step.dependencies for step in steps}
step_map = {step.step_id: step for step in steps}
# Topological sort (Kahn's algorithm)
in_degree = {step_id: 0 for step_id in graph}
for step_id, deps in graph.items():
for dep in deps:
if dep in in_degree:
in_degree[step_id] += 1
# Queue of steps with no dependencies
queue = [step_id for step_id, degree in in_degree.items() if degree == 0]
sorted_order = []
while queue:
# Sort queue by impact/effort for optimal ordering
queue.sort(key=lambda sid: (
-self._priority_score(step_map[sid]), # Higher priority first
step_map[sid].effort_hours # Lower effort first (tie-breaker)
))
current = queue.pop(0)
sorted_order.append(current)
# Update in-degrees
for step_id, deps in graph.items():
if current in deps:
in_degree[step_id] -= 1
if in_degree[step_id] == 0:
queue.append(step_id)
# Return steps in sorted order
return [step_map[step_id] for step_id in sorted_order]
# Private helper methods
def _map_severity_to_impact(self, severity: Severity) -> ImpactLevel:
"""Map gap severity to impact level.
Args:
severity: Gap severity
Returns:
Impact level
"""
mapping = {
Severity.CRITICAL: ImpactLevel.HIGH,
Severity.HIGH: ImpactLevel.HIGH,
Severity.MEDIUM: ImpactLevel.MEDIUM,
Severity.LOW: ImpactLevel.LOW
}
return mapping[severity]
def _generate_verification_criteria(self, gap: AlignmentGap) -> List[str]:
"""Generate verification criteria for a gap.
Args:
gap: Alignment gap
Returns:
List of verification criteria
"""
criteria = []
if gap.category == "documentation":
criteria.append("PROJECT.md exists in .claude/ directory")
criteria.append("All required sections present (GOALS, SCOPE, CONSTRAINTS)")
criteria.append("Content matches project reality")
elif gap.category == "file-organization":
criteria.append("Files organized in standard directories")
criteria.append("No source files in project root")
criteria.append("Import paths updated and working")
elif gap.category == "testing":
criteria.append("Test framework installed and configured")
criteria.append("Tests pass with pytest")
criteria.append("Coverage > 80%")
elif gap.category == "automation":
criteria.append("CI/CD configuration exists")
criteria.append("Automated tests run on commit")
criteria.append("Status checks passing")
else:
criteria.append(f"Verify: {gap.desired_state}")
criteria.append("Manual testing confirms functionality")
return criteria
def _extract_category(self, step: MigrationStep) -> str:
"""Extract category from step (heuristic).
Args:
step: Migration step
Returns:
Category name
"""
title_lower = step.title.lower()
if "project.md" in title_lower or "documentation" in title_lower:
return "documentation"
elif "file" in title_lower or "organization" in title_lower or "directory" in title_lower:
return "file-organization"
elif "test" in title_lower or "coverage" in title_lower:
return "testing"
elif "ci" in title_lower or "automation" in title_lower:
return "automation"
elif "12-factor" in title_lower or "twelve-factor" in title_lower:
return "twelve-factor"
else:
return "other"
def _priority_score(self, step: MigrationStep) -> float:
"""Calculate priority score for a step.
Args:
step: Migration step
Returns:
Priority score (higher = more important)
"""
impact_score = {
ImpactLevel.HIGH: 100,
ImpactLevel.MEDIUM: 50,
ImpactLevel.LOW: 25
}
# Impact/effort ratio
effort = max(step.effort_hours, 0.1)
return impact_score[step.impact_level] / effort
def _calculate_critical_path(self, steps: List[MigrationStep]) -> float:
"""Calculate critical path duration.
Uses dynamic programming to find longest path through dependency graph.
Args:
steps: List of migration steps in execution order
Returns:
Critical path duration in hours
"""
# Build step map
step_map = {step.step_id: step for step in steps}
# Calculate longest path to each step
longest_path = {}
for step in steps:
if not step.dependencies:
# No dependencies - duration is just this step
longest_path[step.step_id] = step.effort_hours
else:
# Duration is max of dependencies + this step
max_dep_path = max(
longest_path.get(dep, 0)
for dep in step.dependencies
if dep in longest_path
)
longest_path[step.step_id] = max_dep_path + step.effort_hours
# Critical path is maximum of all paths
return max(longest_path.values()) if longest_path else 0.0