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

Cuándo Regenerar Código

Cuándo Regenerar Código

Descripción de la cápsula

"¿Debería regenerar esto o arreglarlo?" — es la pregunta que te haces varias veces al día cuando trabajas con AI coding tools. Y la respuesta incorrecta tiene costo real: regenerar cuando deberías editar desperdicia contexto, customizaciones, y tiempo. Editar cuando deberías regenerar produce parches frágiles sobre código fundamentalmente incorrecto.

Esta cápsula cubre un lado del espectro: las señales claras de que regenerar es la mejor opción. No "regenerar cuando el código es malo" — eso no es útil. Señales concretas: "regenerar cuando el approach algorítmico es incorrecto: usó un loop O(n²) donde necesitas un dict lookup O(1)."

Pero regenerar no es gratis. Pierdes customizaciones, pierdes contexto que acumulaste, y el nuevo código puede traer problemas diferentes. Entender el costo de regenerar es tan importante como saber cuándo hacerlo.


Las 5 Señales de que Regenerar es Mejor

Señal 1: El approach algorítmico es fundamentalmente incorrecto

Esta es la señal más clara. El código no tiene un bug puntual — tiene el approach equivocado. Editarlo no lo arregla; necesitas un approach diferente.

Ejemplo: Búsqueda ineficiente

Claude Code generó esto para buscar un usuario por email en una lista:

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")

¿Por qué editar no lo arregla?

El approach es búsqueda lineal O(n). Con 10 usuarios funciona. Con 100,000 usuarios, cada búsqueda recorre la lista completa. No puedes optimizar un loop lineal en una lista — necesitas cambiar la estructura de datos.

# ❌ Editar: agregar un break o un return temprano no resuelve O(n)
# ❌ Editar: agregar caché sobre la lista sigue siendo un parche

# ✅ Regenerar con approach correcto:
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

La señal concreta: Si necesitas cambiar la estructura de datos fundamental (list → dict, array → tree, loop → query), regenera.

Señal 2: La arquitectura es incorrecta

El código funciona pero usa el patrón arquitectónico equivocado. Parchear la arquitectura solo acumula deuda técnica.

Ejemplo: Síncrono donde necesitas asíncrono

Claude Code generó un endpoint que hace 3 llamadas externas secuencialmente:

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,
    }

¿Por qué editar no lo arregla?

El código usa requests (síncrono/bloqueante) en un endpoint de FastAPI (que es async). Las 3 llamadas se ejecutan secuencialmente — si cada una tarda 200ms, el endpoint tarda 600ms. Pero el problema real es que bloquea el event loop de FastAPI.

No puedes "editar" requests.get para que sea async. Necesitas cambiar la librería, el patrón de concurrencia, y la estructura de las llamadas.

# ✅ Regenerar con arquitectura correcta:
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(),
    }

La señal concreta: Si necesitas cambiar de síncrono a asíncrono, de monolítico a modular, de polling a websockets, o de in-memory a database — regenera la función o el archivo.

Señal 3: Más del 50% del código necesita cambiar

Si vas a editar más de la mitad de las líneas, estás reescribiendo de todas formas. Pero lo estás haciendo de la forma más lenta: editando línea por línea en vez de generar el código correcto de una vez.

Ejemplo: CRUD con validación incorrecta

Claude Code generó un endpoint de creación de tareas con múltiples problemas:

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

Problemas que requieren edición:

  1. ❌ description no debería ser required (no todas las tareas la tienen)
  2. ❌ priority debería ser un Enum, no un string libre
  3. ❌ due_date debería ser datetime, no str
  4. ❌ assigned_to debería ser Optional[int] (user_id), no str
  5. ❌ Usa global y lista en memoria en vez de base de datos
  6. ❌ No hay autenticación
  7. ❌ No hay validación de que priority sea un valor válido
  8. ❌ No hay response model
  9. ❌ created_at es un string en vez de un datetime

Conteo: 9 problemas en un código de ~25 líneas. Editar cada uno sería más lento que regenerar con un prompt mejor.

La señal concreta: Cuenta los cambios necesarios. Si necesitas modificar más del 50% de las líneas, regenera con un prompt que especifique todos los requisitos.

Señal 4: El código no cumple los requisitos

A veces Claude Code genera código que funciona perfectamente... para un requisito diferente al que pediste. La lógica de negocio es incorrecta. No es un bug — es la feature equivocada.

Ejemplo: Cálculo de descuento incorrecto

Pediste: "Descuento del 10% para compras mayores a $100, 20% para compras mayores a $500."

Claude Code generó:

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,
    }

Parece correcto, pero el requisito real es más complejo:

Requisito real:
- Descuento del 10% sobre el monto que excede $100 (no sobre el total)
- Descuento adicional del 20% sobre el monto que excede $500
- Los descuentos son progresivos (como brackets de impuestos)

Ejemplo: compra de $700
- Primeros $100: sin descuento
- $100 a $500 ($400): 10% = $40
- $500 a $700 ($200): 20% = $40
- Total descuento: $80
- Final: $620

Lo que Claude Code calculó:
- $700 * 20% = $140 de descuento
- Final: $560
- INCORRECTO: $60 de diferencia

Editar la lógica condicional para implementar brackets progresivos requiere reescribir toda la función de cálculo. Es más claro regenerar con el requisito bien explicado.

La señal concreta: Si la lógica de negocio es fundamentalmente diferente a lo que necesitas, regenera explicando el requisito correcto con ejemplos de input/output.

Señal 5: La deuda técnica sería masiva si se parcha

A veces el código funciona y podrías editarlo, pero cada edición agrega complejidad a un diseño que ya es frágil. El resultado sería un Frankenstein de parches.

Ejemplo: Validación dispersa

Claude Code generó validación inline en cada endpoint en vez de centralizarla:

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")
    # ... crear 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")
    # ... actualizar 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")
    # ... crear tasks

El problema: La misma validación se repite 3 veces (y crecerá con cada endpoint). Editar los 3 endpoints para agregar una nueva regla de validación significa tocar 3 lugares. Si falta una, tienes inconsistencia.

¿Podrías editarlo? Sí — moverías la validación a Pydantic validators. Pero tocarías 4 archivos (el model y 3 endpoints) y cada edición es propensa a errores.

¿Es mejor regenerar? Sí — regenerar el schema con Pydantic validators es más limpio:

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):
    # Validación automática por Pydantic
    pass

@app.patch("/tasks/{task_id}")
async def update_task(task_id: int, task: TaskUpdate):
    # Validación automática por Pydantic
    pass

@app.post("/tasks/bulk")
async def bulk_create(tasks: list[TaskCreate]):
    # Validación automática por Pydantic
    pass

La señal concreta: Si editar requiere tocar el mismo patrón en 3+ lugares, y hay un diseño más limpio que centraliza la lógica — regenera con el diseño correcto desde el inicio.


El Costo de Regenerar: Lo que Pierdes

Regenerar no es gratis. Antes de decidir, considera lo que pagas:

Costo 1: Pierdes customizaciones

Si editaste el código después de la generación inicial — agregaste un header, ajustaste un formato, cambiaste un mensaje de error — todo eso se pierde al regenerar.

Escenario:
1. Claude Code genera endpoint de tareas
2. Tú editas: agregas logging, cambias formato de fecha, 
   ajustas el mensaje de error a español
3. Descubres que el approach es incorrecto
4. Regeneras el endpoint
5. El nuevo código: no tiene tu logging, usa formato 
   de fecha americano, mensajes en inglés

→ Necesitas re-aplicar tus customizaciones
→ Si no las recuerdas todas, pierdes algo

Mitigación: Antes de regenerar, anota las customizaciones que quieres preservar. Inclúyelas en el prompt de regeneración.

Costo 2: Pierdes contexto acumulado

Cada interacción con Claude Code construye contexto. Si llevas 20 minutos editando un archivo, Claude Code entiende tu intención, tu estilo, y tus preferencias. Regenerar desde cero pierde ese contexto.

Con contexto acumulado:
"Agrega validación de email"
→ Claude Code sabe que usas Pydantic, sabe tu estilo de error,
  sabe que prefieres validaciones en el schema

Sin contexto (después de regenerar):
"Agrega validación de email"
→ Claude Code podría usar regex inline, podría no seguir
  el mismo estilo de error, podría poner la validación
  en el endpoint en vez del schema

Mitigación: Al regenerar, incluye en el prompt el contexto que necesitas: "Usa Pydantic validators, mensajes de error en español, el mismo formato que el resto del proyecto."

Costo 3: Puedes obtener problemas nuevos

El nuevo código no es una versión mejorada del anterior — es código completamente nuevo que puede tener sus propios bugs.

Código original:
├── Bug: validación faltante en campo priority
├── Correcto: auth, respuesta, format de fecha
└── Correcto: error handling

Código regenerado:
├── Correcto: validación de priority (el fix que buscabas)
├── NUEVO Bug: no hace auth check
├── NUEVO Bug: fecha en formato incorrecto
└── Correcto: error handling

Resolviste un problema y creaste dos. Esto ocurre porque Claude Code no tiene contexto de lo que ya estaba correcto — genera código desde cero basado solo en tu prompt.

Mitigación: Después de regenerar, haz un code review del nuevo código comparándolo con el anterior. Verifica que lo que ya funcionaba sigue funcionando.

Costo 4: Tiempo de regeneración + revisión

Regenerar parece rápido: escribes un prompt, Claude Code genera, listo. Pero el tiempo real incluye:

Tiempo total de regenerar:
├── Escribir un prompt mejor: 3-5 min
├── Esperar la generación: 1-2 min
├── Revisar el código nuevo: 5-10 min
├── Re-aplicar customizaciones: 3-5 min
├── Testear: 5-10 min
└── Total: 17-32 min

vs Tiempo de editar (si el fix es puntual):
├── Identificar el cambio: 2-3 min
├── Hacer la edición: 1-2 min
├── Testear: 3-5 min
└── Total: 6-10 min

Si el fix es puntual, editar es 3x más rápido. Si el fix requiere cambios masivos, regenerar es más eficiente. La decisión correcta depende de la escala del cambio.


Cómo Regenerar Efectivamente

Si decides regenerar, hazlo bien. Un prompt mejor produce mejor código.

Regla 1: Incluye qué estaba mal la primera vez

❌ Prompt vago:
"Genera un endpoint para crear tareas"

✅ Prompt informado por el error anterior:
"Genera un endpoint POST /tasks para crear tareas. 
La versión anterior tenía estos problemas:
1. Usaba validación inline en vez de Pydantic Field constraints
2. Priority era string libre en vez de Enum
3. No tenía autenticación
4. Usaba lista en memoria en vez de DB

Requisitos:
- Pydantic model con Field(min_length=3, max_length=200) para title
- Priority como Enum: low, medium, high, critical
- Status con default 'pending'
- Depends(get_current_user) para auth
- SQLAlchemy para persistencia
- Response model explícito"

El prompt incluye lo que aprendiste del primer intento. Eso evita que Claude Code repita los mismos errores.

Regla 2: Da ejemplos de input/output

"El cálculo de descuento debe ser progresivo (como tax brackets):
- Primeros $100: sin descuento
- $100 a $500: 10% sobre el excedente
- Más de $500: 20% sobre el excedente de $500

Ejemplo: compra de $700
- Primeros $100 → $0 descuento
- $100-$500 ($400) → $40 descuento (10%)
- $500-$700 ($200) → $40 descuento (20%)
- Total descuento: $80
- Final: $620"

Los ejemplos concretos eliminan ambigüedad y te dan un test case para verificar el código generado.

Regla 3: Especifica lo que debe preservarse

"Regenera la función calculate_discount. 
PRESERVAR:
- El response format: {original_total, discount, final_total}
- El endpoint path: POST /api/orders/calculate-discount
- Los type hints y el modelo Pydantic Order
- Los mensajes de error en español

CAMBIAR:
- La lógica de cálculo de descuento (ver requisitos arriba)
- Agregar validación de que total > 0"

Esto reduce los costos de regeneración — Claude Code sabe qué mantener y qué cambiar.

Regla 4: Especifica el contexto del proyecto

"Este endpoint es parte de una API FastAPI con:
- SQLAlchemy async (usa AsyncSession, no Session)
- Pydantic v2 (usa model_config en vez de class Config)
- Autenticación con JWT (ya existe get_current_user)
- Logging con structlog
- Los imports están en la convención del proyecto:
  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"

Contexto del proyecto evita que Claude Code genere código que no encaja con tu stack.


Regenerar Parcial: No Todo o Nada

Un error común es pensar que regenerar significa "borrar todo y empezar de cero." En la práctica, a menudo la mejor opción es regenerar solo la parte problemática.

El espectro de regeneración

Regenerar archivo completo ← → Regenerar función ← → Editar

Cuándo regenerar archivo completo:
├── El archivo tiene < 100 líneas
├── El 80%+ necesita cambiar
└── La estructura del archivo es incorrecta

Cuándo regenerar una función:
├── La función tiene el approach incorrecto
├── El resto del archivo está bien
└── La función es independiente (pocos side effects)

Cuándo editar:
├── Solo 1-5 líneas necesitan cambiar
├── El fix es claro y puntual
└── El approach es correcto

Ejemplo: Regenerar solo una función

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()


# ✅ Esta función está bien — NO regenerar
@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


# ❌ Esta función tiene el approach incorrecto — REGENERAR
@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,
    )

La función get_task_stats carga TODAS las tareas en memoria y hace cálculos en Python. Con 50,000 tareas, esto es un desastre de performance. Necesitas SQL aggregations:

Prompt para regenerar SOLO get_task_stats:

"Regenera solo la función get_task_stats. El approach actual
carga todas las tareas en memoria y calcula en Python. 
Necesito que use SQL aggregations (COUNT, AVG) directamente
en la base de datos.

La función debe:
- Usar db.query con func.count y func.avg de SQLAlchemy
- Filtrar por owner_id del current_user
- Calcular avg_completion_time con SQL, no Python
- Mantener el mismo response model TaskStats
- Mantener los mismos Depends (get_current_user, get_db)
- Una sola query eficiente en vez de cargar todas las filas"

Resultado: regeneras una función de 20 líneas, mantienes el endpoint create_task intacto, y preservas la estructura del archivo.


Conexión con Proyecto

En el proyecto integrador (Módulo 8)

Vas a encontrar código donde regenerar es la respuesta correcta. La clave es identificar la señal:

Proyecto integrador — posibles decisiones de regenerar:

1. Una función de búsqueda que usa loop en Python 
   en vez de SQL WHERE → Señal 1 (approach algorítmico)

2. Llamadas síncronas a servicios externos 
   en endpoints async → Señal 2 (arquitectura incorrecta)

3. Un endpoint con 8+ problemas de validación, 
   naming, y estructura → Señal 3 (>50% necesita cambiar)

4. Un cálculo que implementa la lógica de negocio 
   de forma incorrecta → Señal 4 (no cumple requisitos)

5. Validación duplicada en 4 endpoints diferentes 
   → Señal 5 (deuda técnica masiva si se parcha)

Para cada decisión de regenerar, documentarás:

  • ✅ Qué señal identificaste
  • ✅ Qué se perdería al regenerar (customizaciones, contexto)
  • ✅ Qué incluiste en el prompt para mitigar las pérdidas
  • ✅ Que revisaste el código nuevo para verificar que no introdujo problemas

Troubleshooting

Problema 1: "Regeneré y el nuevo código tiene problemas diferentes"

Causa: El prompt no incluyó suficiente contexto o no especificó lo que debía preservarse. Solución: Antes de regenerar, anota todo lo que está correcto en el código actual. Inclúyelo en el prompt: "PRESERVAR: el formato de response, la autenticación, los mensajes en español. CAMBIAR: la lógica de cálculo." Después de regenerar, haz code review comparando viejo vs nuevo.

Problema 2: "No sé si el approach es incorrecto o solo tiene bugs"

Causa: A veces es difícil distinguir entre un approach correcto con bugs y un approach fundamentalmente incorrecto. Solución: Pregunta: "Si arreglo todos los bugs, ¿el código va a funcionar correctamente a escala?" Si la respuesta es sí, edita. Si la respuesta es "funcionará pero será lento / inseguro / imposible de mantener," regenera.

Problema 3: "Regeneré con mejor prompt pero Claude Code generó algo muy diferente a lo que esperaba"

Causa: Los LLMs no son deterministas. El mismo prompt puede producir resultados diferentes. Solución: Sé más específico en el prompt. Incluye: la firma exacta de la función, los imports que debe usar, el response format, y un ejemplo de input/output. Cuanto más específico, menos variación.

Problema 4: "No estoy seguro si es 50% o 30% lo que necesita cambiar"

Causa: La regla del 50% es una heurística, no un número mágico. Solución: La pregunta real no es el porcentaje exacto — es "¿será más rápido editar o regenerar?" Si tienes que pensar mucho sobre cuántos cambios necesitas, probablemente son suficientes para regenerar. Si puedes listar los cambios en 30 segundos, edita.


Ejercicios

Ejercicio 1: Identificar la señal (Medio)

Para cada código, identifica cuál de las 5 señales aplica y justifica por qué regenerar es mejor que editar.

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

Código 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()}
Ver solución

Código A — Señal 1: Approach algorítmico incorrecto

El código carga TODOS los eventos de la base de datos, los filtra en Python, los ordena en Python, y toma los primeros 10. Con 100,000 eventos, esto carga 100,000 registros cuando solo necesitas 10.

Regenerar con: SELECT * FROM events WHERE date > NOW() ORDER BY date LIMIT 10 — una sola query SQL que hace todo en la base de datos.

Editar no resuelve: podrías optimizar el sort o el filtro en Python, pero el problema fundamental es cargar todos los registros.

Código B — Señal 5: Deuda técnica masiva si se parcha

Toda la validación es inline en el endpoint. Si agregas más endpoints que crean o actualizan usuarios, tendrás que duplicar las mismas validaciones. La solución correcta es usar Pydantic Field constraints y validators.

Regenerar el schema con:

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

Editar "funcionaría" pero perpetúa el anti-patrón de validación dispersa.

Ejercicio 2: Calcular el costo de regenerar (Medio)

Este código fue generado por Claude Code y luego editaste manualmente 3 cosas (marcadas con comentarios):

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  # TU EDICIÓN: agregaste logging

logger = logging.getLogger(__name__)  # TU EDICIÓN

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}")  # TU EDICIÓN
    
    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

El problema: La validación price < 0 debería ser price <= 0 (un producto no puede costar $0), pero además te das cuenta de que necesitas agregar validación de stock, categoría existente, y nombre único. ¿Regeneras o editas? Calcula el costo de cada opción.

Ver solución

Opción A: Editar

Cambios necesarios:
1. Cambiar < 0 por <= 0 (1 carácter)
2. Agregar validación de stock >= 0 (2 líneas)
3. Agregar verificación de categoría existente (3-4 líneas + query)
4. Agregar verificación de nombre único (3-4 líneas + query)

Costo:
├── Tiempo: ~10 minutos
├── Customizaciones preservadas: ✅ logging (3 líneas)
├── Riesgo: bajo (cambios puntuales)
└── Mantenibilidad: media (validación inline, crece con el tiempo)

Opción B: Regenerar la función

Lo que se pierde:
├── 3 líneas de logging que tú agregaste
└── El mensaje de error en español

Costo:
├── Tiempo: ~15-20 minutos (prompt + review + re-agregar logging)
├── Customizaciones perdidas: logging, mensajes en español
├── Riesgo: medio (código nuevo puede tener bugs nuevos)
└── Mantenibilidad: alta SI mueves validación a Pydantic

Decisión correcta: Editar

El code base tiene pocas validaciones que agregar (3-4), las customizaciones son valiosas (logging), y el approach general es correcto. Editar es más rápido y preserva tu trabajo.

PERO: si decidieras que la validación debería estar en el schema Pydantic (no en el endpoint), entonces regenerar el schema tiene sentido — es un cambio de diseño, no solo agregar validaciones.

Ejercicio 3: Escribir prompt de regeneración (Difícil)

Tienes este código que necesita regenerarse (Señal 2 — arquitectura incorrecta). Escribe el prompt de regeneración que produciría el mejor resultado:

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}
Ver solución
Regenera el endpoint GET /api/health/full con estas correcciones:

PROBLEMAS DEL CÓDIGO ACTUAL:
1. Es síncrono (def) cuando debería ser async — bloquea el event loop
2. Crea conexiones nuevas a DB y Redis en cada request 
   en vez de usar los pools del proyecto
3. Usa psycopg2 (sync) en vez del engine de SQLAlchemy async existente
4. Las 3 verificaciones se ejecutan secuencialmente (total: hasta 15s) 
   cuando podrían ejecutarse en paralelo con asyncio.gather
5. Usa requests (sync) en vez de httpx (async)
6. No tiene timeout individual por check

REQUISITOS:
- Endpoint async que use asyncio.gather para checks paralelos
- DB check usando la sesión de SQLAlchemy existente (from app.database import get_db)
- Redis check usando el cliente Redis existente (from app.cache import redis_client)
- External API check usando httpx.AsyncClient con timeout de 3 segundos
- Timeout individual de 5 segundos por check (si tarda más, reportar "timeout")
- Response format: {"status": "healthy"|"degraded"|"unhealthy", 
  "checks": {"database": {...}, "cache": {...}, "external_api": {...}},
  "response_time_ms": <total>}

PRESERVAR:
- El path del endpoint: GET /api/health/full
- La lógica de status: healthy si todo OK, degraded si algo falla
- Los mismos 3 checks: database, cache, external_api

CONTEXTO:
- Proyecto FastAPI async con SQLAlchemy async y Redis
- Python 3.11+

Este prompt incluye: qué estaba mal, qué necesitas, qué preservar, y el contexto del proyecto. Es 10x más probable que produzca código correcto que un prompt vago.

Ejercicio 4: Regenerar parcial vs total (Difícil)

Este archivo tiene dos funciones. Una está bien y otra no. Decide qué regenerar y qué mantener:

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

list_tasks — MANTENER (está bien)

  • ✅ Usa paginación con OFFSET/LIMIT
  • ✅ Filtros opcionales bien implementados
  • ✅ Query construida con SQLAlchemy correctamente
  • ✅ Ordena por created_at descendiente
  • ✅ Limita el tamaño de página (max 100)

category_stats — REGENERAR (approach incorrecto)

  • ❌ Carga TODAS las categorías en memoria
  • ❌ Para CADA categoría, hace una query separada (N+1)
  • ❌ Carga TODAS las tareas de cada categoría para contar en Python
  • ❌ Con 50 categorías y 1000 tareas por categoría = 50 queries + 50,000 objetos en memoria

Prompt de regeneración:

Regenera SOLO la función category_stats. 
La función list_tasks está correcta — NO la toques.

Problema: la función actual tiene N+1 queries y 
carga datos en memoria. Necesito UNA query SQL que 
haga GROUP BY con COUNT y agregaciones.

La query debería:
- JOIN categories con tasks
- Filtrar por owner_id del current_user
- GROUP BY category
- COUNT total y COUNT completed en SQL
- Calcular completion_rate en SQL o Python (pero con 
  los datos ya agregados, no fila por fila)

Mantener: response_model list[CategoryStats],
los mismos Depends, el mismo endpoint path.

Decisión: regenerar parcial. Solo la función problemática. El archivo y la otra función se mantienen intactos.


Resumen

  • Hay 5 señales claras de que regenerar es mejor que editar:
    1. El approach algorítmico es fundamentalmente incorrecto (O(n²) → O(1))
    2. La arquitectura es incorrecta (sync → async, monolítico → modular)
    3. Más del 50% del código necesita cambiar
    4. El código no cumple los requisitos de negocio
    5. La deuda técnica sería masiva si se parcha (validación duplicada × 5 endpoints)
  • Regenerar tiene 4 costos que debes considerar: pierdes customizaciones, pierdes contexto, puedes obtener problemas nuevos, y toma más tiempo del que parece
  • Para regenerar efectivamente: incluye qué estaba mal, da ejemplos de I/O, especifica qué preservar, y da contexto del proyecto
  • Regenerar no es todo o nada — a veces regeneras una función pero mantienes el resto del archivo
  • Después de regenerar, siempre haz code review del nuevo código comparándolo con el anterior

Recursos Adicionales

  1. Joel Spolsky — Things You Should Never Do, Part I - El artículo clásico sobre los peligros de reescribir código desde cero
  2. Martin Fowler — Refactoring vs Rewriting - Cuándo refactorizar incrementalmente vs reescribir
  3. Big Ball of Mud — Brian Foote, Joseph Yoder - Cómo el código se degrada cuando se parcha sin criterio
  4. Anthropic — Claude Code Best Practices - Mejores prácticas para prompts de generación de código
  5. Pydantic V2 Documentation — Validators - Cómo centralizar validación en schemas (vs inline)

Siguiente cápsula: Cuándo Editar Manualmente — el otro lado del espectro.


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