Módulo 8: Proyecto Integrador

Fase 3: Corrección — Implementar y Justificar Cada Fix

Fase 3: Corrección — Implementar y Justificar Cada Fix

Descripción de la cápsula

Tienes un findings document con 12-18 problemas documentados. Ahora viene la parte donde demuestras no solo que puedes encontrar problemas, sino que puedes resolverlos correctamente. Esta fase tiene tres componentes: priorizar qué corregir primero, implementar cada corrección, y justificar por qué tu fix es correcto.

La justificación es igual de importante que el fix. Cualquier developer puede cambiar una línea de código. Un developer profesional explica por qué ese cambio es necesario, por qué la corrección es la adecuada, y cómo verificó que funciona. Eso es lo que te diferencia.


Priorización: Qué Corregir Primero

La regla: impacto descendente

No corriges en el orden que encontraste los problemas. Corriges en orden de impacto:

Critical → High → Medium → Low

¿Por qué? Porque en un escenario real, si solo tuvieras tiempo para corregir la mitad de los problemas, los Critical y High son los que no puedes dejar pasar. Un security hole en producción es una emergencia. Un naming incorrecto es una mejora.

Cómo organizar tus fixes

Ordena tu findings document por severidad y agrupa:

## Correcciones a Implementar

### Bloque 1: Critical (implementar inmediatamente)
1. Finding #X: JWT secret hardcoded → config.py
2. Finding #Y: SQL injection en búsqueda → routes/users.py
3. Finding #Z: Contraseña almacenada en texto plano → routes/auth.py
4. Finding #W: Endpoint sin autenticación → routes/users.py

### Bloque 2: High (implementar a continuación)
5. Finding #A: Lógica de delete incorrecta → services/task_service.py
6. Finding #B: Autorización faltante en get_task_by_id → services/task_service.py
7. Finding #C: Validación de password insuficiente → models.py
8. Finding #D: Estadísticas incluyen tareas eliminadas → services/task_service.py

### Bloque 3: Medium (implementar después)
9.  Finding #E: Paginación off-by-one → services/task_service.py
10. Finding #F: División por cero en estadísticas → services/task_service.py
11. Finding #G: Validación de título insuficiente → models.py
12. Finding #H: Filtros con SQL injection → services/task_service.py

### Bloque 4: Low (implementar si hay tiempo)
13. Finding #I: Import inexistente → main.py
14. Finding #J: Validaciones de prioridad/estado faltantes → models.py
15. Finding #K: Paginación acepta page=0 → routes/tasks.py

Decidir: Regenerar vs Editar

Antes de empezar a corregir, para cada finding decide si vas a:

  • Editar manualmente: Cambiar las líneas específicas del problema
  • Regenerar con Claude Code: Pedir a Claude Code que regenere la función o bloque completo

Framework de decisión (Módulo 7)

CriterioEditarRegenerar
Scope del cambio1-5 líneasFunción completa o más
Claridad del fixSabes exactamente qué cambiarNo estás seguro del approach correcto
Riesgo de efectos secundariosBajo — cambio localizadoAlto — muchas dependencias
Porcentaje del código correcto>90% está bien<50% está bien
Complejidad del códigoSimple, legibleComplejo, difícil de modificar

Ejemplos de cuándo editar

Finding: JWT secret hardcoded en config.py
→ EDITAR: Cambiar una línea de `os.getenv("...", "fallback")` 
  a `os.environ["..."]`. Fix puntual, claro, sin riesgo.

Finding: Validación de password acepta 4 chars en vez de 8
→ EDITAR: Cambiar `if len(v) < 4` a `if len(v) < 8`. 
  Una línea, el resto de la función está bien.

Ejemplos de cuándo regenerar

Finding: La función get_tasks tiene SQL injection + offset 
incorrecto + filtros con concatenación
→ REGENERAR: La función tiene 3 problemas diferentes. 
  Es más seguro regenerar la función completa con los 
  requisitos correctos que parchear 3 cosas.

Finding: El flujo de delete es hard delete cuando debería 
ser soft delete. Afecta delete_task + get_tasks + get_user_stats
→ REGENERAR las 3 funciones: El cambio afecta múltiples 
  funciones y la lógica base es incorrecta.

Cómo documentar la decisión

Para cada fix, incluye:

**Decisión regenerar/editar:** [Editar / Regenerar]
**Justificación:** [Por qué elegiste esta opción]

Correcciones por Categoría

A continuación se presentan las correcciones para los problemas del codebase. Intenta implementar las tuyas primero antes de consultar estas soluciones. Las soluciones están ocultas para que las uses como referencia después de tu intento.


Corrección: Security — JWT Secret Hardcoded

Archivo: config.py Problema: JWT_SECRET_KEY tiene un fallback hardcoded que se usaría en producción si la variable de entorno no existe. Requisito violado: RF-02.3, RF-06.1

Ver solución
import os


class Settings:
    APP_NAME: str = "TaskFlow API"
    APP_VERSION: str = "1.0.0"
    DEBUG: bool = os.getenv("DEBUG", "false").lower() == "true"

    DATABASE_URL: str = os.getenv("DATABASE_URL", "sqlite:///./taskflow.db")
    DATABASE_PATH: str = "taskflow.db"

    JWT_SECRET_KEY: str = os.environ["JWT_SECRET_KEY"]
    JWT_ALGORITHM: str = "HS256"
    JWT_EXPIRATION_MINUTES: int = 30

    BCRYPT_ROUNDS: int = 12

    DEFAULT_PAGE_SIZE: int = 10
    MAX_PAGE_SIZE: int = 100


settings = Settings()

Por qué este fix es correcto:

  • os.environ["JWT_SECRET_KEY"] lanza KeyError si la variable no existe, lo cual es el comportamiento correcto: la aplicación no debe arrancar sin configuración de seguridad crítica
  • Se eliminó el string "super-secret-key-taskflow-2026" que cualquier persona con acceso al código fuente podría usar para forjar tokens
  • También se corrigió DEBUG para que no esté hardcoded como True en producción

Decisión: Editar. El fix es puntual (2 líneas), el contexto es claro, y el resto del archivo está bien.


Corrección: Security — Contraseña Almacenada en Texto Plano

Archivo: routes/auth.py Problema: En la función register(), la contraseña se inserta directamente en la base de datos sin hashear. Requisito violado: RF-01.2

Ver solución
@router.post("/register", response_model=UserResponse)
async def register(user: UserCreate):
    existing = execute_query(
        "SELECT id FROM users WHERE email = ?", (user.email,)
    )
    if existing:
        raise HTTPException(status_code=400, detail="Email already registered")

    hashed = hash_password(user.password)

    user_id = execute_insert(
        "INSERT INTO users (email, name, password) VALUES (?, ?, ?)",
        (user.email, user.name, hashed),
    )

    created_user = execute_query(
        "SELECT id, email, name, created_at FROM users WHERE id = ?",
        (user_id,),
    )

    row = dict(created_user[0])
    return UserResponse(**row)

Por qué este fix es correcto:

  • Se llama a hash_password(user.password) antes de insertar en la DB
  • hash_password usa bcrypt con salt, lo cual es el estándar de la industria
  • La verificación en login ya usa verify_password que compara contra el hash, así que el flujo completo es consistente

Decisión: Editar. Solo faltaba una línea (hashed = hash_password(user.password)) y cambiar user.password por hashed en el INSERT. El resto de la función está bien.


Corrección: Security — SQL Injection en Búsqueda de Usuarios

Archivo: routes/users.py Problema: El endpoint /users/search usa f-string para construir la query SQL y no requiere autenticación. Requisito violado: RF-06.2, RF-02.4

Ver solución
@router.get("/search")
async def search_users(
    query: str,
    current_user: dict = Depends(get_current_user),
):
    results = execute_query(
        "SELECT id, email, name FROM users WHERE name LIKE ?",
        (f"%{query}%",),
    )
    return [dict(row) for row in results]

Por qué este fix es correcto:

  • Se reemplazó el f-string en la query SQL con un parámetro ? (parameterized query)
  • El %{query}% se pasa como parámetro, no se concatena en la query
  • Se agregó Depends(get_current_user) para requerir autenticación
  • SQLite escapa automáticamente los parámetros, previniendo SQL injection

Decisión: Editar. Los dos cambios son puntuales y claros: parameterizar la query y agregar el dependency de autenticación.


Corrección: Security — SQL Injection en Filtros de Tareas

Archivo: services/task_service.py Problema: La función get_tasks() usa f-strings para insertar status y priority en la query SQL. Requisito violado: RF-06.2

Ver solución
def get_tasks(
    user_id: int,
    status: Optional[str] = None,
    priority: Optional[str] = None,
    page: int = 1,
    size: int = 10,
) -> dict:
    base_query = "SELECT * FROM tasks WHERE user_id = ?"
    count_query = "SELECT COUNT(*) as total FROM tasks WHERE user_id = ?"
    params = [user_id]

    if status:
        base_query += " AND status = ?"
        count_query += " AND status = ?"
        params.append(status)

    if priority:
        base_query += " AND priority = ?"
        count_query += " AND priority = ?"
        params.append(priority)

    offset = (page - 1) * size
    base_query += f" ORDER BY created_at DESC LIMIT ? OFFSET ?"
    query_params = params + [size, offset]

    with get_db() as conn:
        cursor = conn.cursor()

        cursor.execute(count_query, tuple(params))
        total = cursor.fetchone()["total"]

        cursor.execute(base_query, tuple(query_params))
        rows = cursor.fetchall()

    tasks = [dict(row) for row in rows]
    return {
        "tasks": tasks,
        "total": total,
        "page": page,
        "size": size,
    }

Por qué este fix es correcto:

  • Se reemplazaron los f-strings f" AND status = '{status}'" con parámetros " AND status = ?" y params.append(status)
  • Se corrigió el offset de page * size a (page - 1) * size (se soluciona el off-by-one de paginación al mismo tiempo)
  • LIMIT y OFFSET también usan parámetros
  • Los params de count_query y base_query se manejan correctamente por separado

Decisión: Regenerar. La función tenía 3 problemas (SQL injection en status, SQL injection en priority, offset incorrecto). Con tantos cambios interrelacionados, es más seguro regenerar la función completa.


Corrección: Logic — Autorización Faltante en get_task_by_id

Archivo: services/task_service.py Problema: get_task_by_id() no filtra por user_id, permitiendo que cualquier usuario autenticado vea cualquier tarea. Requisito violado: RF-03.4, RF-03.7, RF-05.6

Ver solución
def get_task_by_id(task_id: int, user_id: int) -> Optional[dict]:
    query = """
        SELECT id, title, description, priority, status, 
               user_id, created_at, updated_at
        FROM tasks WHERE id = ?
    """
    rows = execute_query(query, (task_id,))
    if not rows:
        return None

    task = dict(rows[0])

    if task["user_id"] != user_id:
        from fastapi import HTTPException
        raise HTTPException(
            status_code=403,
            detail="You don't have permission to access this task"
        )

    return task

Por qué este fix es correcto:

  • Primero verifica si la tarea existe (retorna None → 404)
  • Luego verifica si pertenece al usuario (retorna 403)
  • Según RF-05.6, debe ser 403 (no 404) cuando la tarea existe pero pertenece a otro usuario
  • Este cambio afecta a get_task, update_task, y delete_task que todos llaman a get_task_by_id, así que se propaga automáticamente

Decisión: Editar. La query está bien, solo falta la verificación de propiedad. Se agrega un bloque de 4 líneas.


Corrección: Logic — Delete es Hard Delete en vez de Soft Delete

Archivo: services/task_service.py Problema: delete_task() ejecuta DELETE FROM tasks en vez de cambiar el estado a "deleted". Requisito violado: RF-03.6

Ver solución
def delete_task(task_id: int, user_id: int) -> bool:
    existing = get_task_by_id(task_id, user_id)
    if not existing:
        return False

    query = "UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?"
    execute_query(query, ("deleted", datetime.utcnow().isoformat(), task_id))
    return True

Por qué este fix es correcto:

  • Cambia DELETE FROM tasks por UPDATE tasks SET status = 'deleted'
  • Esto implementa soft delete: la tarea sigue en la base de datos pero con estado "deleted"
  • Se actualiza updated_at para registrar cuándo se "eliminó"
  • Los endpoints de listar y estadísticas necesitarán filtrar tareas con estado "deleted" (correcciones separadas)

Decisión: Editar. El cambio es de una línea de SQL, reemplazando DELETE por UPDATE.


Corrección: Logic — Estadísticas Incluyen Tareas Eliminadas

Archivo: services/task_service.py Problema: get_user_stats() cuenta todas las tareas, incluyendo las eliminadas (soft deleted). Requisito violado: RF-04.2

Ver solución
def get_user_stats(user_id: int) -> dict:
    query = """
        SELECT status, priority FROM tasks 
        WHERE user_id = ? AND status != ?
    """
    rows = execute_query(query, (user_id, "deleted"))

    total = len(rows)
    by_status = {}
    by_priority = {}

    for row in rows:
        row_dict = dict(row)
        status = row_dict["status"]
        priority = row_dict["priority"]

        by_status[status] = by_status.get(status, 0) + 1
        by_priority[priority] = by_priority.get(priority, 0) + 1

    if total == 0:
        completion_percentage = 0.0
    else:
        completion_percentage = (
            by_status.get("completed", 0) / total * 100
        )

    return {
        "total_tasks": total,
        "by_status": by_status,
        "by_priority": by_priority,
        "completion_percentage": round(completion_percentage, 2),
    }

Por qué este fix es correcto:

  • Se agregó AND status != ? con parámetro "deleted" para excluir tareas eliminadas
  • Se agregó manejo del caso total == 0 para evitar división por cero (corrige otro finding al mismo tiempo)
  • Las estadísticas ahora solo reflejan tareas activas, según RF-04.2

Decisión: Editar. Los cambios son localizados: agregar condición al WHERE y agregar el guard para división por cero.


Corrección: Edge Case — División por Cero en Estadísticas

Archivo: services/task_service.py Problema: get_user_stats() divide por total sin verificar que sea mayor a cero. Crashea para usuarios sin tareas. Requisito violado: RF-04.1 (debe retornar estadísticas válidas)

Nota: Esta corrección ya está incluida en la corrección anterior de estadísticas. La solución combinada aborda ambos problemas.


Corrección: Edge Case — Paginación Off-by-One

Archivo: services/task_service.py Problema: El offset se calcula como page * size en vez de (page - 1) * size, causando que la primera página (page=1) se salte los primeros registros. Requisito violado: RF-03.2

Nota: Esta corrección ya está incluida en la corrección de get_tasks() de la sección de SQL injection.


Corrección: Hallucination — Import Inexistente

Archivo: main.py Problema: from pydantic_settings import BaseSettings importa una clase de un paquete que no está en requirements.txt. El paquete pydantic-settings es un paquete separado de pydantic y no está instalado. Requisito violado: N/A (hallucination)

Ver solución
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from config import settings
from database import init_db
from routes import auth, tasks, users

app = FastAPI(
    title=settings.APP_NAME,
    version=settings.APP_VERSION,
    debug=settings.DEBUG,
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


@app.on_event("startup")
async def startup():
    init_db()


app.include_router(auth.router)
app.include_router(tasks.router)
app.include_router(users.router)


@app.get("/health")
async def health_check():
    return {
        "status": "healthy",
        "app": settings.APP_NAME,
        "version": settings.APP_VERSION,
    }

Por qué este fix es correcto:

  • Se eliminó from pydantic_settings import BaseSettings que no se usa en el archivo y causaría ModuleNotFoundError al arrancar
  • El paquete pydantic-settings no está en requirements.txt y la clase BaseSettings no se utiliza en ninguna parte del archivo
  • El resto del archivo funciona correctamente sin ese import

Decisión: Editar. Eliminar una línea de import. Fix trivial.


Corrección: Hallucination — Función Inexistente en bcrypt

Archivo: services/auth_service.py Problema: from bcrypt import hashpw, gensalt, checkpw, verify_hash importa verify_hash, una función que no existe en el paquete bcrypt. Las funciones reales de bcrypt son hashpw, gensalt, checkpw, y kdf. Requisito violado: N/A (hallucination)

Ver solución
from bcrypt import hashpw, gensalt, checkpw

Por qué este fix es correcto:

  • verify_hash no existe en la API de bcrypt — la función equivalente es checkpw
  • El código ya usa checkpw correctamente en verify_password(), por lo que verify_hash es un import muerto
  • Este tipo de hallucination es más sutil que un paquete inexistente: el paquete bcrypt es real, pero la función es inventada

Decisión: Editar. Eliminar verify_hash del import. Fix trivial.


Corrección: Hallucination — Parámetro Inexistente en fetchall()

Archivo: services/task_service.py Problema: cursor.fetchall(as_dict=True) pasa un parámetro as_dict que no existe en sqlite3.Cursor.fetchall(). El método no acepta ningún parámetro. Requisito violado: N/A (hallucination)

Ver solución
cursor.execute(base_query, tuple(params))
rows = cursor.fetchall()

Por qué este fix es correcto:

  • fetchall() de sqlite3 no acepta parámetros — causará TypeError: fetchall() got an unexpected keyword argument 'as_dict'
  • Para obtener resultados como diccionarios, se usa conn.row_factory = sqlite3.Row (que ya está configurado en database.py)
  • Este hallucination es peligroso porque no falla al arrancar sino al ejecutar la query de listar tareas

Decisión: Editar. Eliminar el parámetro. Fix trivial.


Corrección: Validation — Password Acepta 4 Caracteres

Archivo: models.py Problema: La validación de password acepta 4 caracteres mínimo, pero RF-01.4 requiere 8 caracteres mínimo. Requisito violado: RF-01.4

Ver solución
class UserCreate(BaseModel):
    email: EmailStr
    name: str
    password: str

    @field_validator("password")
    @classmethod
    def validate_password(cls, v):
        if len(v) < 8:
            raise ValueError("Password must be at least 8 characters")
        return v

Por qué este fix es correcto:

  • Se cambió < 4 a < 8 para cumplir con RF-01.4
  • Se actualizó el mensaje de error para reflejar el requisito correcto

Decisión: Editar. Cambio de un número en una línea.


Corrección: Validation — Título Acepta 1 Carácter

Archivo: models.py Problema: La validación de título solo verifica que no esté vacío (>= 1 carácter), pero RF-05.1 requiere mínimo 3 y máximo 100 caracteres. Requisito violado: RF-05.1

Ver solución
class TaskCreate(BaseModel):
    title: str
    description: Optional[str] = ""
    priority: str = "medium"
    status: str = "pending"

    @field_validator("title")
    @classmethod
    def validate_title(cls, v):
        if len(v) < 3:
            raise ValueError("Title must be at least 3 characters")
        if len(v) > 100:
            raise ValueError("Title must be at most 100 characters")
        return v

    @field_validator("priority")
    @classmethod
    def validate_priority(cls, v):
        allowed = {"low", "medium", "high"}
        if v not in allowed:
            raise ValueError(f"Priority must be one of: {', '.join(allowed)}")
        return v

    @field_validator("status")
    @classmethod
    def validate_status(cls, v):
        allowed = {"pending", "in_progress", "completed"}
        if v not in allowed:
            raise ValueError(f"Status must be one of: {', '.join(allowed)}")
        return v

Por qué este fix es correcto:

  • Se cambió < 1 a < 3 y se agregó límite máximo de 100 caracteres (RF-05.1)
  • Se agregaron validadores para priority (RF-05.2) y status (RF-05.3)
  • Los validadores usan sets para verificación rápida y mensajes de error claros

Decisión: Regenerar la clase completa. Se necesitaban 3 validadores nuevos además de corregir el existente. Regenerar era más limpio que añadir uno por uno.


Corrección: Edge Case — Paginación Acepta page=0

Archivo: routes/tasks.py Problema: El parámetro page acepta 0 porque tiene ge=0 en vez de ge=1. Requisito violado: RF-05.4

Ver solución
@router.get("/", response_model=TaskListResponse)
async def list_tasks(
    status: Optional[str] = Query(default=None),
    priority: Optional[str] = Query(default=None),
    page: int = Query(default=1, ge=1),
    size: int = Query(default=10, ge=1, le=100),
    current_user: dict = Depends(get_current_user),
):
    result = get_tasks(
        user_id=current_user["user_id"],
        status=status,
        priority=priority,
        page=page,
        size=size,
    )
    return TaskListResponse(**result)

Por qué este fix es correcto:

  • Se cambió ge=0 a ge=1 para que page empiece en 1 (RF-05.4)
  • FastAPI validará automáticamente y retornará 422 si se envía page=0

Decisión: Editar. Cambio de un carácter: 0 → 1.


Cómo Verificar Cada Fix

Después de implementar cada corrección, verifica que funciona:

Verificación de fixes de seguridad

# 1. Verificar que la app no arranca sin JWT_SECRET_KEY
unset JWT_SECRET_KEY
uvicorn main:app --reload  # Debe fallar con KeyError

# 2. Verificar que la contraseña se hashea
export JWT_SECRET_KEY="test-secret"
uvicorn main:app --reload

curl -X POST http://localhost:8000/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email": "verify@test.com", "name": "Verify", "password": "securepass123"}'

sqlite3 taskflow.db "SELECT password FROM users WHERE email='verify@test.com'"
# Debe mostrar un hash de bcrypt, no "securepass123"

# 3. Verificar que SQL injection no funciona
curl -s "http://localhost:8000/users/search?query=test'%20OR%20'1'='1" \
  -H "Authorization: Bearer $TOKEN"
# No debe retornar todos los usuarios

# 4. Verificar que búsqueda requiere auth
curl -s "http://localhost:8000/users/search?query=test"
# Debe retornar 401, no resultados

Verificación de fixes de lógica

# 1. Verificar soft delete
curl -X DELETE http://localhost:8000/tasks/1 \
  -H "Authorization: Bearer $TOKEN"

sqlite3 taskflow.db "SELECT id, status FROM tasks WHERE id = 1"
# Debe mostrar status = 'deleted', no estar vacío

# 2. Verificar autorización
# Con token de User 2, intentar ver tarea de User 1
curl -s http://localhost:8000/tasks/1 \
  -H "Authorization: Bearer $TOKEN_USER2"
# Debe retornar 403, no la tarea

# 3. Verificar paginación
curl -s "http://localhost:8000/tasks/?page=1&size=5" \
  -H "Authorization: Bearer $TOKEN"
# Debe retornar las primeras 5 tareas, no saltarse ninguna

Verificación de fixes de edge cases

# 1. Verificar estadísticas sin tareas
curl -s http://localhost:8000/users/stats \
  -H "Authorization: Bearer $TOKEN_NEW_USER"
# Debe retornar {"total_tasks": 0, ...}, no error 500

# 2. Verificar estadísticas sin tareas eliminadas
# (después de soft delete)
curl -s http://localhost:8000/users/stats \
  -H "Authorization: Bearer $TOKEN"
# No debe incluir tareas con status "deleted"

# 3. Verificar validaciones
curl -X POST http://localhost:8000/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email": "weak@test.com", "name": "Weak", "password": "1234"}'
# Debe retornar 422, no 200

curl -X POST http://localhost:8000/tasks/ \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"title": "AB", "priority": "medium"}'
# Debe retornar 422 (título < 3 caracteres)

curl -X POST http://localhost:8000/tasks/ \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"title": "Tarea válida", "priority": "urgente"}'
# Debe retornar 422 (prioridad inválida)

Formato de Documentación de Justificaciones

Para cada corrección, documenta usando este formato:

# Justificaciones de Correcciones — TaskFlow API

## Fix #1: [Título del finding]

**Finding:** #[número] — [descripción breve]
**Severidad:** [Critical/High/Medium/Low]
**Archivo:** [nombre del archivo]
**Líneas afectadas:** [rango de líneas]

### Qué cambié
[Descripción concisa del cambio — qué líneas cambiaron y cómo]

### Por qué el código original era incorrecto
[Explicación referenciando el requisito funcional violado.
Usar el ID del requisito: RF-XX.X]

### Por qué mi corrección es correcta
[Explicación de por qué el fix resuelve el problema.
Incluir cómo verificaste que funciona.]

### Decisión regenerar/editar
- **Decisión:** [Editar / Regenerar]
- **Justificación:** [Por qué elegiste esta opción]

### Verificación
- **Método:** [Cómo verificaste que el fix funciona]
- **Resultado:** [Qué observaste]

---

## Fix #2: [Título del finding]
[Mismo formato]

Errores Comunes en Esta Fase

Error 1: Corregir sin justificar

Cambiar una línea de código no demuestra conocimiento. Explicar por qué la cambias y por qué tu versión es correcta sí lo demuestra. Cada fix sin justificación es un fix incompleto.

Error 2: Corregir los Low antes que los Critical

Es tentador empezar por lo fácil (cambiar un número de 4 a 8). Pero en un entorno profesional, si solo tienes 30 minutos, los security holes son lo primero. Practica la priorización.

Error 3: No verificar que el fix funciona

"Cambié la línea, debe funcionar" es una suposición. Ejecuta el endpoint, verifica el resultado, confirma que el fix resuelve el problema sin introducir otros.

Error 4: Introducir nuevos bugs al corregir

Cada corrección tiene el potencial de romper algo más. Después de cada fix, ejecuta las pruebas básicas para confirmar que no rompiste nada:

# Smoke test después de cada fix
curl -s http://localhost:8000/health
curl -s -X POST http://localhost:8000/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email": "smoke@test.com", "name": "Smoke", "password": "smoketest123"}'
curl -s -X POST http://localhost:8000/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email": "smoke@test.com", "password": "smoketest123"}'

Error 5: Regenerar todo el codebase

Si le dices a Claude Code "regenera todo el codebase corregido", pierdes:

  • La oportunidad de demostrar que entiendes cada problema
  • La documentación de qué cambió y por qué
  • La práctica de la habilidad real (corrección quirúrgica)

Regenera funciones específicas cuando el framework lo justifica. No regeneres archivos completos.


Resumen de Correcciones Esperadas

Al final de esta fase, deberías haber implementado correcciones para todos los problemas encontrados. Este es un mapa de las correcciones principales por archivo:

config.py

  • ✅ JWT secret sin fallback hardcoded
  • ✅ DEBUG no hardcoded como True

main.py

  • ✅ Import inexistente eliminado

models.py

  • ✅ Validación de password: 8 caracteres mínimo
  • ✅ Validación de título: 3-100 caracteres
  • ✅ Validación de prioridad: solo low/medium/high
  • ✅ Validación de estado: solo pending/in_progress/completed

routes/auth.py

  • ✅ Contraseña hasheada antes de almacenar

routes/tasks.py

  • ✅ Paginación: page >= 1

routes/users.py

  • ✅ SQL injection corregido en búsqueda
  • ✅ Autenticación agregada en búsqueda

services/task_service.py

  • ✅ SQL injection corregido en filtros
  • ✅ Paginación offset corregido
  • ✅ Soft delete implementado
  • ✅ Verificación de propiedad en get_task_by_id
  • ✅ Estadísticas excluyen tareas eliminadas
  • ✅ División por cero manejada

Checkpoint de la Fase de Corrección

Antes de pasar a la fase de entrega y retrospectiva, verifica:

  • ✅ Todos los findings de severidad Critical están corregidos
  • ✅ Todos los findings de severidad High están corregidos
  • ✅ La mayoría de findings Medium están corregidos
  • ✅ Cada corrección tiene justificación escrita
  • ✅ Cada corrección fue verificada ejecutando el endpoint
  • ✅ Para cada fix, documentaste la decisión regenerar/editar
  • ✅ No introdujiste nuevos bugs (smoke tests pasan)
  • ✅ El codebase corregido se ejecuta sin errores

Siguiente cápsula: Entrega y Retrospectiva — El formato completo de entrega, la rúbrica de auto-evaluación, y las preguntas guía para la retrospectiva.


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