11_tech_debt_tracker.py

Download
python 202 lines 7.6 KB
  1"""
  2Technical Debt Tracker
  3
  4Demonstrates technical debt management concepts from software engineering:
  5- Debt categorization (code, architecture, test, documentation, security)
  6- Interest rate modelling: ongoing cost that grows each sprint
  7- ROI-based prioritization: benefit of paying off vs. effort required
  8- Debt report generation with actionable recommendations
  9- Sprint-by-sprint simulation of debt accumulation vs. payoff
 10"""
 11
 12from dataclasses import dataclass
 13from enum import Enum
 14from typing import Optional
 15
 16
 17class DebtType(Enum):
 18    CODE = "Code Quality"
 19    ARCHITECTURE = "Architecture"
 20    TEST = "Test Coverage"
 21    DOCUMENTATION = "Documentation"
 22    SECURITY = "Security"
 23    DEPENDENCY = "Dependency"
 24
 25
 26class Severity(Enum):
 27    CRITICAL = 4
 28    HIGH = 3
 29    MEDIUM = 2
 30    LOW = 1
 31
 32
 33@dataclass
 34class DebtItem:
 35    """A single technical debt item."""
 36    id: str
 37    description: str
 38    debt_type: DebtType
 39    severity: Severity
 40    effort_days: float          # estimated days to fix
 41    interest_rate: float        # extra cost per sprint (in hours) if not fixed
 42    added_sprint: int           # sprint number when debt was introduced
 43    paid_off_sprint: Optional[int] = None
 44
 45    @property
 46    def is_active(self) -> bool:
 47        return self.paid_off_sprint is None
 48
 49    def accumulated_interest(self, current_sprint: int) -> float:
 50        """Total hours lost to this debt up to current_sprint."""
 51        if not self.is_active:
 52            end = self.paid_off_sprint
 53        else:
 54            end = current_sprint
 55        sprints_active = max(0, end - self.added_sprint)
 56        return sprints_active * self.interest_rate
 57
 58    def roi_score(self, current_sprint: int) -> float:
 59        """
 60        ROI = (interest saved over next 10 sprints) / effort_days
 61        Higher score → pay off sooner.
 62        """
 63        if self.effort_days == 0:
 64            return float("inf")
 65        future_savings = self.interest_rate * 10  # hours saved
 66        return future_savings / self.effort_days
 67
 68
 69def generate_report(items: list[DebtItem], current_sprint: int) -> None:
 70    """Print a structured technical debt report."""
 71    active = [i for i in items if i.is_active]
 72    paid = [i for i in items if not i.is_active]
 73
 74    total_interest = sum(i.accumulated_interest(current_sprint) for i in active)
 75    total_effort = sum(i.effort_days for i in active)
 76
 77    print("=" * 68)
 78    print("  TECHNICAL DEBT REPORT")
 79    print(f"  Sprint {current_sprint}  |  Active items: {len(active)}  |  Resolved: {len(paid)}")
 80    print("=" * 68)
 81
 82    print(f"\nDebt Summary:")
 83    print(f"  Total accumulated interest (active debt) : {total_interest:.1f} hours")
 84    print(f"  Estimated payoff effort                  : {total_effort:.1f} dev-days")
 85    print(f"  Interest / sprint (ongoing burn)         : "
 86          f"{sum(i.interest_rate for i in active):.1f} hours/sprint")
 87
 88    # Breakdown by type
 89    print("\nDebt by Category:")
 90    by_type: dict[str, list[DebtItem]] = {}
 91    for item in active:
 92        by_type.setdefault(item.debt_type.value, []).append(item)
 93    for dtype, group in sorted(by_type.items()):
 94        hrs = sum(i.accumulated_interest(current_sprint) for i in group)
 95        print(f"  {dtype:<22} {len(group):>2} items   {hrs:>6.1f} h accumulated")
 96
 97    # Prioritized list
 98    print("\nPrioritized Payoff List (by ROI):")
 99    print(f"  {'ID':<8} {'Description':<32} {'Sev':<9} {'Effort':>7} {'ROI':>6}  Recommendation")
100    print("  " + "-" * 76)
101    ranked = sorted(active, key=lambda i: i.roi_score(current_sprint), reverse=True)
102    for rank, item in enumerate(ranked, 1):
103        roi = item.roi_score(current_sprint)
104        roi_str = f"{roi:.2f}" if roi != float("inf") else "∞"
105        if rank <= 2:
106            rec = ">>> FIX NOW"
107        elif item.severity == Severity.CRITICAL:
108            rec = ">> Urgent"
109        elif roi > 2.0:
110            rec = "> This sprint"
111        else:
112            rec = "  Backlog"
113        print(f"  {item.id:<8} {item.description:<32} {item.severity.name:<9} "
114              f"{item.effort_days:>5.1f}d  {roi_str:>6}  {rec}")
115
116    # Resolved items
117    if paid:
118        print(f"\nResolved Debt ({len(paid)} items):")
119        for item in paid:
120            sprint_count = item.paid_off_sprint - item.added_sprint
121            total = item.accumulated_interest(item.paid_off_sprint)
122            print(f"  [{item.id}] {item.description[:40]}  "
123                  f"(paid sprint {item.paid_off_sprint}, cost {total:.0f}h over {sprint_count} sprints)")
124
125    print("=" * 68)
126
127
128def simulate_sprints(items: list[DebtItem], total_sprints: int,
129                     payoff_capacity_hours: float = 8.0) -> None:
130    """
131    Simulate debt accumulation over sprints.
132    Each sprint: pay off highest-ROI items if capacity allows.
133    """
134    print("\nSPRINT SIMULATION (auto-payoff with 8h capacity/sprint)")
135    print(f"{'Sprint':>7}  {'Active':>7}  {'Interest/sp':>11}  {'Accumulated':>12}  {'Action'}")
136    print("-" * 70)
137
138    payoff_sprint = {}
139    paid_ids: set[str] = set()
140
141    for sprint in range(1, total_sprints + 1):
142        active = [i for i in items if i.added_sprint <= sprint and i.id not in paid_ids]
143        interest_this_sprint = sum(i.interest_rate for i in active)
144        accumulated = sum(i.accumulated_interest(sprint) for i in active)
145
146        # Attempt payoff
147        budget = payoff_capacity_hours
148        action_parts = []
149        for item in sorted(active, key=lambda i: i.roi_score(sprint), reverse=True):
150            cost_hours = item.effort_days * 8
151            if cost_hours <= budget:
152                budget -= cost_hours
153                paid_ids.add(item.id)
154                payoff_sprint[item.id] = sprint
155                action_parts.append(item.id)
156
157        action = "Paid: " + ", ".join(action_parts) if action_parts else "—"
158        print(f"{sprint:>7}  {len(active):>7}  {interest_this_sprint:>11.1f}  "
159              f"{accumulated:>12.1f}  {action}")
160
161
162if __name__ == "__main__":
163    CURRENT_SPRINT = 8
164
165    debt_items = [
166        DebtItem("TD-001", "No unit tests for payment module",
167                 DebtType.TEST, Severity.CRITICAL,
168                 effort_days=3.0, interest_rate=4.0, added_sprint=2),
169
170        DebtItem("TD-002", "Hardcoded API keys in config.py",
171                 DebtType.SECURITY, Severity.CRITICAL,
172                 effort_days=0.5, interest_rate=6.0, added_sprint=1),
173
174        DebtItem("TD-003", "Monolithic UserService (God Object)",
175                 DebtType.ARCHITECTURE, Severity.HIGH,
176                 effort_days=5.0, interest_rate=3.0, added_sprint=3),
177
178        DebtItem("TD-004", "SQLAlchemy 1.3 → 2.0 migration pending",
179                 DebtType.DEPENDENCY, Severity.HIGH,
180                 effort_days=2.0, interest_rate=1.5, added_sprint=4),
181
182        DebtItem("TD-005", "Copy-paste logic in 6 report generators",
183                 DebtType.CODE, Severity.MEDIUM,
184                 effort_days=1.5, interest_rate=2.0, added_sprint=3),
185
186        DebtItem("TD-006", "No API documentation (OpenAPI spec missing)",
187                 DebtType.DOCUMENTATION, Severity.MEDIUM,
188                 effort_days=2.0, interest_rate=1.0, added_sprint=5),
189
190        DebtItem("TD-007", "N+1 query in product listing endpoint",
191                 DebtType.CODE, Severity.HIGH,
192                 effort_days=0.5, interest_rate=2.5, added_sprint=6),
193
194        DebtItem("TD-008", "No retry/circuit-breaker on payment API calls",
195                 DebtType.ARCHITECTURE, Severity.MEDIUM,
196                 effort_days=1.5, interest_rate=1.0, added_sprint=7,
197                 paid_off_sprint=8),   # already resolved
198    ]
199
200    generate_report(debt_items, CURRENT_SPRINT)
201    simulate_sprints(debt_items, total_sprints=12, payoff_capacity_hours=8.0)