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

Decision Framework: Regenerar vs Editar

Decision Framework: Regenerar vs Editar

Descripción de la cápsula

Las cápsulas 03 y 04 te dieron las señales de cada lado del espectro: cuándo regenerar y cuándo editar. Pero la realidad rara vez es blanco o negro. Los casos más interesantes — y los más frecuentes — están en la zona gris donde la respuesta no es obvia.

Esta cápsula integra todo en un framework de decisión paso a paso: evalúas el daño, estimas tiempos, consideras lo que se pierde, y tomas una decisión informada. No es una fórmula mágica — es un proceso que estructura tu pensamiento para que la decisión sea rápida y justificable.

Y lo más importante: 5 escenarios realistas donde la respuesta no es obvia. Código real con problemas reales donde necesitas aplicar el framework, justificar tu decisión, y defender tu elección. Porque la diferencia entre un developer competente y un developer senior no es tener el framework — es saber aplicarlo bajo presión con matices.


El Framework de Decisión: 5 Pasos

Vista general

┌─────────────────┐
│  1. EVALUAR      │
│     DAÑO         │ → ¿Cuánto del código necesita cambiar?
└───────┬─────────┘
        │
┌───────▼─────────┐
│  2. ESTIMAR      │
│     EDICIÓN      │ → ¿Cuánto tiempo me toma editar?
└───────┬─────────┘
        │
┌───────▼─────────┐
│  3. ESTIMAR      │
│     REGENERACIÓN │ → ¿Cuánto tiempo me toma regenerar?
└───────┬─────────┘
        │
┌───────▼─────────┐
│  4. CONSIDERAR   │
│     PÉRDIDAS     │ → ¿Qué pierdo si regenero?
└───────┬─────────┘
        │
┌───────▼─────────┐
│  5. DECIDIR      │
│                  │ → Editar | Regenerar función | Regenerar archivo
└─────────────────┘

Paso 1: Evaluar el daño

Antes de decidir cómo arreglar, necesitas entender qué tan profundo es el problema.

Preguntas para evaluar:

A. ¿El approach fundamental es correcto?
   SÍ → Probablemente editar
   NO → Probablemente regenerar

B. ¿Cuántas líneas necesitan cambiar?
   1-5     → Editar
   5-20    → Depende (sigue al paso 2)
   20+     → Probablemente regenerar

C. ¿Los cambios son independientes o interconectados?
   Independientes → Editar (cambias uno por uno sin riesgo)
   Interconectados → Regenerar tiene menos riesgo de inconsistencia

D. ¿El problema es de diseño o de implementación?
   Diseño → Regenerar (parchar diseño crea Frankenstein)
   Implementación → Editar (el diseño ya es correcto)

Ejemplo rápido:

Código: endpoint que busca tareas en la DB
Problema: usa loop en Python en vez de SQL WHERE

A. ¿Approach correcto? NO (debería usar SQL, no Python loop)
B. ¿Líneas que cambian? ~15 de 30
C. ¿Cambios interconectados? SÍ (cambiar la query afecta el procesamiento)
D. ¿Diseño o implementación? DISEÑO (el approach es incorrecto)

Evaluación: regenerar la función

Paso 2: Estimar tiempo de edición

Si el paso 1 sugiere editar, estima cuánto tomará:

Fórmula aproximada:
  Tiempo = (# de ediciones × complejidad promedio) + verificación

Donde:
  Edición simple (cambiar operador, agregar línea): 1-2 min
  Edición media (agregar bloque, cambiar lógica): 3-5 min
  Edición compleja (reestructurar función): 5-10 min
  Verificación: 3-5 min (siempre)

Ejemplo:
  3 ediciones simples + 1 media + verificación
  = (3 × 1.5) + (1 × 4) + 4
  = 4.5 + 4 + 4
  = 12.5 min

Paso 3: Estimar tiempo de regeneración

Fórmula aproximada:
  Tiempo = prompt + generación + review + customizaciones + verificación

Donde:
  Prompt detallado: 3-5 min
  Generación: 1-2 min
  Review del código nuevo: 3-10 min (depende de complejidad)
  Re-aplicar customizaciones: 0-10 min (depende de cuántas)
  Verificación: 3-5 min

Ejemplo (función simple sin customizaciones):
  = 3 + 1 + 3 + 0 + 3 = 10 min

Ejemplo (endpoint complejo con logging y audit):
  = 5 + 2 + 8 + 8 + 5 = 28 min

Paso 4: Considerar pérdidas

Este paso es donde muchos developers se equivocan — olvidan lo que pierden al regenerar.

Checklist de pérdidas:

☐ Customizaciones manuales que hice después de la generación
  → Logging, mensajes en español, formato específico
  → ¿Cuántas líneas? ¿Cuánto tiempo tomaron?

☐ Contexto acumulado en la sesión de Claude Code
  → ¿Claude Code entiende mi estilo, mis preferencias?
  → ¿Perder ese contexto hará que el nuevo código sea inconsistente?

☐ Integraciones con el resto del codebase
  → ¿El código actual se integra con otros componentes?
  → ¿El código regenerado mantendrá esas integraciones?

☐ Tests que ya pasan
  → ¿Hay tests que validan el código actual?
  → ¿El código regenerado pasará esos tests sin cambios?

☐ Conocimiento que ya tengo del código
  → "Ya sé exactamente qué hace esta función"
  → "Con código nuevo, tendría que re-leer y re-entender"

Paso 5: Decidir

Con la información de los pasos 1-4, la decisión suele ser clara:

Matriz de decisión:

Si evaluate_daño dice "approach incorrecto" → Regenerar
   (sin importar los otros factores)

Si tiempo_edición < tiempo_regeneración Y pérdidas > 0 → Editar
   (más rápido y preservas customizaciones)

Si tiempo_edición > tiempo_regeneración Y pérdidas = 0 → Regenerar
   (más rápido y no pierdes nada)

Si tiempo_edición ≈ tiempo_regeneración → Editar
   (en empate, editar preserva contexto)

Si ediciones son interconectadas y complejas → Regenerar función
   (reducir riesgo de inconsistencia)

El tie-breaker: Cuando los tiempos son similares, editar gana porque preserva contexto. Solo regenera cuando hay una ventaja clara de tiempo o calidad.


El Espectro Completo de Opciones

No es solo "editar" o "regenerar." Hay un espectro completo:

EDITAR                                                      REGENERAR
  │                                                              │
  ├── Editar 1 línea                                            │
  │   "Cambiar > por <"                                         │
  │                                                              │
  ├── Editar varias líneas                                      │
  │   "Agregar null check, agregar ownership check"             │
  │                                                              │
  ├── Editar un bloque                                          │
  │   "Reescribir el try/except completo"                       │
  │                                                              │
  ├── Regenerar una función                                     │
  │   "Regenerar solo calculate_stats(), mantener el resto"     │
  │                                                              │
  ├── Regenerar varias funciones                                │
  │   "Regenerar las 3 funciones CRUD, mantener imports y       │
  │    configuración"                                            │
  │                                                              │
  └── Regenerar archivo completo ──────────────────────────────┘
      "El archivo entero necesita un approach diferente"

La decisión no es binaria. Y la mayoría de las veces, la respuesta está en el medio: regenerar parte, editar el resto.

Combinaciones comunes

Escenario 1: "Regenerar función + editar imports"
→ La función tiene approach incorrecto
→ Pero los imports y la configuración del archivo están bien
→ Regeneras la función, editas los imports si cambiaron

Escenario 2: "Editar endpoint + regenerar schema"
→ El endpoint está bien pero el schema Pydantic no tiene validaciones
→ Regeneras el schema con Field constraints y validators
→ Editas el endpoint solo si el schema cambió de nombre

Escenario 3: "Regenerar lógica + preservar integración"
→ La lógica de cálculo es incorrecta
→ Pero tiene logging, audit trail, y notificaciones integradas
→ Regeneras la lógica core, preservas las integraciones

Los 5 Escenarios: Practica el Framework

Cada escenario presenta código real con problemas reales. Aplica el framework de 5 pasos, toma tu decisión, y después compara con el análisis detallado.

Escenario 1: El Servicio de Reportes

Claude Code generó este servicio de reportes que tiene varios problemas:

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

logger = logging.getLogger(__name__)

app = FastAPI()


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

    model_config = {"from_attributes": True}


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

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

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

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

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

    # Encontrar usuario más activo
    user_counts = {}
    for task in all_tasks:
        uid = task.owner_id
        user_counts[uid] = user_counts.get(uid, 0) + 1

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

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

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

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

Problemas identificados:

  1. Carga TODAS las tareas en memoria para hacer cálculos en Python
  2. No filtra por el usuario actual (muestra tareas de todos los usuarios)
  3. El conteo de tareas por prioridad y el usuario más activo se calculan en Python
  4. El avg_completion_days usa .days (trunca a enteros) en vez de .total_seconds() / 86400

Aplica el framework y decide.

Ver solución

Paso 1: Evaluar el daño

A. ¿Approach correcto? PARCIALMENTE
   - El approach de "buscar tasks → calcular stats" es correcto
   - PERO la implementación carga todo en memoria (incorrecto)
   - Y le falta el filtro por usuario (bug de seguridad)
   
B. ¿Líneas que cambian? ~30 de 60 (50%)
   
C. ¿Interconectados? SÍ
   - Cambiar las queries afecta todos los cálculos
   
D. ¿Diseño o implementación? IMPLEMENTACIÓN
   - El diseño (endpoint → calcular stats → response) es correcto
   - La implementación (Python loops vs SQL) es incorrecta

Paso 2: Estimar edición

Ediciones necesarias:
1. Agregar filtro owner_id: 2 min (simple)
2. Cambiar total/completed a SQL count: 5 min (media)
3. Cambiar avg_days a SQL avg: 5 min (media)
4. Cambiar most_active a SQL subquery: 8 min (compleja)
5. Cambiar priority_counts a SQL group by: 5 min (media)
6. Fix .days → .total_seconds()/86400: 1 min (simple)
7. Verificación: 5 min

Total estimado: ~31 min

Paso 3: Estimar regeneración

Prompt detallado (incluir qué preservar): 5 min
Generación: 2 min
Review: 5 min
Re-aplicar logging (2 líneas): 2 min
Verificación: 5 min

Total estimado: ~19 min

Paso 4: Considerar pérdidas

Customizaciones:
- 2 líneas de logging (fácil de re-agregar)
- Mensajes de log en español (incluir en prompt)
- ReportResponse schema (incluir en prompt)

Pérdidas: Mínimas (logging simple, fácil de especificar en prompt)

Paso 5: Decidir

→ REGENERAR la función weekly_report

Justificación:

  • 50% del código necesita cambiar
  • Las ediciones son interconectadas (cambiar queries afecta todo)
  • El tiempo de regeneración (~19 min) es significativamente menor que editar (~31 min)
  • Las pérdidas son mínimas (2 líneas de logging)
  • Regenerar produce código más consistente que editar 5 queries individuales

PERO: mantener el schema ReportResponse y los imports. Solo regenerar la función.

Prompt de regeneración:

Regenera SOLO la función weekly_report. Mantén el schema 
ReportResponse y los imports exactamente como están.

Problemas del código actual que debes corregir:
1. Carga todas las tareas en memoria — usa SQL aggregations
2. Falta filtro por current_user.id — agregar
3. avg_completion_days usa .days (trunca) — usar total_seconds/86400
4. Cálculos de usuario más activo y priority counts 
   deben ser SQL, no Python

Preservar:
- El logging con logger.info (mismo formato y mensajes en español)
- Los mismos Depends(get_current_user, get_db)
- El response_model=ReportResponse
- El cálculo de completion_rate con round(_, 4)

Escenario 2: El Middleware de Rate Limiting

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

app = FastAPI()


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

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

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

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

        self.requests[client_ip].append(now)

        response = await call_next(request)

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

        return response


app.add_middleware(RateLimitMiddleware, requests_per_minute=100)

Problemas:

  1. Almacena requests en memoria (se pierde al reiniciar, no funciona con múltiples workers)
  2. request.client.host puede ser None si hay proxy (necesita X-Forwarded-For)
  3. La limpieza de timestamps viejos se hace en cada request (O(n) por request)
  4. El dict self.requests crece sin límite (no limpia IPs inactivas)

¿El middleware completo debería moverse a Redis? ¿O puedes parchear los problemas?

Ver solución

Paso 1: Evaluar

A. ¿Approach correcto? DEPENDE DEL CONTEXTO
   - Para desarrollo/single worker: el approach en memoria es aceptable
   - Para producción/multi worker: necesita Redis
   
B. ¿Líneas que cambian? 
   - Si mantiene en memoria: ~10 de 40 (25%) → editar
   - Si migra a Redis: ~35 de 40 (87%) → regenerar
   
C. ¿Interconectados? Si migra a Redis, sí
   
D. ¿Diseño o implementación?
   - Si se mantiene en memoria: implementación (parchar problemas)
   - Si migra a Redis: diseño (cambio arquitectónico)

La pregunta clave: ¿Este middleware va a producción con múltiples workers?

Si NO (desarrollo o single worker):

→ EDITAR los problemas 2, 3, y 4. Mantener in-memory.

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

3 ediciones puntuales (~12 min). El approach en memoria es válido para el contexto.

Si SÍ (producción con múltiples workers):

→ REGENERAR el middleware completo con Redis.

El approach en memoria es fundamentalmente incorrecto para este contexto. No puedes "editar" un dict en memoria para que se comparta entre workers — necesitas cambiar la arquitectura a Redis.

La lección: La misma pregunta ("¿edito o regenero?") tiene respuestas diferentes según el contexto. El framework te hace considerar el contexto antes de decidir.

Escenario 3: El Sistema de Permisos

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

app = FastAPI()


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


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


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

Problemas:

  1. La verificación de permisos se repite en 3 endpoints (DRY violation)
  2. update_task no usa Pydantic model para el body
  3. delete_task no permite admins borrar tareas de otros usuarios
  4. Ningún endpoint tiene response_model
  5. update_task no usa exclude_unset para updates parciales

Son 5 problemas. ¿Editas o regeneras?

Ver solución

Paso 1: Evaluar

A. ¿Approach correcto? SÍ
   - CRUD endpoints con auth y ownership — correcto
   - La estructura endpoint → query → verify → act es correcta
   
B. ¿Líneas que cambian?
   - Problema 1 (DRY): crear dependency, cambiar 3 funciones (~15 líneas)
   - Problema 2: crear model, cambiar params (~8 líneas)
   - Problema 3: cambiar 1 condición (~3 líneas)
   - Problema 4: agregar 3 response_models (~3 líneas)
   - Problema 5: cambiar lógica de update (~5 líneas)
   Total: ~34 líneas de ~55 = 62%
   
C. ¿Interconectados? PARCIALMENTE
   - Problema 1 (DRY) afecta los 3 endpoints
   - Los otros son independientes
   
D. ¿Diseño o implementación? IMPLEMENTACIÓN
   - El diseño CRUD es correcto
   - La implementación tiene problemas de calidad

Paso 2: Estimar edición

1. Crear dependency (nueva función + cambiar 3 endpoints): 10 min
2. Crear Pydantic model + cambiar update: 5 min
3. Agregar admin check a delete: 2 min
4. Agregar response_model × 3: 2 min
5. Agregar exclude_unset: 3 min
Verificación: 5 min
Total: ~27 min

Paso 3: Estimar regeneración

Prompt (especificar DRY, schemas, admin rule): 5 min
Generación: 2 min
Review: 5 min
Customizaciones: 0 min (no hay)
Verificación: 5 min
Total: ~17 min

Paso 4: Considerar pérdidas

Customizaciones: ninguna visible
Integraciones: ninguna especial
Context: bajo (código simple, no hay estado acumulado)
Pérdidas: mínimas

Paso 5: Decidir

→ COMBINACIÓN: Regenerar el archivo + crear la dependency nueva

Justificación:

  • 62% del código necesita cambiar
  • Regenerar (~17 min) es más rápido que editar (~27 min)
  • No hay customizaciones que perder
  • Los cambios son parcialmente interconectados (el DRY fix afecta todo)

El approach mixto es:

  1. Crear la dependency get_task_or_404 primero (nueva función, no requiere regenerar)
  2. Regenerar los 3 endpoints con instrucción de usar la dependency
  3. Crear el Pydantic model TaskUpdate (nuevo, no requiere regenerar)

Esto produce código más consistente que editar 5 problemas uno por uno.

Escenario 4: El Endpoint de Exportación

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

logger = logging.getLogger(__name__)

app = FastAPI()


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

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

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

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

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

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

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

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

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

Problemas:

  1. StreamingResponse(iter([output.getvalue()])) — carga todo en memoria, no es streaming real. Con 100,000 tareas, consume mucha RAM.
  2. La serialización JSON manual duplica la lógica CSV (debería ser un schema o una función compartida).
  3. No hay límite de exportación — un usuario con 1M de tareas crashea el servidor.

¿Editas o regeneras?

Ver solución

Paso 1: Evaluar

A. ¿Approach correcto? SÍ (mayoritariamente)
   - Endpoint de exportación con filtros y auth: correcto
   - Eager loading de relaciones: correcto
   - Soporte CSV y JSON: correcto
   - El problema es la implementación del streaming y los límites
   
B. ¿Líneas que cambian?
   - Problema 1 (streaming real): ~15 líneas de la sección CSV
   - Problema 2 (DRY): refactorizar serialización (~10 líneas)
   - Problema 3 (límite): agregar ~5 líneas
   Total: ~30 de ~70 = 43%
   
C. ¿Interconectados? PARCIALMENTE
   - El streaming y la serialización son independientes
   - El límite es independiente de todo
   
D. ¿Diseño o implementación? IMPLEMENTACIÓN
   - El diseño del endpoint es correcto
   - La implementación del streaming necesita mejora

Paso 2: Estimar edición: ~22 min Paso 3: Estimar regeneración: ~25 min (alto por las customizaciones)

Paso 4: Considerar pérdidas

Customizaciones SIGNIFICATIVAS:
- ✅ Logging estructurado con extra fields (4 líneas)
- ✅ Headers CSV en español
- ✅ Eager loading de category y tags
- ✅ Formato de fecha con isoformat()
- ✅ Manejo de null en category y tags

Perder estas customizaciones tomaría ~8 min re-aplicar

Paso 5: Decidir

→ EDITAR

Justificación:

  • El approach es correcto (~43% de cambios, cerca del umbral)
  • Las customizaciones son significativas (~8 min para re-aplicar)
  • Los 3 problemas son independientes (puedo editar uno por uno)
  • Editar (~22 min) es más rápido que regenerar (~25 min + riesgo de perder customizaciones)

Plan de edición:

  1. Agregar límite (independiente, 5 min):

    MAX_EXPORT = 10000
    total = query.count()
    if total > MAX_EXPORT:
        raise HTTPException(400, f"Máximo {MAX_EXPORT} tareas por exportación")
  2. Implementar streaming real para CSV (10 min): Cambiar de cargar todo a un generator que yield rows

  3. Extraer función de serialización (7 min): Crear serialize_task(task) y usarla en ambos formats

Escenario 5: El Cálculo de Métricas del Dashboard

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

app = FastAPI()


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

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

    # Tareas de esta semana
    week_tasks = (
        db.query(Task)
        .filter(Task.owner_id == current_user.id, Task.created_at >= week_start)
        .all()
    )
    week_created = len(week_tasks)
    week_completed = len([t for t in week_tasks if t.status == "completed"])

    # Tareas de este mes
    month_tasks = (
        db.query(Task)
        .filter(Task.owner_id == current_user.id, Task.created_at >= month_start)
        .all()
    )
    month_created = len(month_tasks)
    month_completed = len([t for t in month_tasks if t.status == "completed"])

    # Streak: días consecutivos con al menos 1 tarea completada
    streak = 0
    check_date = today_start
    while True:
        day_end = check_date + timedelta(days=1)
        day_completed = (
            db.query(Task)
            .filter(
                Task.owner_id == current_user.id,
                Task.status == "completed",
                Task.completed_at >= check_date,
                Task.completed_at < day_end,
            )
            .count()
        )
        if day_completed > 0:
            streak += 1
            check_date -= timedelta(days=1)
        else:
            break

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

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

Problemas:

  1. Hace 4 queries que cargan todas las tareas en memoria (today, week, month, all) + 1 query por día para el streak
  2. Las 3 queries de periodo (today, week, month) son redundantes — today_tasks es un subconjunto de week_tasks que es subconjunto de month_tasks
  3. El streak loop hace una query por día — si el usuario tiene un streak de 365 días, son 365 queries
  4. No tiene response_model
  5. datetime.utcnow() está deprecado en Python 3.12+ (usar datetime.now(timezone.utc))

¿Editas, regeneras, o haces una combinación?

Ver solución

Paso 1: Evaluar

A. ¿Approach correcto? INCORRECTO
   - 4 queries redundantes que cargan todo en memoria
   - El streak loop es N queries donde N = streak length
   - Esto es approach algorítmico incorrecto (Señal 1 de regenerar)
   
B. ¿Líneas que cambian? ~50 de 65 (77%)
   
C. ¿Interconectados? SÍ
   - Cambiar de queries individuales a SQL aggregations 
     cambia toda la estructura de la función
   
D. ¿Diseño o implementación? DISEÑO de la query strategy
   - El diseño de métricas (today/week/month/streak) es correcto
   - La forma de calcularlas es fundamentalmente incorrecta

Paso 2: Estimar edición: ~40 min (reescribir 5 queries + refactorizar redundancias + streak) Paso 3: Estimar regeneración: ~18 min (no hay customizaciones)

Paso 4: Considerar pérdidas

Customizaciones: NINGUNA
- No hay logging
- No hay mensajes en español
- No hay integraciones especiales
- El código es generado sin modificaciones

Pérdidas: Cero

Paso 5: Decidir

→ REGENERAR la función completa

Justificación:

  • 77% del código necesita cambiar
  • El approach de queries es fundamentalmente incorrecto
  • No hay customizaciones que perder
  • Regenerar (~18 min) es mucho más rápido que editar (~40 min)
  • Las ediciones serían tan interconectadas que esencialmente estarías reescribiendo — mejor hacerlo con un prompt limpio

Prompt de regeneración:

Regenera la función dashboard_metrics con estas correcciones:

PROBLEMAS ACTUALES:
1. 4 queries que cargan todas las tareas en Python — 
   usar SQL COUNT/aggregations
2. Queries redundantes (today ⊂ week ⊂ month) — 
   hacer 1 query con CASE WHEN o 1 query por periodo con COUNT
3. Streak loop hace N queries — usar window function o 
   una sola query con GROUP BY date
4. Falta response_model
5. Usar datetime.now(timezone.utc) en vez de datetime.utcnow()

REQUISITOS:
- Máximo 3-4 queries SQL totales (no una por periodo)
- El streak debe calcularse con máximo 1 query
- Response model Pydantic con nested models
- Mantener la misma estructura de response:
  today: {created, completed}
  week: {created, completed}
  month: {created, completed}
  streak: int
  total: {all, completed}
  completion_rate: float

CONTEXTO:
- SQLAlchemy con PostgreSQL
- from sqlalchemy import func, case, and_
- El modelo Task tiene: id, owner_id, status, created_at, 
  completed_at, priority

Nota: el prompt incluye qué estaba mal, los requisitos exactos, y el contexto del proyecto — las 3 reglas de regeneración efectiva de la cápsula 03.


Resumen del Framework

El proceso en 60 segundos:

1. ¿El approach es correcto?
   NO → Regenerar (función o archivo)
   SÍ → Continuar

2. ¿Cuántas ediciones necesito?
   1-3 simples → Editar
   4+ o complejas → Continuar

3. ¿Hay customizaciones que perder?
   Muchas → Editar (preservar inversión)
   Pocas/Ninguna → Continuar

4. ¿Qué es más rápido?
   Editar < Regenerar → Editar
   Regenerar < Editar → Regenerar

5. ¿Puedo hacer un mix?
   "Regenerar esta función, editar el resto"
   → A menudo la mejor opción

Reglas rápidas para cuando no tienes tiempo de analizar:

✅ Cambiar 1 carácter (> por <) → Editar
✅ Agregar 2-3 líneas (null check) → Editar
✅ Approach algorítmico incorrecto → Regenerar función
✅ Arquitectura incorrecta → Regenerar archivo
✅ >50% necesita cambiar sin customizaciones → Regenerar
✅ <30% necesita cambiar con customizaciones → Editar
✅ No sabes cuál elegir → Editar (preserva contexto, menor riesgo)

Conexión con Proyecto

En el proyecto integrador (Módulo 8)

El framework de esta cápsula es exactamente lo que aplicarás en el proyecto. Para cada problema que encuentres:

  1. Aplica el framework de 5 pasos
  2. Documenta tu decisión y justificación
  3. Ejecuta (editar, regenerar, o combinación)
  4. Verifica el resultado

La documentación de decisiones es parte de la evaluación:

Formato de documentación:

## Problema #3: Búsqueda de tareas ineficiente

**Evaluación:** Approach algorítmico incorrecto (loop Python 
en vez de SQL WHERE). 90% del código necesita cambiar.

**Decisión:** Regenerar función search_tasks

**Justificación:**
- Señal 1: approach fundamentalmente incorrecto
- 0 customizaciones que perder
- Editar tomaría ~25 min, regenerar ~12 min

**Prompt utilizado:** [incluir prompt]

**Verificación:** 
- ✅ La query SQL funciona correctamente
- ✅ Los filtros existentes se mantienen
- ✅ Performance: de 3s a 50ms con 10,000 registros

Troubleshooting

Problema 1: "Apliqué el framework y tomé la decisión, pero el resultado no fue bueno"

Causa: El framework guía la decisión pero no garantiza la ejecución. Si decides regenerar pero el prompt es malo, el resultado será malo. Solución: El framework tiene dos partes: (1) decidir qué hacer, (2) hacerlo bien. Si decidiste regenerar, aplica las técnicas de la cápsula 03. Si decidiste editar, aplica las técnicas de la cápsula 04. La decisión correcta con ejecución incorrecta sigue dando mal resultado.

Problema 2: "Paso demasiado tiempo analizando y poco tiempo ejecutando"

Causa: Parálisis por análisis. El framework es una guía, no un análisis académico. Solución: Con práctica, el framework se vuelve intuitivo. No necesitas calcular minutos exactos — el análisis completo debería tomar 1-2 minutos máximo. Si después de 2 minutos no tienes una decisión clara, edita (es la opción de menor riesgo) y reconsidera si el resultado no es satisfactorio.

Problema 3: "No tengo experiencia para estimar tiempos de edición vs regeneración"

Causa: Las estimaciones mejoran con la práctica. Solución: En las primeras veces, anota cuánto tardó realmente cada opción. Después de 10-20 decisiones, tus estimaciones serán mucho más precisas. Tip: la mayoría de developers subestiman el tiempo de regeneración (olvidan el review y las customizaciones) y sobreestiman el tiempo de edición.

Problema 4: "Cada vez que regenero, el código nuevo tiene problemas diferentes"

Causa: Prompt insuficiente o falta de review post-regeneración. Solución: (1) Incluye en el prompt todo lo que estaba bien en el código anterior. (2) Después de regenerar, haz un code review completo comparando viejo vs nuevo — como si fuera un PR de un colega. (3) Si el tercer intento de regeneración sigue con problemas, edita el mejor de los intentos.


Ejercicios

Ejercicio 6: Crear tu propio escenario (Difícil)

Escribe un escenario de código (30-50 líneas) donde:

  • La decisión correcta NO sea obvia
  • Haya argumentos válidos tanto para editar como para regenerar
  • Incluye al menos 2 customizaciones que se perderían al regenerar
  • Incluye al menos 1 problema de approach (no solo bugs puntuales)

Después, aplica el framework de 5 pasos a tu propio escenario y toma una decisión.

Ver solución (ejemplo)

Escenario propuesto:

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

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

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

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

    # Validar transiciones de estado (approach incorrecto:
    # debería ser una state machine, no if/elif/elif)
    if new_status == "in_progress":
        if task.status != "pending":
            raise HTTPException(400, "Solo se puede iniciar una tarea pendiente")
    elif new_status == "completed":
        if task.status != "in_progress":
            raise HTTPException(400, "Solo se puede completar una tarea en progreso")
    elif new_status == "cancelled":
        if task.status == "completed":
            raise HTTPException(400, "No se puede cancelar una tarea completada")
    else:
        raise HTTPException(400, f"Estado inválido: {new_status}")

    old_status = task.status
    task.status = new_status

    # Customización: historial de cambios
    history = TaskHistory(
        task_id=task.id,
        field="status",
        old_value=old_status,
        new_value=new_status,
        changed_by=current_user.id,
    )
    db.add(history)

    db.commit()
    db.refresh(task)

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

Análisis con el framework:

  • El approach de las transiciones de estado (if/elif) es problemático (se vuelve inmanejable con más estados) — señal de regenerar
  • PERO hay customizaciones valiosas: logging con extra, historial de cambios con TaskHistory, mensajes en español
  • La validación de ownership y null check están correctas
  • El approach de guardar la transición en history está bien

Decisión: EDITAR — Refactorizar solo las transiciones a un dict/state machine, mantener todo lo demás:

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

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

4 líneas reemplazan las 10 líneas de if/elif. Todo el contexto se preserva.


Resumen

  • El framework de 5 pasos estructura la decisión: evaluar daño → estimar edición → estimar regeneración → considerar pérdidas → decidir
  • La decisión no es binaria — hay un espectro desde editar una línea hasta regenerar todo el archivo
  • Las combinaciones son frecuentemente la mejor opción: regenerar una función, editar el resto
  • En caso de empate, editar gana porque preserva contexto y tiene menor riesgo
  • Los 5 escenarios demuestran que el contexto cambia la respuesta — no hay regla universal
  • El framework es un punto de partida que refinas con experiencia — después de 20+ decisiones, se vuelve intuitivo
  • Documenta tus decisiones: la justificación es tan importante como el resultado

Recursos Adicionales

  1. Martin Fowler — When to Rewrite - El patrón Strangler Fig para migrar gradualmente vs reescribir
  2. Joel Spolsky — Things You Should Never Do - Perspectiva clásica sobre los peligros de reescribir
  3. Refactoring Guru — Refactoring Techniques - Catálogo de técnicas de edición incremental
  4. Anthropic — Claude Code Best Practices - Optimizar prompts para generación y edición de código
  5. The Pragmatic Programmer — Software Entropy - Cómo prevenir la degradación gradual del código
  6. Working Effectively with Legacy Code — Michael Feathers - Técnicas para hacer cambios seguros en código existente

Siguiente módulo: Proyecto Integrador — aplica todo lo aprendido en un codebase completo con problemas reales.


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