Module 7: Subagents for Debugging, Regenerate vs Edit

Decision Framework: Regenerate vs Edit

Decision Framework: Regenerate vs Edit

Capsule overview

Capsules 03 and 04 gave you the signals for each side of the spectrum: when to regenerate and when to edit. But reality is rarely black or white. The most interesting cases — and the most frequent — are in the gray zone where the answer isn't obvious.

This capsule integrates everything into a step-by-step decision framework: you assess the damage, estimate times, consider what's lost, and make an informed decision. It's not a magic formula — it's a process that structures your thinking so the decision is fast and justifiable.

And most importantly: 5 realistic scenarios where the answer isn't obvious. Real code with real problems where you need to apply the framework, justify your decision, and defend your choice. Because the difference between a competent developer and a senior developer isn't having the framework — it's knowing how to apply it under pressure with nuance.


The Decision Framework: 5 Steps

Overview

┌─────────────────┐
│  1. ASSESS       │
│     DAMAGE       │ → How much of the code needs to change?
└───────┬─────────┘
        │
┌───────▼─────────┐
│  2. ESTIMATE     │
│     EDITING      │ → How long does it take me to edit?
└───────┬─────────┘
        │
┌───────▼─────────┐
│  3. ESTIMATE     │
│     REGENERATION │ → How long does it take me to regenerate?
└───────┬─────────┘
        │
┌───────▼─────────┐
│  4. CONSIDER     │
│     LOSSES       │ → What do I lose if I regenerate?
└───────┬─────────┘
        │
┌───────▼─────────┐
│  5. DECIDE       │
│                  │ → Edit | Regenerate function | Regenerate file
└─────────────────┘

Step 1: Assess the damage

Before deciding how to fix, you need to understand how deep the problem is.

Questions to assess:

A. Is the fundamental approach correct?
   YES → Probably edit
   NO → Probably regenerate

B. How many lines need to change?
   1-5     → Edit
   5-20    → It depends (go to step 2)
   20+     → Probably regenerate

C. Are the changes independent or interconnected?
   Independent → Edit (you change them one by one with no risk)
   Interconnected → Regenerating has less risk of inconsistency

D. Is the problem a design or an implementation problem?
   Design → Regenerate (patching a design creates a Frankenstein)
   Implementation → Edit (the design is already correct)

Quick example:

Code: an endpoint that searches for tasks in the DB
Problem: uses a Python loop instead of SQL WHERE

A. Correct approach? NO (should use SQL, not a Python loop)
B. Lines that change? ~15 of 30
C. Interconnected changes? YES (changing the query affects the processing)
D. Design or implementation? DESIGN (the approach is incorrect)

Assessment: regenerate the function

Step 2: Estimate editing time

If step 1 suggests editing, estimate how long it will take:

Approximate formula:
  Time = (# of edits × average complexity) + verification

Where:
  Simple edit (change an operator, add a line): 1-2 min
  Medium edit (add a block, change logic): 3-5 min
  Complex edit (restructure a function): 5-10 min
  Verification: 3-5 min (always)

Example:
  3 simple edits + 1 medium + verification
  = (3 × 1.5) + (1 × 4) + 4
  = 4.5 + 4 + 4
  = 12.5 min

Step 3: Estimate regeneration time

Approximate formula:
  Time = prompt + generation + review + customizations + verification

Where:
  Detailed prompt: 3-5 min
  Generation: 1-2 min
  Review of the new code: 3-10 min (depends on complexity)
  Re-apply customizations: 0-10 min (depends on how many)
  Verification: 3-5 min

Example (simple function without customizations):
  = 3 + 1 + 3 + 0 + 3 = 10 min

Example (complex endpoint with logging and audit):
  = 5 + 2 + 8 + 8 + 5 = 28 min

Step 4: Consider losses

This step is where many developers go wrong — they forget what they lose by regenerating.

Losses checklist:

☐ Manual customizations I made after the generation
  → Logging, messages in Spanish, specific format
  → How many lines? How long did they take?

☐ Context accumulated in the Claude Code session
  → Does Claude Code understand my style, my preferences?
  → Will losing that context make the new code inconsistent?

☐ Integrations with the rest of the codebase
  → Does the current code integrate with other components?
  → Will the regenerated code maintain those integrations?

☐ Tests that already pass
  → Are there tests that validate the current code?
  → Will the regenerated code pass those tests without changes?

☐ Knowledge I already have of the code
  → "I already know exactly what this function does"
  → "With new code, I'd have to re-read and re-understand"

Step 5: Decide

With the information from steps 1-4, the decision is usually clear:

Decision matrix:

If assess_damage says "incorrect approach" → Regenerate
   (regardless of the other factors)

If edit_time < regeneration_time AND losses > 0 → Edit
   (faster and you preserve customizations)

If edit_time > regeneration_time AND losses = 0 → Regenerate
   (faster and you lose nothing)

If edit_time ≈ regeneration_time → Edit
   (in a tie, editing preserves context)

If the edits are interconnected and complex → Regenerate function
   (reduce the risk of inconsistency)

The tie-breaker: When the times are similar, editing wins because it preserves context. Only regenerate when there's a clear advantage in time or quality.


The Complete Spectrum of Options

It's not just "edit" or "regenerate." There's a full spectrum:

EDIT                                                       REGENERATE
  │                                                              │
  ├── Edit 1 line                                               │
  │   "Change > to <"                                           │
  │                                                              │
  ├── Edit several lines                                        │
  │   "Add null check, add ownership check"                     │
  │                                                              │
  ├── Edit a block                                              │
  │   "Rewrite the entire try/except"                           │
  │                                                              │
  ├── Regenerate a function                                     │
  │   "Regenerate only calculate_stats(), keep the rest"        │
  │                                                              │
  ├── Regenerate several functions                              │
  │   "Regenerate the 3 CRUD functions, keep imports and        │
  │    configuration"                                            │
  │                                                              │
  └── Regenerate the entire file ──────────────────────────────┘
      "The whole file needs a different approach"

The decision isn't binary. And most of the time, the answer is in the middle: regenerate part, edit the rest.

Common combinations

Scenario 1: "Regenerate function + edit imports"
→ The function has the wrong approach
→ But the file's imports and configuration are fine
→ You regenerate the function, edit the imports if they changed

Scenario 2: "Edit endpoint + regenerate schema"
→ The endpoint is fine but the Pydantic schema has no validations
→ You regenerate the schema with Field constraints and validators
→ You edit the endpoint only if the schema changed its name

Scenario 3: "Regenerate logic + preserve integration"
→ The calculation logic is incorrect
→ But it has logging, an audit trail, and integrated notifications
→ You regenerate the core logic, preserve the integrations

The 5 Scenarios: Practice the Framework

Each scenario presents real code with real problems. Apply the 5-step framework, make your decision, and then compare it with the detailed analysis.

Scenario 1: The Reports Service

Claude Code generated this reports service that has several problems:

from datetime import datetime, timedelta
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session
from sqlalchemy import func, and_
from pydantic import BaseModel
from app.database import get_db
from app.models import Task, User
from app.dependencies.auth import get_current_user
import logging

logger = logging.getLogger(__name__)

app = FastAPI()


class ReportResponse(BaseModel):
    period: str
    total_tasks: int
    completed_tasks: int
    completion_rate: float
    avg_completion_days: float
    most_active_user: str | None
    tasks_by_priority: dict

    model_config = {"from_attributes": True}


@app.get("/api/reports/weekly", response_model=ReportResponse)
async def weekly_report(
    current_user=Depends(get_current_user),
    db: Session = Depends(get_db),
):
    logger.info(f"Generando reporte semanal para user {current_user.id}")

    one_week_ago = datetime.utcnow() - timedelta(days=7)

    all_tasks = (
        db.query(Task)
        .filter(Task.created_at >= one_week_ago)
        .all()
    )

    total = len(all_tasks)
    completed = [t for t in all_tasks if t.status == "completed"]
    completion_rate = len(completed) / total if total > 0 else 0

    avg_days = 0
    for task in completed:
        if task.completed_at:
            delta = (task.completed_at - task.created_at).days
            avg_days += delta
    if len(completed) > 0:
        avg_days = avg_days / len(completed)

    # Find the most active user
    user_counts = {}
    for task in all_tasks:
        uid = task.owner_id
        user_counts[uid] = user_counts.get(uid, 0) + 1

    most_active_id = max(user_counts, key=user_counts.get) if user_counts else None
    most_active_user = None
    if most_active_id:
        user = db.query(User).filter(User.id == most_active_id).first()
        most_active_user = user.name if user else None

    # Tasks by priority
    priority_counts = {}
    for task in all_tasks:
        p = task.priority
        priority_counts[p] = priority_counts.get(p, 0) + 1

    logger.info(f"Reporte generado: {total} tareas, {completion_rate:.1%} completion")

    return ReportResponse(
        period="weekly",
        total_tasks=total,
        completed_tasks=len(completed),
        completion_rate=round(completion_rate, 4),
        avg_completion_days=round(avg_days, 2),
        most_active_user=most_active_user,
        tasks_by_priority=priority_counts,
    )

Identified problems:

  1. Loads ALL the tasks into memory to do calculations in Python
  2. Doesn't filter by the current user (shows tasks from all users)
  3. The task count by priority and the most active user are calculated in Python
  4. avg_completion_days uses .days (truncates to integers) instead of .total_seconds() / 86400

Apply the framework and decide.

See solution

Step 1: Assess the damage

A. Correct approach? PARTIALLY
   - The "search tasks → calculate stats" approach is correct
   - BUT the implementation loads everything into memory (incorrect)
   - And it's missing the filter by user (security bug)
   
B. Lines that change? ~30 of 60 (50%)
   
C. Interconnected? YES
   - Changing the queries affects all the calculations
   
D. Design or implementation? IMPLEMENTATION
   - The design (endpoint → calculate stats → response) is correct
   - The implementation (Python loops vs SQL) is incorrect

Step 2: Estimate editing

Necessary edits:
1. Add owner_id filter: 2 min (simple)
2. Change total/completed to SQL count: 5 min (medium)
3. Change avg_days to SQL avg: 5 min (medium)
4. Change most_active to SQL subquery: 8 min (complex)
5. Change priority_counts to SQL group by: 5 min (medium)
6. Fix .days → .total_seconds()/86400: 1 min (simple)
7. Verification: 5 min

Total estimated: ~31 min

Step 3: Estimate regeneration

Detailed prompt (include what to preserve): 5 min
Generation: 2 min
Review: 5 min
Re-apply logging (2 lines): 2 min
Verification: 5 min

Total estimated: ~19 min

Step 4: Consider losses

Customizations:
- 2 lines of logging (easy to re-add)
- Log messages in Spanish (include in the prompt)
- ReportResponse schema (include in the prompt)

Losses: Minimal (simple logging, easy to specify in the prompt)

Step 5: Decide

→ REGENERATE the weekly_report function

Justification:

  • 50% of the code needs to change
  • The edits are interconnected (changing queries affects everything)
  • The regeneration time (~19 min) is significantly less than editing (~31 min)
  • The losses are minimal (2 lines of logging)
  • Regenerating produces more consistent code than editing 5 individual queries

BUT: keep the ReportResponse schema and the imports. Only regenerate the function.

Regeneration prompt:

Regenerate ONLY the weekly_report function. Keep the 
ReportResponse schema and the imports exactly as they are.

Problems with the current code that you must fix:
1. Loads all tasks into memory — use SQL aggregations
2. Missing filter by current_user.id — add it
3. avg_completion_days uses .days (truncates) — use total_seconds/86400
4. The most active user and priority count calculations 
   should be SQL, not Python

Preserve:
- The logging with logger.info (same format and messages in Spanish)
- The same Depends(get_current_user, get_db)
- The response_model=ReportResponse
- The completion_rate calculation with round(_, 4)

Scenario 2: The Rate Limiting Middleware

import time
from collections import defaultdict
from fastapi import FastAPI, Request, HTTPException
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import JSONResponse

app = FastAPI()


class RateLimitMiddleware(BaseHTTPMiddleware):
    def __init__(self, app, requests_per_minute: int = 60):
        super().__init__(app)
        self.requests_per_minute = requests_per_minute
        self.requests: dict[str, list[float]] = defaultdict(list)

    async def dispatch(self, request: Request, call_next):
        client_ip = request.client.host
        now = time.time()

        self.requests[client_ip] = [
            ts for ts in self.requests[client_ip]
            if now - ts < 60
        ]

        if len(self.requests[client_ip]) >= self.requests_per_minute:
            return JSONResponse(
                status_code=429,
                content={"detail": "Rate limit exceeded. Try again later."},
                headers={
                    "Retry-After": "60",
                    "X-RateLimit-Limit": str(self.requests_per_minute),
                    "X-RateLimit-Remaining": "0",
                },
            )

        self.requests[client_ip].append(now)

        response = await call_next(request)

        remaining = self.requests_per_minute - len(self.requests[client_ip])
        response.headers["X-RateLimit-Limit"] = str(self.requests_per_minute)
        response.headers["X-RateLimit-Remaining"] = str(max(0, remaining))

        return response


app.add_middleware(RateLimitMiddleware, requests_per_minute=100)

Problems:

  1. Stores requests in memory (lost on restart, doesn't work with multiple workers)
  2. request.client.host can be None if there's a proxy (needs X-Forwarded-For)
  3. The cleanup of old timestamps happens on every request (O(n) per request)
  4. The self.requests dict grows without limit (doesn't clean up inactive IPs)

Should the whole middleware be moved to Redis? Or can you patch the problems?

See solution

Step 1: Assess

A. Correct approach? IT DEPENDS ON THE CONTEXT
   - For development/single worker: the in-memory approach is acceptable
   - For production/multi worker: it needs Redis
   
B. Lines that change? 
   - If it stays in memory: ~10 of 40 (25%) → edit
   - If it migrates to Redis: ~35 of 40 (87%) → regenerate
   
C. Interconnected? If it migrates to Redis, yes
   
D. Design or implementation?
   - If it stays in memory: implementation (patch the problems)
   - If it migrates to Redis: design (architectural change)

The key question: Is this middleware going to production with multiple workers?

If NOT (development or single worker):

→ EDIT problems 2, 3, and 4. Keep it in-memory.

async def dispatch(self, request: Request, call_next):
    client_ip = request.headers.get("X-Forwarded-For", "").split(",")[0].strip()
    if not client_ip:
        client_ip = request.client.host if request.client else "unknown"
    
    now = time.time()
    window_start = now - 60
    
    self.requests[client_ip] = [
        ts for ts in self.requests[client_ip] if ts > window_start
    ]
    
    # Clean up inactive IPs periodically
    if len(self.requests) > 10000:
        inactive = [
            ip for ip, timestamps in self.requests.items()
            if not timestamps or timestamps[-1] < window_start
        ]
        for ip in inactive:
            del self.requests[ip]
    # ... rest the same

3 targeted edits (~12 min). The in-memory approach is valid for the context.

If YES (production with multiple workers):

→ REGENERATE the entire middleware with Redis.

The in-memory approach is fundamentally incorrect for this context. You can't "edit" an in-memory dict to make it shared between workers — you need to change the architecture to Redis.

The lesson: The same question ("do I edit or regenerate?") has different answers depending on the context. The framework makes you consider the context before deciding.

Scenario 3: The Permission System

from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session
from app.database import get_db
from app.models import Task, User
from app.dependencies.auth import get_current_user

app = FastAPI()


@app.get("/tasks/{task_id}")
async def get_task(
    task_id: int,
    current_user=Depends(get_current_user),
    db: Session = Depends(get_db),
):
    task = db.query(Task).filter(Task.id == task_id).first()
    if not task:
        raise HTTPException(404, "Task not found")
    if task.owner_id != current_user.id:
        raise HTTPException(403, "Not authorized")
    return task


@app.patch("/tasks/{task_id}")
async def update_task(
    task_id: int,
    title: str | None = None,
    current_user=Depends(get_current_user),
    db: Session = Depends(get_db),
):
    task = db.query(Task).filter(Task.id == task_id).first()
    if not task:
        raise HTTPException(404, "Task not found")
    if task.owner_id != current_user.id:
        raise HTTPException(403, "Not authorized")
    if title:
        task.title = title
    db.commit()
    return task


@app.delete("/tasks/{task_id}")
async def delete_task(
    task_id: int,
    current_user=Depends(get_current_user),
    db: Session = Depends(get_db),
):
    task = db.query(Task).filter(Task.id == task_id).first()
    if not task:
        raise HTTPException(404, "Task not found")
    if task.owner_id != current_user.id:
        raise HTTPException(403, "Not authorized")
    db.delete(task)
    db.commit()
    return {"detail": "Deleted"}

Problems:

  1. The permission check is repeated in 3 endpoints (DRY violation)
  2. update_task doesn't use a Pydantic model for the body
  3. delete_task doesn't let admins delete other users' tasks
  4. No endpoint has a response_model
  5. update_task doesn't use exclude_unset for partial updates

That's 5 problems. Do you edit or regenerate?

See solution

Step 1: Assess

A. Correct approach? YES
   - CRUD endpoints with auth and ownership — correct
   - The endpoint → query → verify → act structure is correct
   
B. Lines that change?
   - Problem 1 (DRY): create a dependency, change 3 functions (~15 lines)
   - Problem 2: create a model, change params (~8 lines)
   - Problem 3: change 1 condition (~3 lines)
   - Problem 4: add 3 response_models (~3 lines)
   - Problem 5: change the update logic (~5 lines)
   Total: ~34 lines of ~55 = 62%
   
C. Interconnected? PARTIALLY
   - Problem 1 (DRY) affects all 3 endpoints
   - The others are independent
   
D. Design or implementation? IMPLEMENTATION
   - The CRUD design is correct
   - The implementation has quality problems

Step 2: Estimate editing

1. Create a dependency (new function + change 3 endpoints): 10 min
2. Create a Pydantic model + change update: 5 min
3. Add admin check to delete: 2 min
4. Add response_model × 3: 2 min
5. Add exclude_unset: 3 min
Verification: 5 min
Total: ~27 min

Step 3: Estimate regeneration

Prompt (specify DRY, schemas, admin rule): 5 min
Generation: 2 min
Review: 5 min
Customizations: 0 min (there are none)
Verification: 5 min
Total: ~17 min

Step 4: Consider losses

Customizations: none visible
Integrations: none special
Context: low (simple code, no accumulated state)
Losses: minimal

Step 5: Decide

→ COMBINATION: Regenerate the file + create the new dependency

Justification:

  • 62% of the code needs to change
  • Regenerating (~17 min) is faster than editing (~27 min)
  • There are no customizations to lose
  • The changes are partially interconnected (the DRY fix affects everything)

The mixed approach is:

  1. Create the get_task_or_404 dependency first (new function, doesn't require regenerating)
  2. Regenerate the 3 endpoints with an instruction to use the dependency
  3. Create the TaskUpdate Pydantic model (new, doesn't require regenerating)

This produces more consistent code than editing 5 problems one by one.

Scenario 4: The Export Endpoint

import csv
import io
from fastapi import FastAPI, Depends, Query
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session, joinedload
from app.database import get_db
from app.models import Task, Category, Tag
from app.dependencies.auth import get_current_user
import logging

logger = logging.getLogger(__name__)

app = FastAPI()


@app.get("/api/tasks/export")
async def export_tasks(
    format: str = Query("csv", regex="^(csv|json)$"),
    status: str | None = Query(None),
    current_user=Depends(get_current_user),
    db: Session = Depends(get_db),
):
    logger.info(
        f"Exportando tareas en formato {format}",
        extra={"user_id": current_user.id, "format": format},
    )

    query = (
        db.query(Task)
        .options(joinedload(Task.category), joinedload(Task.tags))
        .filter(Task.owner_id == current_user.id)
    )

    if status:
        query = query.filter(Task.status == status)

    tasks = query.order_by(Task.created_at.desc()).all()

    if format == "csv":
        output = io.StringIO()
        writer = csv.writer(output)
        writer.writerow([
            "ID", "Título", "Descripción", "Estado",
            "Prioridad", "Categoría", "Tags", "Creado", "Actualizado",
        ])

        for task in tasks:
            writer.writerow([
                task.id,
                task.title,
                task.description or "",
                task.status,
                task.priority,
                task.category.name if task.category else "",
                ", ".join(t.name for t in task.tags) if task.tags else "",
                task.created_at.isoformat(),
                task.updated_at.isoformat() if task.updated_at else "",
            ])

        output.seek(0)
        logger.info(f"Exportación CSV completada: {len(tasks)} tareas")
        return StreamingResponse(
            iter([output.getvalue()]),
            media_type="text/csv",
            headers={"Content-Disposition": "attachment; filename=tasks.csv"},
        )

    else:
        task_list = []
        for task in tasks:
            task_list.append({
                "id": task.id,
                "title": task.title,
                "description": task.description,
                "status": task.status,
                "priority": task.priority,
                "category": task.category.name if task.category else None,
                "tags": [t.name for t in task.tags] if task.tags else [],
                "created_at": task.created_at.isoformat(),
                "updated_at": task.updated_at.isoformat() if task.updated_at else None,
            })

        logger.info(f"Exportación JSON completada: {len(tasks)} tareas")
        return task_list

Problems:

  1. StreamingResponse(iter([output.getvalue()])) — loads everything into memory, it's not real streaming. With 100,000 tasks, it consumes a lot of RAM.
  2. The manual JSON serialization duplicates the CSV logic (it should be a schema or a shared function).
  3. There's no export limit — a user with 1M tasks crashes the server.

Do you edit or regenerate?

See solution

Step 1: Assess

A. Correct approach? YES (mostly)
   - Export endpoint with filters and auth: correct
   - Eager loading of relationships: correct
   - CSV and JSON support: correct
   - The problem is the streaming implementation and the limits
   
B. Lines that change?
   - Problem 1 (real streaming): ~15 lines of the CSV section
   - Problem 2 (DRY): refactor the serialization (~10 lines)
   - Problem 3 (limit): add ~5 lines
   Total: ~30 of ~70 = 43%
   
C. Interconnected? PARTIALLY
   - The streaming and the serialization are independent
   - The limit is independent of everything
   
D. Design or implementation? IMPLEMENTATION
   - The endpoint design is correct
   - The streaming implementation needs improvement

Step 2: Estimate editing: ~22 min Step 3: Estimate regeneration: ~25 min (high because of the customizations)

Step 4: Consider losses

SIGNIFICANT customizations:
- ✅ Structured logging with extra fields (4 lines)
- ✅ CSV headers in Spanish
- ✅ Eager loading of category and tags
- ✅ Date format with isoformat()
- ✅ Null handling in category and tags

Losing these customizations would take ~8 min to re-apply

Step 5: Decide

→ EDIT

Justification:

  • The approach is correct (~43% of changes, near the threshold)
  • The customizations are significant (~8 min to re-apply)
  • The 3 problems are independent (I can edit them one by one)
  • Editing (~22 min) is faster than regenerating (~25 min + risk of losing customizations)

Editing plan:

  1. Add a limit (independent, 5 min):

    MAX_EXPORT = 10000
    total = query.count()
    if total > MAX_EXPORT:
        raise HTTPException(400, f"Máximo {MAX_EXPORT} tareas por exportación")
  2. Implement real streaming for CSV (10 min): Change from loading everything to a generator that yields rows

  3. Extract a serialization function (7 min): Create serialize_task(task) and use it in both formats

Scenario 5: The Dashboard Metrics Calculation

from fastapi import FastAPI, Depends
from sqlalchemy.orm import Session
from datetime import datetime, timedelta
from app.database import get_db
from app.models import Task
from app.dependencies.auth import get_current_user

app = FastAPI()


@app.get("/api/dashboard/metrics")
async def dashboard_metrics(
    current_user=Depends(get_current_user),
    db: Session = Depends(get_db),
):
    now = datetime.utcnow()
    today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
    week_start = today_start - timedelta(days=today_start.weekday())
    month_start = today_start.replace(day=1)

    # Today's tasks
    today_tasks = (
        db.query(Task)
        .filter(Task.owner_id == current_user.id, Task.created_at >= today_start)
        .all()
    )
    today_created = len(today_tasks)
    today_completed = len([t for t in today_tasks if t.status == "completed"])

    # This week's tasks
    week_tasks = (
        db.query(Task)
        .filter(Task.owner_id == current_user.id, Task.created_at >= week_start)
        .all()
    )
    week_created = len(week_tasks)
    week_completed = len([t for t in week_tasks if t.status == "completed"])

    # This month's tasks
    month_tasks = (
        db.query(Task)
        .filter(Task.owner_id == current_user.id, Task.created_at >= month_start)
        .all()
    )
    month_created = len(month_tasks)
    month_completed = len([t for t in month_tasks if t.status == "completed"])

    # Streak: consecutive days with at least 1 completed task
    streak = 0
    check_date = today_start
    while True:
        day_end = check_date + timedelta(days=1)
        day_completed = (
            db.query(Task)
            .filter(
                Task.owner_id == current_user.id,
                Task.status == "completed",
                Task.completed_at >= check_date,
                Task.completed_at < day_end,
            )
            .count()
        )
        if day_completed > 0:
            streak += 1
            check_date -= timedelta(days=1)
        else:
            break

    # Grand total
    all_tasks = (
        db.query(Task)
        .filter(Task.owner_id == current_user.id)
        .all()
    )
    total = len(all_tasks)
    total_completed = len([t for t in all_tasks if t.status == "completed"])

    return {
        "today": {"created": today_created, "completed": today_completed},
        "week": {"created": week_created, "completed": week_completed},
        "month": {"created": month_created, "completed": month_completed},
        "streak": streak,
        "total": {"all": total, "completed": total_completed},
        "completion_rate": round(total_completed / total, 4) if total > 0 else 0,
    }

Problems:

  1. Runs 4 queries that load all the tasks into memory (today, week, month, all) + 1 query per day for the streak
  2. The 3 period queries (today, week, month) are redundant — today_tasks is a subset of week_tasks which is a subset of month_tasks
  3. The streak loop runs one query per day — if the user has a 365-day streak, that's 365 queries
  4. No response_model
  5. datetime.utcnow() is deprecated in Python 3.12+ (use datetime.now(timezone.utc))

Do you edit, regenerate, or do a combination?

See solution

Step 1: Assess

A. Correct approach? INCORRECT
   - 4 redundant queries that load everything into memory
   - The streak loop is N queries where N = streak length
   - This is an incorrect algorithmic approach (Signal 1 of regenerating)
   
B. Lines that change? ~50 of 65 (77%)
   
C. Interconnected? YES
   - Changing from individual queries to SQL aggregations 
     changes the entire structure of the function
   
D. Design or implementation? DESIGN of the query strategy
   - The metrics design (today/week/month/streak) is correct
   - The way of calculating them is fundamentally incorrect

Step 2: Estimate editing: ~40 min (rewrite 5 queries + refactor redundancies + streak) Step 3: Estimate regeneration: ~18 min (there are no customizations)

Step 4: Consider losses

Customizations: NONE
- No logging
- No messages in Spanish
- No special integrations
- The code is generated without modifications

Losses: Zero

Step 5: Decide

→ REGENERATE the entire function

Justification:

  • 77% of the code needs to change
  • The query approach is fundamentally incorrect
  • There are no customizations to lose
  • Regenerating (~18 min) is much faster than editing (~40 min)
  • The edits would be so interconnected that you'd essentially be rewriting — better to do it with a clean prompt

Regeneration prompt:

Regenerate the dashboard_metrics function with these corrections:

CURRENT PROBLEMS:
1. 4 queries that load all the tasks in Python — 
   use SQL COUNT/aggregations
2. Redundant queries (today ⊂ week ⊂ month) — 
   do 1 query with CASE WHEN or 1 query per period with COUNT
3. The streak loop runs N queries — use a window function or 
   a single query with GROUP BY date
4. Missing response_model
5. Use datetime.now(timezone.utc) instead of datetime.utcnow()

REQUIREMENTS:
- A maximum of 3-4 total SQL queries (not one per period)
- The streak must be calculated with at most 1 query
- Pydantic response model with nested models
- Keep the same response structure:
  today: {created, completed}
  week: {created, completed}
  month: {created, completed}
  streak: int
  total: {all, completed}
  completion_rate: float

CONTEXT:
- SQLAlchemy with PostgreSQL
- from sqlalchemy import func, case, and_
- The Task model has: id, owner_id, status, created_at, 
  completed_at, priority

Note: the prompt includes what was wrong, the exact requirements, and the project context — the 3 rules of effective regeneration from capsule 03.


Framework Summary

The process in 60 seconds:

1. Is the approach correct?
   NO → Regenerate (function or file)
   YES → Continue

2. How many edits do I need?
   1-3 simple → Edit
   4+ or complex → Continue

3. Are there customizations to lose?
   Many → Edit (preserve the investment)
   Few/None → Continue

4. What's faster?
   Edit < Regenerate → Edit
   Regenerate < Edit → Regenerate

5. Can I do a mix?
   "Regenerate this function, edit the rest"
   → Often the best option

Quick rules for when you don't have time to analyze:

✅ Change 1 character (> to <) → Edit
✅ Add 2-3 lines (null check) → Edit
✅ Incorrect algorithmic approach → Regenerate function
✅ Incorrect architecture → Regenerate file
✅ >50% needs to change without customizations → Regenerate
✅ <30% needs to change with customizations → Edit
✅ You don't know which to choose → Edit (preserves context, lower risk)

Connection to the Project

In the capstone project (Module 8)

The framework in this capsule is exactly what you'll apply in the project. For each problem you find:

  1. Apply the 5-step framework
  2. Document your decision and justification
  3. Execute (edit, regenerate, or combination)
  4. Verify the result

Documenting decisions is part of the evaluation:

Documentation format:

## Problem #3: Inefficient task search

**Assessment:** Incorrect algorithmic approach (Python loop 
instead of SQL WHERE). 90% of the code needs to change.

**Decision:** Regenerate the search_tasks function

**Justification:**
- Signal 1: fundamentally incorrect approach
- 0 customizations to lose
- Editing would take ~25 min, regenerating ~12 min

**Prompt used:** [include prompt]

**Verification:** 
- ✅ The SQL query works correctly
- ✅ The existing filters are maintained
- ✅ Performance: from 3s to 50ms with 10,000 records

Troubleshooting

Problem 1: "I applied the framework and made the decision, but the result wasn't good"

Cause: The framework guides the decision but doesn't guarantee the execution. If you decide to regenerate but the prompt is bad, the result will be bad. Solution: The framework has two parts: (1) deciding what to do, (2) doing it well. If you decided to regenerate, apply the techniques from capsule 03. If you decided to edit, apply the techniques from capsule 04. The correct decision with incorrect execution still gives a bad result.

Problem 2: "I spend too much time analyzing and little time executing"

Cause: Analysis paralysis. The framework is a guide, not an academic analysis. Solution: With practice, the framework becomes intuitive. You don't need to calculate exact minutes — the complete analysis should take 1-2 minutes maximum. If after 2 minutes you don't have a clear decision, edit (it's the lower-risk option) and reconsider if the result isn't satisfactory.

Problem 3: "I don't have the experience to estimate editing vs regeneration times"

Cause: Estimates improve with practice. Solution: The first few times, note down how long each option actually took. After 10-20 decisions, your estimates will be much more accurate. Tip: most developers underestimate the regeneration time (they forget the review and the customizations) and overestimate the editing time.

Problem 4: "Every time I regenerate, the new code has different problems"

Cause: An insufficient prompt or a lack of post-regeneration review. Solution: (1) Include in the prompt everything that was right in the previous code. (2) After regenerating, do a complete code review comparing old vs new — as if it were a colleague's PR. (3) If the third regeneration attempt still has problems, edit the best of the attempts.


Exercises

Exercise 6: Create your own scenario (Hard)

Write a code scenario (30-50 lines) where:

  • The correct decision is NOT obvious
  • There are valid arguments both for editing and for regenerating
  • Include at least 2 customizations that would be lost by regenerating
  • Include at least 1 approach problem (not just targeted bugs)

Then, apply the 5-step framework to your own scenario and make a decision.

See solution (example)

Proposed scenario:

import logging
from fastapi import FastAPI, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from sqlalchemy import func
from app.database import get_db
from app.models import Task, TaskHistory
from app.dependencies.auth import get_current_user

logger = logging.getLogger(__name__)
app = FastAPI()

@app.patch("/tasks/{task_id}/status")
async def change_status(
    task_id: int,
    new_status: str,
    current_user=Depends(get_current_user),
    db: Session = Depends(get_db),
):
    logger.info(
        "Cambio de status solicitado",
        extra={"task_id": task_id, "new_status": new_status, 
               "user_id": current_user.id},
    )

    task = db.query(Task).filter(Task.id == task_id).first()
    if not task:
        raise HTTPException(404, "Tarea no encontrada")
    if task.owner_id != current_user.id:
        raise HTTPException(403, "No autorizado")

    # Validate state transitions (incorrect approach:
    # should be a state machine, not if/elif/elif)
    if new_status == "in_progress":
        if task.status != "pending":
            raise HTTPException(400, "Solo se puede iniciar una tarea pendiente")
    elif new_status == "completed":
        if task.status != "in_progress":
            raise HTTPException(400, "Solo se puede completar una tarea en progreso")
    elif new_status == "cancelled":
        if task.status == "completed":
            raise HTTPException(400, "No se puede cancelar una tarea completada")
    else:
        raise HTTPException(400, f"Estado inválido: {new_status}")

    old_status = task.status
    task.status = new_status

    # Customization: change history
    history = TaskHistory(
        task_id=task.id,
        field="status",
        old_value=old_status,
        new_value=new_status,
        changed_by=current_user.id,
    )
    db.add(history)

    db.commit()
    db.refresh(task)

    logger.info(
        "Status cambiado exitosamente",
        extra={"task_id": task_id, "old": old_status, "new": new_status},
    )
    return task

Analysis with the framework:

  • The state transitions approach (if/elif) is problematic (it becomes unmanageable with more states) — a signal to regenerate
  • BUT there are valuable customizations: logging with extra, change history with TaskHistory, messages in Spanish
  • The ownership validation and null check are correct
  • The approach of saving the transition in history is fine

Decision: EDIT — Refactor only the transitions to a dict/state machine, keep everything else:

VALID_TRANSITIONS = {
    "pending": {"in_progress", "cancelled"},
    "in_progress": {"completed", "cancelled"},
    "completed": set(),
    "cancelled": {"pending"},
}

if new_status not in VALID_TRANSITIONS.get(task.status, set()):
    raise HTTPException(
        400, 
        f"No se puede cambiar de '{task.status}' a '{new_status}'"
    )

4 lines replace the 10 lines of if/elif. All the context is preserved.


Summary

  • The 5-step framework structures the decision: assess damage → estimate editing → estimate regeneration → consider losses → decide
  • The decision is not binary — there's a spectrum from editing one line to regenerating the entire file
  • Combinations are frequently the best option: regenerate one function, edit the rest
  • In case of a tie, editing wins because it preserves context and has lower risk
  • The 5 scenarios demonstrate that context changes the answer — there's no universal rule
  • The framework is a starting point you refine with experience — after 20+ decisions, it becomes intuitive
  • Document your decisions: the justification is as important as the result

Additional Resources

  1. Martin Fowler — When to Rewrite - The Strangler Fig pattern for migrating gradually vs rewriting
  2. Joel Spolsky — Things You Should Never Do - A classic perspective on the dangers of rewriting
  3. Refactoring Guru — Refactoring Techniques - A catalog of incremental editing techniques
  4. Anthropic — Claude Code Best Practices - Optimize prompts for code generation and editing
  5. The Pragmatic Programmer — Software Entropy - How to prevent the gradual degradation of code
  6. Working Effectively with Legacy Code — Michael Feathers - Techniques for making safe changes in existing code

Next module: Capstone Project — apply everything you've learned in a complete codebase with real problems.


Debugging & Code Review with Claude Code — Module 7, Capsule 05 Claude Code Agentic Development Path — Guide #6 of 11