← Back to DevBytes

Managing Technical Debt in Large Codebases

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:

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:

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']

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles