Módulo 7: Subagents para Debugging, Regenerar vs Editar

Cuándo Editar Manualmente

Cuándo Editar Manualmente

Descripción de la cápsula

La cápsula anterior cubrió cuándo regenerar. Esta cubre el otro lado del espectro: cuándo editar manualmente es la mejor opción. Y aquí hay una verdad incómoda: la mayoría de las veces, editar es la respuesta correcta. La tentación de regenerar es fuerte — parece más rápido, más limpio, más "AI-first." Pero en la práctica, el 70-80% de los problemas en código AI-generated se resuelven mejor editando que regenerando.

¿Por qué? Porque la mayoría de los problemas son puntuales. Un off-by-one error. Un operador incorrecto. Un campo que falta en el response. Un > que debería ser >=. Regenerar todo el archivo por un problema de un carácter es como demoler una casa porque un grifo gotea.

Esta cápsula te da las señales claras de que editar es mejor, las técnicas para editar eficientemente con asistencia de Claude Code, y la confianza de saber que a veces la solución más simple es la correcta.


Las 5 Señales de que Editar es Mejor

Señal 1: El 90% del código está correcto

La señal más obvia y la más frecuente. El código funciona bien en general — solo tiene un problema puntual que puedes identificar y corregir.

Ejemplo: Operador incorrecto en filtro

Claude Code generó un endpoint de búsqueda de tareas:

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: debería ser <
                Task.status != "completed",
            )
        )
        .order_by(Task.due_date.asc())
        .all()
    )
    return overdue

Análisis:

  • ✅ Estructura del endpoint: correcta
  • ✅ Autenticación: correcta (usa get_current_user)
  • ✅ Query con SQLAlchemy: bien construida
  • ✅ Filtro por owner: correcto
  • ✅ Filtro por status: correcto
  • ✅ Ordenamiento: correcto
  • ✅ Response model: correcto
  • ❌ Un solo problema: Task.due_date > now debería ser Task.due_date < now

¿Regenerar? No. El 95% del código está perfecto. Regenerar te daría código diferente que podría perder el ordering, la estructura de query, o los filtros correctos.

¿Editar? Sí. Cambiar > por < toma 2 segundos:

Task.due_date < now,  # Fix: tareas cuya fecha límite YA PASÓ

La señal concreta: Si puedes describir el problema en una oración y señalar la línea exacta, edita.

Señal 2: El fix es puntual y claro

No solo el código está mayoritariamente correcto — el fix es obvio. No necesitas pensar en alternativas, no hay ambigüedad, no hay trade-offs. Sabes exactamente qué cambiar.

Ejemplo: Falta un campo en el 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
    # Falta: created_at y 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

El fix: Agregar dos campos al 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}

¿Regenerar el endpoint? No — el endpoint está bien. Solo falta un campo en el schema.

La señal concreta: Si el fix es "agregar X" o "cambiar Y por Z" y no hay efectos secundarios, edita.

Señal 3: El contexto se perdería al regenerar

Has invertido tiempo en customizar el código: logging, mensajes en español, format específico, integración con otros componentes del proyecto. Regenerar perdería ese trabajo.

Ejemplo: Endpoint con customizaciones extensas

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

El bug: El db.commit() no está en un try/except — si el commit falla (e.g., constraint violation), la respuesta es un 500 genérico en vez de un error descriptivo.

¿Regenerar? Sería una muy mala idea. Este endpoint tiene:

  • Logging estructurado con extra fields
  • Audit log integrado
  • Background task para notificaciones
  • Mensajes en español
  • Integración con send_task_notification

Regenerar perdería todo esto. Claude Code no tiene contexto de tu sistema de audit logs, tu servicio de notificaciones, ni tu formato de logging.

¿Editar? Sí. Agrega un try/except alrededor del 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.",
        )

La señal concreta: Si el código tiene customizaciones que tomaron más de 5 minutos crear, y el fix es más simple que re-aplicar las customizaciones, edita.

Señal 4: El issue es un patrón conocido

Has visto este tipo de error antes. No necesitas investigar ni pensar — sabes exactamente qué lo causa y cómo arreglarlo. Es pattern matching puro.

Ejemplo: N+1 query en un 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  # Acceder a task.tags en la serialización causa N+1

Si ya conoces el patrón N+1, la solución es inmediata:

from sqlalchemy.orm import joinedload

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

Agregar .options(joinedload(Task.tags)) es una línea. No necesitas pensar, no necesitas regenerar, no necesitas consultar documentación. Es un patrón que ya internalizaste.

La señal concreta: Si dices "ah, este es un [nombre del patrón]" y sabes la solución de memoria, edita.

Patrones conocidos comunes:

PatrónFix conocido
N+1 queryjoinedload o selectinload
Off-by-one en paginaciónAjustar offset = (page - 1) * size
Missing await en asyncAgregar await
datetime.now() sin timezonedatetime.now(timezone.utc)
Dict access sin .get()Cambiar d["key"] por d.get("key")
Missing null checkAgregar if x is not None:
Import circularMover import dentro de la función

Señal 5: Regenerar tomaría más tiempo que editar

A veces, incluso si el código tiene varios problemas, editarlos es más rápido que regenerar. Regenerar requiere: escribir prompt, esperar generación, revisar código nuevo, re-aplicar customizaciones, testear. Si puedes hacer 5 ediciones en 10 minutos pero regenerar tomaría 25, edita.

Ejemplo: 4 ediciones en un 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: falta check de None (task not found)
    # Edit 2: falta check de ownership (task.owner_id != current_user.id)
    
    update_data = updates.model_dump(exclude_unset=True)
    # Edit 3: falta validación de que update_data no esté vacío
    
    for field, value in update_data.items():
        setattr(task, field, value)
    
    db.commit()
    db.refresh(task)
    return task  # Edit 4: falta response_model en el decorador

4 ediciones necesarias:

@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

Tiempo de editar: ~8 minutos (4 ediciones claras) Tiempo de regenerar: ~20 minutos (prompt + generación + review + verificar)

La señal concreta: Si puedes listar todas las ediciones necesarias en menos de 2 minutos y cada una es clara, edita.


Técnicas de Edición Eficiente con Claude Code

Editar no significa hacerlo todo manualmente. Puedes usar Claude Code para asistirte en ediciones precisas sin regenerar todo.

Técnica 1: Pedir solo el fix, no el archivo completo

❌ "Regenera el endpoint update_task con estos fixes..."
   → Claude Code genera todo el archivo de nuevo
   → Pierdes customizaciones, introduces variabilidad

✅ "En el endpoint update_task (línea 28 de routers/tasks.py), 
    agrega un check de ownership después de buscar la task. 
    Solo muéstrame las líneas que cambian, no el archivo completo."
   → Claude Code te da las 3 líneas exactas para agregar
   → Preservas todo el contexto

Técnica 2: Pedir verificación antes de editar

"Antes de hacer cambios, revisa este endpoint y dime:
1. ¿Tiene null checks donde los necesita?
2. ¿Los permisos se verifican correctamente?
3. ¿El response model es correcto?
4. ¿Hay edge cases no manejados?

Solo identifica los problemas — yo haré las ediciones."

Claude Code analiza y te da una lista de problemas. Tú decides cuáles arreglar y cómo. Esto combina la capacidad de análisis de AI con tu control sobre el código.

Técnica 3: Pedir la edición en contexto

"Tengo este código en task_service.py:

[pegas las líneas 40-60]

El problema es que la línea 48 usa .all() y carga todo 
en memoria. Cámbiala para usar .first() ya que solo 
necesito un registro. Solo muéstrame la línea cambiada."

Al dar el contexto exacto (líneas específicas), Claude Code puede hacer la edición precisa sin tocar nada más.

Técnica 4: Batch de ediciones relacionadas

Si tienes 5 ediciones en el mismo archivo, puedes pedirlas todas juntas:

"En routers/tasks.py, necesito estos cambios:

1. Línea 25: agregar response_model=TaskResponse al decorador
2. Línea 32: agregar check 'if not task: raise HTTPException(404)'
3. Línea 35: agregar check de ownership
4. Línea 42: cambiar db.commit() por un try/except con rollback
5. Línea 48: agregar response_model al decorador de list_tasks

Muéstrame cada cambio con 2 líneas de contexto arriba y abajo 
para que pueda ubicarlos fácilmente."

Técnica 5: Editar y pedir review del edit

Después de hacer tus ediciones, pásale el código modificado a Claude Code:

"Hice estos cambios en update_task. Revisa si:
1. El check de ownership es correcto
2. El try/except no silencia errores que deberían propagarse
3. No introduje nuevos problemas

[pegas el código editado]"

Esto cierra el loop: editas → verificas → corriges si necesario. Es más rápido que regenerar y más seguro que editar sin verificación.


El Anti-Patrón: Regenerar Compulsivo

Hay un anti-patrón que vale la pena nombrar: el regenerador compulsivo. Es el developer que regenera ante cualquier problema, sin importar qué tan pequeño sea.

Síntomas del regenerador compulsivo:

1. Ve un typo → regenera toda la función
2. Falta un campo → regenera el schema completo
3. Un if necesita un else → regenera el bloque entero
4. Un test falla → regenera el test desde cero
5. Un import está mal → regenera el archivo

Consecuencias:
├── Pierde tiempo en cada regeneración (~15-20 min vs 2 min de edit)
├── Pierde customizaciones repetidamente
├── Nunca desarrolla habilidad de leer y modificar código
├── Cada regeneración puede introducir problemas nuevos
├── Depende 100% de AI para cualquier cambio
└── En un día, pierde 2-3 horas en regeneraciones innecesarias

El antídoto: Antes de regenerar, pregunta: "¿Puedo describir el fix en una oración?" Si sí, edita.


Editar vs Regenerar: La Matemática del Tiempo

Para internalizar cuándo editar, ayuda ver los números:

Escenario: endpoint con 1 bug (operador incorrecto)

Editar:
├── Encontrar la línea: 1 min
├── Cambiar > por <: 10 seg
├── Verificar: 2 min
└── Total: ~3 min

Regenerar:
├── Escribir prompt: 3 min
├── Esperar generación: 1 min
├── Leer código nuevo (50 líneas): 3 min
├── Comparar con el anterior: 2 min
├── Verificar que no perdiste nada: 3 min
├── Testear: 2 min
└── Total: ~14 min

Diferencia: 11 minutos por cada instancia
Si esto pasa 5 veces al día: 55 minutos perdidos
En una semana: ~4.5 horas
Escenario: endpoint con 8 problemas fundamentales

Editar:
├── Identificar 8 problemas: 5 min
├── Editar cada uno: 3 min × 8 = 24 min
├── Verificar interacciones: 5 min
├── Testear: 5 min
└── Total: ~39 min

Regenerar:
├── Escribir prompt detallado: 5 min
├── Esperar generación: 1 min
├── Leer código nuevo: 5 min
├── Re-aplicar customizaciones: 5 min
├── Testear: 5 min
└── Total: ~21 min

Diferencia: Regenerar ahorra 18 minutos

El punto de cruce suele estar alrededor de 4-5 ediciones significativas. Menos de 4, editar es más rápido. Más de 5, regenerar empieza a ganar.

Pero esto depende de la complejidad de cada edición y del costo de las customizaciones perdidas. Es una heurística, no una regla absoluta.


Caso Especial: Editar con Asistencia de Claude Code

Hay un punto medio entre "editar manualmente" y "regenerar todo" que es extremadamente poderoso: pedir a Claude Code que haga ediciones quirúrgicas.

En vez de:
"Regenera la función calculate_stats"

Di:
"En calculate_stats, cambia solo la query para que use 
func.count() en vez de len() en Python. Mantén todo 
lo demás igual — los filtros, el response format, 
los Depends, todo."

Ejemplo práctico:

# Código actual:
@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}

Prompt de edición asistida:

Edita get_stats para que use SQL aggregations en vez de 
cargar todo en memoria. Específicamente:
- Reemplaza db.query(Task).all() + len() por 
  db.query(func.count(Task.id)).scalar()
- Reemplaza el list comprehension por un .filter().count()
- Mantén el decorador, los Depends, y el formato de response

Solo muéstrame la función editada.

Resultado:

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}

Esto no es regenerar — es edición asistida. Claude Code entiende que debe mantener el contexto y solo cambiar lo que le pediste.


Conexión con Proyecto

En el proyecto integrador (Módulo 8)

La mayoría de los fixes en el proyecto integrador serán ediciones, no regeneraciones. Esto es intencional: refleja la realidad del día a día con AI coding tools.

Distribución típica en el proyecto:

Ediciones puntuales:         ~60% de los fixes
├── Operadores incorrectos
├── Null checks faltantes
├── Campos faltantes en schemas
├── Imports incorrectos
└── Off-by-one errors

Ediciones con asistencia:    ~25% de los fixes
├── Agregar eager loading
├── Cambiar validación inline por Pydantic
├── Agregar error handling
└── Corregir queries SQL

Regeneraciones:              ~15% de los fixes
├── Función con approach algorítmico incorrecto
├── Endpoint síncrono que debería ser async
└── Lógica de negocio completamente equivocada

Para cada fix, documentarás:

  • ✅ Qué tipo de cambio hiciste (edición, edición asistida, regeneración)
  • ✅ Por qué elegiste ese tipo
  • ✅ Cuánto tiempo te tomó
  • ✅ Qué verificaste después del cambio

Troubleshooting

Problema 1: "No estoy seguro de si el fix es puntual o si hay problemas más profundos"

Causa: A veces un bug visible es síntoma de un problema de diseño. Solución: Aplica la regla de los 3 fixes. Si arreglas un bug y aparecen otros 2 en el mismo componente, probablemente el diseño es el problema — no los bugs individuales. En ese caso, reconsidera regenerar. Pero si arreglas un bug y todo lo demás funciona, el fix puntual era correcto.

Problema 2: "Edité y ahora el código se ve inconsistente"

Causa: Tus ediciones usan un estilo diferente al código generado. Solución: Mantén el estilo del código existente. Si el archivo usa logging con logger.info(), no uses print(). Si el archivo usa HTTPException(status_code=404), no uses HTTPException(404). Consistencia es más importante que tu preferencia personal.

Problema 3: "Hice varias ediciones y perdí el hilo de qué cambié"

Causa: Sin tracking de cambios, es fácil perder el contexto. Solución: Usa git diff frecuentemente para ver qué cambió. Haz commits incrementales — un commit por cada fix lógico. Si pierdes el hilo, git diff te muestra exactamente qué editaste.

Problema 4: "La edición fue correcta pero rompió algo en otro archivo"

Causa: Dependencias que no consideraste. Solución: Antes de editar, pregunta: "¿Este cambio afecta otros archivos?" Si cambias un schema, verifica los endpoints que lo usan. Si cambias una función, verifica quién la llama. Usa las técnicas de investigación de la cápsula 02 para mapear el impacto antes de editar.


Ejercicios

Ejercicio 1: Editar o regenerar — decide (Medio)

Para cada código, decide si editarías o regenerarías. Justifica con la señal que aplica.

Código 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: no maneja user == None

Código 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}

Requerimiento real: necesitas Fibonacci con memoización y soporte hasta n=10000 sin stackoverflow.

Código 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: debería devolver 204 No Content
Ver solución

Código A — EDITAR (Señal 1: 90% correcto)

Todo está bien excepto que falta un null check. Agrega 2 líneas:

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

Código B — REGENERAR (Señal 4: No cumple requisitos)

El requerimiento real pide memoización y soporte hasta n=10000. El approach iterativo actual funciona para n pequeño pero no tiene memoización (para llamadas repetidas) ni protección para n muy grande. Además, la respuesta de error debería usar HTTPException, no un dict.

Regenerar con: "Genera un endpoint Fibonacci con functools.lru_cache para memoización, validación con Path(ge=1, le=10000), y HTTPException para errores."

Código C — EDITAR (Señal 2: Fix puntual y claro)

El único problema es el response. Cambiar return {"message": "Task deleted"} por Response(status_code=204):

from fastapi import Response

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

Y agregar status_code=204 al decorador:

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

Dos ediciones simples. Todo lo demás (auth, ownership check, query) está correcto.

Ejercicio 2: Edición eficiente con Claude Code (Medio)

Tienes este código con 3 problemas. Escribe los prompts que le darías a Claude Code para que te ayude a editar (NO regenerar) cada problema:

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),
):
    # Problema 1: No tiene autenticación
    # Problema 2: No valida que el precio sea positivo
    # Problema 3: No tiene response model

    new_product = Product(**product.model_dump())
    db.add(new_product)
    db.commit()
    db.refresh(new_product)
    return new_product
Ver solución

Prompt para Problema 1 (autenticación):

En create_product (routers/products.py), necesito agregar 
autenticación. El proyecto usa Depends(get_current_user) 
de app.dependencies.auth. Muéstrame:
1. El import que necesito agregar
2. El parámetro que agrego a la función
No regeneres la función — solo muéstrame qué líneas agregar.

Prompt para Problema 2 (validación):

El schema ProductCreate en schemas/product.py tiene un 
campo 'price: float'. Necesito agregar validación de que 
sea positivo. ¿Debo usar Field(gt=0) o un validator? 
Solo muéstrame la línea del schema que cambia.

Prompt para Problema 3 (response model):

Al decorador @app.post("/products") necesito agregar 
response_model y status_code. El modelo de response es 
ProductResponse de app.schemas. Muéstrame cómo queda 
el decorador — solo esa línea.

Cada prompt pide una edición puntual, no una regeneración. El resultado son 3 cambios quirúrgicos que preservan todo el contexto existente.

Ejercicio 3: Encontrar los fixes sin ayuda de AI (Medio)

Sin usar Claude Code, encuentra y corrige los bugs en este código. Esto entrena tu capacidad de editar sin depender de 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
Ver solución

Bug 1: Paginación base-0 vs base-1

# Actual: page=0 es la primera página
page: int = Query(0, ge=0)

# La mayoría de APIs usan base-1: page=1 es la primera
page: int = Query(1, ge=1)

# Y el offset se calcula como:
offset = (page - 1) * size  # No: page * size

Con page=0, size=20, el offset es 0 (correcto). Con page=1, size=20, el offset es 20 (salta la primera página). Esto es un off-by-one.

Bug 2: Falta ordenamiento

Sin un ORDER BY, SQLAlchemy devuelve registros en orden no determinístico. La paginación sin orden consistente puede devolver registros duplicados o saltados entre páginas.

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

Bug 3: No valida que status sea un valor válido

Si un usuario envía ?status=invalid_value, la query no falla pero devuelve una lista vacía — lo que parece un bug de "no hay tareas" cuando en realidad el filtro es incorrecto.

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)

Tres ediciones puntuales, ninguna requiere regenerar.

Ejercicio 4: Edición quirúrgica con contexto (Difícil)

Tienes un archivo de 120 líneas donde solo una función (líneas 45-70) tiene un problema. El resto del archivo está correcto y tiene customizaciones. Escribe el proceso completo que seguirías para hacer la edición sin perder nada:

Ver solución
Proceso de edición quirúrgica:

1. LEER: Abrir el archivo y leer la función problemática 
   (líneas 45-70). Entender qué hace y cuál es el bug.

2. ENTENDER DEPENDENCIAS: ¿La función es llamada desde 
   otros archivos? ¿Cambiará la firma o el tipo de retorno?
   Si sí, necesito verificar los callers.

3. VERIFICAR CUSTOMIZACIONES: ¿Hay logging, comentarios, 
   o integraciones en la función que no quiero perder?
   Las anoto.

4. PLANIFICAR: Describo el fix en una oración:
   "Cambiar la query de .all() + len() a .count()"

5. HACER LA EDICIÓN:
   - Si es 1-3 líneas: edito directamente
   - Si es 5-10 líneas: le pido a Claude Code la edición 
     específica con contexto de las líneas actuales

6. VERIFICAR:
   - git diff para ver exactamente qué cambié
   - ¿Las customizaciones siguen ahí?
   - ¿La firma de la función cambió? Si sí, ¿actualicé 
     los callers?
   - Testeo manual o con tests existentes

7. COMMIT: Un commit con mensaje descriptivo:
   "Fix: use SQL count instead of loading all rows 
   in get_task_stats"

Ejercicio 5: Batch de ediciones (Difícil)

Este endpoint tiene 5 problemas. Todos son editables (no requieren regenerar). Identifica los 5 y escribe las correcciones:

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"}
Ver solución

Problema 1: Falta null check para task

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

Problema 2: Falta verificación de ownership

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

Problema 3: Los parámetros deberían ser un modelo Pydantic, no query parameters individuales

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,
    ...
):

Problema 4: PUT debería usar response_model y devolver el objeto actualizado

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

Problema 5: No hay validación de que status y priority sean valores válidos (debería usar Enums en el schema Pydantic)

Cinco ediciones, cada una puntual y clara. El approach general (PUT endpoint que reemplaza un task) es correcto — solo faltan validaciones y buenas prácticas.


Resumen

  • La mayoría de los problemas en código AI-generated se resuelven editando, no regenerando (~70-80% de los casos)
  • Hay 5 señales claras de que editar es mejor:
    1. El 90% del código está correcto
    2. El fix es puntual y claro (lo describes en una oración)
    3. El contexto se perdería al regenerar (customizaciones, logging, integraciones)
    4. El issue es un patrón conocido (N+1, off-by-one, missing null check)
    5. Regenerar tomaría más tiempo que editar (< 4-5 cambios)
  • Edición asistida con Claude Code es un punto medio poderoso: pides ediciones quirúrgicas sin regenerar todo
  • El anti-patrón del regenerador compulsivo desperdicia horas por semana y nunca desarrolla habilidad de lectura de código
  • El punto de cruce entre editar y regenerar suele estar en 4-5 ediciones significativas — menos de eso, editar es más rápido
  • Después de editar, siempre verifica: git diff para revisar cambios, tests para verificar funcionalidad

Recursos Adicionales

  1. Refactoring Guru — Code Smells - Identificar problemas en código y decidir cómo corregirlos
  2. Martin Fowler — Refactoring - Técnicas de edición incremental de código
  3. Working Effectively with Legacy Code — Michael Feathers - Cómo hacer ediciones seguras en código existente
  4. SQLAlchemy — Eager Loading - Referencia para el fix de N+1 con joinedload/selectinload
  5. Pydantic V2 — Field Constraints - Centralizar validación en schemas en vez de inline

Siguiente cápsula: Decision Framework: Regenerar vs Editar — el framework completo con escenarios prácticos.


Debugging & Code Review with Claude Code — Módulo 7, Cápsula 04 Claude Code Agentic Development Path — Guía #6 de 11