Saltar al contenido
Fantástico Mundo de Jon
RSS

El PRD va primero: la especificación que impide al agente escribir el código equivocado

El modelo no se equivoca por estupidez: se equivoca porque cerró solo la laguna que dejó el prompt, y lo hizo de una manera plausible. Lo que cambia en el código cuando el contrato va 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 mistral-small3.2:24b, revisada por el autor. Leer el original en portugués

Pedí al agente un endpoint de listado con paginación y filtro. Frase y media de prompt. Volvió en menos de un minuto: tipado, validado, con prueba, todo indentado correctamente.

Pasó la revisión, porque no había nada visiblemente equivocado. Lo equivocado eran las decisiones que yo no pedí y no leí:

  • pagina comenzaba en 0, mientras que el resto de la API comienza en 1;
  • el ordenamiento era ORDER BY nombre, sin desempate — con dos proveedores del mismo nombre, la paginación repite un registro y salta otro;
  • total contaba toda la tabla, no el resultado del filtro;
  • tamanho no tenía techo: ?tamanho=100000 trae toda la tabla;
  • página más allá del final devolvía 404.

Nada de esto es tontería del modelo. Cada ítem es una laguna que yo dejé en el pedido y él cerró solo, con el valor más común de lo que ya vio. El problema no es que él se equivoque — es que se equivoca plausible. Código absurdo lo capto en diez segundos; código razonable que decidió lo opuesto al resto del sistema pasa la revisión y se convierte en error tres semanas después.

El cuello de botella cambió de lugar

En 2023 el cuello de botella era el modelo escribir código que compila. 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 convirtió en la precisión de la especificación que recibe el agente.

Reescribir el prompt hasta que él acierte es lotería: cada ronda descubro más cosas que debería haber dicho. La prosa tiene un defecto estructural — no hay lugar para lo que yo no pensé. Una spec sí tiene, y una sección vacía molesta.

Spec-driven no es PRD-driven

Los dos documentos describen la misma entrega, y es tentador concluir que uno sustituye al otro. No sustituye — ellos responden preguntas diferentes, para lectores diferentes.

El PRD responde por qué hacer y para quién. El lector es gente: quien prioriza, quien vende, quien va a heredar esto dentro de un año. Existe para alinear la decisión del producto, y por eso lleva contexto de negocio, métrica de éxito y alternativa descartada.

La spec responde qué exactamente debe hacer el código. El lector es el agente. Existe para eliminar ambigüedad de 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 característica una tarea, un diff
vida útil mientras la característica exista actualizada o borrada junto con el commit
error típico demasiado vago para priorizar demasiado vago para implementar

Juntar los dos en un solo archivo parece economía y sale caro. El agente no separa contexto motivacional de requisito: él pondera todo lo que está en el prompt con el mismo peso. “Proveedores son centrales en el flujo de compras” no es inofensivo allí dentro — es token de entrada compitiendo con la línea que dice que total cuenta el resultado del filtro.

El PRD sigue sirviendo. Solo no es lo que yo 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.

El mismo pedido, escrito de las dos formas

La tarea es pequeña y real: listar proveedores, con paginación y búsqueda. Primero, cómo yo 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 el de qué archivo, si existen tres?

Ahora la misma tarea como archivo versionado, en docs/specs/fornecedores-listagem.md.

# spec: 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 él va sin máscara.

Ahí 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 del 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 vuelve es verificable línea por 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 estaba escrito como caso de borde
db('fornecedores').orderBy('nome', 'asc').orderBy('id', 'asc')
  .limit(tamanho).offset((pagina - 1) * tamanho);

Este es el tipo de error que la revisión no capta. 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 justificación de negocio, persona, métrica de adopción y párrafo motivacional. 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 vuelve, ella sale. “Proveedores son centrales en el flujo de compras” sale. “total cuenta el resultado del filtro” se queda.

La spec anterior cabe en sesenta líneas. Cuando yo escribía una página y media, el agente obedecía a la mitad — y yo no sabía qué mitad.

Fuera de alcance es la sección que más paga

Si pudiera mantener una sola sección, sería esta. Es donde el agente más inventa, y inventa para arriba: crea capa de repositorio que no existía, añade caché, cambia el validador, refactoriza al vecino “de paso”, escribe migración con índice nuevo.

No es sabotaje: en el material en que él fue entrenado, “código bueno de listado” viene con esas cosas, y sin frontera escrita él completa el patrón. Apuntando al archivo que ya existe, él usa lo que está allí — y el diff queda pequeño suficiente para que yo revise de verdad.

Criterio de aceptación necesita ser ejecutable

“Debe funcionar correctamente” no es criterio, es deseo. 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 eso.

pnpm vitest run testes/fornecedores.listagem.spec.ts

Test Files 1 passed (1) Tests 5 passed (5)

el criterio de aceptación es esto quedar verde

Con el criterio así, el agente cierra el ciclo solo: ejecuta, lee el fallo, corrige, ejecuta de nuevo. Sin eso, quien cierra el ciclo soy yo, una ronda por vez, en el chat.

Del endpoint al sistema entero

Un endpoint no prueba ningún método. La verdadera prueba fue generar un CRUD de agenda médica así, con una spec por tarea, nunca una spec del sistema. Documento único de treinta páginas devuelve un agente que hace todo a medias y nada hasta el final.

Agenda es un dominio cruel para prompt en prosa, porque casi toda regla es una decisión que nadie enuncia:

  • dos pedidos para la misma hora: ¿el segundo es error, cola o ajuste?
  • cancelar borra el registro o marca canceladoEm? (historial clínico no se borra — pero el agente no sabe eso)
  • hora cancelada vuelve a la agenda en el momento, 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 horario, y la base de datos 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 él decide bien — con la respuesta más común del entrenamiento, que en clínica suele ser la equivocada: DELETE real en lugar de cancelación lógica.

La regla que quedó: una tarea que cabe en un diff revisable, una spec, un archivo de prueba. Cuando la spec pasa de sesenta líneas, no es spec grande — son dos tareas.

Dónde vive la spec

Spec pegado al prompt muere con la sesión. El archivo queda en el repositorio, versionado con el código, y es citado por camino:

# la solicitud completa, después de que existe la especificación
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 del Aider importa más de lo que parece: él carga la spec en el contexto sin ponerla en el conjunto de archivos editables. Ya vi agente “resolver” una divergencia entre spec y código reescribiendo la spec — lo cual es rigurosamente el peor desenlace 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 lugar de decidir” cambia decisión silenciosa, que es cara, por 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 Kiro, un IDE que hace obligatorio el archivo de requisito antes de generar código — no soy el único yendo por ahí.

Dónde esto 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é lo que quiero, la spec es lo que estoy tratando de descubrir. Ahí 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, decisión silenciosa del agente no cuesta nada.

El corte: escribo spec cuando el código va a ser mantenido por otra persona — incluyendo yo dentro de 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 yo especifico mal, él ejecuta el error con precisión — deja de ser el autor del bug y se convierte en el ejecutor del 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 yo reviso: en lugar de leer el código buscando lo que está equivocado, 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 hipótesis mía. La spec entra en cada llamada como token de entrada, y eso se cobra — con modelo en línea, contexto se convierte en cuenta al final del mes. Cuando el modelo funciona en su propia placa la cuenta es otra, y está en el post sobre VRAM.

No tengo ese número, y prefiero decir eso a llenar una tabla. Lo que falta comparar, entre el prompt en prosa de dos líneas y la spec versionada de sesenta, son tres cosas: cuántas rondas hasta que el diff entra sin retoque, cuántas correcciones aparecen después de la revisión, y cuántos tokens de entrada cada uno consume.

Cuánto tiempo cuesta escribir la spec anterior es el número que decide si el método vale, y es el primero que voy a cronometrar. Todavía no lo he cronometrado.

Si yo empezara de nuevo, cambiaría el orden: escribiría los cinco nombres de prueba primero y dejaría que el resto saliera de ellos. De arriba abajo, gasto tiempo en la parte descriptiva — que el agente inferiría solo — y llego con prisa a los casos de borde, la única sección que él no puede adivinar.

La spec no hace al modelo más capaz. Solo le quita la oportunidad de adivinar.