Module 7: Subagents for Debugging, Regenerate vs Edit

When to Edit Manually

When to Edit Manually

Capsule overview

The previous capsule covered when to regenerate. This one covers the other side of the spectrum: when editing manually is the best option. And here's an uncomfortable truth: most of the time, editing is the correct answer. The temptation to regenerate is strong — it seems faster, cleaner, more "AI-first." But in practice, 70-80% of the problems in AI-generated code are better solved by editing than by regenerating.

Why? Because most problems are targeted. An off-by-one error. An incorrect operator. A missing field in the response. A > that should be >=. Regenerating the whole file for a one-character problem is like demolishing a house because a faucet drips.

This capsule gives you the clear signals that editing is better, the techniques to edit efficiently with Claude Code's assistance, and the confidence of knowing that sometimes the simplest solution is the correct one.


The 5 Signals That Editing Is Better

Signal 1: 90% of the code is correct

The most obvious signal and the most frequent. The code works well in general — it just has a targeted problem you can identify and fix.

Example: Incorrect operator in a filter

Claude Code generated a task search endpoint:

from fastapi import FastAPI, Depends, Query, HTTPException
from sqlalchemy.orm import Session
from sqlalchemy import and_
from datetime import datetime
from app.database import get_db
from app.models import Task
from app.schemas import TaskResponse
from app.dependencies.auth import get_current_user

app = FastAPI()

@app.get("/tasks/overdue", response_model=list[TaskResponse])
async def get_overdue_tasks(
    current_user=Depends(get_current_user),
    db: Session = Depends(get_db),
):
    now = datetime.utcnow()
    overdue = (
        db.query(Task)
        .filter(
            and_(
                Task.owner_id == current_user.id,
                Task.due_date > now,  # Bug: should be <
                Task.status != "completed",
            )
        )
        .order_by(Task.due_date.asc())
        .all()
    )
    return overdue

Analysis:

  • ✅ Endpoint structure: correct
  • ✅ Authentication: correct (uses get_current_user)
  • ✅ SQLAlchemy query: well built
  • ✅ Filter by owner: correct
  • ✅ Filter by status: correct
  • ✅ Ordering: correct
  • ✅ Response model: correct
  • ❌ A single problem: Task.due_date > now should be Task.due_date < now

Regenerate? No. 95% of the code is perfect. Regenerating would give you different code that could lose the ordering, the query structure, or the correct filters.

Edit? Yes. Changing > to < takes 2 seconds:

Task.due_date < now,  # Fix: tasks whose due date has ALREADY PASSED

The concrete signal: If you can describe the problem in one sentence and point to the exact line, edit.

Signal 2: The fix is targeted and clear

Not only is the code mostly correct — the fix is obvious. You don't need to think about alternatives, there's no ambiguity, there are no trade-offs. You know exactly what to change.

Example: A missing field in the response

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

app = FastAPI()

class TaskResponse(BaseModel):
    id: int
    title: str
    description: str | None
    status: str
    priority: str
    owner_id: int
    # Missing: created_at and updated_at

    model_config = {"from_attributes": True}

@app.get("/tasks/{task_id}", response_model=TaskResponse)
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:
        from fastapi import HTTPException
        raise HTTPException(404, "Task not found")
    if task.owner_id != current_user.id:
        from fastapi import HTTPException
        raise HTTPException(403, "Not authorized")
    return task

The fix: Add two fields to the schema:

class TaskResponse(BaseModel):
    id: int
    title: str
    description: str | None
    status: str
    priority: str
    owner_id: int
    created_at: datetime
    updated_at: datetime | None

    model_config = {"from_attributes": True}

Regenerate the endpoint? No — the endpoint is fine. Only a field is missing from the schema.

The concrete signal: If the fix is "add X" or "change Y to Z" and there are no side effects, edit.

Signal 3: The context would be lost by regenerating

You've invested time customizing the code: logging, messages in Spanish, a specific format, integration with other components of the project. Regenerating would lose that work.

Example: Endpoint with extensive customizations

import logging
from fastapi import FastAPI, Depends, HTTPException, BackgroundTasks
from sqlalchemy.orm import Session
from app.database import get_db
from app.models import Task, AuditLog
from app.schemas import TaskCreate, TaskResponse
from app.dependencies.auth import get_current_user
from app.services.notification import send_task_notification

logger = logging.getLogger(__name__)

app = FastAPI()

@app.post("/tasks", response_model=TaskResponse, status_code=201)
async def create_task(
    task: TaskCreate,
    background_tasks: BackgroundTasks,
    current_user=Depends(get_current_user),
    db: Session = Depends(get_db),
):
    logger.info(
        "Creando tarea",
        extra={"user_id": current_user.id, "task_title": task.title},
    )

    new_task = Task(**task.model_dump(), owner_id=current_user.id)
    db.add(new_task)

    audit = AuditLog(
        action="task_created",
        entity_type="task",
        user_id=current_user.id,
        details=f"Tarea '{task.title}' creada",
    )
    db.add(audit)

    db.commit()
    db.refresh(new_task)

    background_tasks.add_task(
        send_task_notification,
        user_id=current_user.id,
        task_id=new_task.id,
        action="created",
    )

    logger.info(
        "Tarea creada exitosamente",
        extra={"user_id": current_user.id, "task_id": new_task.id},
    )
    return new_task

The bug: The db.commit() isn't in a try/except — if the commit fails (e.g., a constraint violation), the response is a generic 500 instead of a descriptive error.

Regenerate? It would be a very bad idea. This endpoint has:

  • Structured logging with extra fields
  • Integrated audit log
  • A background task for notifications
  • Messages in Spanish
  • Integration with send_task_notification

Regenerating would lose all of this. Claude Code has no context of your audit log system, your notification service, or your logging format.

Edit? Yes. Add a try/except around the commit:

    try:
        db.commit()
        db.refresh(new_task)
    except Exception as e:
        db.rollback()
        logger.error(
            "Error al crear tarea",
            extra={"user_id": current_user.id, "error": str(e)},
        )
        raise HTTPException(
            status_code=409,
            detail="No se pudo crear la tarea. Verifica que los datos sean válidos.",
        )

The concrete signal: If the code has customizations that took more than 5 minutes to create, and the fix is simpler than re-applying the customizations, edit.

Signal 4: The issue is a known pattern

You've seen this type of error before. You don't need to investigate or think — you know exactly what causes it and how to fix it. It's pure pattern matching.

Example: N+1 query in a relationship

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

app = FastAPI()

@app.get("/tasks", response_model=list[TaskWithTags])
async def list_tasks_with_tags(
    current_user=Depends(get_current_user),
    db: Session = Depends(get_db),
):
    tasks = (
        db.query(Task)
        .filter(Task.owner_id == current_user.id)
        .all()
    )
    return tasks  # Accessing task.tags during serialization causes N+1

If you already know the N+1 pattern, the solution is immediate:

from sqlalchemy.orm import joinedload

    tasks = (
        db.query(Task)
        .options(joinedload(Task.tags))
        .filter(Task.owner_id == current_user.id)
        .all()
    )

Adding .options(joinedload(Task.tags)) is one line. You don't need to think, you don't need to regenerate, you don't need to consult documentation. It's a pattern you've already internalized.

The concrete signal: If you say "ah, this is a [pattern name]" and you know the solution by heart, edit.

Common known patterns:

PatternKnown fix
N+1 queryjoinedload or selectinload
Off-by-one in paginationAdjust offset = (page - 1) * size
Missing await in asyncAdd await
datetime.now() without timezonedatetime.now(timezone.utc)
Dict access without .get()Change d["key"] to d.get("key")
Missing null checkAdd if x is not None:
Circular importMove the import inside the function

Signal 5: Regenerating would take longer than editing

Sometimes, even if the code has several problems, editing them is faster than regenerating. Regenerating requires: writing a prompt, waiting for generation, reviewing the new code, re-applying customizations, testing. If you can make 5 edits in 10 minutes but regenerating would take 25, edit.

Example: 4 edits in an endpoint

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

app = FastAPI()

class TaskUpdate(BaseModel):
    title: str | None = None
    description: str | None = None
    status: str | None = None
    priority: str | None = None

@app.patch("/tasks/{task_id}")
async def update_task(
    task_id: int,
    updates: TaskUpdate,
    current_user=Depends(get_current_user),
    db: Session = Depends(get_db),
):
    task = db.query(Task).filter(Task.id == task_id).first()
    # Edit 1: missing None check (task not found)
    # Edit 2: missing ownership check (task.owner_id != current_user.id)
    
    update_data = updates.model_dump(exclude_unset=True)
    # Edit 3: missing validation that update_data isn't empty
    
    for field, value in update_data.items():
        setattr(task, field, value)
    
    db.commit()
    db.refresh(task)
    return task  # Edit 4: missing response_model in the decorator

4 necessary edits:

@app.patch("/tasks/{task_id}", response_model=TaskResponse)  # Edit 4
async def update_task(
    task_id: int,
    updates: TaskUpdate,
    current_user=Depends(get_current_user),
    db: Session = Depends(get_db),
):
    task = db.query(Task).filter(Task.id == task_id).first()

    if not task:                                                 # Edit 1
        raise HTTPException(404, "Task not found")

    if task.owner_id != current_user.id:                        # Edit 2
        raise HTTPException(403, "Not authorized")

    update_data = updates.model_dump(exclude_unset=True)

    if not update_data:                                          # Edit 3
        raise HTTPException(400, "No fields to update")

    for field, value in update_data.items():
        setattr(task, field, value)
    
    db.commit()
    db.refresh(task)
    return task

Time to edit: ~8 minutes (4 clear edits) Time to regenerate: ~20 minutes (prompt + generation + review + verification)

The concrete signal: If you can list all the necessary edits in less than 2 minutes and each one is clear, edit.


Efficient Editing Techniques with Claude Code

Editing doesn't mean doing everything manually. You can use Claude Code to assist you with precise edits without regenerating everything.

Technique 1: Ask only for the fix, not the whole file

❌ "Regenerate the update_task endpoint with these fixes..."
   → Claude Code generates the whole file again
   → You lose customizations, introduce variability

✅ "In the update_task endpoint (line 28 of routers/tasks.py), 
    add an ownership check after looking up the task. 
    Only show me the lines that change, not the whole file."
   → Claude Code gives you the exact 3 lines to add
   → You preserve all the context

Technique 2: Ask for a review before editing

"Before making changes, review this endpoint and tell me:
1. Does it have null checks where it needs them?
2. Are the permissions checked correctly?
3. Is the response model correct?
4. Are there unhandled edge cases?

Just identify the problems — I'll make the edits."

Claude Code analyzes and gives you a list of problems. You decide which to fix and how. This combines AI's analysis capability with your control over the code.

Technique 3: Ask for the edit in context

"I have this code in task_service.py:

[you paste lines 40-60]

The problem is that line 48 uses .all() and loads everything 
into memory. Change it to use .first() since I only 
need one record. Only show me the changed line."

By giving the exact context (specific lines), Claude Code can make the precise edit without touching anything else.

Technique 4: Batch of related edits

If you have 5 edits in the same file, you can ask for them all together:

"In routers/tasks.py, I need these changes:

1. Line 25: add response_model=TaskResponse to the decorator
2. Line 32: add a check 'if not task: raise HTTPException(404)'
3. Line 35: add an ownership check
4. Line 42: change db.commit() to a try/except with rollback
5. Line 48: add response_model to the list_tasks decorator

Show me each change with 2 lines of context above and below 
so I can locate them easily."

Technique 5: Edit and ask for a review of the edit

After making your edits, pass the modified code to Claude Code:

"I made these changes in update_task. Review whether:
1. The ownership check is correct
2. The try/except doesn't silence errors that should propagate
3. I didn't introduce new problems

[you paste the edited code]"

This closes the loop: you edit → verify → fix if necessary. It's faster than regenerating and safer than editing without verification.


The Anti-Pattern: Compulsive Regenerating

There's an anti-pattern worth naming: the compulsive regenerator. It's the developer who regenerates at any problem, no matter how small.

Symptoms of the compulsive regenerator:

1. Sees a typo → regenerates the whole function
2. A field is missing → regenerates the entire schema
3. An if needs an else → regenerates the entire block
4. A test fails → regenerates the test from scratch
5. An import is wrong → regenerates the file

Consequences:
├── Loses time on each regeneration (~15-20 min vs 2 min for an edit)
├── Loses customizations repeatedly
├── Never develops the skill of reading and modifying code
├── Each regeneration can introduce new problems
├── Depends 100% on AI for any change
└── In a day, loses 2-3 hours on unnecessary regenerations

The antidote: Before regenerating, ask: "Can I describe the fix in one sentence?" If yes, edit.


Edit vs Regenerate: The Math of Time

To internalize when to edit, it helps to see the numbers:

Scenario: endpoint with 1 bug (incorrect operator)

Edit:
├── Find the line: 1 min
├── Change > to <: 10 sec
├── Verify: 2 min
└── Total: ~3 min

Regenerate:
├── Write prompt: 3 min
├── Wait for generation: 1 min
├── Read new code (50 lines): 3 min
├── Compare with the previous one: 2 min
├── Verify you didn't lose anything: 3 min
├── Test: 2 min
└── Total: ~14 min

Difference: 11 minutes per instance
If this happens 5 times a day: 55 minutes lost
In a week: ~4.5 hours
Scenario: endpoint with 8 fundamental problems

Edit:
├── Identify 8 problems: 5 min
├── Edit each one: 3 min × 8 = 24 min
├── Verify interactions: 5 min
├── Test: 5 min
└── Total: ~39 min

Regenerate:
├── Write a detailed prompt: 5 min
├── Wait for generation: 1 min
├── Read the new code: 5 min
├── Re-apply customizations: 5 min
├── Test: 5 min
└── Total: ~21 min

Difference: Regenerating saves 18 minutes

The crossover point is usually around 4-5 significant edits. Fewer than 4, editing is faster. More than 5, regenerating starts to win.

But this depends on the complexity of each edit and the cost of the lost customizations. It's a heuristic, not an absolute rule.


Special Case: Editing with Claude Code's Assistance

There's a middle ground between "edit manually" and "regenerate everything" that's extremely powerful: asking Claude Code to make surgical edits.

Instead of:
"Regenerate the calculate_stats function"

Say:
"In calculate_stats, change only the query so it uses 
func.count() instead of len() in Python. Keep everything 
else the same — the filters, the response format, 
the Depends, everything."

Practical example:

# Current code:
@app.get("/stats")
async def get_stats(db: Session = Depends(get_db)):
    tasks = db.query(Task).all()
    total = len(tasks)
    completed = len([t for t in tasks if t.status == "completed"])
    return {"total": total, "completed": completed}

Assisted edit prompt:

Edit get_stats so it uses SQL aggregations instead of 
loading everything into memory. Specifically:
- Replace db.query(Task).all() + len() with 
  db.query(func.count(Task.id)).scalar()
- Replace the list comprehension with a .filter().count()
- Keep the decorator, the Depends, and the response format

Only show me the edited function.

Result:

from sqlalchemy import func

@app.get("/stats")
async def get_stats(db: Session = Depends(get_db)):
    total = db.query(func.count(Task.id)).scalar()
    completed = (
        db.query(func.count(Task.id))
        .filter(Task.status == "completed")
        .scalar()
    )
    return {"total": total, "completed": completed}

This isn't regenerating — it's assisted editing. Claude Code understands that it must keep the context and only change what you asked for.


Connection to the Project

In the capstone project (Module 8)

Most of the fixes in the capstone project will be edits, not regenerations. This is intentional: it reflects the reality of day-to-day work with AI coding tools.

Typical distribution in the project:

Targeted edits:              ~60% of the fixes
├── Incorrect operators
├── Missing null checks
├── Missing fields in schemas
├── Incorrect imports
└── Off-by-one errors

Assisted edits:              ~25% of the fixes
├── Add eager loading
├── Change inline validation to Pydantic
├── Add error handling
└── Fix SQL queries

Regenerations:               ~15% of the fixes
├── A function with an incorrect algorithmic approach
├── A synchronous endpoint that should be async
└── Completely wrong business logic

For each fix, you'll document:

  • ✅ What type of change you made (edit, assisted edit, regeneration)
  • ✅ Why you chose that type
  • ✅ How long it took you
  • ✅ What you verified after the change

Troubleshooting

Problem 1: "I'm not sure if the fix is targeted or if there are deeper problems"

Cause: Sometimes a visible bug is a symptom of a design problem. Solution: Apply the rule of 3 fixes. If you fix a bug and 2 more appear in the same component, the design is probably the problem — not the individual bugs. In that case, reconsider regenerating. But if you fix a bug and everything else works, the targeted fix was correct.

Problem 2: "I edited and now the code looks inconsistent"

Cause: Your edits use a different style from the generated code. Solution: Keep the existing code's style. If the file uses logging with logger.info(), don't use print(). If the file uses HTTPException(status_code=404), don't use HTTPException(404). Consistency is more important than your personal preference.

Problem 3: "I made several edits and lost track of what I changed"

Cause: Without change tracking, it's easy to lose context. Solution: Use git diff frequently to see what changed. Make incremental commits — one commit per logical fix. If you lose track, git diff shows you exactly what you edited.

Problem 4: "The edit was correct but broke something in another file"

Cause: Dependencies you didn't consider. Solution: Before editing, ask: "Does this change affect other files?" If you change a schema, verify the endpoints that use it. If you change a function, verify who calls it. Use the investigation techniques from capsule 02 to map the impact before editing.


Exercises

Exercise 1: Edit or regenerate — decide (Medium)

For each piece of code, decide whether you'd edit or regenerate. Justify with the signal that applies.

Code A:

from fastapi import FastAPI, Depends
from sqlalchemy.orm import Session
from app.database import get_db
from app.models import User
from app.schemas import UserResponse

app = FastAPI()

@app.get("/users/{user_id}", response_model=UserResponse)
async def get_user(user_id: int, db: Session = Depends(get_db)):
    user = db.query(User).filter(User.id == user_id).first()
    return user  # Bug: doesn't handle user == None

Code B:

from fastapi import FastAPI

app = FastAPI()

@app.get("/api/fibonacci/{n}")
async def fibonacci(n: int):
    if n <= 0:
        return {"error": "n must be positive"}
    if n == 1:
        return {"result": 0}
    if n == 2:
        return {"result": 1}

    a, b = 0, 1
    for _ in range(n - 2):
        a, b = b, a + b
    return {"result": b}

Real requirement: you need Fibonacci with memoization and support up to n=10000 without a stack overflow.

Code C:

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

app = FastAPI()

@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 {"message": "Task deleted"}  # Bug: should return 204 No Content
See solution

Code A — EDIT (Signal 1: 90% correct)

Everything is fine except that a null check is missing. Add 2 lines:

    user = db.query(User).filter(User.id == user_id).first()
    if not user:
        raise HTTPException(404, "User not found")
    return user

Code B — REGENERATE (Signal 4: Doesn't meet requirements)

The real requirement asks for memoization and support up to n=10000. The current iterative approach works for small n but has no memoization (for repeated calls) or protection for very large n. Also, the error response should use HTTPException, not a dict.

Regenerate with: "Generate a Fibonacci endpoint with functools.lru_cache for memoization, validation with Path(ge=1, le=10000), and HTTPException for errors."

Code C — EDIT (Signal 2: Targeted and clear fix)

The only problem is the response. Change return {"message": "Task deleted"} to Response(status_code=204):

from fastapi import Response

    db.delete(task)
    db.commit()
    return Response(status_code=204)

And add status_code=204 to the decorator:

@app.delete("/tasks/{task_id}", status_code=204)

Two simple edits. Everything else (auth, ownership check, query) is correct.

Exercise 2: Efficient editing with Claude Code (Medium)

You have this code with 3 problems. Write the prompts you'd give Claude Code to help you edit (NOT regenerate) each problem:

from fastapi import FastAPI, Depends
from sqlalchemy.orm import Session
from app.database import get_db
from app.models import Product
from app.schemas import ProductCreate

app = FastAPI()

@app.post("/products")
async def create_product(
    product: ProductCreate,
    db: Session = Depends(get_db),
):
    # Problem 1: No authentication
    # Problem 2: Doesn't validate that the price is positive
    # Problem 3: No response model

    new_product = Product(**product.model_dump())
    db.add(new_product)
    db.commit()
    db.refresh(new_product)
    return new_product
See solution

Prompt for Problem 1 (authentication):

In create_product (routers/products.py), I need to add 
authentication. The project uses Depends(get_current_user) 
from app.dependencies.auth. Show me:
1. The import I need to add
2. The parameter I add to the function
Don't regenerate the function — just show me which lines to add.

Prompt for Problem 2 (validation):

The ProductCreate schema in schemas/product.py has a 
'price: float' field. I need to add validation that 
it's positive. Should I use Field(gt=0) or a validator? 
Only show me the schema line that changes.

Prompt for Problem 3 (response model):

To the @app.post("/products") decorator I need to add 
response_model and status_code. The response model is 
ProductResponse from app.schemas. Show me what the 
decorator looks like — just that line.

Each prompt asks for a targeted edit, not a regeneration. The result is 3 surgical changes that preserve all the existing context.

Exercise 3: Find the fixes without AI help (Medium)

Without using Claude Code, find and fix the bugs in this code. This trains your ability to edit without depending on AI:

from fastapi import FastAPI, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from app.database import get_db
from app.models import Task
from app.schemas import TaskResponse
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),
    page: int = Query(0, ge=0),
    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)

    offset = page * size
    tasks = query.offset(offset).limit(size).all()
    return tasks
See solution

Bug 1: 0-based vs 1-based pagination

# Current: page=0 is the first page
page: int = Query(0, ge=0)

# Most APIs use 1-based: page=1 is the first
page: int = Query(1, ge=1)

# And the offset is calculated as:
offset = (page - 1) * size  # Not: page * size

With page=0, size=20, the offset is 0 (correct). With page=1, size=20, the offset is 20 (skips the first page). This is an off-by-one.

Bug 2: Missing ordering

Without an ORDER BY, SQLAlchemy returns records in non-deterministic order. Pagination without a consistent order can return duplicated or skipped records between pages.

tasks = query.order_by(Task.created_at.desc()).offset(offset).limit(size).all()

Bug 3: Doesn't validate that status is a valid value

If a user sends ?status=invalid_value, the query doesn't fail but returns an empty list — which looks like a "no tasks" bug when in reality the filter is incorrect.

VALID_STATUSES = {"pending", "in_progress", "completed", "cancelled"}

if status:
    if status not in VALID_STATUSES:
        raise HTTPException(400, f"Invalid status. Must be one of: {VALID_STATUSES}")
    query = query.filter(Task.status == status)

Three targeted edits, none require regenerating.

Exercise 4: Surgical editing with context (Hard)

You have a 120-line file where only one function (lines 45-70) has a problem. The rest of the file is correct and has customizations. Write the complete process you'd follow to make the edit without losing anything:

See solution
Surgical editing process:

1. READ: Open the file and read the problematic function 
   (lines 45-70). Understand what it does and what the bug is.

2. UNDERSTAND DEPENDENCIES: Is the function called from 
   other files? Will the signature or return type change?
   If so, I need to verify the callers.

3. VERIFY CUSTOMIZATIONS: Is there logging, comments, 
   or integrations in the function I don't want to lose?
   I note them down.

4. PLAN: I describe the fix in one sentence:
   "Change the query from .all() + len() to .count()"

5. MAKE THE EDIT:
   - If it's 1-3 lines: I edit directly
   - If it's 5-10 lines: I ask Claude Code for the specific 
     edit with context of the current lines

6. VERIFY:
   - git diff to see exactly what I changed
   - Are the customizations still there?
   - Did the function signature change? If so, did I update 
     the callers?
   - Manual testing or with existing tests

7. COMMIT: A commit with a descriptive message:
   "Fix: use SQL count instead of loading all rows 
   in get_task_stats"

Exercise 5: Batch of edits (Hard)

This endpoint has 5 problems. All are editable (they don't require regenerating). Identify the 5 and write the fixes:

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

app = FastAPI()

@app.put("/tasks/{task_id}")
async def replace_task(
    task_id: int,
    title: str,
    description: str,
    status: str,
    priority: str,
    current_user=Depends(get_current_user),
    db: Session = Depends(get_db),
):
    task = db.query(Task).filter(Task.id == task_id).first()
    task.title = title
    task.description = description
    task.status = status
    task.priority = priority
    db.commit()
    return {"status": "updated"}
See solution

Problem 1: Missing null check for task

    task = db.query(Task).filter(Task.id == task_id).first()
    if not task:
        raise HTTPException(404, "Task not found")

Problem 2: Missing ownership check

    if task.owner_id != current_user.id:
        raise HTTPException(403, "Not authorized")

Problem 3: The parameters should be a Pydantic model, not individual query parameters

class TaskReplace(BaseModel):
    title: str = Field(min_length=1, max_length=200)
    description: str | None = None
    status: str
    priority: str

@app.put("/tasks/{task_id}")
async def replace_task(
    task_id: int,
    task_data: TaskReplace,
    ...
):

Problem 4: PUT should use response_model and return the updated object

@app.put("/tasks/{task_id}", response_model=TaskResponse)
...
    db.refresh(task)
    return task

Problem 5: There's no validation that status and priority are valid values (it should use Enums in the Pydantic schema)

Five edits, each targeted and clear. The general approach (a PUT endpoint that replaces a task) is correct — only validations and best practices are missing.


Summary

  • Most problems in AI-generated code are solved by editing, not regenerating (~70-80% of cases)
  • There are 5 clear signals that editing is better:
    1. 90% of the code is correct
    2. The fix is targeted and clear (you describe it in one sentence)
    3. The context would be lost by regenerating (customizations, logging, integrations)
    4. The issue is a known pattern (N+1, off-by-one, missing null check)
    5. Regenerating would take longer than editing (< 4-5 changes)
  • Assisted editing with Claude Code is a powerful middle ground: you ask for surgical edits without regenerating everything
  • The anti-pattern of the compulsive regenerator wastes hours per week and never develops the skill of reading code
  • The crossover point between editing and regenerating is usually around 4-5 significant edits — fewer than that, editing is faster
  • After editing, always verify: git diff to review changes, tests to verify functionality

Additional Resources

  1. Refactoring Guru — Code Smells - Identify problems in code and decide how to fix them
  2. Martin Fowler — Refactoring - Techniques for incremental code editing
  3. Working Effectively with Legacy Code — Michael Feathers - How to make safe edits in existing code
  4. SQLAlchemy — Eager Loading - A reference for the N+1 fix with joinedload/selectinload
  5. Pydantic V2 — Field Constraints - Centralize validation in schemas instead of inline

Next capsule: Decision Framework: Regenerate vs Edit — the complete framework with practical scenarios.


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