Understanding Technical Debt
Technical debt is a metaphor coined by Ward Cunningham that describes the accumulated consequences of expedient technical decisions made during software development. Just like financial debt, technical debt incurs "interest" over time — the longer you defer addressing it, the more effort will be required to fix it later. In large codebases, this debt compounds exponentially as interdependencies multiply and institutional knowledge fades.
At its core, technical debt represents the gap between the current state of your code and what it ideally should be. This gap manifests in many forms: hastily written functions that lack tests, outdated dependencies with known vulnerabilities, monolithic architectures that resist scaling, undocumented business logic buried in obscure modules, and inconsistent coding patterns scattered across hundreds of files.
The Mental Model: Principal and Interest
Think of technical debt in two parts. The principal is the original shortcut — the missing test, the hardcoded value, the bypassed validation. The interest is the ongoing cost: developers spending extra hours deciphering tangled logic, bugs emerging from unguarded edge cases, onboarding time stretching from days to weeks. In a large codebase with thousands of contributors, even small debts can generate massive interest payments across the organization.
Why Technical Debt Matters in Large Codebases
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →In codebases spanning millions of lines and hundreds of services, technical debt is not merely an inconvenience — it becomes a strategic liability. Here's why it demands serious attention:
- Velocity Decay: Every sprint, teams allocate an increasing percentage of capacity to navigating existing debt rather than delivering new value. A team that once shipped 10 features per sprint may drop to 3, with the remaining effort consumed by workarounds and firefighting.
- Quality Spiral: Debt begets debt. When developers see compromised code, they often build more compromises on top — either because the existing interfaces force them to, or because the cultural bar has been lowered.
- Hiring and Retention: Talented engineers grow frustrated working in codebases where simple changes require heroic effort. Your debt becomes a hidden cost in recruiting and turnover.
- Risk Accumulation: Security vulnerabilities, compliance failures, and catastrophic outages often trace back to debt that was deemed "acceptable" years earlier.
- Architectural Rigidity: Large-scale refactors become prohibitively expensive, locking the organization into outdated technology choices.
Identifying Technical Debt in Your Codebase
Before you can manage debt, you need to see it. In large codebases, manual inspection is impossible — you need systematic detection methods. Here are practical approaches:
1. Static Analysis for Code Smells
Configure automated tools to scan your entire codebase on every commit. The following ESLint configuration targets common debt indicators in JavaScript projects:
// .eslintrc.js — aggressive debt-detection configuration
module.exports = {
extends: ['eslint:recommended'],
rules: {
// Detect overly complex functions (high cyclomatic complexity = debt hotspot)
'complexity': ['error', { max: 10 }],
// Flag functions with too many parameters (often indicates tangled interfaces)
'max-params': ['warn', { max: 4 }],
// Warn on files exceeding a length threshold (monolithic modules)
'max-lines': ['warn', { max: 300, skipBlankLines: true, skipComments: true }],
// Detect TODO comments — these are explicit debt markers
'no-warning-comments': ['warn', { terms: ['todo', 'fixme', 'hack', 'xxx'], location: 'anywhere' }],
// Enforce consistent naming to reduce cognitive debt
'id-length': ['warn', { min: 3, max: 40, exceptions: ['id', 'db', 'fs', 'i', 'j'] }],
// Catch duplicate code patterns (many tools flag this; ESLint needs a plugin)
'no-duplicate-imports': 'error'
}
};
2. Tracking Debt with SonarQube Quality Gates
SonarQube provides a unified dashboard for technical debt across polyglot codebases. Configure quality gates that fail your CI pipeline when debt exceeds thresholds:
// sonar-project.properties
sonar.projectKey=org.example:payment-service
sonar.sources=src/main/java
sonar.java.binaries=target/classes
sonar.tests=src/test/java
# Quality Gate thresholds embedded in analysis configuration
sonar.qualitygate.name=HardCodedQualityGate
sonar.qualitygate.conditions=coverage<80,bugs>0,vulnerabilities>0,code_smells>500
# Track "Sprint Debt" — issues introduced in the last 21 days
sonar.leak.period=21_days
# Exclude generated code to avoid noise
sonar.exclusions=**/generated/**,**/proto/**,**/vendor/**
3. Scripting Custom Debt Metrics
Sometimes you need custom heuristics. This Python script scans a codebase and calculates a composite "debt score" per file, helping you prioritize remediation:
#!/usr/bin/env python3
"""
debt_scanner.py — Scans a codebase and assigns a technical debt score to each file.
Higher scores indicate files that should be prioritized for refactoring.
"""
import os
import re
from collections import defaultdict
from datetime import datetime
class DebtScanner:
def __init__(self, root_path):
self.root_path = root_path
self.results = []
def scan_file(self, filepath):
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
lines = content.split('\n')
debt_score = 0
indicators = []
# Indicator 1: File length (monolithic files are harder to maintain)
if len(lines) > 500:
debt_score += 30
indicators.append(f'FILE_SIZE:{len(lines)}lines')
elif len(lines) > 300:
debt_score += 15
indicators.append(f'FILE_SIZE:{len(lines)}lines')
# Indicator 2: TODO/FIXME/HACK density
todo_count = len(re.findall(r'(TODO|FIXME|HACK|XXX|OPTIMIZE)', content, re.IGNORECASE))
if todo_count > 5:
debt_score += todo_count * 3
indicators.append(f'TODO_COUNT:{todo_count}')
# Indicator 3: Comment-to-code ratio (very low = undocumented; very high = outdated docs)
comment_lines = len([l for l in lines if l.strip().startswith(('//', '#', '/*', '*'))])
code_lines = len([l for l in lines if l.strip() and not l.strip().startswith(('//', '#', '/*', '*'))])
ratio = comment_lines / max(code_lines, 1)
if ratio < 0.05:
debt_score += 20
indicators.append('UNDOCUMENTED')
elif ratio > 0.5:
debt_score += 10 # Potentially outdated comments
indicators.append('COMMENT_HEAVY')
# Indicator 4: Deep nesting (indicates complex control flow)
max_indent = max((len(l) - len(l.lstrip())) for l in lines if l.strip()) if lines else 0
if max_indent > 80: # 20 levels of 4-space indentation
debt_score += 25
indicators.append(f'DEEP_NESTING:{max_indent}chars')
# Indicator 5: Repeated string patterns (potential copy-paste code)
pattern_counts = defaultdict(int)
for line in lines:
stripped = line.strip()
if len(stripped) > 40:
pattern_counts[stripped[:60]] += 1
duplicate_chunks = sum(1 for c in pattern_counts.values() if c > 3)
if duplicate_chunks > 0:
debt_score += duplicate_chunks * 5
indicators.append(f'DUPLICATE_CHUNKS:{duplicate_chunks}')
return {'file': filepath, 'score': debt_score, 'indicators': indicators}
def scan_all(self, extensions=None):
if extensions is None:
extensions = {'.js', '.ts', '.py', '.java', '.rb', '.go', '.rs'}
for root, dirs, files in os.walk(self.root_path):
# Skip common non-source directories
dirs[:] = [d for d in dirs if d not in ('node_modules', '.git', 'vendor', 'dist', 'build')]
for file in files:
if any(file.endswith(ext) for ext in extensions):
full_path = os.path.join(root, file)
self.results.append(self.scan_file(full_path))
return sorted(self.results, key=lambda r: r['score'], reverse=True)
# Usage
scanner = DebtScanner('./src')
results = scanner.scan_all()
print(f"\n{'='*60}")
print(f"Technical Debt Scan — {datetime.now().strftime('%Y-%m-%d %H:%M')}")
print(f"Files analyzed: {len(results)}")
print(f"{'='*60}\n")
# Show top 20 debt hotspots
print("Top 20 Debt Hotspots:\n")
for i, result in enumerate(results[:20]):
print(f"#{i+1} [{result['score']}pts] {result['file']}")
for indicator in result['indicators']:
print(f" ↳ {indicator}")
print()
# Summary statistics
total_debt = sum(r['score'] for r in results)
avg_debt = total_debt / max(len(results), 1)
print(f"\nTotal Codebase Debt Score: {total_debt}")
print(f"Average Debt per File: {avg_debt:.1f}")
print(f"Files with Critical Debt (>100pts): {sum(1 for r in results if r['score'] > 100)}")
Strategies for Managing Technical Debt
Managing debt in large codebases requires a portfolio approach — you can't pay everything down at once, nor should you. Here are proven strategies, organized by scope:
1. The Boy Scout Rule: Incremental Improvement
Leave every file you touch better than you found it. This isn't about massive refactors — it's about small, safe improvements made during normal development. Here's a practical example of a debt-reducing code change made during a routine bug fix:
// BEFORE: Debt-laden function — multiple responsibilities, no error handling,
// hardcoded values, and duplicate validation logic
function processOrder(orderData) {
// Validate manually (duplicated across 12 files)
if(!orderData.customerId) { return; }
if(!orderData.items || orderData.items.length == 0) { return; }
// Hardcoded tax calculation
var taxRate = 0.08;
var subtotal = 0;
for(var i=0; i 10) {
total = total * 0.95; // Magic number
}
db.insert('orders', {
customerId: orderData.customerId,
total: total,
items: JSON.stringify(orderData.items),
date: new Date().toISOString()
});
sendEmail(customer.email, 'Order Confirmed'); // No async handling
return total;
}
// AFTER: Debt reduced incrementally during a bug fix session
// Extracted validation, used service modules, added error handling
import { validateOrder } from '@/validation/orderValidator';
import { calculateTax } from '@/tax/taxCalculator';
import { calculateDiscount } from '@/pricing/discountEngine';
import { OrderRepository } from '@/data/OrderRepository';
import { NotificationService } from '@/services/NotificationService';
import { AppError } from '@/errors/AppError';
async function processOrder(orderData: OrderInput): Promise {
// 1. Validate using centralized module (eliminates duplication)
const validation = validateOrder(orderData);
if (!validation.valid) {
throw new AppError('VALIDATION_FAILED', validation.errors);
}
// 2. Use configured tax rate from service, not hardcoded
const subtotal = orderData.items.reduce(
(sum, item) => sum + item.price * item.quantity, 0
);
const taxRate = await calculateTax(orderData.customerId, subtotal);
const tax = subtotal * taxRate;
let total = subtotal + tax;
// 3. Repository pattern separates data access from business logic
const customer = await OrderRepository.findCustomer(orderData.customerId);
if (!customer) {
throw new AppError('CUSTOMER_NOT_FOUND', { customerId: orderData.customerId });
}
if (customer.status === 'inactive') {
throw new AppError('INACTIVE_CUSTOMER', { customerId: orderData.customerId });
}
// 4. Centralized discount logic prevents inconsistent calculations
const discount = await calculateDiscount(customer, total);
total = total - discount;
// 5. Async persistence with proper error propagation
const order = await OrderRepository.create({
customerId: orderData.customerId,
total,
items: orderData.items,
createdAt: new Date()
});
// 6. Fire-and-forget notification with error logging (non-blocking)
NotificationService.sendConfirmation(customer.email, order.id)
.catch(err => console.error('Notification failed', err));
return total;
}
2. The Debt Backlog: Explicit Tracking
Create a dedicated backlog for technical debt items, distinct from feature work. Each debt item should include a concrete cost estimate and a clear definition of "done." Here's a structured format you can implement in your issue tracker:
// Example: YAML template for technical debt tickets
// Store these in a 'tech-debt/' directory within your repository
ticket_id: TD-2024-0142
title: "Refactor PaymentGateway to use Strategy Pattern"
discovered_date: 2024-03-15
estimated_principal: "3 story points" # Effort to fix
accrued_interest_per_sprint: "0.5 story points" # Ongoing drag on velocity
affected_modules:
- src/payments/gateway.js
- src/payments/processors/*.js
- src/checkout/CheckoutFlow.js
symptoms:
- "Adding a new payment method requires modifying 7 files"
- "Gateway has 14 methods with overlapping responsibilities"
- "Unit test coverage at 23% because mocking is painful"
proposed_solution:
summary: "Extract payment processors behind a common interface"
acceptance_criteria:
- "New payment methods can be added by implementing a single interface"
- "Gateway class reduced to <200 lines"
- "Test coverage exceeds 80%"
- "All existing payment integration tests pass"
risk_of_not_fixing:
- "Planned Stripe Connect integration will cost 3x estimated effort"
- "Each new payment method compounds the spaghetti architecture"
- "Risk of payment processing bugs increases with each modification"
priority: "HIGH" # Based on velocity impact and upcoming work
3. Automated Debt Budgets in CI/CD
Prevent new debt from being introduced by enforcing budgets in your pipeline. This script fails the build if the debt delta for a pull request exceeds a threshold:
#!/usr/bin/env bash
# .github/scripts/debt-gate.sh — CI/CD debt budget enforcement
# Fails the build if a PR introduces more technical debt than it resolves
set -euo pipefail
DEBT_TOOL="sonar-scanner"
MAX_NEW_DEBT_POINTS=10 # Allow small intentional debt, block large accruals
REPORT_FILE="debt-report.json"
echo "🔍 Running technical debt analysis on current branch..."
# Run analysis on feature branch
sonar-scanner -Dsonar.projectBaseDir=. \
-Dsonar.branch.name="${GITHUB_HEAD_REF:-$(git branch --show-current)}" \
-Dsonar.qualitygate.wait=true \
-Dsonar.qualitygate.timeout=300
# Extract debt metrics
NEW_BUGS=$(jq '.measures[] | select(.metric=="bugs") | .value' "$REPORT_FILE")
NEW_CODE_SMELLS=$(jq '.measures[] | select(.metric=="code_smells") | .value' "$REPORT_FILE")
NEW_VULNERABILITIES=$(jq '.measures[] | select(.metric=="vulnerabilities") | .value' "$REPORT_FILE")
# Calculate composite debt score (weighted)
DEBT_SCORE=$(( NEW_BUGS * 5 + NEW_CODE_SMELLS * 1 + NEW_VULNERABILITIES * 10 ))
echo ""
echo "═══════════════════════════════════"
echo " Technical Debt Budget Report"
echo "═══════════════════════════════════"
echo " New Bugs: ${NEW_BUGS:-0}"
echo " New Code Smells: ${NEW_CODE_SMELLS:-0}"
echo " New Vulnerabilities: ${NEW_VULNERABILITIES:-0}"
echo " Composite Score: ${DEBT_SCORE:-0}"
echo " Budget Limit: ${MAX_NEW_DEBT_POINTS}"
echo "═══════════════════════════════════"
if [ "${DEBT_SCORE:-0}" -gt "$MAX_NEW_DEBT_POINTS" ]; then
echo ""
echo "⛔ TECHNICAL DEBT BUDGET EXCEEDED"
echo " This PR introduces ${DEBT_SCORE} debt points against a budget of ${MAX_NEW_DEBT_POINTS}."
echo " Please address existing issues before merging or discuss an exception"
echo " with the architecture team in #tech-debt-exceptions."
echo ""
exit 1
fi
echo "✅ Technical debt budget check passed."
exit 0
4. Architecture Decision Records for Debt Prevention
Many debts arise from undocumented architectural choices that later developers don't understand. Architecture Decision Records (ADRs) create a living history that prevents uninformed shortcuts:
# Architecture Decision Record: ADR-0042
# Date: 2024-06-15
# Status: Accepted
# Context: We need to choose between monolith decomposition strategies
## Decision
We will use the Strangler Fig pattern to extract the billing module from the
monolithic `core-service` into a standalone `billing-service`.
## Rationale
- Allows gradual migration without a big-bang rewrite
- Production traffic can be routed incrementally (10% → 50% → 100%)
- Rollback is trivial at any stage by reverting the routing rule
- Maintains backward compatibility during the 6-month transition window
## Consequences (Debt Prevention)
- TEMPORARY DEBT: The routing proxy introduces latency of ~3ms until migration completes
- ACCEPTED CONSTRAINT: All new billing features MUST be built in billing-service,
not in core-service/billing/*
- SUNSET PLAN: core-service/billing/* will be removed by 2025-01-01
- TESTING REQUIREMENT: Integration tests must pass against both old and new paths
until cutover is complete
## Alternatives Considered
- Big-bang rewrite: Rejected due to unacceptable risk to payment processing
- Parallel run with reconciliation: Rejected due to doubled infrastructure costs
- Do nothing: Rejected because core-service billing module has 47 known bugs (TD-2023-*)
Practical Refactoring Patterns for Debt Reduction
When you've identified debt hotspots, these patterns help you reduce debt systematically without breaking production:
1. Extract Method/Object to Reduce Complexity
// BEFORE: A 250-line function with cyclomatic complexity of 23
// This function handles validation, transformation, persistence, and notification
async function checkoutPipeline(cart, user, paymentMethod, shippingAddress) {
// 50 lines of cart validation
if (!cart || !cart.items) { /* ... */ }
// ... many validation branches ...
// 60 lines of price calculation with discounts, taxes, promotions
let subtotal = 0;
// ... complex pricing logic with 8 conditional branches ...
// 40 lines of inventory reservation
// ... locking, atomic updates, rollback logic ...
// 30 lines of payment processing
// ... gateway selection, retry logic, fraud checks ...
// 40 lines of order creation and email notification
// ... database writes, event publishing, email templating ...
// 30 lines of logging and metrics
}
// AFTER: Decomposed into orchestrated steps, each testable in isolation
// Main orchestrator — complexity reduced to coordination only
async function checkoutPipeline(cart, user, paymentMethod, shippingAddress) {
const pipeline = new CheckoutPipeline([
new ValidateCartStep(cartValidator),
new CalculatePricingStep(pricingEngine),
new ReserveInventoryStep(inventoryManager),
new ProcessPaymentStep(paymentGateway, fraudDetector),
new CreateOrderStep(orderRepository, eventBus),
new SendConfirmationStep(notificationService),
]);
const context = { cart, user, paymentMethod, shippingAddress };
try {
const result = await pipeline.execute(context);
metrics.recordCheckout(result.orderId, 'success');
return result;
} catch (error) {
await pipeline.compensate(context, error.currentStep);
metrics.recordCheckout(null, 'failure');
throw error;
}
}
// Each step is now a focused class with single responsibility
class ValidateCartStep implements PipelineStep {
constructor(private validator: CartValidator) {}
async execute(ctx: CheckoutContext): Promise {
const errors = this.validator.validate(ctx.cart);
if (errors.length > 0) {
throw new CheckoutError('CART_VALIDATION_FAILED', errors);
}
return ctx;
}
async compensate(ctx: CheckoutContext): Promise {
// No side effects to undo — validation is read-only
}
}
2. Replace Magic Numbers with Centralized Configuration
// BEFORE: Magic numbers scattered across 18 files
// Each instance represents a potential divergence bug
function calculateShipping(weight) {
if (weight < 5) return 9.99; // Magic number
if (weight < 20) return 14.99; // Magic number
return 24.99; // Magic number
}
function estimateDelivery(distance) {
if (distance < 100) return 1; // 1 day — magic number
if (distance < 500) return 3; // 3 days — magic number
return 7; // 7 days — magic number
}
// AFTER: Centralized configuration with environment-specific overrides
// config/shipping-rates.ts
export const SHIPPING_CONFIG = {
tiers: [
{ maxWeight: 5, cost: 9.99, label: 'Light Package' },
{ maxWeight: 20, cost: 14.99, label: 'Standard Package' },
{ maxWeight: Infinity, cost: 24.99, label: 'Heavy Package' }
],
deliveryEstimates: {
local: { maxDistance: 100, days: 1 },
regional: { maxDistance: 500, days: 3 },
longHaul: { maxDistance: Infinity, days: 7 }
}
} as const;
// config/shipping-rates.eu.ts (override for European market)
export const EU_SHIPPING_OVERRIDES = {
tiers: [
{ maxWeight: 5, cost: 7.50 }, // EUR pricing
{ maxWeight: 20, cost: 11.25 },
{ maxWeight: Infinity, cost: 18.75 }
]
};
// Usage
import { SHIPPING_CONFIG } from '@/config/shipping-rates';
import { getRegionalOverrides } from '@/config/regionResolver';
function calculateShipping(weight: number, region: string): number {
const config = getRegionalOverrides(region) ?? SHIPPING_CONFIG;
const tier = config.tiers.find(t => weight < t.maxWeight);
return tier?.cost ?? config.tiers[config.tiers.length - 1].cost;
}
3. Introduce Test Coverage as Debt Insurance
Tests don't eliminate existing debt, but they prevent debt from spreading. When you add tests to a debt-laden module, you create a safety net that enables future refactoring:
// Characterizing existing behavior before refactoring a legacy module
// These are "characterization tests" — they lock down current behavior,
// including bugs, so you can refactor without fear of regression
import { LegacyDiscountCalculator } from '../src/discounts/legacy-calculator';
import { createTestOrder, createTestCustomer } from './fixtures';
describe('LegacyDiscountCalculator — Characterization Tests', () => {
let calculator: LegacyDiscountCalculator;
beforeEach(() => {
calculator = new LegacyDiscountCalculator();
// This constructor has 8 undocumented dependencies injected via global state.
// We set them up as the legacy system expects, without judging correctness.
global.DISCOUNT_RULES_PATH = './test-fixtures/rules.xml';
global.PROMO_ACTIVE = true;
});
// Test 1: Document the current (possibly buggy) behavior for VIP customers
it('calculates discount for VIP customer with >10 orders', () => {
const customer = createTestCustomer({ tier: 'VIP', orderCount: 15 });
const order = createTestOrder({ subtotal: 200 });
const discount = calculator.calculate(customer, order);
// We assert the CURRENT behavior, not the IDEAL behavior.
// The system currently gives 15% to VIPs with >10 orders,
// but the business docs say it should be 20%. This discrepancy
// IS the technical debt we're characterizing.
expect(discount).toBe(30); // 15% of 200 = 30 (actual, not ideal 40)
expect(discount).toBeLessThan(40); // Documenting the gap
});
// Test 2: Edge case — what happens with negative subtotals (refunds)?
it('handles negative subtotals by returning 0 discount', () => {
const customer = createTestCustomer({ tier: 'VIP', orderCount: 5 });
const order = createTestOrder({ subtotal: -50 });
const discount = calculator.calculate(customer, order);
expect(discount).toBe(0);
// This behavior is correct but undocumented — we're preserving it
});
// Test 3: The known bug — promo flag is read once at startup
it('does NOT react to mid-session promo flag changes (KNOWN BUG #LEG-42)', () => {
const customer = createTestCustomer({ tier: 'REGULAR', orderCount: 1 });
const order = createTestOrder({ subtotal: 100 });
// Calculate once with promo active
const discount1 = calculator.calculate(customer, order);
// Simulate promo ending mid-session
global.PROMO_ACTIVE = false;
const discount2 = calculator.calculate(customer, order);
// BUG: Should be different, but calculator caches the promo flag
expect(discount2).toBe(discount1); // Locking in the buggy behavior
// TODO: Fix in TD-2024-0089 — requires cache invalidation architecture
});
// Test 4: Null-safety — what happens with missing customer data?
it('returns 0 when customer has no tier (defensive, not documented)', () => {
const customer = createTestCustomer({ tier: null, orderCount: 5 });
const order = createTestOrder({ subtotal: 100 });
// This works by accident — a null tier falls through all switch cases
expect(calculator.calculate(customer, order)).toBe(0);
});
});
Organizational Best Practices
Technical debt management is as much a cultural practice as a technical one. These organizational patterns have proven effective in large codebases:
- Debt Sprints: Allocate 10-20% of sprint capacity exclusively to debt reduction. Rotate the focus across teams to prevent burnout and ensure broad ownership.
- Debt Dashboards: Maintain a visible dashboard showing debt trends over time. When teams see the line trending down, motivation increases. When it trends up, leadership can intervene before it becomes critical.
- Refactoring Guild: Establish a cross-functional group of senior engineers who review debt backlogs, approve major refactoring proposals, and mentor teams on safe debt reduction techniques.
- Debt Budgets per Team: Give each team a quantified debt allowance. Teams that stay under budget earn autonomy; teams exceeding it get support from the refactoring guild.
- Celebrate Debt Reduction: Publicly recognize engineers who eliminate significant debt. A refactoring that removes 500 lines of dead code or consolidates 12 duplicate functions deserves as much celebration as a new feature.
- Onboarding Debt Audits: Have new team members document debt they encounter during their first two weeks. Fresh eyes spot debt that tenured developers have learned to work around unconsciously.
Measuring Technical Debt Over Time
You can't manage what you don't measure. This script generates a trend report that tracks debt metrics across commits, giving you a historical view:
#!/usr/bin/env python3
"""
debt_trend_reporter.py — Generates a historical technical debt trend report
by analyzing git history and running debt metrics at each sampled commit.
"""
import subprocess
import json
import os
from datetime import datetime, timedelta
from collections import defaultdict
def get_commit_history(repo_path, days_back=90):
"""Sample commits at weekly intervals to build a trend line."""
commits = []
cmd = [
'git', '-C', repo_path, 'log',
'--format=%H|%aI|%s',
'--since', f'{days_back} days ago',
'--reverse'
]
output = subprocess.check_output(cmd, text=True)
for line in output.strip().split('\n'):
if not line:
continue
hash_val, date_str, subject = line.split('|', 2)
commits.append({
'hash': hash_val,
'date': date_str,
'subject': subject
})
return commits
def analyze_debt_at_commit(repo_path, commit_hash):
"""Run debt metrics at a specific commit snapshot."""
# Create a temporary checkout (or use git worktree)
temp_dir = f'/tmp/debt_analysis_{commit_hash[:8]}'
subprocess.run(['git', '-C', repo_path, 'checkout', commit_hash], check=True)
# Run your debt scanner (reuse the DebtScanner from earlier example)
# For brevity, we'll simulate with file counts and TODO counts
result = subprocess.run(
['grep', '-r', '-c', '-E', '(TODO|FIXME|HACK|XXX)', '--include=*.{js,ts,py,java}',
'--exclude-dir=node_modules', '--exclude-dir=.git', '.'],
capture_output=True, text=True, cwd=repo_path
)
todo_counts = {}
for line in result.stdout.strip().split('\n'):
if ':' in line:
filepath, count = line.rsplit(':', 1)
todo_counts[filepath] = int(count)
total_todos = sum(todo_counts.values())
# Count lines of code as a rough complexity metric
loc_result = subprocess.run(
['find', '.', '-name', '*.js', '-o', '-name', '*.ts', '-o', '-name', '*.py',
'!', '-path', '*/node_modules/*', '!', '-path', '*/.git/*',
'-exec', 'cat', '{}', ';'],
capture_output=True, text=True, cwd=repo_path
)
total_loc = len(loc_result.stdout.split('\n')) if loc_result.stdout else 0
return {
'commit': commit_hash[:12],
'total_todos': total_todos,
'total_loc': total_loc,
'todo_density': round(total_todos / max(total_loc, 1) * 1000, 2), # per 1K LOC
'files_with_debt': len(todo_counts)
}
def generate_trend_report(repo_path='.', days=90):
"""Generate a Markdown report showing debt trends over time."""
commits = get_commit_history(repo_path, days)
# Sample weekly (too many commits otherwise)
sampled = []
last_week = None
for commit in commits:
week = datetime.fromisoformat(commit['date']).isocalendar()[1]
if week != last_week:
sampled.append(commit)
last_week = week
print(f"\n{'='*70}")
print(f"TECHNICAL DEBT TREND REPORT")
print(f"Period: {sampled[0]['date'][:10]} to {sampled[-1]['date'][:10]}")
print(f"Sampled commits: {len(sampled)}")
print(f"{'='*70}\n")
print(f"{'Date':<12} {'Commit':<14} {'TODOs':>8} {'LOC':>10} {'Density':>10} {'Files':>8}")
print(f"{'-'*12} {'-'*14} {'-'*8} {'-'*10} {'-'*10} {'-'*8}")
previous = None
for commit in sampled:
metrics = analyze_debt_at_commit(repo_path, commit['hash'])
delta = ''
if previous:
change = metrics['total_todos']