Module 7: Subagents for Debugging, Regenerate vs Edit

When to Regenerate Code

When to Regenerate Code

Capsule overview

"Should I regenerate this or fix it?" — it's the question you ask yourself several times a day when working with AI coding tools. And the wrong answer has a real cost: regenerating when you should edit wastes context, customizations, and time. Editing when you should regenerate produces fragile patches over fundamentally incorrect code.

This capsule covers one side of the spectrum: the clear signals that regenerating is the best option. Not "regenerate when the code is bad" — that's not useful. Concrete signals: "regenerate when the algorithmic approach is incorrect: it used an O(n²) loop where you need an O(1) dict lookup."

But regenerating isn't free. You lose customizations, you lose the context you've accumulated, and the new code may bring different problems. Understanding the cost of regenerating is as important as knowing when to do it.


The 5 Signals That Regenerating Is Better

Signal 1: The algorithmic approach is fundamentally incorrect

This is the clearest signal. The code doesn't have a targeted bug — it has the wrong approach. Editing it doesn't fix it; you need a different approach.

Example: Inefficient search

Claude Code generated this to search for a user by email in a list:

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()

class User(BaseModel):
    id: int
    email: str
    name: str

users_db: list[User] = []

@app.get("/users/search")
async def search_user(email: str):
    for user in users_db:
        if user.email == email:
            return user
    raise HTTPException(status_code=404, detail="User not found")

Why doesn't editing fix it?

The approach is O(n) linear search. With 10 users it works. With 100,000 users, each search walks the entire list. You can't optimize a linear loop over a list — you need to change the data structure.

# ❌ Edit: adding a break or an early return doesn't solve O(n)
# ❌ Edit: adding a cache over the list is still a patch

# ✅ Regenerate with the correct approach:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()

class User(BaseModel):
    id: int
    email: str
    name: str

users_by_email: dict[str, User] = {}

@app.get("/users/search")
async def search_user(email: str):
    user = users_by_email.get(email)
    if not user:
        raise HTTPException(status_code=404, detail="User not found")
    return user

The concrete signal: If you need to change the fundamental data structure (list → dict, array → tree, loop → query), regenerate.

Signal 2: The architecture is incorrect

The code works but uses the wrong architectural pattern. Patching the architecture only accumulates technical debt.

Example: Synchronous where you need asynchronous

Claude Code generated an endpoint that makes 3 external calls sequentially:

import requests
from fastapi import FastAPI

app = FastAPI()

@app.get("/api/dashboard")
def get_dashboard(user_id: int):
    user_response = requests.get(f"http://user-service/users/{user_id}")
    user = user_response.json()

    tasks_response = requests.get(f"http://task-service/tasks?user_id={user_id}")
    tasks = tasks_response.json()

    notifications_response = requests.get(
        f"http://notification-service/notifications?user_id={user_id}"
    )
    notifications = notifications_response.json()

    return {
        "user": user,
        "tasks": tasks,
        "notifications": notifications,
    }

Why doesn't editing fix it?

The code uses requests (synchronous/blocking) in a FastAPI endpoint (which is async). The 3 calls run sequentially — if each takes 200ms, the endpoint takes 600ms. But the real problem is that it blocks FastAPI's event loop.

You can't "edit" requests.get to make it async. You need to change the library, the concurrency pattern, and the structure of the calls.

# ✅ Regenerate with the correct architecture:
import httpx
import asyncio
from fastapi import FastAPI

app = FastAPI()

@app.get("/api/dashboard")
async def get_dashboard(user_id: int):
    async with httpx.AsyncClient() as client:
        user_task = client.get(f"http://user-service/users/{user_id}")
        tasks_task = client.get(f"http://task-service/tasks?user_id={user_id}")
        notif_task = client.get(
            f"http://notification-service/notifications?user_id={user_id}"
        )

        user_resp, tasks_resp, notif_resp = await asyncio.gather(
            user_task, tasks_task, notif_task
        )

    return {
        "user": user_resp.json(),
        "tasks": tasks_resp.json(),
        "notifications": notif_resp.json(),
    }

The concrete signal: If you need to change from synchronous to asynchronous, from monolithic to modular, from polling to websockets, or from in-memory to database — regenerate the function or the file.

Signal 3: More than 50% of the code needs to change

If you're going to edit more than half the lines, you're rewriting anyway. But you're doing it the slowest way: editing line by line instead of generating the correct code all at once.

Example: CRUD with incorrect validation

Claude Code generated a task creation endpoint with multiple problems:

from fastapi import FastAPI
from pydantic import BaseModel
from typing import Optional
from datetime import datetime

app = FastAPI()

class Task(BaseModel):
    title: str
    description: str
    priority: str
    due_date: str
    assigned_to: str

tasks = []
next_id = 1

@app.post("/tasks")
def create_task(task: Task):
    global next_id
    new_task = {
        "id": next_id,
        "title": task.title,
        "description": task.description,
        "priority": task.priority,
        "due_date": task.due_date,
        "assigned_to": task.assigned_to,
        "status": "open",
        "created_at": str(datetime.now()),
    }
    tasks.append(new_task)
    next_id += 1
    return new_task

Problems that require editing:

  1. ❌ description shouldn't be required (not every task has one)
  2. ❌ priority should be an Enum, not a free string
  3. ❌ due_date should be datetime, not str
  4. ❌ assigned_to should be Optional[int] (user_id), not str
  5. ❌ Uses global and an in-memory list instead of a database
  6. ❌ There's no authentication
  7. ❌ There's no validation that priority is a valid value
  8. ❌ There's no response model
  9. ❌ created_at is a string instead of a datetime

Count: 9 problems in ~25 lines of code. Editing each one would be slower than regenerating with a better prompt.

The concrete signal: Count the necessary changes. If you need to modify more than 50% of the lines, regenerate with a prompt that specifies all the requirements.

Signal 4: The code doesn't meet the requirements

Sometimes Claude Code generates code that works perfectly... for a requirement different from the one you asked for. The business logic is incorrect. It's not a bug — it's the wrong feature.

Example: Incorrect discount calculation

You asked for: "10% discount for purchases over $100, 20% for purchases over $500."

Claude Code generated:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Order(BaseModel):
    items: list[dict]
    total: float

@app.post("/api/orders/calculate-discount")
async def calculate_discount(order: Order):
    if order.total > 500:
        discount = order.total * 0.20
    elif order.total > 100:
        discount = order.total * 0.10
    else:
        discount = 0

    return {
        "original_total": order.total,
        "discount": discount,
        "final_total": order.total - discount,
    }

It looks correct, but the real requirement is more complex:

Real requirement:
- 10% discount on the amount that exceeds $100 (not on the total)
- An additional 20% discount on the amount that exceeds $500
- The discounts are progressive (like tax brackets)

Example: purchase of $700
- First $100: no discount
- $100 to $500 ($400): 10% = $40
- $500 to $700 ($200): 20% = $40
- Total discount: $80
- Final: $620

What Claude Code computed:
- $700 * 20% = $140 discount
- Final: $560
- INCORRECT: $60 difference

Editing the conditional logic to implement progressive brackets requires rewriting the whole calculation function. It's clearer to regenerate with the requirement well explained.

The concrete signal: If the business logic is fundamentally different from what you need, regenerate by explaining the correct requirement with input/output examples.

Signal 5: The technical debt would be massive if patched

Sometimes the code works and you could edit it, but each edit adds complexity to a design that's already fragile. The result would be a Frankenstein of patches.

Example: Scattered validation

Claude Code generated inline validation in each endpoint instead of centralizing it:

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import Optional

app = FastAPI()

class TaskInput(BaseModel):
    title: str
    priority: str
    status: str

@app.post("/tasks")
async def create_task(task: TaskInput):
    if len(task.title) < 3:
        raise HTTPException(400, "Title too short")
    if len(task.title) > 200:
        raise HTTPException(400, "Title too long")
    if task.priority not in ["low", "medium", "high", "critical"]:
        raise HTTPException(400, "Invalid priority")
    if task.status not in ["pending", "in_progress", "completed", "cancelled"]:
        raise HTTPException(400, "Invalid status")
    # ... create task

@app.patch("/tasks/{task_id}")
async def update_task(task_id: int, task: TaskInput):
    if len(task.title) < 3:
        raise HTTPException(400, "Title too short")
    if len(task.title) > 200:
        raise HTTPException(400, "Title too long")
    if task.priority not in ["low", "medium", "high", "critical"]:
        raise HTTPException(400, "Invalid priority")
    if task.status not in ["pending", "in_progress", "completed", "cancelled"]:
        raise HTTPException(400, "Invalid status")
    # ... update task

@app.post("/tasks/bulk")
async def bulk_create(tasks: list[TaskInput]):
    for task in tasks:
        if len(task.title) < 3:
            raise HTTPException(400, "Title too short")
        if len(task.title) > 200:
            raise HTTPException(400, "Title too long")
        if task.priority not in ["low", "medium", "high", "critical"]:
            raise HTTPException(400, "Invalid priority")
        if task.status not in ["pending", "in_progress", "completed", "cancelled"]:
            raise HTTPException(400, "Invalid status")
    # ... create tasks

The problem: The same validation is repeated 3 times (and it will grow with each endpoint). Editing the 3 endpoints to add a new validation rule means touching 3 places. If you miss one, you have inconsistency.

Could you edit it? Yes — you'd move the validation to Pydantic validators. But you'd touch 4 files (the model and 3 endpoints) and each edit is error-prone.

Is it better to regenerate? Yes — regenerating the schema with Pydantic validators is cleaner:

from enum import Enum
from fastapi import FastAPI
from pydantic import BaseModel, Field

app = FastAPI()

class Priority(str, Enum):
    LOW = "low"
    MEDIUM = "medium"
    HIGH = "high"
    CRITICAL = "critical"

class Status(str, Enum):
    PENDING = "pending"
    IN_PROGRESS = "in_progress"
    COMPLETED = "completed"
    CANCELLED = "cancelled"

class TaskCreate(BaseModel):
    title: str = Field(min_length=3, max_length=200)
    priority: Priority
    status: Status = Status.PENDING

class TaskUpdate(BaseModel):
    title: str | None = Field(default=None, min_length=3, max_length=200)
    priority: Priority | None = None
    status: Status | None = None

@app.post("/tasks")
async def create_task(task: TaskCreate):
    # Automatic validation by Pydantic
    pass

@app.patch("/tasks/{task_id}")
async def update_task(task_id: int, task: TaskUpdate):
    # Automatic validation by Pydantic
    pass

@app.post("/tasks/bulk")
async def bulk_create(tasks: list[TaskCreate]):
    # Automatic validation by Pydantic
    pass

The concrete signal: If editing requires touching the same pattern in 3+ places, and there's a cleaner design that centralizes the logic — regenerate with the correct design from the start.


The Cost of Regenerating: What You Lose

Regenerating isn't free. Before deciding, consider what you pay:

Cost 1: You lose customizations

If you edited the code after the initial generation — added a header, adjusted a format, changed an error message — all of that is lost when you regenerate.

Scenario:
1. Claude Code generates the tasks endpoint
2. You edit: add logging, change the date format, 
   localize the error message to Spanish
3. You discover the approach is incorrect
4. You regenerate the endpoint
5. The new code: doesn't have your logging, uses the American 
   date format, messages in English

→ You need to re-apply your customizations
→ If you don't remember all of them, you lose something

Mitigation: Before regenerating, note the customizations you want to preserve. Include them in the regeneration prompt.

Cost 2: You lose accumulated context

Each interaction with Claude Code builds context. If you've spent 20 minutes editing a file, Claude Code understands your intent, your style, and your preferences. Regenerating from scratch loses that context.

With accumulated context:
"Add email validation"
→ Claude Code knows you use Pydantic, knows your error style,
  knows you prefer validations in the schema

Without context (after regenerating):
"Add email validation"
→ Claude Code might use an inline regex, might not follow
  the same error style, might put the validation
  in the endpoint instead of the schema

Mitigation: When regenerating, include in the prompt the context you need: "Use Pydantic validators, error messages in Spanish, the same format as the rest of the project."

Cost 3: You may get new problems

The new code isn't an improved version of the previous one — it's completely new code that may have its own bugs.

Original code:
├── Bug: missing validation on the priority field
├── Correct: auth, response, date format
└── Correct: error handling

Regenerated code:
├── Correct: priority validation (the fix you wanted)
├── NEW Bug: doesn't do an auth check
├── NEW Bug: date in the wrong format
└── Correct: error handling

You solved one problem and created two. This happens because Claude Code has no context of what was already correct — it generates code from scratch based only on your prompt.

Mitigation: After regenerating, do a code review of the new code, comparing it with the previous one. Verify that what already worked still works.

Cost 4: Regeneration time + review

Regenerating seems fast: you write a prompt, Claude Code generates, done. But the real time includes:

Total time to regenerate:
├── Write a better prompt: 3-5 min
├── Wait for the generation: 1-2 min
├── Review the new code: 5-10 min
├── Re-apply customizations: 3-5 min
├── Test: 5-10 min
└── Total: 17-32 min

vs Time to edit (if the fix is targeted):
├── Identify the change: 2-3 min
├── Make the edit: 1-2 min
├── Test: 3-5 min
└── Total: 6-10 min

If the fix is targeted, editing is 3x faster. If the fix requires massive changes, regenerating is more efficient. The right decision depends on the scale of the change.


How to Regenerate Effectively

If you decide to regenerate, do it well. A better prompt produces better code.

Rule 1: Include what was wrong the first time

❌ Vague prompt:
"Generate an endpoint to create tasks"

✅ Prompt informed by the previous error:
"Generate a POST /tasks endpoint to create tasks. 
The previous version had these problems:
1. Used inline validation instead of Pydantic Field constraints
2. Priority was a free string instead of an Enum
3. Had no authentication
4. Used an in-memory list instead of a DB

Requirements:
- Pydantic model with Field(min_length=3, max_length=200) for title
- Priority as an Enum: low, medium, high, critical
- Status with a default of 'pending'
- Depends(get_current_user) for auth
- SQLAlchemy for persistence
- Explicit response model"

The prompt includes what you learned from the first attempt. That prevents Claude Code from repeating the same mistakes.

Rule 2: Give input/output examples

"The discount calculation must be progressive (like tax brackets):
- First $100: no discount
- $100 to $500: 10% on the excess
- More than $500: 20% on the excess over $500

Example: purchase of $700
- First $100 → $0 discount
- $100-$500 ($400) → $40 discount (10%)
- $500-$700 ($200) → $40 discount (20%)
- Total discount: $80
- Final: $620"

Concrete examples eliminate ambiguity and give you a test case to verify the generated code.

Rule 3: Specify what must be preserved

"Regenerate the calculate_discount function. 
PRESERVE:
- The response format: {original_total, discount, final_total}
- The endpoint path: POST /api/orders/calculate-discount
- The type hints and the Pydantic Order model
- The error messages in Spanish

CHANGE:
- The discount calculation logic (see requirements above)
- Add validation that total > 0"

This reduces the regeneration costs — Claude Code knows what to keep and what to change.

Rule 4: Specify the project context

"This endpoint is part of a FastAPI API with:
- SQLAlchemy async (use AsyncSession, not Session)
- Pydantic v2 (use model_config instead of class Config)
- JWT authentication (get_current_user already exists)
- Logging with structlog
- The imports follow the project's convention:
  from app.models import Task
  from app.schemas.task import TaskCreate, TaskResponse
  from app.dependencies.auth import get_current_user
  from app.database import get_db"

Project context prevents Claude Code from generating code that doesn't fit your stack.


Partial Regeneration: Not All or Nothing

A common mistake is thinking that regenerating means "delete everything and start from scratch." In practice, the best option is often to regenerate only the problematic part.

The regeneration spectrum

Regenerate entire file ← → Regenerate function ← → Edit

When to regenerate the entire file:
├── The file has < 100 lines
├── 80%+ needs to change
└── The file's structure is incorrect

When to regenerate a function:
├── The function has the wrong approach
├── The rest of the file is fine
└── The function is independent (few side effects)

When to edit:
├── Only 1-5 lines need to change
├── The fix is clear and targeted
└── The approach is correct

Example: Regenerate only one function

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.schemas.task import TaskCreate, TaskResponse, TaskStats
from app.dependencies.auth import get_current_user


app = FastAPI()


# ✅ This function is fine — do NOT regenerate
@app.post("/tasks", response_model=TaskResponse)
async def create_task(
    task: TaskCreate,
    current_user: User = Depends(get_current_user),
    db: Session = Depends(get_db),
):
    new_task = Task(**task.model_dump(), owner_id=current_user.id)
    db.add(new_task)
    db.commit()
    db.refresh(new_task)
    return new_task


# ❌ This function has the wrong approach — REGENERATE
@app.get("/tasks/stats", response_model=TaskStats)
async def get_task_stats(
    current_user: User = Depends(get_current_user),
    db: Session = Depends(get_db),
):
    all_tasks = db.query(Task).filter(Task.owner_id == current_user.id).all()

    total = len(all_tasks)
    completed = len([t for t in all_tasks if t.status == "completed"])
    pending = len([t for t in all_tasks if t.status == "pending"])
    in_progress = len([t for t in all_tasks if t.status == "in_progress"])

    avg_completion_time = 0
    for task in all_tasks:
        if task.completed_at and task.created_at:
            avg_completion_time += (task.completed_at - task.created_at).total_seconds()
    if completed > 0:
        avg_completion_time /= completed

    return TaskStats(
        total=total,
        completed=completed,
        pending=pending,
        in_progress=in_progress,
        avg_completion_seconds=avg_completion_time,
    )

The get_task_stats function loads ALL tasks into memory and does calculations in Python. With 50,000 tasks, this is a performance disaster. You need SQL aggregations:

Prompt to regenerate ONLY get_task_stats:

"Regenerate only the get_task_stats function. The current approach
loads all tasks into memory and calculates in Python. 
I need it to use SQL aggregations (COUNT, AVG) directly
in the database.

The function should:
- Use db.query with func.count and func.avg from SQLAlchemy
- Filter by the current_user's owner_id
- Calculate avg_completion_time with SQL, not Python
- Keep the same TaskStats response model
- Keep the same Depends (get_current_user, get_db)
- A single efficient query instead of loading all the rows"

Result: you regenerate a 20-line function, keep the create_task endpoint intact, and preserve the file's structure.


Connection to the Project

In the capstone project (Module 8)

You're going to find code where regenerating is the correct answer. The key is identifying the signal:

Capstone project — possible regeneration decisions:

1. A search function that uses a Python loop 
   instead of SQL WHERE → Signal 1 (algorithmic approach)

2. Synchronous calls to external services 
   in async endpoints → Signal 2 (incorrect architecture)

3. An endpoint with 8+ validation, 
   naming, and structure problems → Signal 3 (>50% needs to change)

4. A calculation that implements the business logic 
   incorrectly → Signal 4 (doesn't meet requirements)

5. Duplicate validation in 4 different endpoints 
   → Signal 5 (massive technical debt if patched)

For each regeneration decision, you'll document:

  • ✅ Which signal you identified
  • ✅ What would be lost by regenerating (customizations, context)
  • ✅ What you included in the prompt to mitigate the losses
  • ✅ That you reviewed the new code to verify it didn't introduce problems

Troubleshooting

Problem 1: "I regenerated and the new code has different problems"

Cause: The prompt didn't include enough context or didn't specify what should be preserved. Solution: Before regenerating, note everything that's correct in the current code. Include it in the prompt: "PRESERVE: the response format, the authentication, the messages in Spanish. CHANGE: the calculation logic." After regenerating, do a code review comparing old vs new.

Problem 2: "I don't know if the approach is incorrect or just has bugs"

Cause: Sometimes it's hard to distinguish between a correct approach with bugs and a fundamentally incorrect approach. Solution: Ask: "If I fix all the bugs, will the code work correctly at scale?" If the answer is yes, edit. If the answer is "it'll work but it'll be slow / insecure / impossible to maintain," regenerate.

Problem 3: "I regenerated with a better prompt but Claude Code generated something very different from what I expected"

Cause: LLMs aren't deterministic. The same prompt can produce different results. Solution: Be more specific in the prompt. Include: the exact function signature, the imports it should use, the response format, and an input/output example. The more specific, the less variation.

Problem 4: "I'm not sure if it's 50% or 30% that needs to change"

Cause: The 50% rule is a heuristic, not a magic number. Solution: The real question isn't the exact percentage — it's "will it be faster to edit or regenerate?" If you have to think hard about how many changes you need, they're probably enough to regenerate. If you can list the changes in 30 seconds, edit.


Exercises

Exercise 1: Identify the signal (Medium)

For each piece of code, identify which of the 5 signals applies and justify why regenerating is better than editing.

Code A:

from fastapi import FastAPI
from datetime import datetime

app = FastAPI()

@app.get("/api/events")
async def get_upcoming_events():
    events = get_all_events_from_db()
    upcoming = []
    now = datetime.now()
    for event in events:
        event_date = datetime.strptime(event["date"], "%Y-%m-%d")
        if event_date > now:
            upcoming.append(event)
    upcoming.sort(key=lambda e: e["date"])
    return upcoming[:10]

Code B:

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()

class UserCreate(BaseModel):
    username: str
    email: str
    age: int

@app.post("/users")
async def create_user(user: UserCreate):
    if not user.username:
        raise HTTPException(400, "Username required")
    if len(user.username) < 2:
        raise HTTPException(400, "Username too short")
    if "@" not in user.email:
        raise HTTPException(400, "Invalid email")
    if user.age < 0:
        raise HTTPException(400, "Invalid age")
    if user.age > 150:
        raise HTTPException(400, "Invalid age")
    # ... save to db
    return {"id": 1, **user.model_dump()}
See solution

Code A — Signal 1: Incorrect algorithmic approach

The code loads ALL the events from the database, filters them in Python, sorts them in Python, and takes the first 10. With 100,000 events, this loads 100,000 records when you only need 10.

Regenerate with: SELECT * FROM events WHERE date > NOW() ORDER BY date LIMIT 10 — a single SQL query that does everything in the database.

Editing doesn't solve it: you could optimize the sort or the filter in Python, but the fundamental problem is loading all the records.

Code B — Signal 5: Massive technical debt if patched

All the validation is inline in the endpoint. If you add more endpoints that create or update users, you'll have to duplicate the same validations. The correct solution is to use Pydantic Field constraints and validators.

Regenerate the schema with:

class UserCreate(BaseModel):
    username: str = Field(min_length=2)
    email: EmailStr
    age: int = Field(ge=0, le=150)

Editing "would work" but perpetuates the anti-pattern of scattered validation.

Exercise 2: Calculate the cost of regenerating (Medium)

This code was generated by Claude Code and then you manually edited 3 things (marked with comments):

from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session
from app.database import get_db
from app.models import Product
from app.schemas import ProductCreate, ProductResponse
from app.dependencies.auth import get_current_user
import logging  # YOUR EDIT: you added logging

logger = logging.getLogger(__name__)  # YOUR EDIT

app = FastAPI()

@app.post("/products", response_model=ProductResponse)
async def create_product(
    product: ProductCreate,
    current_user = Depends(get_current_user),
    db: Session = Depends(get_db),
):
    logger.info(f"Creando producto: {product.name} por user {current_user.id}")  # YOUR EDIT
    
    if product.price < 0:
        raise HTTPException(400, "El precio no puede ser negativo")
    
    new_product = Product(**product.model_dump())
    db.add(new_product)
    db.commit()
    db.refresh(new_product)
    return new_product

The problem: The price < 0 validation should be price <= 0 (a product can't cost $0), but you also realize you need to add stock validation, existing category validation, and unique name validation. Do you regenerate or edit? Calculate the cost of each option.

See solution

Option A: Edit

Necessary changes:
1. Change < 0 to <= 0 (1 character)
2. Add stock >= 0 validation (2 lines)
3. Add existing category check (3-4 lines + query)
4. Add unique name check (3-4 lines + query)

Cost:
├── Time: ~10 minutes
├── Customizations preserved: ✅ logging (3 lines)
├── Risk: low (targeted changes)
└── Maintainability: medium (inline validation, grows over time)

Option B: Regenerate the function

What's lost:
├── 3 lines of logging that you added
└── The error message in Spanish

Cost:
├── Time: ~15-20 minutes (prompt + review + re-adding logging)
├── Customizations lost: logging, messages in Spanish
├── Risk: medium (new code may have new bugs)
└── Maintainability: high IF you move validation to Pydantic

Correct decision: Edit

The codebase has few validations to add (3-4), the customizations are valuable (logging), and the general approach is correct. Editing is faster and preserves your work.

BUT: if you decided the validation should be in the Pydantic schema (not in the endpoint), then regenerating the schema makes sense — it's a design change, not just adding validations.

Exercise 3: Write a regeneration prompt (Hard)

You have this code that needs to be regenerated (Signal 2 — incorrect architecture). Write the regeneration prompt that would produce the best result:

import time
from fastapi import FastAPI

app = FastAPI()

@app.get("/api/health/full")
def full_health_check():
    results = {}
    
    try:
        import psycopg2
        conn = psycopg2.connect("postgresql://localhost/mydb")
        conn.close()
        results["database"] = "healthy"
    except Exception:
        results["database"] = "unhealthy"
    
    try:
        import redis
        r = redis.Redis()
        r.ping()
        results["cache"] = "healthy"
    except Exception:
        results["cache"] = "unhealthy"
    
    try:
        import requests
        resp = requests.get("http://external-api/status", timeout=5)
        results["external_api"] = "healthy" if resp.status_code == 200 else "unhealthy"
    except Exception:
        results["external_api"] = "unhealthy"
    
    return {"status": "healthy" if all(v == "healthy" for v in results.values()) else "degraded", "checks": results}
See solution
Regenerate the GET /api/health/full endpoint with these corrections:

PROBLEMS WITH THE CURRENT CODE:
1. It's synchronous (def) when it should be async — it blocks the event loop
2. It creates new connections to DB and Redis on every request 
   instead of using the project's pools
3. It uses psycopg2 (sync) instead of the existing async SQLAlchemy engine
4. The 3 checks run sequentially (total: up to 15s) 
   when they could run in parallel with asyncio.gather
5. It uses requests (sync) instead of httpx (async)
6. It has no per-check individual timeout

REQUIREMENTS:
- An async endpoint that uses asyncio.gather for parallel checks
- DB check using the existing SQLAlchemy session (from app.database import get_db)
- Redis check using the existing Redis client (from app.cache import redis_client)
- External API check using httpx.AsyncClient with a 3-second timeout
- A 5-second individual timeout per check (if it takes longer, report "timeout")
- Response format: {"status": "healthy"|"degraded"|"unhealthy", 
  "checks": {"database": {...}, "cache": {...}, "external_api": {...}},
  "response_time_ms": <total>}

PRESERVE:
- The endpoint path: GET /api/health/full
- The status logic: healthy if everything OK, degraded if something fails
- The same 3 checks: database, cache, external_api

CONTEXT:
- Async FastAPI project with async SQLAlchemy and Redis
- Python 3.11+

This prompt includes: what was wrong, what you need, what to preserve, and the project context. It's 10x more likely to produce correct code than a vague prompt.

Exercise 4: Partial vs total regeneration (Hard)

This file has two functions. One is fine and the other isn't. Decide what to regenerate and what to keep:

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, Category
from app.schemas import TaskResponse, CategoryStats
from app.dependencies.auth import get_current_user
from typing import Optional

app = FastAPI()

@app.get("/tasks", response_model=list[TaskResponse])
async def list_tasks(
    status: Optional[str] = Query(None),
    priority: Optional[str] = Query(None),
    page: int = Query(1, ge=1),
    size: int = Query(20, ge=1, le=100),
    current_user = Depends(get_current_user),
    db: Session = Depends(get_db),
):
    query = db.query(Task).filter(Task.owner_id == current_user.id)

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

    offset = (page - 1) * size
    tasks = query.order_by(Task.created_at.desc()).offset(offset).limit(size).all()
    return tasks


@app.get("/categories/stats", response_model=list[CategoryStats])
async def category_stats(
    current_user = Depends(get_current_user),
    db: Session = Depends(get_db),
):
    categories = db.query(Category).all()
    stats = []
    for cat in categories:
        tasks = db.query(Task).filter(
            Task.category_id == cat.id,
            Task.owner_id == current_user.id,
        ).all()
        completed = sum(1 for t in tasks if t.status == "completed")
        total = len(tasks)
        stats.append(CategoryStats(
            category_name=cat.name,
            total_tasks=total,
            completed_tasks=completed,
            completion_rate=completed / total if total > 0 else 0,
        ))
    return stats
See solution

list_tasks — KEEP (it's fine)

  • ✅ Uses pagination with OFFSET/LIMIT
  • ✅ Optional filters well implemented
  • ✅ Query built correctly with SQLAlchemy
  • ✅ Sorts by created_at descending
  • ✅ Limits the page size (max 100)

category_stats — REGENERATE (wrong approach)

  • ❌ Loads ALL the categories into memory
  • ❌ For EACH category, runs a separate query (N+1)
  • ❌ Loads ALL the tasks of each category to count them in Python
  • ❌ With 50 categories and 1000 tasks per category = 50 queries + 50,000 objects in memory

Regeneration prompt:

Regenerate ONLY the category_stats function. 
The list_tasks function is correct — do NOT touch it.

Problem: the current function has N+1 queries and 
loads data into memory. I need ONE SQL query that 
does a GROUP BY with COUNT and aggregations.

The query should:
- JOIN categories with tasks
- Filter by the current_user's owner_id
- GROUP BY category
- COUNT total and COUNT completed in SQL
- Calculate completion_rate in SQL or Python (but with 
  the data already aggregated, not row by row)

Keep: response_model list[CategoryStats],
the same Depends, the same endpoint path.

Decision: partial regeneration. Only the problematic function. The file and the other function stay intact.


Summary

  • There are 5 clear signals that regenerating is better than editing:
    1. The algorithmic approach is fundamentally incorrect (O(n²) → O(1))
    2. The architecture is incorrect (sync → async, monolithic → modular)
    3. More than 50% of the code needs to change
    4. The code doesn't meet the business requirements
    5. The technical debt would be massive if patched (duplicate validation × 5 endpoints)
  • Regenerating has 4 costs you must consider: you lose customizations, you lose context, you may get new problems, and it takes more time than it seems
  • To regenerate effectively: include what was wrong, give I/O examples, specify what to preserve, and give the project context
  • Regenerating isn't all or nothing — sometimes you regenerate one function but keep the rest of the file
  • After regenerating, always do a code review of the new code, comparing it with the previous one

Additional Resources

  1. Joel Spolsky — Things You Should Never Do, Part I - The classic article on the dangers of rewriting code from scratch
  2. Martin Fowler — Refactoring vs Rewriting - When to refactor incrementally vs rewrite
  3. Big Ball of Mud — Brian Foote, Joseph Yoder - How code degrades when patched without judgment
  4. Anthropic — Claude Code Best Practices - Best practices for code generation prompts
  5. Pydantic V2 Documentation — Validators - How to centralize validation in schemas (vs inline)

Next capsule: When to Edit Manually — the other side of the spectrum.


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