El PRD viene antes: la especificación que impide que el agente escriba código incorrecto
El modelo no comete errores por ignorancia: comete errores porque cerró solo la brecha que dejó el prompt, y la cerró de una manera plausible. ¿Qué cambia en el código cuando el contrato viene antes de la solicitud?
¿Con prisa? Pide el TL;DR a Claude — lee la página y la resume.
es Traducción automática por qwen3:32b, revisada por el autor. Leer el original en portugués
Solicité al agente un endpoint de listado con paginación y filtro. Media frase de prompt. Regresó en menos de un minuto: tipado, validado, con prueba, todo bien indentado.
Pasó en revisión, porque no había nada visiblemente malo. Lo malo eran las decisiones que yo no pedí y no leí:
paginacomenzaba en0, mientras que el resto de la API comienza en1;- el orden era
ORDER BY nombre, sin desempate — con dos proveedores del mismo nombre, la paginación repite un registro y salta otro; totalcontaba toda la tabla, no el resultado del filtro;tamañono tenía techo:?tamaño=100000extrae toda la tabla;- página más allá del final devolvía
404.
Nada de esto es torpeza del modelo. Cada ítem es una laguna que dejé en la solicitud y él cerró solo, con el valor más común que ya había visto. El problema no es que se equivoque — es que se equivoca de forma plausible. Código absurdo lo detecto en diez segundos; código razonable que tomó una decisión opuesta al resto del sistema pasa la revisión y se convierte en un bug tres semanas después.
El cuello de botella cambió de lugar
En 2023 el cuello de botella era el modelo escribiendo código que compilaba. En julio de 2025, con Sonnet 4 y Opus 4 en Claude Code, con Cursor, con Aider, escribir el código de una tarea bien delimitada dejó de ser la parte difícil. El cuello de botella se volvió la precisión de la especificación que recibe el agente.
Reescribir el prompt hasta que acierte es una lotería: en cada iteración descubro otra cosa que debería haber dicho. La prosa tiene un defecto estructural — no tiene lugar para lo que no se me ocurrió. Una spec sí lo tiene, y una sección vacía incomoda.
Spec-driven no es PRD-driven
Los dos documentos describen la misma entrega, y es tentador concluir que uno sustituye al otro. No lo sustituye — responden preguntas diferentes, para lectores diferentes.
El PRD responde por qué hacerlo y para quién. El lector es gente: quien prioriza, quien vende, quien heredará esto dentro de un año. Existe para alinear decisiones de producto, y por eso lleva contexto de negocio, métrica de éxito y alternativa descartada.
La spec responde exactamente qué debe hacer el código. El lector es el agente. Existe para eliminar ambigüedades en la implementación, y dentro de ella todo lo que no cambia el código es ruido.
| PRD | spec de tarea | |
|---|---|---|
| lector | personas que deciden | el agente que implementa |
| responde | por qué, para quién, cuánto vale | qué entra, qué sale, qué no hacer |
| granularidad | una feature | una tarea, un diff |
| vida útil | mientras exista la feature | actualizada o eliminada junto con el commit |
| error típico | demasiado vago para priorizar | demasiado vago para implementar |
Unirlos en un solo archivo parece ahorrar y termina costando. El agente no separa el contexto motivacional de los requisitos: pondera todo en el prompt con el mismo peso. “Los proveedores son centrales para el flujo de compras” no es inofensivo allí dentro — es un token de entrada compitiendo con la línea que dice que total cuenta el resultado del filtro.
El PRD sigue siendo útil. Solo que no es lo que entrego al agente. Cuando existe, la spec nace de él — no como resumen, sino como lo que queda después de quitar todo lo que no se convierte en código.
La misma solicitud, escrita de ambas formas
La tarea es pequeña y real: listar proveedores, con paginación y búsqueda. Primero, como la escribía antes.
Cria um endpoint GET de listagem de fornecedores com paginação
e um filtro de busca por nome. Segue o padrão dos outros endpoints.
Cada término aquí delega una decisión en silencio. “Paginación” es offset o cursor? “Búsqueda” es prefijo, LIKE o igualdad? “El patrón de los otros endpoints” es de qué archivo, si hay tres?
Ahora la misma tarea como archivo versionado, en docs/specs/proveedores-listado.md.
# especificación: GET /api/fornecedores
# # contrato
GET /api/fornecedores?pagina=1&tamanho=20&busca=&status=ativo&ordem=nome
| parâmetro | tipo | padrão | validação |
|-----------|---------|--------|----------------------------------------|
| pagina | inteiro | 1 | mínimo 1; 0 ou negativo → 400 |
| tamanho | inteiro | 20 | 1 a 100; acima de 100 → 400 |
| busca | string | — | 2 a 80 chars; nome E cnpj; sem acento |
| status | enum | todos | ativo, inativo, todos; fora → 400 |
| ordem | enum | nome | nome, criadoEm, -criadoEm |
El cuerpo de la respuesta entra como ejemplo literal, no como descripción:
{
"itens": [
{ "id": "b7c1…", "nome": "Metalúrgica Andrade", "cnpj": "12345678000190",
"status": "ativo", "criadoEm": "2025-07-14T12:00:00Z" }
],
"pagina": 1, "tamanho": 20, "total": 137
}
El cnpj sin máscara está en el ejemplo — vale más que el párrafo explicando que va sin máscara.
Luego viene la parte que decide el resultado.
# # casos de borde
- lista vazia → 200 com `itens: []` e `total: 0`. Nunca 404.
- página além do fim → 200 com `itens: []`. Nunca 404.
- busca com 1 caractere → 400. Não é busca, é varredura de tabela.
- CNPJ mascarado (12.345.678/0001-90) → normaliza para dígitos e compara.
- empate: `ORDER BY nome ASC, id ASC`, sempre. Sem o desempate por id
a paginação repete registro entre páginas.
- `total` conta o resultado do filtro, não a tabela.
- registro com `deletadoEm` preenchido nunca aparece, em nenhum status.
# # fuera de alcance
- não criar índice nem migration; é outra tarefa
- não tocar em GET /api/fornecedores/:id
- não adicionar cache, Redis ou repositório novo — usar o `db`
que já existe em src/infra/db.ts
- não trocar o validador; o projeto usa Zod, mantenha
- sem paginação por cursor nesta entrega
Lo que cambia en el código que regresa es verificable línea a línea. El caso del desempate, por ejemplo:
// sin la especificación — paginación inestable cuando hay nombres repetidos
db('fornecedores').orderBy('nome', 'asc')
.limit(tamanho).offset((pagina - 1) * tamanho);
// con la especificación — el desempate se escribió como caso de borde
db('fornecedores').orderBy('nome', 'asc').orderBy('id', 'asc')
.limit(tamanho).offset((pagina - 1) * tamanho);
Este es el tipo de bug que la revisión no atrapa. No se rompe con diez líneas en la tabla y desaparece cuando intentas reproducirlo. Aparece en la quinta página, en producción.
Lo que no entra en la spec
Fuera quedan justificaciones de negocio, perfiles de usuario, métricas de adopción y párrafos motivacionales. No cambian una línea del código generado y ocupan contexto que el agente usaría mejor leyendo el repositorio.
La regla: si borrar la frase no cambia el código que regresa, se va. “Los proveedores son centrales para el flujo de compras” se va. “total cuenta el resultado del filtro” se queda.
La spec de arriba cabe en sesenta líneas. Cuando escribía una página y media, el agente obedecía a la mitad — y no sabía cuál mitad.
Fuera de alcance es la sección que más paga
Si pudiera mantener solo una sección, sería esta. Es donde el agente más inventa, y lo hace para arriba: crea una capa de repositorio que no existía, añade caché, cambia el validador, refactorea el vecino “de paso”, escribe una migración con índice nuevo.
No es sabotaje: en el material en que lo entrenaron, “código bueno de listado” viene con esas cosas, y sin frontera escrita completa el patrón. Apuntando al archivo que ya existe, usa lo que está allí — y el diff queda pequeño bastante para que yo revise de verdad.
El criterio de aceptación debe ser ejecutable
“Debe funcionar correctamente” no es un criterio, es un deseo. El criterio es el nombre de la prueba y el comando que la ejecuta.
# # criterio de aceptación
testes em testes/fornecedores.listagem.spec.ts, todos verdes:
1. deve_retornar_200_e_lista_vazia_quando_nao_ha_resultado
2. deve_retornar_400_quando_tamanho_acima_de_100
3. deve_paginar_sem_repetir_registro_quando_ha_nomes_iguais
(3 fornecedores de mesmo nome; páginas 1 e 2 com tamanho 2;
os conjuntos de id não podem se intersectar)
4. deve_ignorar_mascara_de_cnpj_na_busca
5. deve_excluir_registro_deletado_logicamente
comando: pnpm vitest run testes/fornecedores.listagem.spec.ts
Cada nombre de prueba salió de un caso de borde de la sección anterior — la traducción es mecánica, y hoy escribo los casos de borde ya pensando en esto.
pnpm vitest run testes/proveedores.listagem.spec.ts Test Files 1 passed (1) Tests 5 passed (5)
Con el criterio así, el agente cierra el ciclo solo: ejecuta, lee el fallo, corrige, ejecuta de nuevo. Sin esto, quien cierra el ciclo soy yo, una iteración por vez, en el chat.
Del endpoint al sistema completo
Un endpoint no prueba ningún método. La prueba real fue generar un CRUD de agenda médica así — «MEDIR» entidades, «MEDIR» endpoints —, con una spec por tarea, nunca una spec del sistema. Un documento único de treinta páginas devuelve un agente que hace todo a medias y nada hasta el final.
La agenda es un dominio cruel para un prompt en prosa, porque casi toda regla es una decisión que nadie enuncia:
- dos pedidos para el mismo horario: el segundo es error, cola o ajuste?
- cancelar borra el registro o marca
canceladoEn? (el historial clínico no se borra — pero el agente no lo sabe) - el horario cancelado vuelve a la agenda de inmediato, o solo después de confirmación?
- la duración viene del procedimiento o de la agenda del profesional?
- la hora llega en qué huso, y la base guarda qué?
- feriado y bloqueo de vacaciones entran en el cálculo de disponibilidad?
Ninguna de estas aparece en un pedido de “haz el CRUD de agenda”. Todas aparecen en producción. Y cada una que el agente decide solo la decide bien — con la respuesta más común del entrenamiento, que en clínica suele ser la equivocada: DELETE real en vez de cancelación lógica.
La regla que quedó: una tarea que quepa en un diff revisable, una spec, un archivo de prueba. Cuando la spec pasa de sesenta líneas, no es una spec grande — son dos tareas.
Dónde vive la spec
Spec pegada al prompt muere con la sesión. El archivo queda en el repositorio, versionado con el código, y se cita por ruta:
# todo el pedido, después de que la especificación existe
claude "implemente docs/specs/fornecedores-listagem.md"
# en Aider es la misma idea: la especificación entra como lectura, no como edición
aider --read docs/specs/fornecedores-listagem.md src/rotas/fornecedores.ts
El --read de Aider importa más de lo que parece: carga la spec en el contexto sin incluirla en el conjunto de archivos editables. Ya vi a un agente “resolver” una divergencia entre spec y código reescribiendo la spec — lo peor posible, porque borra la única cosa que servía de referencia.
El CLAUDE.md del proyecto — .cursorrules, en Cursor — declara la convención una vez:
Specs de tarefa ficam em docs/specs/. Antes de implementar, leia a spec
citada. Se algo do pedido não estiver na spec, pergunte em vez de decidir.
La última frase es la que más cambia el comportamiento: “pregunta en vez de decidir” cambia la decisión silenciosa, que es cara, por una pregunta, que es barata.
Fuera del agente la spec también paga: se convierte en descripción del PR casi sin edición, y la revisión deja de ser “esto parece correcto?” para ser “esto coincide con el archivo al lado?”. AWS lanzó esta semana el Kiro, un IDE que hace obligatorio el archivo de requisitos antes de generar código — no soy el único que va por ahí.
Dónde no compensa
El costo fijo de escribir la spec no desaparece cuando la tarea es pequeña. No escribo spec para:
- tarea de cinco minutos: renombrar variable, ajustar log, subir versión de dependencia. La spec cuesta más que la tarea.
- exploración: cuando no sé qué quiero, la spec es lo que intento descubrir. Allí el prompt vago es la herramienta correcta — pido tres enfoques y elijo uno. La spec viene después, a partir de lo que elegí.
- prototipo descartable: si el código muere el viernes, la decisión silenciosa del agente no cuesta nada.
El corte: escribo spec cuando el código será mantenido por otra persona — incluido yo en tres meses — o cuando el error es silencioso. Fuera de eso, prompt y revisión bastan.
El límite honesto
La spec no hace al agente más inteligente. Si especifico mal, ejecuta el error con precisión — deja de ser el autor del bug y se convierte en el ejecutor mío. Es mejor, porque el bug queda en un archivo revisable, pero no es magia.
La spec no sustituye la revisión. Cambia lo que reviso: en vez de leer el código buscando lo que está mal, leo el diff contra el archivo. Es una pregunta más fácil de responder.
Y el costo es cierto mientras el beneficio sigue siendo mi hipótesis. La spec entra en cada llamada como token de entrada, y eso se cobra — con modelo online, el contexto se convierte en factura al final del mes. Cuando el modelo corre en tu propia placa la cuenta es otra, y está en el post sobre VRAM.
| Mismo pedido | Rodadas hasta que entre el diff | Correcciones después de la revisión | Tokens de entrada |
|---|---|---|---|
| Prompt en prosa, 2 líneas | «MEDIR» | «MEDIR» | «MEDIR» |
| Spec versionada, 60 líneas | «MEDIR» | «MEDIR» | «MEDIR» |
Escribir la spec de arriba me cuesta «MEDIR» minutos — el número que decide si el método vale, y el primero que voy a cronometrar.
Si recomezara, cambiaría el orden: escribiría los cinco nombres de prueba primero y dejaría el resto salir de ellos. De arriba hacia abajo, gasto tiempo en la parte descriptiva — que el agente inferiría solo — y llego apurado a los casos de borde, la única sección que no puede adivinar.
La spec no hace al modelo más capaz. Solo le quita la oportunidad de adivinar.