Módulo 4: Code Review de Output AI

Checklist Profesional de Code Review para AI Code

Checklist Profesional de Code Review para AI Code

Descripción de la cápsula

En la cápsula anterior aprendiste a priorizar: la pirámide te dice qué revisar primero. Ahora necesitas saber qué revisar exactamente en cada nivel. Esta cápsula construye tu checklist profesional de code review para código generado por AI — el artefacto más importante de todo el módulo.

Este no es un checklist genérico de "buenas prácticas." Cada item es específico, verificable, y accionable. No dice "revisar el código" — dice "verificar que cada import existe ejecutando pip show <paquete> o buscando en la documentación oficial." No dice "revisar seguridad" — dice "confirmar que no hay string concatenation en queries SQL."

El checklist tiene 20 items organizados en 5 categorías, alineados con la pirámide de prioridades. Es una herramienta viva: la usas hoy, la adaptas mañana, la mejoras con cada review. Al final de tu carrera, este checklist será diferente — pero siempre tendrá como base lo que construyes aquí.


Principios del Checklist

Antes de ver los items, entiende los principios que los guían:

1. Específico y verificable

❌ Malo:  "Revisar la seguridad del código"
✅ Bueno: "Verificar que no hay API keys, passwords, o tokens 
          hardcoded en el código fuente"

❌ Malo:  "Verificar que el código funciona"
✅ Bueno: "Ejecutar el endpoint con un input válido y uno inválido 
          para verificar happy path y error handling"

2. Accionable en menos de 5 minutos

Cada item del checklist debe ser verificable en 1-5 minutos. Si un item toma más, necesita dividirse en sub-items.

3. Priorizado por impacto

Los primeros items del checklist son los más importantes. Si solo revisas los primeros 5, cubres los riesgos más altos.

4. Adaptado a AI code

Cada item existe porque AI comete este error con frecuencia. Items que no son relevantes para AI (como "verificar que no hay merge conflicts") no están incluidos.


El Checklist: 20 Items en 5 Categorías

Categoría 1: Seguridad (Items 1-5)

Estos se revisan siempre, sin excepción. No importa el tamaño del PR, el nivel de urgencia, o tu confianza en el código.


Item 1: No hay secrets hardcoded en el código

QUÉ VERIFICAR:
- API keys (Stripe, SendGrid, AWS, etc.)
- Passwords y tokens de acceso
- Connection strings de bases de datos
- Claves de encripción

CÓMO VERIFICAR:
- Buscar strings sospechosos: "sk_", "Bearer ", "password", 
  "secret", "key", "token"
- Verificar que os.getenv() NO tiene fallback con valores reales
- Revisar archivos de configuración (.env.example, config.py)

EJEMPLO DE FALLA (AI genera frecuentemente):
# AI genera esto con frecuencia
SECRET_KEY = os.getenv("SECRET_KEY", "mi-clave-secreta-de-desarrollo")
DATABASE_URL = "postgresql://admin:password123@localhost:5432/mydb"
STRIPE_KEY = "sk_live_abc123def456"
# Versión correcta
SECRET_KEY = os.environ["SECRET_KEY"]  # Falla si no existe — bien
DATABASE_URL = os.environ["DATABASE_URL"]
STRIPE_KEY = os.environ["STRIPE_API_KEY"]
SEVERIDAD SI FALLA: Critical
TIEMPO ESTIMADO: 1-2 minutos

Item 2: Queries SQL usan parameterized queries (no string concatenation)

QUÉ VERIFICAR:
- Cualquier query SQL construida con f-strings, .format(), o + concatenation
- Inputs del usuario que llegan directamente a queries
- ORM queries que permiten raw SQL

CÓMO VERIFICAR:
- Buscar: f"SELECT, f"INSERT, f"UPDATE, f"DELETE
- Buscar: .format( en contexto de queries
- Buscar: cursor.execute( con strings concatenadas

EJEMPLO DE FALLA:
# SQL Injection — AI genera esto regularmente con sqlite3
cursor.execute(f"SELECT * FROM users WHERE name = '{name}'")

# Con .format() — igual de peligroso
query = "SELECT * FROM users WHERE id = {}".format(user_id)
cursor.execute(query)
# Correcto — parameterized query
cursor.execute("SELECT * FROM users WHERE name = ?", (name,))

# Con SQLAlchemy
stmt = select(User).where(User.name == name)
SEVERIDAD SI FALLA: Critical
TIEMPO ESTIMADO: 2-3 minutos

Item 3: Endpoints sensibles tienen autenticación y autorización

QUÉ VERIFICAR:
- Endpoints que modifican datos (POST, PUT, PATCH, DELETE)
- Endpoints que exponen datos sensibles
- Endpoints de admin
- Que auth !== solo autenticación — también autorización 
  (¿el usuario PUEDE hacer esta acción?)

CÓMO VERIFICAR:
- Revisar cada endpoint: ¿tiene Depends() con auth?
- Verificar que los roles/permisos son correctos
- Buscar endpoints que acceden a datos de otros usuarios

EJEMPLO DE FALLA:
# Sin auth — cualquiera puede borrar usuarios
@app.delete("/users/{user_id}")
async def delete_user(user_id: int):
    db.execute("DELETE FROM users WHERE id = ?", (user_id,))
    return {"status": "deleted"}
# Con auth y autorización
@app.delete("/users/{user_id}")
async def delete_user(
    user_id: int,
    current_user: User = Depends(get_current_admin_user),
):
    if current_user.role != "admin":
        raise HTTPException(status_code=403, detail="Not authorized")
    db.execute("DELETE FROM users WHERE id = ?", (user_id,))
    return {"status": "deleted"}
SEVERIDAD SI FALLA: Critical
TIEMPO ESTIMADO: 2-3 minutos

Item 4: Inputs del usuario están validados y sanitizados

QUÉ VERIFICAR:
- Inputs que llegan como query params, path params, o body
- Tipos correctos (int vs str, etc.)
- Rangos razonables (age > 0, price >= 0, page >= 1)
- Longitud máxima en strings
- Formatos esperados (email, URL, fecha)

CÓMO VERIFICAR:
- Revisar modelos Pydantic: ¿tienen Field() con constraints?
- Revisar Query() y Path(): ¿tienen ge, le, min_length, max_length?
- ¿Hay inputs que llegan como `dict` o `Any`? — Red flag

EJEMPLO DE FALLA:
# Sin validación — acepta cualquier cosa
@app.post("/products")
async def create_product(data: dict):
    save_product(data)
    return data
# Con validación completa
class ProductCreate(BaseModel):
    name: str = Field(..., min_length=1, max_length=200)
    price: Decimal = Field(..., gt=0, le=999999)
    category: str = Field(..., min_length=1, max_length=50)
    description: Optional[str] = Field(None, max_length=5000)

@app.post("/products", response_model=ProductResponse)
async def create_product(product: ProductCreate):
    return save_product(product)
SEVERIDAD SI FALLA: High
TIEMPO ESTIMADO: 2-3 minutos

Item 5: Mensajes de error no exponen información interna

QUÉ VERIFICAR:
- Stack traces en responses HTTP
- Rutas del sistema de archivos
- Nombres de tablas/columnas de la DB
- Versiones de software
- Detalles de configuración

CÓMO VERIFICAR:
- Buscar: str(e), str(exc), repr(e) en error handlers
- Buscar: detail= con variables que podrían exponer info
- Verificar que los except blocks usan mensajes genéricos

EJEMPLO DE FALLA:
# Expone información interna
@app.get("/users/{user_id}")
async def get_user(user_id: int):
    try:
        user = db.query(f"SELECT * FROM users WHERE id = {user_id}")
        return user
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))
        # Podría exponer: "relation 'users' does not exist" 
        # o rutas del filesystem
# Mensaje genérico para el cliente, detalle en logs
import logging

logger = logging.getLogger(__name__)

@app.get("/users/{user_id}")
async def get_user(user_id: int):
    try:
        user = db.get_user(user_id)
        if not user:
            raise HTTPException(status_code=404, detail="User not found")
        return user
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Error fetching user {user_id}: {e}")
        raise HTTPException(
            status_code=500,
            detail="Internal server error",
        )
SEVERIDAD SI FALLA: Medium-High
TIEMPO ESTIMADO: 1-2 minutos

Categoría 2: Lógica de Negocio (Items 6-9)

Estos se revisan siempre que el código implemente reglas de negocio. Si es puro boilerplate sin lógica, puedes pasar rápido.


Item 6: El código resuelve el problema que pediste (no uno diferente)

QUÉ VERIFICAR:
- ¿El endpoint hace lo que especificaste en el prompt?
- ¿Hay funcionalidad que nadie pidió?
- ¿Falta funcionalidad que sí pediste?
- ¿El approach es el que esperabas?

CÓMO VERIFICAR:
- Comparar tu prompt/requisito original con el código generado
- Listar: qué pediste, qué recibiste, qué sobra, qué falta

ESTE ITEM ES ESPECÍFICO DE AI:
Un developer humano rara vez resuelve un problema diferente al pedido.
AI lo hace frecuentemente — resuelve un problema SIMILAR pero no 
idéntico. El código se ve correcto porque resuelve UN problema, 
solo que no es el tuyo.

SEVERIDAD SI FALLA: High
TIEMPO ESTIMADO: 2-3 minutos

Item 7: Cálculos y condiciones son correctos

QUÉ VERIFICAR:
- Operadores de comparación: > vs >=, < vs <=, == vs !=
- Cálculos matemáticos: orden de operaciones, redondeo, precisión
- Tipo de datos para dinero: Decimal, no float
- Fórmulas de negocio: descuentos, taxes, comisiones, tarifas

CÓMO VERIFICAR:
- Trazar manualmente con 2-3 valores de ejemplo
- Prestar atención especial a boundary values (límite exacto)
- Verificar contra la regla de negocio documentada

EJEMPLO DE FALLA:
# "Descuento del 10% para compras mayores a $100"

# AI genera: (¿es > o >= ?)
def apply_discount(total: Decimal) -> Decimal:
    if total >= Decimal("100"):  # >= cuando debería ser >
        return total * Decimal("0.90")
    return total

# Verificación manual:
# total = 100 → con >=, aplica descuento (¿correcto? "mayores a" = >)
# total = 100.01 → con >, aplica descuento (correcto)
# total = 99.99 → no aplica (correcto en ambos)
SEVERIDAD SI FALLA: High
TIEMPO ESTIMADO: 3-5 minutos (requiere pensar con valores reales)

Item 8: Estados y transiciones son válidos

QUÉ VERIFICAR:
- ¿Los estados posibles son los correctos?
- ¿Las transiciones de estado son válidas?
  (e.g., no puedes ir de "cancelled" a "active")
- ¿Hay estados faltantes?
- ¿Hay transiciones que no deberían existir?

CÓMO VERIFICAR:
- Dibujar el diagrama de estados mentalmente (o en papel)
- Verificar que el código no permite transiciones inválidas
- Buscar: dónde se cambia el status y qué validaciones hay

EJEMPLO DE FALLA:
# AI permite cualquier transición — no hay validación
@app.patch("/orders/{order_id}/status")
async def update_order_status(order_id: str, new_status: str):
    order = get_order(order_id)
    order.status = new_status  # ¿De "delivered" a "pending"? Sí, permite
    save_order(order)
    return order
# Transiciones validadas
VALID_TRANSITIONS = {
    "pending": ["confirmed", "cancelled"],
    "confirmed": ["processing", "cancelled"],
    "processing": ["shipped", "cancelled"],
    "shipped": ["delivered"],
    "delivered": ["returned"],
    "cancelled": [],
    "returned": [],
}

@app.patch("/orders/{order_id}/status")
async def update_order_status(order_id: str, new_status: OrderStatus):
    order = get_order(order_id)
    
    valid_next = VALID_TRANSITIONS.get(order.status, [])
    if new_status.value not in valid_next:
        raise HTTPException(
            status_code=400,
            detail=f"Cannot transition from {order.status} to {new_status.value}",
        )
    
    order.status = new_status.value
    save_order(order)
    return order
SEVERIDAD SI FALLA: High
TIEMPO ESTIMADO: 3-5 minutos

Item 9: Operaciones que deben ser atómicas lo son

QUÉ VERIFICAR:
- Secuencias de operaciones que deben completarse todas o ninguna
- Transferencias de dinero (débito + crédito)
- Operaciones de inventario (reservar + cobrar)
- Multi-step workflows (crear usuario + enviar email + asignar rol)

CÓMO VERIFICAR:
- Para cada secuencia: ¿qué pasa si el paso N falla?
- ¿Hay rollback? ¿Hay transacciones de DB?
- ¿Hay idempotency keys para prevenir duplicados?

EJEMPLO DE FALLA:
# No atómico — si save_transfer falla, el sender ya perdió dinero
async def transfer(sender_id: str, receiver_id: str, amount: Decimal):
    sender = get_account(sender_id)
    sender.balance -= amount
    save_account(sender)  # Si esto pasa...
    
    receiver = get_account(receiver_id)
    receiver.balance += amount
    save_account(receiver)  # ...pero esto falla → dinero desapareció
    
    save_transfer(sender_id, receiver_id, amount)
# Atómico con transacción de DB
async def transfer(sender_id: str, receiver_id: str, amount: Decimal):
    async with db.transaction():
        sender = await get_account_for_update(sender_id)
        if sender.balance < amount:
            raise InsufficientFundsError()
        
        sender.balance -= amount
        receiver = await get_account_for_update(receiver_id)
        receiver.balance += amount
        
        await save_transfer(sender_id, receiver_id, amount)
SEVERIDAD SI FALLA: High-Critical (dependiendo del contexto)
TIEMPO ESTIMADO: 2-3 minutos

Categoría 3: Edge Cases (Items 10-13)

Estos se revisan frecuentemente, especialmente en código que recibe input de usuarios o sistemas externos.


Item 10: Maneja null/None, vacío, y valores por defecto

QUÉ VERIFICAR:
- ¿Qué pasa si un campo Optional es None?
- ¿Qué pasa con listas vacías?
- ¿Qué pasa con strings vacíos ("" vs None)?
- ¿Los defaults son razonables?

CÓMO VERIFICAR:
- Para cada input Optional: seguir el flujo con valor None
- Para cada lista: ¿funciona con []?
- Buscar: accesos a .attribute sin check de None

EJEMPLO DE FALLA:
# Crashea si user.address es None
def get_shipping_zone(user: User) -> str:
    return user.address.state  # AttributeError si address es None
# Maneja None correctamente
def get_shipping_zone(user: User) -> str:
    if not user.address:
        raise ValueError("User has no shipping address configured")
    return user.address.state
SEVERIDAD SI FALLA: Medium
TIEMPO ESTIMADO: 2-3 minutos

Item 11: Error handling es completo y correcto

QUÉ VERIFICAR:
- ¿Los errores esperados se manejan? (DB not found, service timeout)
- ¿Los HTTP status codes son correctos? (404 vs 400 vs 500)
- ¿Los errores se propagan correctamente?
- ¿Los try/except no son demasiado amplios? (no `except Exception`)

CÓMO VERIFICAR:
- Para cada operación que puede fallar: ¿hay handling?
- ¿Los except blocks son específicos?
- ¿Se distingue entre error del cliente (4xx) y del servidor (5xx)?

EJEMPLO DE FALLA:
# Catch demasiado amplio — oculta bugs reales
@app.get("/users/{user_id}")
async def get_user(user_id: int):
    try:
        return db.get_user(user_id)
    except Exception:
        raise HTTPException(status_code=404, detail="Not found")
        # ¿Y si el error es un connection timeout? 
        # Devuelve 404 en vez de 500/503
# Manejo específico por tipo de error
@app.get("/users/{user_id}")
async def get_user(user_id: int):
    try:
        user = db.get_user(user_id)
    except ConnectionError:
        raise HTTPException(status_code=503, detail="Service unavailable")
    except DatabaseError as e:
        logger.error(f"Database error: {e}")
        raise HTTPException(status_code=500, detail="Internal error")
    
    if not user:
        raise HTTPException(status_code=404, detail="User not found")
    
    return user
SEVERIDAD SI FALLA: Medium-High
TIEMPO ESTIMADO: 2-3 minutos

Item 12: Paginación y límites son correctos

QUÉ VERIFICAR:
- ¿El cálculo de páginas es correcto? (ceil, no floor)
- ¿Qué pasa con página 0 o negativa?
- ¿Hay límite máximo en per_page/limit?
- ¿La paginación se hace en la DB o carga todo en memoria?

CÓMO VERIFICAR:
- Calcular manualmente: 21 items / 20 per_page = ¿2 o 1 páginas?
- Probar: page=0, page=-1, per_page=0, per_page=999999

SEVERIDAD SI FALLA: Medium
TIEMPO ESTIMADO: 1-2 minutos

Item 13: Operaciones concurrentes no causan race conditions

QUÉ VERIFICAR:
- ¿Dos requests simultáneos pueden corromper datos?
- ¿Hay read-modify-write sin locking?
- ¿Los contadores se incrementan atómicamente?
- ¿Las reservas de inventario usan locking?

CÓMO VERIFICAR:
- Para cada write operation: ¿qué pasa si llegan 2 requests al 
  mismo tiempo?
- Buscar: patterns de read → modify → write sin transacción

EJEMPLO DE FALLA:
# Race condition en inventario
@app.post("/purchase/{product_id}")
async def purchase(product_id: str):
    product = get_product(product_id)
    if product.stock > 0:        # Thread A lee stock=1
        product.stock -= 1        # Thread A: stock=0
        save_product(product)     # Thread A guarda
        # Thread B también leyó stock=1 ANTES de que A guardara
        # Thread B: stock=0, vende lo mismo → oversold
        return {"status": "purchased"}
    raise HTTPException(status_code=400, detail="Out of stock")
# Con locking optimista
@app.post("/purchase/{product_id}")
async def purchase(product_id: str):
    async with db.transaction():
        product = await get_product_for_update(product_id)
        if product.stock <= 0:
            raise HTTPException(status_code=400, detail="Out of stock")
        product.stock -= 1
        await save_product(product)
    return {"status": "purchased"}
SEVERIDAD SI FALLA: High (en operaciones de inventario/financieras)
TIEMPO ESTIMADO: 2-3 minutos

Categoría 4: AI-Specific (Items 14-17)

Estos items solo existen porque el código fue generado por AI. No los encontrarías en un checklist de code review tradicional.


Item 14: Todos los imports existen y son correctos

QUÉ VERIFICAR:
- ¿Cada paquete importado existe en pip/PyPI?
- ¿Las funciones/clases importadas existen en esos paquetes?
- ¿Los imports coinciden con la versión del paquete que usas?
- ¿Hay imports de funciones que suenan reales pero no existen?

CÓMO VERIFICAR:
- Para cada import no estándar: verificar en docs del paquete
- Buscar en PyPI si el paquete existe
- Si un import te suena "raro" pero plausible → verificar

EJEMPLO DE FALLA (hallucination clásica):
from fastapi import FastAPI, BackgroundScheduler  # BackgroundScheduler no existe en FastAPI
from pydantic import BaseModel, validate_email     # validate_email no es de pydantic
from sqlalchemy.ext.async import AsyncSession       # El path correcto es sqlalchemy.ext.asyncio
from sklearn.metrics import roc_auc_multiclass      # No existe, es roc_auc_score
SEVERIDAD SI FALLA: High (el código no ejecuta)
TIEMPO ESTIMADO: 2-3 minutos

Item 15: No hay sobre-ingeniería para el problema

QUÉ VERIFICAR:
- ¿Hay patrones de diseño que no se necesitan? (Factory, Strategy, 
  Observer para un CRUD simple)
- ¿Hay abstracciones que solo tienen una implementación?
- ¿La complejidad del código es proporcional a la complejidad 
  del problema?
- ¿Hay clases abstractas sin implementaciones concretas?

CÓMO VERIFICAR:
- Comparar: ¿qué tan simple es el problema vs qué tan complejo 
  es el código?
- Contar clases/archivos: ¿son proporcionales a la funcionalidad?
- Buscar: abc.ABC, @abstractmethod, __init_subclass__

EJEMPLO DE FALLA:
# Para un endpoint que devuelve "Hello, World"
# AI genera un sistema con Factory, Strategy, y Registry:

class GreetingStrategy(abc.ABC):
    @abc.abstractmethod
    def greet(self, name: str) -> str: ...

class EnglishGreeting(GreetingStrategy):
    def greet(self, name: str) -> str:
        return f"Hello, {name}"

class GreetingFactory:
    _strategies: dict = {}
    
    @classmethod
    def register(cls, lang: str, strategy: GreetingStrategy):
        cls._strategies[lang] = strategy
    
    @classmethod
    def get(cls, lang: str) -> GreetingStrategy:
        return cls._strategies[lang]

# Cuando lo único que necesitas es:
@app.get("/hello/{name}")
async def hello(name: str):
    return {"message": f"Hello, {name}"}
SEVERIDAD SI FALLA: Low-Medium (funciona, pero es innecesariamente complejo)
TIEMPO ESTIMADO: 1-2 minutos

Item 16: Las APIs y funciones de librerías se usan correctamente

QUÉ VERIFICAR:
- ¿Las funciones se llaman con los parámetros correctos?
- ¿Los parámetros tienen los nombres correctos?
- ¿Se usa la versión actual de la API, no una deprecated?
- ¿Los valores de retorno se manejan correctamente?

CÓMO VERIFICAR:
- Comparar cada llamada a librería con la documentación oficial
- Buscar: deprecation warnings, versiones de API
- Verificar que los parámetros named son correctos

EJEMPLO DE FALLA:
# AI usa API de versión anterior
import jwt

# pyjwt < 2.0 (deprecated)
token = jwt.encode(payload, secret, algorithm="HS256")
# En pyjwt >= 2.0, encode() devuelve str, no bytes
# AI podría agregar .decode("utf-8") que ya no es necesario

decoded = jwt.decode(token, secret, algorithm="HS256")
# En pyjwt >= 2.0, el parámetro es algorithms=[...], no algorithm=...
# Esto lanza un error sutil
# Versión correcta para pyjwt >= 2.0
import jwt

token = jwt.encode(payload, secret, algorithm="HS256")  # Devuelve str
decoded = jwt.decode(token, secret, algorithms=["HS256"])  # Lista, no string
SEVERIDAD SI FALLA: Medium-High (puede causar runtime errors sutiles)
TIEMPO ESTIMADO: 3-5 minutos (requiere verificar docs)

Item 17: No hay código que mezcle patrones de diferentes frameworks

QUÉ VERIFICAR:
- ¿Se mezcla Flask con FastAPI?
- ¿Se mezcla Django ORM con SQLAlchemy?
- ¿Se usan patrones de un framework en otro?
- ¿Los decoradores y middleware son del framework correcto?

CÓMO VERIFICAR:
- Verificar que todos los imports son del mismo ecosistema
- Buscar patrones que "se ven raros" para el framework usado
- Verificar que los decoradores corresponden al framework

EJEMPLO DE FALLA:
from fastapi import FastAPI
from flask import jsonify  # ¿Flask en un proyecto FastAPI?

app = FastAPI()

@app.route("/users")  # @app.route es Flask, no FastAPI
def get_users():       # Sin async — patrón Flask, no FastAPI
    users = get_all_users()
    return jsonify(users)  # jsonify es Flask — FastAPI usa return directo
SEVERIDAD SI FALLA: Medium (puede causar errores difíciles de diagnosticar)
TIEMPO ESTIMADO: 1-2 minutos

Categoría 5: Calidad de Código (Items 18-20)

Estos se revisan cuando hay tiempo. Son importantes para mantenibilidad pero no bloquean el merge.


Item 18: Los response models filtran datos sensibles

QUÉ VERIFICAR:
- ¿Los endpoints devuelven solo los campos necesarios?
- ¿Hay password_hash, tokens, o datos internos en el response?
- ¿Los response_model de Pydantic están definidos?
- ¿SELECT * se convierte en response filtrado?

CÓMO VERIFICAR:
- Comparar: ¿qué campos tiene el model de DB vs qué devuelve 
  el endpoint?
- Buscar: response_model= en cada endpoint

EJEMPLO DE FALLA:
# Devuelve todo, incluyendo password_hash
@app.get("/users/{user_id}")
async def get_user(user_id: int):
    return db.get_user(user_id)
    # Response: {"id": 1, "name": "Ana", "email": "...", 
    #            "password_hash": "$2b$12$...", "role": "admin"}
# Solo devuelve campos públicos
class UserPublic(BaseModel):
    id: int
    name: str
    email: str

@app.get("/users/{user_id}", response_model=UserPublic)
async def get_user(user_id: int):
    return db.get_user(user_id)
SEVERIDAD SI FALLA: Medium-High (data leak)
TIEMPO ESTIMADO: 1-2 minutos

Item 19: La estructura del código sigue las convenciones del proyecto

QUÉ VERIFICAR:
- ¿Los archivos nuevos están en el directorio correcto?
- ¿Los nombres de archivos siguen las convenciones?
- ¿Las funciones están en el módulo correcto? 
  (no lógica de negocio en routes)
- ¿Los imports siguen el orden del proyecto?

CÓMO VERIFICAR:
- Comparar estructura del código nuevo con código existente
- Verificar: ¿service logic en service.py o en routes.py?

SEVERIDAD SI FALLA: Low
TIEMPO ESTIMADO: 1 minuto

Item 20: Los tests (si existen) verifican comportamiento real

QUÉ VERIFICAR:
- ¿Los tests verifican el comportamiento correcto, no solo 
  que "no crashea"?
- ¿Hay tests para los edge cases identificados?
- ¿Los assertions verifican valores específicos?
- ¿Los tests son independientes entre sí?
- Si AI generó los tests, ¿verifican lo correcto o solo 
  confirman lo que AI cree que es correcto?

CÓMO VERIFICAR:
- Leer cada assertion: ¿qué verifica exactamente?
- Buscar: tests que solo verifican status_code 200 sin 
  revisar el body
- Buscar: tests con assert True (no verifican nada)

EJEMPLO DE FALLA:
# Test que no verifica nada útil
def test_create_user():
    response = client.post("/users", json={"name": "Ana", "email": "ana@test.com"})
    assert response.status_code == 200  # Solo verifica que no crashea
    # No verifica: ¿el usuario se creó? ¿Los datos son correctos?
    # ¿El password se hasheó? ¿El response tiene el formato correcto?
# Test que verifica comportamiento real
def test_create_user():
    response = client.post(
        "/users",
        json={"name": "Ana", "email": "ana@test.com", "password": "secure123"},
    )
    assert response.status_code == 201
    data = response.json()
    assert data["name"] == "Ana"
    assert data["email"] == "ana@test.com"
    assert "password" not in data
    assert "password_hash" not in data
    assert "id" in data
SEVERIDAD SI FALLA: Medium
TIEMPO ESTIMADO: 3-5 minutos

Checklist de Bolsillo: Versión Rápida

Para uso diario, esta es la versión compacta que puedes tener a mano:

CODE REVIEW DE AI CODE — CHECKLIST RÁPIDO
==========================================

SEGURIDAD (siempre):
□ 1.  No hay secrets hardcoded
□ 2.  SQL usa parameterized queries
□ 3.  Endpoints sensibles tienen auth
□ 4.  Inputs validados y sanitizados
□ 5.  Errores no exponen info interna

LÓGICA (siempre que haya reglas):
□ 6.  Resuelve el problema pedido (no uno diferente)
□ 7.  Cálculos y condiciones correctos (> vs >=)
□ 8.  Transiciones de estado válidas
□ 9.  Operaciones atómicas cuando necesario

EDGE CASES (frecuentemente):
□ 10. Maneja None/vacío/defaults
□ 11. Error handling completo y correcto
□ 12. Paginación/límites correctos
□ 13. Sin race conditions en writes

AI-SPECIFIC (siempre en código AI):
□ 14. Todos los imports existen
□ 15. No hay sobre-ingeniería
□ 16. APIs de librerías usadas correctamente
□ 17. No mezcla frameworks

CALIDAD (cuando hay tiempo):
□ 18. Response models filtran datos sensibles
□ 19. Estructura sigue convenciones del proyecto
□ 20. Tests verifican comportamiento real

Conexión con Proyecto

El checklist como herramienta del proyecto integrador

En el módulo 8, vas a recibir un codebase FastAPI con 15-20 problemas. Tu checklist es tu arma principal. Cada item del checklist corresponde a un tipo de problema que podrías encontrar:

Item del checklistProblema típico en M8
Item 1 (secrets)API key hardcoded en config.py
Item 2 (SQL)String concatenation en una query de búsqueda
Item 7 (cálculos)Descuento calculado con operador incorrecto
Item 14 (imports)Import de función que no existe en la librería
Item 15 (sobre-ingeniería)Factory pattern para un solo tipo de objeto

Si tu checklist no tiene un item para un tipo de problema, no lo encontrarás. Por eso el checklist es una herramienta viva — lo mejoras cada vez que un problema se te escapa.


Troubleshooting

Problema 1: "20 items es demasiado — no puedo recordarlos todos"

Causa: No necesitas memorizarlos. Necesitas internalizarlos. Solución: Empieza con los primeros 5 (seguridad). Úsalos en cada review. Cuando sean automáticos, agrega los siguientes 4 (lógica). Progresa gradualmente. En 2-3 semanas, los 20 serán segunda naturaleza. Mientras tanto, ten el checklist de bolsillo visible.

Problema 2: "Algunos items no aplican a mi código"

Causa: No todo código tiene SQL, transiciones de estado, o concurrencia. Solución: Marca "N/A" en items que no aplican y sigue adelante. El checklist cubre los casos más comunes — si un item no aplica, son 5 segundos de "N/A", no 5 minutos de revisión. Nunca elimines items del checklist; mejor tener un item que no aplica el 80% del tiempo que perder un issue crítico el 20%.

Problema 3: "El item 16 (APIs correctas) toma mucho tiempo"

Causa: Verificar documentación de cada librería es lento. Solución: No verificas CADA llamada. Verificas: (1) imports que no reconoces, (2) funciones con parámetros que te suenan raros, (3) cualquier cosa que te genere duda. Para llamadas estándar que has usado 100 veces (como FastAPI() o BaseModel), confía en tu experiencia. El item 16 es para detectar hallucinations sutiles, no para re-verificar toda la documentación.

Problema 4: "Mi equipo tiene su propio checklist — ¿uso ambos?"

Causa: Checklists existentes probablemente son para código humano. Solución: Fusiona. Tu checklist de equipo probablemente tiene items de la Categoría 1 (seguridad) y Categoría 5 (calidad). Los items de Categoría 4 (AI-Specific) probablemente no están. Agrega los items 14-17 al checklist de tu equipo. Propón el cambio como "items adicionales para código generado por AI."


Ejercicios

Ejercicio 1: Aplicar el checklist (Medio)

Aplica el checklist de 20 items al siguiente código. Para cada item, marca: ✅ (pasa), ❌ (falla), ⚠️ (parcial), o N/A.

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List, Optional
from datetime import datetime
import uuid

app = FastAPI()

products_db = {}

class Product(BaseModel):
    name: str
    price: float
    category: str
    stock: int

class ProductResponse(Product):
    id: str
    created_at: datetime

@app.post("/products", response_model=ProductResponse)
async def create_product(product: Product):
    product_id = str(uuid.uuid4())
    record = ProductResponse(
        id=product_id,
        created_at=datetime.utcnow(),
        **product.model_dump(),
    )
    products_db[product_id] = record
    return record

@app.get("/products", response_model=List[ProductResponse])
async def list_products(category: Optional[str] = None):
    products = list(products_db.values())
    if category:
        products = [p for p in products if p.category == category]
    return products

@app.get("/products/{product_id}", response_model=ProductResponse)
async def get_product(product_id: str):
    if product_id not in products_db:
        raise HTTPException(status_code=404, detail="Product not found")
    return products_db[product_id]
Ver solución
#ItemResultadoNota
1Secrets hardcoded✅No hay secrets
2SQL parameterizedN/ANo hay SQL
3Auth en endpoints⚠️POST sin auth — ¿debería tener?
4Inputs validados⚠️Sin min_length, price puede ser negativo
5Errores no exponen info✅Solo "Product not found"
6Resuelve problema pedido✅CRUD básico correcto
7Cálculos correctosN/ANo hay cálculos
8Transiciones de estadoN/ANo hay estados
9Operaciones atómicasN/ASingle-step operations
10Maneja None/vacío✅Optional category funciona con None
11Error handling⚠️Solo 404, no hay handling para otros errores
12Paginación❌No hay paginación — devuelve TODO
13Race conditionsN/AIn-memory, single process
14Imports existen✅Todos son imports reales
15Sobre-ingeniería✅Simple y proporcional
16APIs correctas✅FastAPI y Pydantic usados correctamente
17Mezcla frameworks✅Solo FastAPI
18Response filtra datos✅ProductResponse no tiene datos sensibles
19Estructura convenciones✅Estructura estándar
20TestsN/ANo hay tests

Resumen: 2 issues principales:

  1. price: float debería ser Decimal con gt=0 — acepta negativos y tiene imprecisión
  2. Sin paginación en list_products — peligroso con muchos productos

Ejercicio 2: Mejorar un item del checklist (Medio)

El item 4 dice: "Inputs del usuario están validados y sanitizados."

Reescribe este item para hacerlo más específico y verificable. Tu versión debe incluir:

  • Exactamente qué verificar (3-5 puntos)
  • Cómo verificar cada punto
  • Un ejemplo de falla
Ver solución

Item 4 mejorado: Inputs del usuario están validados con Pydantic constraints

QUÉ VERIFICAR:
1. ¿Cada string tiene max_length definido?
   → Previene DoS por inputs de 10MB
   
2. ¿Los números tienen rangos definidos? (ge, le, gt, lt)
   → Previene precios negativos, cantidades de 999999999
   
3. ¿Los Enums se usan para valores limitados?
   → status debería ser Enum, no str libre
   
4. ¿Hay inputs que llegan como dict o Any?
   → Bypass completo de validación
   
5. ¿Los emails usan EmailStr, no str?
   → str acepta "no-soy-email" como email

CÓMO VERIFICAR:
- Abrir cada BaseModel: revisar cada Field()
- Buscar campos sin constraints: str sin max_length, int sin ge/le
- Buscar: dict, Any, str donde debería haber Enum

EJEMPLO DE FALLA:
# Sin constraints
class UserCreate(BaseModel):
    name: str              # Puede ser "" o un string de 10MB
    age: int               # Puede ser -50 o 999999
    role: str              # Puede ser "superadmin_hackeado"
    email: str             # Puede ser "no soy email"
# Con constraints
class UserCreate(BaseModel):
    name: str = Field(..., min_length=1, max_length=100)
    age: int = Field(..., ge=0, le=150)
    role: UserRole          # Enum con valores válidos
    email: EmailStr

Ejercicio 3: Encontrar el item faltante (Difícil)

El checklist de 20 items no cubre un escenario que encontraste en tu review: el código generado por AI usa datetime.utcnow() que está deprecated en Python 3.12+ (favor de usar datetime.now(timezone.utc)).

¿En qué categoría agregarías este item? Escribe el item completo con el formato del checklist.

Ver solución

Categoría: AI-Specific (junto a items 14-17)

Este item va en AI-Specific porque AI entrena con código que incluye datetime.utcnow() extensivamente (era el standard por años). AI seguirá generando este patrón incluso después de que fue deprecated. Un developer humano actualizado lo sabría; AI no necesariamente.

Item 16.5: No usa APIs deprecated del lenguaje o librerías

QUÉ VERIFICAR:
- datetime.utcnow() → deprecated en Python 3.12+
- datetime.utcfromtimestamp() → deprecated
- asyncio.get_event_loop() → changed behavior en 3.10+
- pkg_resources → replaced by importlib.resources
- unittest.TestCase con nose-style → prefer pytest

CÓMO VERIFICAR:
- Buscar: utcnow(), utcfromtimestamp()
- Verificar versión de Python del proyecto
- Consultar "What's New" docs de la versión actual

EJEMPLO DE FALLA:
# Deprecated en Python 3.12+
from datetime import datetime
created_at = datetime.utcnow()  # No timezone-aware
# Correcto
from datetime import datetime, timezone
created_at = datetime.now(timezone.utc)  # Timezone-aware
SEVERIDAD SI FALLA: Low-Medium (funciona pero genera deprecation warnings,
                     y puede causar bugs de timezone)
TIEMPO ESTIMADO: 1-2 minutos

Nota: Este es un ejemplo perfecto de por qué el checklist es una herramienta viva. Cada review donde algo se te escapa es una oportunidad de agregar un item.

Ejercicio 4: Checklist para tu dominio (Difícil)

Agrega 3 items al checklist que sean específicos para tu dominio de trabajo. Si trabajas en fintech, healthtech, e-commerce, o cualquier otro dominio, hay checks específicos que el checklist genérico no cubre.

Ver ejemplos por dominio

Fintech:

  • Item F1: ¿Los montos usan Decimal con precisión definida, nunca float?
  • Item F2: ¿Hay audit trail para cada transacción? (quién, cuándo, qué)
  • Item F3: ¿Las operaciones financieras tienen idempotency keys?

Healthtech:

  • Item H1: ¿Los datos de salud están cifrados en reposo (HIPAA)?
  • Item H2: ¿Hay logs de acceso a records de pacientes?
  • Item H3: ¿Los endpoints de datos médicos requieren autenticación multi-factor?

E-commerce:

  • Item E1: ¿Los precios se calculan server-side, nunca desde el cliente?
  • Item E2: ¿El inventario se verifica al momento del pago, no solo al agregar al carrito?
  • Item E3: ¿Los cupones tienen validación de uso único/expiración?

Tu turno: Define 3 items para TU dominio con el formato completo (qué verificar, cómo verificar, ejemplo de falla).


Resumen

En esta cápsula construiste:

  • Un checklist profesional de 20 items organizado en 5 categorías alineadas con la pirámide
  • Categoría 1 (Seguridad): 5 items que se revisan siempre — secrets, SQL, auth, validación, error messages
  • Categoría 2 (Lógica): 4 items para reglas de negocio — problema correcto, cálculos, estados, atomicidad
  • Categoría 3 (Edge Cases): 4 items para inputs inesperados — None, error handling, paginación, concurrencia
  • Categoría 4 (AI-Specific): 4 items que solo aplican a código AI — imports falsos, sobre-ingeniería, APIs incorrectas, mezcla de frameworks
  • Categoría 5 (Calidad): 3 items para mantenibilidad — response models, estructura, tests
  • Un checklist de bolsillo para uso diario
  • El principio de que el checklist es una herramienta viva que mejoras con cada review

Próxima cápsula: Red Flags en Código AI — profundizar en los items 14-17 con ejemplos detallados y patrones específicos.


Recursos Adicionales

  1. Google — Code Review Developer Guide - El estándar de code review que inspira este checklist
  2. OWASP — Web Security Testing Guide - Guía exhaustiva para los items de seguridad
  3. Pydantic — Field Types and Validators - Referencia para validación de inputs (item 4)
  4. FastAPI — Security Best Practices - Autenticación y autorización en FastAPI (item 3)
  5. Python — What's New in 3.12 - Deprecations relevantes para items AI-specific

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