Módulo 2: Code Review Automático en PRs

Manejar PRs Grandes con Chunking

Manejar PRs Grandes con Chunking

Descripción

Tu bot funciona perfecto en PRs típicos (5-20 archivos). Pero tarde o temprano llega el PR grande: 50, 80, 200 archivos. ¿Qué hace el bot ahora?

Tres opciones malas:

  1. Pasar todo al modelo → context window saturado, calidad degrada, costos explotan
  2. Truncar arbitrariamente → review parcial sin transparencia, los issues importantes quedan afuera
  3. Skipear silenciosamente → el equipo no recibe ningún feedback, justo cuando más lo necesita

Esta cápsula cubre el camino correcto: chunking inteligente. Vas a aprender a partir el PR en chunks que sí caben, priorizar qué chunks revisar, agregar resultados de múltiples chunks en un único review coherente, y comunicar transparentemente cuando un PR excede los límites.

Al terminar, tu bot va a manejar PRs de cualquier tamaño con calidad consistente — o decir explícitamente "este PR es demasiado grande para review automático" cuando corresponda.


Cuándo Chunkear

Definir umbrales claros antes de implementar:

PR CHICO (default review en 1 pasada):
- ≤20 archivos relevantes
- ≤2,000 líneas de diff
- Costo estimado: $0.01-0.03

PR MEDIANO (review chunkeado):
- 21-50 archivos relevantes
- 2,001-8,000 líneas de diff
- Costo estimado: $0.05-0.15
- Estrategia: chunking por archivo o por feature

PR GRANDE (review parcial + warning):
- 51-100 archivos relevantes
- 8,001-20,000 líneas de diff
- Costo estimado: $0.20-0.50
- Estrategia: chunking + priorización agresiva, marcar como
  "review parcial" en el summary

PR DEMASIADO GRANDE (skipear con mensaje):
- >100 archivos relevantes
- >20,000 líneas de diff
- Estrategia: postear comment explicando, sugerir dividir el PR

Los umbrales son ajustables según el modelo y presupuesto. Con Haiku, puedes ser más permisivo; con Sonnet/Opus, más conservador.


Estrategia 1: Chunking por Archivo

La más simple: revisar un archivo por llamada al modelo.

"""Review por archivo."""
import json
from anthropic import Anthropic

client = Anthropic()

def review_single_file(file_data: dict, conventions: str) -> dict:
    """Review de un solo archivo. Retorna {summary, comments}."""
    patch = file_data.get("patch", "")
    if not patch:
        return {"summary": "", "comments": []}
    
    prompt = f"""Review este archivo del PR.

CONVENCIONES:
{conventions}

ARCHIVO: {file_data['filename']}
DIFF:

{patch}


Devuelve JSON con:
{{
  "summary": "1 oración con verdict del archivo",
  "comments": [
    {{"line": N, "severity": "critical|warning|suggestion", "body": "..."}}
  ]
}}
"""
    response = client.messages.create(
        model="claude-haiku-4-5",
        max_tokens=2000,
        messages=[{"role": "user", "content": prompt}],
    )
    
    text = response.content[0].text.strip()
    # Limpiar code fences
    if text.startswith("```"):
        text = "\n".join(text.split("\n")[1:-1])
    
    try:
        return json.loads(text)
    except json.JSONDecodeError:
        return {"summary": "Error parsing review", "comments": []}

# Review todos los archivos relevantes
all_results = []
for f in relevant_files:
    result = review_single_file(f, conventions)
    # Agregar path a cada comment
    for c in result["comments"]:
        c["path"] = f["filename"]
    all_results.append({
        "filename": f["filename"],
        "summary": result["summary"],
        "comments": result["comments"],
    })

Ventajas:

  • Cada llamada es chica → context window holgado, calidad alta
  • Paralelizable (con cuidado de rate limits)
  • Costos predecibles (linear con archivos)

Desventajas:

  • El modelo no ve cómo los archivos se relacionan entre sí
  • Issues cross-file (ej. cambio en un archivo rompe asunción en otro) se pierden
  • Más llamadas API = más latencia (a menos que paralelices)

Estrategia 2: Chunking por Feature

Para PRs medianos donde los archivos están relacionados, agrupar por feature:

def group_by_feature(files: list) -> dict:
    """Agrupar archivos por feature inferido del path."""
    groups = {}
    for f in files:
        # Heurística: primer dir significativo
        parts = f["filename"].split("/")
        if len(parts) > 2:
            feature = f"{parts[0]}/{parts[1]}"  # ej. "src/payments"
        else:
            feature = parts[0]
        groups.setdefault(feature, []).append(f)
    return groups

groups = group_by_feature(relevant_files)

for feature, files_in_feature in groups.items():
    print(f"Reviewing feature: {feature} ({len(files_in_feature)} archivos)")
    review = review_feature(files_in_feature, conventions, feature)
    all_results.append(review)

Función review_feature: combina los patches de los archivos del grupo y los pasa juntos. El modelo ve el contexto completo del feature.

Cuándo usar: PRs que tocan varias áreas distintas. Cada área se revisa por separado, cada una con sus archivos relacionados juntos.


Estrategia 3: Priorización + Top N

Para PRs grandes, no puedes revisar todos los archivos. Prioriza los más importantes:

def prioritize_files(files: list, max_files: int = 30) -> list:
    """Devolver los top N archivos más relevantes."""
    
    def priority_score(f):
        score = 0
        
        # Más cambios = más relevante
        score += f["changes"] * 1
        
        # Archivos críticos pesan más
        critical_paths = ["payments", "auth", "security", "database"]
        if any(p in f["filename"] for p in critical_paths):
            score += 100
        
        # Archivos nuevos pesan más (más superficie de bugs nuevos)
        if f["status"] == "added":
            score += 50
        
        # Tests pesan menos (es una decisión opinable)
        if "test" in f["filename"]:
            score *= 0.5
        
        return score
    
    sorted_files = sorted(files, key=priority_score, reverse=True)
    return sorted_files[:max_files]

prioritized = prioritize_files(relevant_files, max_files=30)

Heurísticas comunes para priority_score:

  • Cantidad de cambios (líneas modificadas)
  • Path crítico (auth, payments, security, etc.)
  • Status (added > modified > renamed)
  • Tipo de archivo (código fuente > tests > config)
  • Tamaño del archivo (cambios chicos en archivos grandes son riesgosos)

Agregando Resultados Multi-Chunk

Después de revisar todos los chunks, necesitas un único review publicado en el PR (no varios).

def aggregate_reviews(per_chunk_results: list) -> dict:
    """Combinar reviews de chunks en uno solo."""
    
    # Aplanar todos los comments
    all_comments = []
    for r in per_chunk_results:
        all_comments.extend(r["comments"])
    
    # Conteo por severidad
    by_severity = {"critical": 0, "warning": 0, "suggestion": 0}
    for c in all_comments:
        by_severity[c.get("severity", "suggestion")] += 1
    
    # Summary agregado: combinar summaries por feature
    feature_summaries = "\n".join(
        f"- **{r['filename']}**: {r['summary']}"
        for r in per_chunk_results
        if r.get("summary")
    )
    
    overall = f"""# 🤖 Code Review (Claude Code)

Este PR fue revisado en **{len(per_chunk_results)} chunks** debido al tamaño.

## Resumen por área

{feature_summaries}

## Severidad total

- 🚨 Critical: {by_severity['critical']}
- ⚠️ Warning: {by_severity['warning']}
- 💡 Suggestion: {by_severity['suggestion']}

Detalles inline en cada archivo.
"""
    
    return {
        "summary": overall,
        "comments": all_comments,
    }

aggregated = aggregate_reviews(all_results)

Ahora aggregated tiene la estructura del review API y se puede publicar como un único review.


Caso Extremo: PR Demasiado Grande

Cuando el PR excede los umbrales (>100 archivos, >20K líneas), revisarlo automáticamente no aporta valor proporcional al costo. Mejor: comunicar transparentemente.

def post_too_large_message(pr_number, repo, token, file_count, line_count):
    """Postear comment al PR explicando por qué no se revisa."""
    body = f"""# 🤖 Code Review

Este PR tiene **{file_count} archivos** y **{line_count} líneas** modificadas — excede los límites para review automático.

## Sugerencias

1. **Considera dividir el PR** en cambios más chicos. PRs grandes son difíciles de revisar (humano y automatizado).
2. Si el PR es necesariamente grande (ej. migración masiva), pide review manual a un humano del equipo.
3. Si quieres review parcial automatizado de archivos críticos, agrega la label `review-critical-only`.

## Para activar review parcial

Agrega label `review-critical-only` y los archivos en paths críticos (auth, payments, etc.) se revisarán automáticamente.
"""
    
    url = f"https://api.github.com/repos/{repo}/issues/{pr_number}/comments"
    headers = {"Authorization": f"Bearer {token}", "Accept": "application/vnd.github+json"}
    requests.post(url, headers=headers, json={"body": body})

Resultado: transparencia. El equipo sabe por qué no recibió review automático y tiene opciones (dividir el PR, review manual, label para review parcial).


Implementación Integrada

Combinando todo en el script principal:

"""Script principal: review con chunking inteligente."""
import os
import requests

# 1. Obtener archivos del PR
files = get_pr_files()  # función de cápsula 02
relevant_files = filter_relevant(files)

file_count = len(relevant_files)
total_changes = sum(f["changes"] for f in relevant_files)

# 2. Decidir estrategia según tamaño
if file_count == 0:
    print("No hay archivos relevantes.")
    sys.exit(0)

elif file_count <= 20 and total_changes <= 2000:
    # PR chico: review en 1 pasada
    print(f"PR chico ({file_count} archivos). Review único.")
    review = review_combined(relevant_files, conventions)

elif file_count <= 50 and total_changes <= 8000:
    # PR mediano: chunking por feature
    print(f"PR mediano ({file_count} archivos). Chunking por feature.")
    groups = group_by_feature(relevant_files)
    per_chunk_results = [review_feature(files_g, conventions, name) 
                         for name, files_g in groups.items()]
    review = aggregate_reviews(per_chunk_results)

elif file_count <= 100 and total_changes <= 20000:
    # PR grande: priorización + top 30
    print(f"PR grande ({file_count} archivos). Priorización + top 30.")
    prioritized = prioritize_files(relevant_files, max_files=30)
    per_chunk_results = [review_single_file(f, conventions) for f in prioritized]
    review = aggregate_reviews(per_chunk_results)
    
    # Marcar como review parcial
    review["summary"] = "**⚠️ Review parcial** — solo top 30 archivos.\n\n" + review["summary"]

else:
    # PR demasiado grande
    print(f"PR demasiado grande ({file_count} archivos, {total_changes} líneas).")
    post_too_large_message(pr_number, repo, github_token, file_count, total_changes)
    sys.exit(0)

# 3. Validar y publicar el review
valid_comments = validate_all(review["comments"], files)
publish_review(pr_number, repo, github_token, commit_sha, review["summary"], valid_comments)

Resultado: un script que escala gracefully de PRs chicos a gigantes, con feedback apropiado en cada caso.


Optimizaciones Adicionales

Paralelización con cuidado

Si revisas archivos en paralelo, respetar rate limits de Anthropic:

import concurrent.futures
from anthropic import Anthropic

client = Anthropic()

def review_file_safe(file_data, conventions):
    """Wrapper con retry."""
    for attempt in range(3):
        try:
            return review_single_file(file_data, conventions)
        except APIError as e:
            if e.status_code == 429:  # rate limit
                time.sleep(2 ** attempt)  # exponential backoff
            else:
                raise
    return {"summary": "Failed after retries", "comments": []}

# Paralelizar con cuidado: max 5 concurrent
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
    results = list(executor.map(
        lambda f: review_file_safe(f, conventions),
        prioritized,
    ))

Beneficio: review de 30 archivos pasa de ~5 minutos a ~1 minuto. Riesgo: si te pasas del rate limit, todo falla.

Cache de resultados

Si el PR no cambió en algunas líneas pero hubo push, puedes cachear reviews previos por commit SHA + archivo. No es trivial pero ahorra llamadas en re-ejecuciones.


Trampas Comunes

Error 1: Pasar el PR completo siempre

Síntoma: En PRs grandes, calidad de review degrada visiblemente. El bot da comments genéricos o se contradice.

Por qué pasa: Context window saturado.

Cómo corregir: Implementar umbrales y chunking. La regla del 70% del context window aplica acá.

Error 2: Chunking sin agregación

Síntoma: El bot postea N reviews separados (uno por chunk) en el mismo PR.

Por qué pasa: El script publica cada chunk inmediatamente en lugar de acumular y agregar.

Cómo corregir: Acumular resultados y publicar un único review al final con aggregate_reviews.

Error 3: Priorización opaca

Síntoma: El bot revisó 30 archivos. ¿Cuáles 30 de los 80? El equipo no sabe.

Por qué pasa: El summary no comunica qué se revisó vs qué no.

Cómo corregir: En el summary del review parcial, listar explícitamente: "Se revisaron 30 archivos prioritarios. No se revisó: [lista de los 50 restantes con razón breve]".

Error 4: No respetar rate limits al paralelizar

Síntoma: Reviews fallan con 429 cuando el PR es grande.

Por qué pasa: ThreadPoolExecutor con max_workers=20 lanza 20 llamadas simultáneas. Anthropic rate-limits.

Cómo corregir: max_workers=5 típicamente seguro. Con backoff exponencial en caso de 429.

Error 5: Skipear silenciosamente

Síntoma: El bot no comenta en PRs grandes. El equipo no sabe si es bug o intencional.

Por qué pasa: El script hace sys.exit(0) sin comentar.

Cómo corregir: Siempre postear un comment explicando por qué no se revisó. Transparencia sobre silencio.


Diagnóstico

Pregunta 1: ¿Tienes umbrales claros para "PR chico/mediano/grande/demasiado grande"?

Si no, vas a tomar decisiones inconsistentes. Definir umbrales basados en tu modelo y presupuesto.

Pregunta 2: ¿Tu chunking publica un review único o varios separados?

Varios separados es ruido. Aggregar y publicar uno solo es la práctica.

Pregunta 3: Cuando haces priorización, ¿comunicas qué archivos se omitieron?

Sin comunicarlo, el equipo no sabe qué tan completa es la review.

Pregunta 4: ¿Paralelizas reviews respetando rate limits?

Si paralelizas sin límite, vas a chocarte con 429s. max_workers=5 + backoff es razonable.

Pregunta 5: Cuando un PR es demasiado grande, ¿posteas un mensaje explicativo o skipeas silenciosamente?

Silencio = mala UX. Mensaje explicativo + sugerencias = profesional.


Ejercicios

Ejercicio 1: Implementar umbrales (Fácil)

Define los 4 umbrales (chico, mediano, grande, demasiado grande) en tu script con valores ajustados a tu uso. Documéntalos en CLAUDE.md o el README del workflow.

Ejercicio 2: Chunking por archivo con agregación (Medio)

Implementa:

  1. Función review_single_file que revisa un archivo
  2. Loop sobre archivos prioritizados
  3. Función aggregate_reviews que combina resultados
  4. Publicación de un único review al final
Ver checklist
  • Cada archivo se revisa en una llamada API separada
  • Cada comment tiene path correcto
  • El summary agregado lista resultados por archivo/feature
  • Solo se publica un review (no N)

Ejercicio 3: Priorización con score function (Difícil)

Implementa priority_score que considera:

  1. Cantidad de cambios
  2. Path crítico (basado en lista configurable)
  3. Status del archivo (added > modified)
  4. Tamaño absoluto del archivo

Verifica con un PR de prueba que los archivos críticos quedan en el top.


Resumen

  • Umbrales claros evitan decisiones ad-hoc en PRs de tamaño variable
  • 4 estrategias según tamaño: review único, chunking por feature, priorización + top N, mensaje explicativo
  • Aggregar resultados en un único review evita ruido en el PR
  • Priorización transparente comunica qué se revisó y qué se omitió
  • Paralelización con cuidado acelera reviews grandes sin chocar rate limits
  • Mensaje explícito en PRs demasiado grandes mantiene transparencia

Próxima cápsula: Módulo 3 — GitLab CI/CD y SDK Headless. Tu bot funciona en GitHub Actions, maneja PRs chicos y grandes, opera con convenciones del equipo. La siguiente pregunta natural: ¿qué pasa si tu equipo (o tu cliente) usa GitLab? El SDK headless es la respuesta — escribes la lógica una vez, corre en cualquier plataforma.


Recursos Adicionales

  1. Anthropic Rate Limits — Límites por tier
  2. Python concurrent.futures — Paralelización segura
  3. GitHub PR Files endpoint — Hasta 3000 archivos por PR
  4. Effective batch processing with LLMs — Anthropic batch API para casos avanzados
  5. Code review at Google: large CL — Cómo Google maneja PRs grandes (referencia conceptual)
  6. The 200-line rule — Argumento sobre tamaño ideal de PRs