857 lines
34 KiB
Python
857 lines
34 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Tech Debt Prioritizer
|
|
|
|
Takes a debt inventory (from scanner or manual JSON) and calculates interest rate,
|
|
effort estimates, and produces a prioritized backlog with recommended sprint allocation.
|
|
Uses cost-of-delay vs effort scoring and various prioritization frameworks.
|
|
|
|
Usage:
|
|
python debt_prioritizer.py debt_inventory.json
|
|
python debt_prioritizer.py debt_inventory.json --output prioritized_backlog.json
|
|
python debt_prioritizer.py debt_inventory.json --team-size 6 --sprint-capacity 80
|
|
python debt_prioritizer.py debt_inventory.json --framework wsjf --output results.json
|
|
"""
|
|
|
|
import json
|
|
import argparse
|
|
import sys
|
|
import math
|
|
from collections import defaultdict, Counter
|
|
from datetime import datetime, timedelta
|
|
from typing import Dict, List, Any, Optional, Tuple
|
|
from dataclasses import dataclass, asdict
|
|
|
|
|
|
@dataclass
|
|
class EffortEstimate:
|
|
"""Represents effort estimation for a debt item."""
|
|
size_points: int
|
|
hours_estimate: float
|
|
risk_factor: float # 1.0 = low risk, 1.5 = medium, 2.0+ = high
|
|
skill_level_required: str # junior, mid, senior, expert
|
|
confidence: float # 0.0-1.0
|
|
|
|
|
|
@dataclass
|
|
class BusinessImpact:
|
|
"""Represents business impact assessment for a debt item."""
|
|
customer_impact: int # 1-10 scale
|
|
revenue_impact: int # 1-10 scale
|
|
team_velocity_impact: int # 1-10 scale
|
|
quality_impact: int # 1-10 scale
|
|
security_impact: int # 1-10 scale
|
|
|
|
|
|
@dataclass
|
|
class InterestRate:
|
|
"""Represents the interest rate calculation for technical debt."""
|
|
daily_cost: float # cost per day if left unfixed
|
|
frequency_multiplier: float # how often this code is touched
|
|
team_impact_multiplier: float # how many developers affected
|
|
compound_rate: float # how quickly this debt makes other debt worse
|
|
|
|
|
|
class DebtPrioritizer:
|
|
"""Main class for prioritizing technical debt items."""
|
|
|
|
def __init__(self, team_size: int = 5, sprint_capacity_hours: int = 80):
|
|
self.team_size = team_size
|
|
self.sprint_capacity_hours = sprint_capacity_hours
|
|
self.debt_items = []
|
|
self.prioritized_items = []
|
|
|
|
# Prioritization framework weights
|
|
self.framework_weights = {
|
|
"cost_of_delay": {
|
|
"business_value": 0.3,
|
|
"urgency": 0.3,
|
|
"risk_reduction": 0.2,
|
|
"team_productivity": 0.2
|
|
},
|
|
"wsjf": {
|
|
"business_value": 0.25,
|
|
"time_criticality": 0.25,
|
|
"risk_reduction": 0.25,
|
|
"effort": 0.25
|
|
},
|
|
"rice": {
|
|
"reach": 0.25,
|
|
"impact": 0.25,
|
|
"confidence": 0.25,
|
|
"effort": 0.25
|
|
}
|
|
}
|
|
|
|
def load_debt_inventory(self, file_path: str) -> bool:
|
|
"""Load debt inventory from JSON file."""
|
|
try:
|
|
with open(file_path, 'r', encoding='utf-8') as f:
|
|
data = json.load(f)
|
|
|
|
# Handle different input formats
|
|
if isinstance(data, dict) and 'debt_items' in data:
|
|
self.debt_items = data['debt_items']
|
|
elif isinstance(data, list):
|
|
self.debt_items = data
|
|
else:
|
|
raise ValueError("Invalid debt inventory format")
|
|
|
|
print(f"Loaded {len(self.debt_items)} debt items from {file_path}")
|
|
return True
|
|
|
|
except Exception as e:
|
|
print(f"Error loading debt inventory: {e}")
|
|
return False
|
|
|
|
def analyze_and_prioritize(self, framework: str = "cost_of_delay") -> Dict[str, Any]:
|
|
"""
|
|
Analyze debt items and create prioritized backlog.
|
|
|
|
Args:
|
|
framework: Prioritization framework to use
|
|
|
|
Returns:
|
|
Dictionary containing prioritized backlog and analysis
|
|
"""
|
|
print(f"Analyzing {len(self.debt_items)} debt items...")
|
|
print(f"Using {framework} prioritization framework")
|
|
print("=" * 50)
|
|
|
|
# Step 1: Enrich debt items with estimates
|
|
enriched_items = []
|
|
for item in self.debt_items:
|
|
enriched_item = self._enrich_debt_item(item)
|
|
enriched_items.append(enriched_item)
|
|
|
|
# Step 2: Calculate prioritization scores
|
|
for item in enriched_items:
|
|
if framework == "cost_of_delay":
|
|
item["priority_score"] = self._calculate_cost_of_delay_score(item)
|
|
elif framework == "wsjf":
|
|
item["priority_score"] = self._calculate_wsjf_score(item)
|
|
elif framework == "rice":
|
|
item["priority_score"] = self._calculate_rice_score(item)
|
|
else:
|
|
raise ValueError(f"Unknown prioritization framework: {framework}")
|
|
|
|
# Step 3: Sort by priority score
|
|
self.prioritized_items = sorted(enriched_items,
|
|
key=lambda x: x["priority_score"],
|
|
reverse=True)
|
|
|
|
# Step 4: Generate sprint allocation recommendations
|
|
sprint_allocation = self._generate_sprint_allocation()
|
|
|
|
# Step 5: Generate insights and recommendations
|
|
insights = self._generate_insights()
|
|
|
|
# Step 6: Create visualization data
|
|
charts_data = self._generate_charts_data()
|
|
|
|
return {
|
|
"metadata": {
|
|
"analysis_date": datetime.now().isoformat(),
|
|
"framework_used": framework,
|
|
"team_size": self.team_size,
|
|
"sprint_capacity_hours": self.sprint_capacity_hours,
|
|
"total_items_analyzed": len(self.debt_items)
|
|
},
|
|
"prioritized_backlog": self.prioritized_items,
|
|
"sprint_allocation": sprint_allocation,
|
|
"insights": insights,
|
|
"charts_data": charts_data,
|
|
"recommendations": self._generate_recommendations()
|
|
}
|
|
|
|
def _enrich_debt_item(self, item: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Enrich debt item with detailed estimates and impact analysis."""
|
|
enriched = item.copy()
|
|
|
|
# Generate effort estimate
|
|
effort = self._estimate_effort(item)
|
|
enriched["effort_estimate"] = asdict(effort)
|
|
|
|
# Generate business impact assessment
|
|
business_impact = self._assess_business_impact(item)
|
|
enriched["business_impact"] = asdict(business_impact)
|
|
|
|
# Calculate interest rate
|
|
interest_rate = self._calculate_interest_rate(item, business_impact)
|
|
enriched["interest_rate"] = asdict(interest_rate)
|
|
|
|
# Calculate cost of delay
|
|
enriched["cost_of_delay"] = self._calculate_cost_of_delay(interest_rate, effort)
|
|
|
|
# Assign categories and tags
|
|
enriched["category"] = self._categorize_debt_item(item)
|
|
enriched["impact_tags"] = self._generate_impact_tags(item, business_impact)
|
|
|
|
return enriched
|
|
|
|
def _estimate_effort(self, item: Dict[str, Any]) -> EffortEstimate:
|
|
"""Estimate effort required to fix debt item."""
|
|
debt_type = item.get("type", "unknown")
|
|
severity = item.get("severity", "medium")
|
|
|
|
# Base effort estimation by debt type
|
|
base_efforts = {
|
|
"todo_comment": (1, 2),
|
|
"missing_docstring": (1, 4),
|
|
"long_line": (0.5, 1),
|
|
"large_function": (4, 16),
|
|
"high_complexity": (8, 32),
|
|
"duplicate_code": (6, 24),
|
|
"large_file": (16, 64),
|
|
"syntax_error": (2, 8),
|
|
"security_risk": (4, 40),
|
|
"architecture_debt": (40, 160),
|
|
"test_debt": (8, 40),
|
|
"dependency_debt": (4, 24)
|
|
}
|
|
|
|
min_hours, max_hours = base_efforts.get(debt_type, (4, 16))
|
|
|
|
# Adjust by severity
|
|
severity_multipliers = {
|
|
"low": 0.5,
|
|
"medium": 1.0,
|
|
"high": 1.5,
|
|
"critical": 2.0
|
|
}
|
|
|
|
multiplier = severity_multipliers.get(severity, 1.0)
|
|
hours_estimate = (min_hours + max_hours) / 2 * multiplier
|
|
|
|
# Convert to story points (assuming 6 hours per point)
|
|
size_points = max(1, round(hours_estimate / 6))
|
|
|
|
# Determine risk factor
|
|
risk_factor = 1.0
|
|
if debt_type in ["architecture_debt", "security_risk", "large_file"]:
|
|
risk_factor = 1.8
|
|
elif debt_type in ["high_complexity", "duplicate_code"]:
|
|
risk_factor = 1.4
|
|
elif debt_type in ["syntax_error", "dependency_debt"]:
|
|
risk_factor = 1.2
|
|
|
|
# Determine skill level required
|
|
skill_requirements = {
|
|
"architecture_debt": "expert",
|
|
"security_risk": "senior",
|
|
"high_complexity": "senior",
|
|
"large_function": "mid",
|
|
"duplicate_code": "mid",
|
|
"dependency_debt": "mid",
|
|
"test_debt": "mid",
|
|
"todo_comment": "junior",
|
|
"missing_docstring": "junior",
|
|
"long_line": "junior"
|
|
}
|
|
|
|
skill_level = skill_requirements.get(debt_type, "mid")
|
|
|
|
# Confidence based on debt type clarity
|
|
confidence_levels = {
|
|
"todo_comment": 0.9,
|
|
"missing_docstring": 0.9,
|
|
"long_line": 0.95,
|
|
"syntax_error": 0.8,
|
|
"large_function": 0.7,
|
|
"duplicate_code": 0.6,
|
|
"high_complexity": 0.5,
|
|
"architecture_debt": 0.3,
|
|
"security_risk": 0.4
|
|
}
|
|
|
|
confidence = confidence_levels.get(debt_type, 0.6)
|
|
|
|
return EffortEstimate(
|
|
size_points=size_points,
|
|
hours_estimate=hours_estimate,
|
|
risk_factor=risk_factor,
|
|
skill_level_required=skill_level,
|
|
confidence=confidence
|
|
)
|
|
|
|
def _assess_business_impact(self, item: Dict[str, Any]) -> BusinessImpact:
|
|
"""Assess business impact of debt item."""
|
|
debt_type = item.get("type", "unknown")
|
|
severity = item.get("severity", "medium")
|
|
|
|
# Base impact scores by debt type (1-10 scale)
|
|
impact_profiles = {
|
|
"security_risk": (9, 8, 7, 9, 10), # customer, revenue, velocity, quality, security
|
|
"architecture_debt": (6, 7, 9, 8, 4),
|
|
"large_function": (3, 4, 7, 6, 2),
|
|
"high_complexity": (4, 5, 8, 7, 3),
|
|
"duplicate_code": (3, 4, 6, 6, 2),
|
|
"syntax_error": (7, 6, 8, 9, 3),
|
|
"test_debt": (5, 5, 7, 8, 3),
|
|
"dependency_debt": (6, 5, 6, 7, 7),
|
|
"todo_comment": (1, 1, 2, 2, 1),
|
|
"missing_docstring": (2, 2, 4, 3, 1)
|
|
}
|
|
|
|
base_impacts = impact_profiles.get(debt_type, (3, 3, 5, 5, 3))
|
|
|
|
# Adjust by severity
|
|
severity_adjustments = {
|
|
"low": 0.6,
|
|
"medium": 1.0,
|
|
"high": 1.4,
|
|
"critical": 1.8
|
|
}
|
|
|
|
adjustment = severity_adjustments.get(severity, 1.0)
|
|
|
|
# Apply adjustment and cap at 10
|
|
adjusted_impacts = [min(10, max(1, round(impact * adjustment)))
|
|
for impact in base_impacts]
|
|
|
|
return BusinessImpact(
|
|
customer_impact=adjusted_impacts[0],
|
|
revenue_impact=adjusted_impacts[1],
|
|
team_velocity_impact=adjusted_impacts[2],
|
|
quality_impact=adjusted_impacts[3],
|
|
security_impact=adjusted_impacts[4]
|
|
)
|
|
|
|
def _calculate_interest_rate(self, item: Dict[str, Any],
|
|
business_impact: BusinessImpact) -> InterestRate:
|
|
"""Calculate interest rate for technical debt."""
|
|
|
|
# Base daily cost calculation
|
|
velocity_impact = business_impact.team_velocity_impact
|
|
quality_impact = business_impact.quality_impact
|
|
|
|
# Daily cost in "developer hours lost"
|
|
daily_cost = (velocity_impact * 0.5) + (quality_impact * 0.3)
|
|
|
|
# Frequency multiplier based on code location and type
|
|
file_path = item.get("file_path", "")
|
|
debt_type = item.get("type", "unknown")
|
|
|
|
# Estimate frequency based on file path patterns
|
|
frequency_multiplier = 1.0
|
|
if any(pattern in file_path.lower() for pattern in ["main", "core", "auth", "api"]):
|
|
frequency_multiplier = 2.0
|
|
elif any(pattern in file_path.lower() for pattern in ["util", "helper", "common"]):
|
|
frequency_multiplier = 1.5
|
|
elif any(pattern in file_path.lower() for pattern in ["test", "spec", "config"]):
|
|
frequency_multiplier = 0.5
|
|
|
|
# Team impact multiplier
|
|
team_impact_multiplier = min(self.team_size, 8) / 5.0 # Normalize around team of 5
|
|
|
|
# Compound rate - how this debt creates more debt
|
|
compound_rates = {
|
|
"architecture_debt": 0.1, # Creates 10% more debt monthly
|
|
"duplicate_code": 0.08,
|
|
"high_complexity": 0.05,
|
|
"large_function": 0.03,
|
|
"test_debt": 0.04,
|
|
"security_risk": 0.02, # Doesn't compound much, but high initial impact
|
|
"todo_comment": 0.01
|
|
}
|
|
|
|
compound_rate = compound_rates.get(debt_type, 0.02)
|
|
|
|
return InterestRate(
|
|
daily_cost=daily_cost,
|
|
frequency_multiplier=frequency_multiplier,
|
|
team_impact_multiplier=team_impact_multiplier,
|
|
compound_rate=compound_rate
|
|
)
|
|
|
|
def _calculate_cost_of_delay(self, interest_rate: InterestRate,
|
|
effort: EffortEstimate) -> float:
|
|
"""Calculate total cost of delay if debt is not fixed."""
|
|
|
|
# Estimate delay in days (assuming debt gets fixed eventually)
|
|
estimated_delay_days = effort.hours_estimate / (self.sprint_capacity_hours / 14) # 2-week sprints
|
|
|
|
# Calculate cumulative cost
|
|
daily_cost = (interest_rate.daily_cost *
|
|
interest_rate.frequency_multiplier *
|
|
interest_rate.team_impact_multiplier)
|
|
|
|
# Add compound interest effect
|
|
compound_effect = (1 + interest_rate.compound_rate) ** (estimated_delay_days / 30)
|
|
|
|
total_cost = daily_cost * estimated_delay_days * compound_effect
|
|
|
|
return round(total_cost, 2)
|
|
|
|
def _categorize_debt_item(self, item: Dict[str, Any]) -> str:
|
|
"""Categorize debt item into high-level categories."""
|
|
debt_type = item.get("type", "unknown")
|
|
|
|
categories = {
|
|
"code_quality": ["large_function", "high_complexity", "duplicate_code",
|
|
"long_line", "missing_docstring"],
|
|
"architecture": ["architecture_debt", "large_file"],
|
|
"security": ["security_risk", "hardcoded_secrets"],
|
|
"testing": ["test_debt", "missing_tests"],
|
|
"maintenance": ["todo_comment", "commented_code"],
|
|
"dependencies": ["dependency_debt", "outdated_packages"],
|
|
"infrastructure": ["deployment_debt", "monitoring_gaps"],
|
|
"documentation": ["missing_docstring", "outdated_docs"]
|
|
}
|
|
|
|
for category, types in categories.items():
|
|
if debt_type in types:
|
|
return category
|
|
|
|
return "other"
|
|
|
|
def _generate_impact_tags(self, item: Dict[str, Any],
|
|
business_impact: BusinessImpact) -> List[str]:
|
|
"""Generate impact tags for debt item."""
|
|
tags = []
|
|
|
|
if business_impact.security_impact >= 7:
|
|
tags.append("security-critical")
|
|
if business_impact.customer_impact >= 7:
|
|
tags.append("customer-facing")
|
|
if business_impact.revenue_impact >= 7:
|
|
tags.append("revenue-impact")
|
|
if business_impact.team_velocity_impact >= 7:
|
|
tags.append("velocity-blocker")
|
|
if business_impact.quality_impact >= 7:
|
|
tags.append("quality-risk")
|
|
|
|
# Add effort-based tags
|
|
effort_hours = item.get("effort_estimate", {}).get("hours_estimate", 0)
|
|
if effort_hours <= 4:
|
|
tags.append("quick-win")
|
|
elif effort_hours >= 40:
|
|
tags.append("major-initiative")
|
|
|
|
return tags
|
|
|
|
def _calculate_cost_of_delay_score(self, item: Dict[str, Any]) -> float:
|
|
"""Calculate priority score using cost-of-delay framework."""
|
|
business_impact = item["business_impact"]
|
|
effort = item["effort_estimate"]
|
|
|
|
# Business value (weighted average of impacts)
|
|
business_value = (
|
|
business_impact["customer_impact"] * 0.3 +
|
|
business_impact["revenue_impact"] * 0.3 +
|
|
business_impact["quality_impact"] * 0.2 +
|
|
business_impact["team_velocity_impact"] * 0.2
|
|
)
|
|
|
|
# Urgency (how quickly value decreases)
|
|
urgency = item["interest_rate"]["daily_cost"] * 10 # Scale to 1-10
|
|
urgency = min(10, max(1, urgency))
|
|
|
|
# Risk reduction
|
|
risk_reduction = business_impact["security_impact"] * 0.6 + business_impact["quality_impact"] * 0.4
|
|
|
|
# Team productivity impact
|
|
team_productivity = business_impact["team_velocity_impact"]
|
|
|
|
# Combine with weights
|
|
weights = self.framework_weights["cost_of_delay"]
|
|
numerator = (
|
|
business_value * weights["business_value"] +
|
|
urgency * weights["urgency"] +
|
|
risk_reduction * weights["risk_reduction"] +
|
|
team_productivity * weights["team_productivity"]
|
|
)
|
|
|
|
# Divide by effort (adjusted for risk)
|
|
effort_adjusted = effort["hours_estimate"] * effort["risk_factor"]
|
|
denominator = max(1, effort_adjusted / 8) # Normalize to story points
|
|
|
|
return round(numerator / denominator, 2)
|
|
|
|
def _calculate_wsjf_score(self, item: Dict[str, Any]) -> float:
|
|
"""Calculate priority score using Weighted Shortest Job First (WSJF)."""
|
|
business_impact = item["business_impact"]
|
|
effort = item["effort_estimate"]
|
|
|
|
# Business value
|
|
business_value = (
|
|
business_impact["customer_impact"] * 0.4 +
|
|
business_impact["revenue_impact"] * 0.6
|
|
)
|
|
|
|
# Time criticality
|
|
time_criticality = item["cost_of_delay"] / 10 # Normalize
|
|
time_criticality = min(10, max(1, time_criticality))
|
|
|
|
# Risk reduction
|
|
risk_reduction = (
|
|
business_impact["security_impact"] * 0.5 +
|
|
business_impact["quality_impact"] * 0.5
|
|
)
|
|
|
|
# Job size (effort)
|
|
job_size = effort["size_points"]
|
|
|
|
# WSJF calculation
|
|
numerator = business_value + time_criticality + risk_reduction
|
|
denominator = max(1, job_size)
|
|
|
|
return round(numerator / denominator, 2)
|
|
|
|
def _calculate_rice_score(self, item: Dict[str, Any]) -> float:
|
|
"""Calculate priority score using RICE framework."""
|
|
business_impact = item["business_impact"]
|
|
effort = item["effort_estimate"]
|
|
|
|
# Reach (how many developers/users affected)
|
|
reach = min(10, self.team_size * business_impact["team_velocity_impact"] / 5)
|
|
|
|
# Impact
|
|
impact = (
|
|
business_impact["customer_impact"] * 0.3 +
|
|
business_impact["revenue_impact"] * 0.3 +
|
|
business_impact["quality_impact"] * 0.4
|
|
)
|
|
|
|
# Confidence
|
|
confidence = effort["confidence"] * 10
|
|
|
|
# Effort
|
|
effort_score = effort["size_points"]
|
|
|
|
# RICE calculation
|
|
rice_score = (reach * impact * confidence) / max(1, effort_score)
|
|
|
|
return round(rice_score, 2)
|
|
|
|
def _generate_sprint_allocation(self) -> Dict[str, Any]:
|
|
"""Generate sprint allocation recommendations."""
|
|
# Calculate total effort needed
|
|
total_effort_hours = sum(item["effort_estimate"]["hours_estimate"]
|
|
for item in self.prioritized_items)
|
|
|
|
# Assume 20% of sprint capacity goes to tech debt
|
|
debt_capacity_per_sprint = self.sprint_capacity_hours * 0.2
|
|
|
|
# Allocate items to sprints
|
|
sprints = []
|
|
current_sprint = {"sprint_number": 1, "items": [], "total_hours": 0, "capacity_used": 0}
|
|
|
|
for item in self.prioritized_items:
|
|
item_effort = item["effort_estimate"]["hours_estimate"]
|
|
|
|
if current_sprint["total_hours"] + item_effort <= debt_capacity_per_sprint:
|
|
current_sprint["items"].append(item)
|
|
current_sprint["total_hours"] += item_effort
|
|
current_sprint["capacity_used"] = current_sprint["total_hours"] / debt_capacity_per_sprint
|
|
else:
|
|
# Start new sprint
|
|
sprints.append(current_sprint)
|
|
current_sprint = {
|
|
"sprint_number": len(sprints) + 1,
|
|
"items": [item],
|
|
"total_hours": item_effort,
|
|
"capacity_used": item_effort / debt_capacity_per_sprint
|
|
}
|
|
|
|
# Add the last sprint
|
|
if current_sprint["items"]:
|
|
sprints.append(current_sprint)
|
|
|
|
# Calculate summary statistics
|
|
total_sprints_needed = len(sprints)
|
|
high_priority_items = len([item for item in self.prioritized_items
|
|
if item.get("priority", "medium") in ["high", "critical"]])
|
|
|
|
return {
|
|
"total_debt_hours": round(total_effort_hours, 1),
|
|
"debt_capacity_per_sprint": debt_capacity_per_sprint,
|
|
"total_sprints_needed": total_sprints_needed,
|
|
"high_priority_items": high_priority_items,
|
|
"sprint_plan": sprints[:6], # Show first 6 sprints
|
|
"recommendations": [
|
|
f"Allocate {debt_capacity_per_sprint} hours per sprint to tech debt",
|
|
f"Focus on {high_priority_items} high-priority items first",
|
|
f"Estimated {total_sprints_needed} sprints to clear current backlog"
|
|
]
|
|
}
|
|
|
|
def _generate_insights(self) -> Dict[str, Any]:
|
|
"""Generate insights from the prioritized debt analysis."""
|
|
|
|
# Category distribution
|
|
categories = Counter(item["category"] for item in self.prioritized_items)
|
|
|
|
# Effort distribution
|
|
total_effort = sum(item["effort_estimate"]["hours_estimate"]
|
|
for item in self.prioritized_items)
|
|
effort_by_category = defaultdict(float)
|
|
for item in self.prioritized_items:
|
|
effort_by_category[item["category"]] += item["effort_estimate"]["hours_estimate"]
|
|
|
|
# Priority distribution
|
|
priorities = Counter()
|
|
for item in self.prioritized_items:
|
|
score = item["priority_score"]
|
|
if score >= 8:
|
|
priorities["critical"] += 1
|
|
elif score >= 5:
|
|
priorities["high"] += 1
|
|
elif score >= 2:
|
|
priorities["medium"] += 1
|
|
else:
|
|
priorities["low"] += 1
|
|
|
|
# Risk analysis
|
|
high_risk_items = [item for item in self.prioritized_items
|
|
if item["effort_estimate"]["risk_factor"] >= 1.5]
|
|
|
|
# Quick wins identification
|
|
quick_wins = [item for item in self.prioritized_items
|
|
if (item["effort_estimate"]["hours_estimate"] <= 8 and
|
|
item["priority_score"] >= 3)]
|
|
|
|
# Cost analysis
|
|
total_cost_of_delay = sum(item["cost_of_delay"] for item in self.prioritized_items)
|
|
avg_interest_rate = sum(item["interest_rate"]["daily_cost"]
|
|
for item in self.prioritized_items) / len(self.prioritized_items)
|
|
|
|
return {
|
|
"category_distribution": dict(categories),
|
|
"total_effort_hours": round(total_effort, 1),
|
|
"effort_by_category": {k: round(v, 1) for k, v in effort_by_category.items()},
|
|
"priority_distribution": dict(priorities),
|
|
"high_risk_items_count": len(high_risk_items),
|
|
"quick_wins_count": len(quick_wins),
|
|
"total_cost_of_delay": round(total_cost_of_delay, 1),
|
|
"average_daily_interest_rate": round(avg_interest_rate, 2),
|
|
"top_categories_by_effort": sorted(effort_by_category.items(),
|
|
key=lambda x: x[1], reverse=True)[:3]
|
|
}
|
|
|
|
def _generate_charts_data(self) -> Dict[str, Any]:
|
|
"""Generate data for charts and visualizations."""
|
|
|
|
# Priority vs Effort scatter plot data
|
|
scatter_data = []
|
|
for item in self.prioritized_items:
|
|
scatter_data.append({
|
|
"x": item["effort_estimate"]["hours_estimate"],
|
|
"y": item["priority_score"],
|
|
"label": item.get("description", "")[:50],
|
|
"category": item["category"],
|
|
"size": item["cost_of_delay"]
|
|
})
|
|
|
|
# Category effort distribution (pie chart)
|
|
effort_by_category = defaultdict(float)
|
|
for item in self.prioritized_items:
|
|
effort_by_category[item["category"]] += item["effort_estimate"]["hours_estimate"]
|
|
|
|
pie_data = [{"category": k, "effort": round(v, 1)}
|
|
for k, v in effort_by_category.items()]
|
|
|
|
# Priority timeline (bar chart)
|
|
timeline_data = []
|
|
cumulative_effort = 0
|
|
for i, item in enumerate(self.prioritized_items[:20]): # Top 20 items
|
|
cumulative_effort += item["effort_estimate"]["hours_estimate"]
|
|
timeline_data.append({
|
|
"item_rank": i + 1,
|
|
"description": item.get("description", "")[:30],
|
|
"effort": item["effort_estimate"]["hours_estimate"],
|
|
"cumulative_effort": round(cumulative_effort, 1),
|
|
"priority_score": item["priority_score"]
|
|
})
|
|
|
|
# Interest rate trend (line chart data structure)
|
|
interest_trend_data = []
|
|
for i, item in enumerate(self.prioritized_items):
|
|
interest_trend_data.append({
|
|
"item_index": i,
|
|
"daily_cost": item["interest_rate"]["daily_cost"],
|
|
"category": item["category"]
|
|
})
|
|
|
|
return {
|
|
"priority_effort_scatter": scatter_data,
|
|
"category_effort_distribution": pie_data,
|
|
"priority_timeline": timeline_data,
|
|
"interest_rate_trend": interest_trend_data[:50] # Limit for performance
|
|
}
|
|
|
|
def _generate_recommendations(self) -> List[str]:
|
|
"""Generate actionable recommendations based on analysis."""
|
|
recommendations = []
|
|
|
|
insights = self._generate_insights()
|
|
|
|
# Quick wins recommendation
|
|
if insights["quick_wins_count"] > 0:
|
|
recommendations.append(
|
|
f"Start with {insights['quick_wins_count']} quick wins to build momentum "
|
|
"and demonstrate immediate value from tech debt reduction efforts."
|
|
)
|
|
|
|
# High-risk items
|
|
if insights["high_risk_items_count"] > 5:
|
|
recommendations.append(
|
|
f"Plan careful execution for {insights['high_risk_items_count']} high-risk items. "
|
|
"Consider pair programming, extra testing, and incremental approaches."
|
|
)
|
|
|
|
# Category focus
|
|
top_category = insights["top_categories_by_effort"][0][0]
|
|
recommendations.append(
|
|
f"Focus initial efforts on '{top_category}' category debt, which represents "
|
|
f"the largest effort investment ({insights['top_categories_by_effort'][0][1]:.1f} hours)."
|
|
)
|
|
|
|
# Cost of delay urgency
|
|
if insights["average_daily_interest_rate"] > 5:
|
|
recommendations.append(
|
|
f"High average daily interest rate ({insights['average_daily_interest_rate']:.1f}) "
|
|
"suggests urgent action needed. Consider increasing tech debt budget allocation."
|
|
)
|
|
|
|
# Sprint planning
|
|
sprints_needed = len(self.prioritized_items) / 10 # Rough estimate
|
|
if sprints_needed > 12:
|
|
recommendations.append(
|
|
"Large debt backlog detected. Consider dedicating entire sprints to debt reduction "
|
|
"rather than trying to fit debt work around features."
|
|
)
|
|
|
|
# Team capacity
|
|
total_effort = insights["total_effort_hours"]
|
|
weeks_needed = total_effort / (self.sprint_capacity_hours * 0.2)
|
|
if weeks_needed > 26: # Half a year
|
|
recommendations.append(
|
|
f"With current capacity allocation, debt backlog will take {weeks_needed:.0f} weeks. "
|
|
"Consider increasing tech debt budget or focusing on highest-impact items only."
|
|
)
|
|
|
|
return recommendations
|
|
|
|
|
|
def format_prioritized_report(analysis_result: Dict[str, Any]) -> str:
|
|
"""Format the prioritization analysis in human-readable format."""
|
|
output = []
|
|
|
|
# Header
|
|
output.append("=" * 60)
|
|
output.append("TECHNICAL DEBT PRIORITIZATION REPORT")
|
|
output.append("=" * 60)
|
|
metadata = analysis_result["metadata"]
|
|
output.append(f"Analysis Date: {metadata['analysis_date']}")
|
|
output.append(f"Framework: {metadata['framework_used'].upper()}")
|
|
output.append(f"Team Size: {metadata['team_size']}")
|
|
output.append(f"Sprint Capacity: {metadata['sprint_capacity_hours']} hours")
|
|
output.append("")
|
|
|
|
# Executive Summary
|
|
insights = analysis_result["insights"]
|
|
output.append("EXECUTIVE SUMMARY")
|
|
output.append("-" * 30)
|
|
output.append(f"Total Debt Items: {metadata['total_items_analyzed']}")
|
|
output.append(f"Total Effort Required: {insights['total_effort_hours']} hours")
|
|
output.append(f"Total Cost of Delay: ${insights['total_cost_of_delay']:,.0f}")
|
|
output.append(f"Quick Wins Available: {insights['quick_wins_count']}")
|
|
output.append(f"High-Risk Items: {insights['high_risk_items_count']}")
|
|
output.append("")
|
|
|
|
# Sprint Plan
|
|
sprint_plan = analysis_result["sprint_allocation"]
|
|
output.append("SPRINT ALLOCATION PLAN")
|
|
output.append("-" * 30)
|
|
output.append(f"Sprints Needed: {sprint_plan['total_sprints_needed']}")
|
|
output.append(f"Hours per Sprint: {sprint_plan['debt_capacity_per_sprint']}")
|
|
output.append("")
|
|
|
|
for sprint in sprint_plan["sprint_plan"][:3]: # Show first 3 sprints
|
|
output.append(f"Sprint {sprint['sprint_number']} ({sprint['capacity_used']:.0%} capacity):")
|
|
for item in sprint["items"][:3]: # Top 3 items per sprint
|
|
output.append(f" • {item['description'][:50]}...")
|
|
output.append(f" Effort: {item['effort_estimate']['hours_estimate']:.1f}h, "
|
|
f"Priority: {item['priority_score']}")
|
|
output.append("")
|
|
|
|
# Top Priority Items
|
|
output.append("TOP 10 PRIORITY ITEMS")
|
|
output.append("-" * 30)
|
|
for i, item in enumerate(analysis_result["prioritized_backlog"][:10], 1):
|
|
output.append(f"{i}. [{item['priority_score']:.1f}] {item['description']}")
|
|
output.append(f" Category: {item['category']}, "
|
|
f"Effort: {item['effort_estimate']['hours_estimate']:.1f}h, "
|
|
f"Cost of Delay: ${item['cost_of_delay']:.0f}")
|
|
if item["impact_tags"]:
|
|
output.append(f" Tags: {', '.join(item['impact_tags'])}")
|
|
output.append("")
|
|
|
|
# Recommendations
|
|
output.append("RECOMMENDATIONS")
|
|
output.append("-" * 30)
|
|
for i, rec in enumerate(analysis_result["recommendations"], 1):
|
|
output.append(f"{i}. {rec}")
|
|
output.append("")
|
|
|
|
return "\n".join(output)
|
|
|
|
|
|
def main():
|
|
"""Main entry point for the debt prioritizer."""
|
|
parser = argparse.ArgumentParser(description="Prioritize technical debt backlog")
|
|
parser.add_argument("inventory_file", help="Path to debt inventory JSON file")
|
|
parser.add_argument("--output", help="Output file path")
|
|
parser.add_argument("--format", choices=["json", "text", "both"],
|
|
default="both", help="Output format")
|
|
parser.add_argument("--framework", choices=["cost_of_delay", "wsjf", "rice"],
|
|
default="cost_of_delay", help="Prioritization framework")
|
|
parser.add_argument("--team-size", type=int, default=5, help="Team size")
|
|
parser.add_argument("--sprint-capacity", type=int, default=80,
|
|
help="Sprint capacity in hours")
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Initialize prioritizer
|
|
prioritizer = DebtPrioritizer(args.team_size, args.sprint_capacity)
|
|
|
|
# Load inventory
|
|
if not prioritizer.load_debt_inventory(args.inventory_file):
|
|
sys.exit(1)
|
|
|
|
# Analyze and prioritize
|
|
try:
|
|
analysis_result = prioritizer.analyze_and_prioritize(args.framework)
|
|
except Exception as e:
|
|
print(f"Analysis failed: {e}")
|
|
sys.exit(1)
|
|
|
|
# Output results
|
|
if args.format in ["json", "both"]:
|
|
json_output = json.dumps(analysis_result, indent=2, default=str)
|
|
if args.output:
|
|
output_path = args.output if args.output.endswith('.json') else f"{args.output}.json"
|
|
with open(output_path, 'w') as f:
|
|
f.write(json_output)
|
|
print(f"JSON report written to: {output_path}")
|
|
else:
|
|
print("JSON REPORT:")
|
|
print("=" * 50)
|
|
print(json_output)
|
|
|
|
if args.format in ["text", "both"]:
|
|
text_output = format_prioritized_report(analysis_result)
|
|
if args.output:
|
|
output_path = args.output if args.output.endswith('.txt') else f"{args.output}.txt"
|
|
with open(output_path, 'w') as f:
|
|
f.write(text_output)
|
|
print(f"Text report written to: {output_path}")
|
|
else:
|
|
print("\nTEXT REPORT:")
|
|
print("=" * 50)
|
|
print(text_output)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |