Pilar 3: AI Verification Playbook — confía en la velocidad, verifica la corrección

Search for a command to run...

No comments yet. Be the first to comment.
Línea de apertura (opcional, para el feed): En cada sesión nueva, Claude abre el README, escanea el árbol de directorios, busca el pom.xml, encuentra src/main/java... 33 tool calls para orientarse. Co

Línea de apertura (opcional, para el feed): Llevas tres meses usando Claude en tu proyecto Java. Los mismos errores de hace tres meses siguen apareciendo. == para comparar BigDecimal. LocalDate.now()

Línea de apertura (opcional, para el feed): Tu equipo hizo commit directo a main porque "era un cambio pequeño". Dos horas después, toda la rama de producción estaba rota, tres desarrolladores bloquea

Línea de apertura (opcional, para el feed): Llevas seis meses usando Claude para tus decisiones de arquitectura Java. Eres más rápido. Pero ya no sabes si elegirías Spring Security + JWT o Keycloak si

Línea de apertura (opcional, para el feed): Claude generó el controlador REST en 20 segundos. Pasó code review. Llegó a producción. Dos días después, el campo monto devolvía la ciudad del cliente. Aquí está el playbook de verificación sistemática que lo habría atrapado en 30 segundos.
Parte de la serie "Los 6 pilares del desarrollo asistido por IA" — basado en el material de Thor Henning Hetland (Totto) / Ægis. Adaptado a proyectos Java.
El Pilar 1 le da contexto al modelo. El Pilar 2 elige el modelo correcto para cada tarea. El Pilar 3 responde la pregunta que los dos anteriores no pueden responder por sí solos:
¿Y si el código se ve perfecto… pero está mal?
La IA genera rápido. Eso no está en discusión. El problema es que también alucina con confianza: inventa campos, desalinea DTOs, implementa la lógica que "tiene sentido" en lugar de la que dice la especificación. Y lo hace con el mismo estilo limpio y bien formateado que el código correcto.
La verificación sistemática no es desconfianza en la IA. Es el cinturón de seguridad: no lo usas porque desconfíes del conductor, lo usas porque el mundo tiene variables que ningún conductor controla.
Antes de ver las técnicas, la economía del problema:
| Enfoque | Tiempo | Costo | Bugs detectados |
|---|---|---|---|
| Sin verificación | 0 min | $0 | 0 (hasta que explota) |
| Revisión manual de PR | 30 min | ~$50 | ~60% |
| Verificación sistemática | 5 min | ~$0.10 | ~95% |
| War room en producción | 2–8 h | $200–$800+ | 100%… después del daño |
La regla que vale la pena memorizar:
Si verificar tarda más que reimplementar, lo estás haciendo mal.
5 minutos de verificación sistemática evitan 2 a 8 horas de war room. El ROI es de 10× a 50× en tiempo, y mucho más en reputación y confianza del equipo.
No es una herramienta mágica. Son seis técnicas complementarias. Ninguna alucinación sobrevive a una verificación sistemática que combina las correctas para cada contexto.
Configuración: 5 minutos · Costo: cercano a cero
La idea es deceptivamente simple: lo que entra debe salir igual. Si parseas un objeto, lo serializas de vuelta y lo vuelves a parsear, el resultado debe ser idéntico al original. Si no lo es, la IA inventó o perdió algo.
En Java, el caso más común es la serialización JSON con Jackson. Cuando Claude genera un nuevo DTO o modifica uno existente, esta prueba lo atrapa en 30 segundos:
// PedidoRoundTripTest.java
@SpringBootTest
class PedidoRoundTripTest {
@Autowired
private ObjectMapper objectMapper;
@Test
void roundTrip_PedidoDTO_preservaTodosLosCampos() throws Exception {
PedidoDTO original = PedidoDTO.builder()
.id(1L)
.numero("PED-2024-001")
.monto(new BigDecimal("1250.75"))
.estado(EstadoPedido.CONFIRMADO)
.clienteId(42L)
.build();
// Claude generó la serialización: ¿inventó un campo? ¿perdió uno?
String json = objectMapper.writeValueAsString(original);
PedidoDTO restaurado = objectMapper.readValue(json, PedidoDTO.class);
assertEquals(original, restaurado,
"El round-trip JSON debe preservar todos los campos sin excepción");
}
@Test
void roundTrip_PedidoDTO_conCamposOpcionales() throws Exception {
// Probar con nulls — la IA a veces "adivina" valores por defecto
PedidoDTO conNulos = PedidoDTO.builder()
.id(2L)
.numero("PED-2024-002")
.monto(new BigDecimal("0.01"))
.estado(EstadoPedido.PENDIENTE)
.clienteId(null) // campo opcional
.build();
String json = objectMapper.writeValueAsString(conNulos);
PedidoDTO restaurado = objectMapper.readValue(json, PedidoDTO.class);
assertEquals(conNulos, restaurado);
assertNull(restaurado.getClienteId());
}
}
Para operaciones bidireccionales (por ejemplo, exportar e importar datos entre sistemas), el round-trip se extiende a dos niveles:
Lo que detecta en proyectos Java reales:
@JsonProperty con nombre incorrecto (la IA usa el nombre del campo Java, no el del contrato)
Campos añadidos que no están en la especificación OpenAPI
BigDecimal convertido a Double silenciosamente (pérdida de precisión en montos)
LocalDateTime sin @JsonFormat — diferente representación en cada JVM
Configuración: moderada · Costo: $0
Los unit tests clásicos prueban casos específicos. La IA es muy buena generando tests que ella misma pasa. Las pruebas de propiedades son distintas: definen una regla que debe cumplirse para cualquier input posible, no solo para los tres ejemplos que el modelo eligió.
En Java el motor más directo es jqwik, que funciona sobre JUnit 5:
<!-- pom.xml -->
<dependency>
<groupId>net.jqwik</groupId>
<artifactId>jqwik</artifactId>
<version>1.9.2</version>
<scope>test</scope>
</dependency>
// PedidoPropiedadesTest.java
import net.jqwik.api.*;
import net.jqwik.api.constraints.*;
class PedidoPropiedadesTest {
// Propiedad 1: el monto nunca puede ser negativo
@Property
void monto_siempreNoNegativo(
@ForAll @BigRange(min = "0", max = "999999.99") BigDecimal monto) {
PedidoDTO pedido = new PedidoDTO(1L, "TEST-001", monto, EstadoPedido.PENDIENTE, null);
assertTrue(pedido.getMonto().compareTo(BigDecimal.ZERO) >= 0,
"Un pedido no puede tener monto negativo: " + monto);
}
// Propiedad 2: round-trip para cualquier monto válido
@Property
void roundTrip_cualquierMontoValido(
@ForAll @BigRange(min = "0.01", max = "999999.99") BigDecimal monto)
throws Exception {
PedidoDTO original = new PedidoDTO(1L, "PROP-TEST", monto, EstadoPedido.CONFIRMADO, 10L);
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(original);
PedidoDTO restaurado = mapper.readValue(json, PedidoDTO.class);
assertEquals(0, original.getMonto().compareTo(restaurado.getMonto()),
"El monto no debe perder precisión en la serialización");
}
// Propiedad 3: no puede haber referencia a clienteId inexistente
@Property
void lineaPedido_clienteIdDebeTenerValorPositivoSiPresente(
@ForAll @Positive Long clienteId) {
PedidoDTO pedido = new PedidoDTO(1L, "PROP-002", BigDecimal.TEN,
EstadoPedido.PENDIENTE, clienteId);
assertTrue(pedido.getClienteId() > 0,
"clienteId debe ser positivo si está presente");
}
}
La diferencia con los unit tests convencionales:
"La IA es buena inventando tests que ella misma pasa. Las propiedades no le preguntan un caso: le preguntan siempre."
Un alumno que memoriza 3 ejercicios del libro saca 100 en el examen de esos 3 ejercicios. En el examen real con miles de variaciones, se cae. Las propiedades son el examen real.
Configuración: 5 minutos · Costo: $0.07 por ejecución
Este patrón usa Haiku 4.5 (el modelo más barato) en tres pasos en cadena para verificar lo que generó un modelo más caro:
| Paso | Acción | Costo |
|---|---|---|
| 1 | Haiku 4.5 genera el código | $0.04 |
| 2 | Haiku 4.5 genera las pruebas round-trip | $0.02 |
| 3 | Haiku 4.5 contrasta implementación vs. especificación OpenAPI | $0.01 |
| 4 | Maven / Gradle ejecuta las pruebas | $0.00 |
| 5 | Revisión humana aleatoria de puntos críticos | 3 min |
Total: $0.07 + 3 minutos vs. revisión manual completa: $50 + 30 minutos.
La secuencia de prompts para un controlador Spring Boot:
Prompt 1 (Sonnet 5 — genera el ejemplar):
"Implementa PedidoController con los endpoints POST /pedidos y
GET /pedidos/{id} según esta especificación OpenAPI: [spec aquí].
Usa Spring Boot 3, jakarta.*, y el patrón de respuesta ApiResponse<T>
que ya existe en el proyecto."
Prompt 2 (Haiku 4.5 — genera pruebas):
"Dado este PedidoController [código], escribe pruebas MockMvc que
verifiquen el contrato de la API: status codes, estructura del JSON
de respuesta, y que ningún campo del DTO se pierda o cambie de nombre."
Prompt 3 (Haiku 4.5 — contrasta vs. spec):
"Compara este PedidoController [código] con esta especificación OpenAPI
[spec]. Lista cualquier discrepancia: campos extra, campos faltantes,
tipos incorrectos, o comportamientos no especificados."
Tú revisas los puntos críticos que señala el paso 3. El resto lo verifica la máquina.
"Modelo barato + verificación fuerte > modelo caro + fe ciega."
La debilidad de los tests con datos inventados: la IA elige los datos que mejor funcionan con su implementación. El corpus real usa archivos o registros reales de producción —anonimizados— para descubrir edge cases que nadie anticipó.
En proyectos Java, esto significa mantener un directorio src/test/resources/corpus/ con muestras representativas:
src/test/resources/corpus/
├── pedidos/
│ ├── pedido-monto-maximo.json # monto = 999999.99
│ ├── pedido-cliente-nulo.json # clienteId ausente
│ ├── pedido-caracteres-especiales.json # nombres con ñ, acentos
│ ├── pedido-estado-cancelado.json # flujo de cancelación
│ └── pedido-multiples-lineas.json # 50+ líneas de detalle
// PedidoCorpusTest.java
@ParameterizedTest
@MethodSource("archivosDePedidosReales")
void corpus_cadaArchivoPasaRoundTrip(Path archivoJson) throws Exception {
String jsonOriginal = Files.readString(archivoJson);
PedidoDTO pedido = objectMapper.readValue(jsonOriginal, PedidoDTO.class);
String jsonReserializado = objectMapper.writeValueAsString(pedido);
PedidoDTO restaurado = objectMapper.readValue(jsonReserializado, PedidoDTO.class);
assertEquals(pedido, restaurado,
"El archivo " + archivoJson.getFileName() + " falló el round-trip");
}
static Stream<Path> archivosDePedidosReales() throws IOException {
return Files.list(Path.of("src/test/resources/corpus/pedidos"));
}
Cinco archivos reales descubren más bugs que cien tests inventados.
Este es el patrón que más se repite en producción y que más fácilmente pasa la revisión humana.
Claude genera PedidoDTO para un endpoint de migración Spring Boot 2 → 3. El código es limpio, está bien comentado, compila sin warnings:
// Lo que generó Claude
public record PedidoDTO(
Long id,
String referencia, // ← campo inventado por la IA
String numero,
BigDecimal monto,
EstadoPedido estado
) {}
Pasó la revisión del PR. Pasó los tests unitarios generados por la propia IA. El test de PedidoServiceTest usaba exactamente los mismos campos que el DTO generado.
La especificación OpenAPI decía:
PedidoDTO:
type: object
properties:
id:
type: integer
numero:
type: string
monto:
type: number
estado:
type: string
enum: [PENDIENTE, CONFIRMADO, CANCELADO]
Sin campo referencia. La IA lo inventó porque en el servicio adyacente existía un campo con ese nombre. El sistema compiló, los tests pasaron, el deploy fue a producción.
En producción: el API recibía JSONs sin el campo referencia. Jackson lo deserializaba a null. Downstream, el procesador de pagos esperaba ese campo como parte del contrato interno y fallaba silenciosamente.
@Test
void roundTrip_PedidoDTO_contraCorpusReal() throws Exception {
// Este JSON viene del corpus — formato real del cliente
String jsonReal = """
{
"id": 101,
"numero": "PED-2024-001",
"monto": 450.00,
"estado": "CONFIRMADO"
}
""";
PedidoDTO pedido = objectMapper.readValue(jsonReal, PedidoDTO.class);
String jsonReserializado = objectMapper.writeValueAsString(pedido);
// Si 'referencia' es un campo requerido (no nullable),
// la deserialización habría fallado aquí mismo.
// Si es nullable, el round-trip habría devuelto null
// en lugar de ausente, alertando la discrepancia.
assertThat(jsonReserializado).doesNotContain("referencia");
}
Capturado en 30 segundos. Previno más de 4 horas de war room.
La pregunta que vale plantear en el próximo PR de 400 líneas generado por IA: ¿quién lo habría visto en la revisión manual un viernes a las 6 PM?
La verificación sistemática no es responsabilidad de un desarrollador individual en cada PR. Es una política del pipeline. Así queda integrado en un proyecto Spring Boot típico:
Agrégalo como comentario en el PR template de tu repositorio:
## Checklist — código generado con IA
- [ ] ¿Hay test round-trip para cada DTO nuevo o modificado?
- [ ] ¿El corpus de archivos reales cubre este componente?
- [ ] ¿Haiku verificó la implementación contra la spec OpenAPI?
- [ ] ¿El test diferencial compara el nuevo comportamiento con el anterior?
- [ ] ¿Spot-check humano de 3 min en los campos críticos (montos, IDs, estados)?
- [ ] ¿`CLAUDE.md` tiene la regla: "todo parser nuevo requiere round-trip"?
CLAUDE.md## Reglas de verificación (obligatorias)
Todo componente nuevo que parsee, serialice o transforme datos DEBE incluir:
1. Test round-trip con JUnit 5 + Jackson (o el serializador del proyecto)
2. Al menos un archivo del corpus real en `src/test/resources/corpus/`
3. Test de propiedades con jqwik para invariantes de negocio críticas
Si la spec OpenAPI cambia, el test Haiku verify confirma alineación antes del merge.
| KPI | Meta | Qué medir en Java |
|---|---|---|
| Tasa de captura pre-producción | >95% | Bugs de IA encontrados antes del merge / total |
| Tiempo de verificación | <10% del tiempo de desarrollo | Si la feature tomó 2h → verificar ≤12 min |
| ROI | 10×–50× | Tiempo evitado en war room / tiempo invertido en verificación |
| Falsos positivos | <5% | Tests que fallan sin bug real — si molesta siempre, nadie lo usa |
| Bugs de IA en producción | Cercano a cero | El objetivo final |
Cuando llega un bug de IA a producción, la pregunta correcta no es "¿quién lo aprobó en el PR?". La pregunta es: "¿qué estrategia de verificación faltaba?". La respuesta se convierte en una nueva regla en CLAUDE.md.
Identifica los 2–3 DTOs más críticos de tu proyecto Spring Boot (los que mueven dinero, estados de negocio, o integran con sistemas externos)
Escribe un test round-trip para cada uno — si ya tienes @SpringBootTest es cuestión de agregar el test
Agrega al CLAUDE.md del proyecto: "Todo DTO nuevo requiere test round-trip"
Crea src/test/resources/corpus/ con 5–10 JSONs reales anonimizados
Integra el checklist Pre-PR al template del repositorio
Implementa el patrón Haiku verify para los endpoints de mayor tráfico
Añade jqwik para las invariantes de negocio críticas (montos, estados, referencias)
Conecta los corpus tests al CI/CD — si falla el round-trip, el pipeline no avanza
Meta: tasa de captura pre-producción >95%
"Haiku barato + verificación fuerte es matemáticamente superior a un modelo caro con fe ciega."
La IA sin verificación es velocidad hacia el precipicio. Con el playbook de verificación, es el 10× de productividad que promete — sin pagar el precio en producción.
El Pilar 1 te da el contexto. El Pilar 2 te ahorra dinero. El Pilar 3 hace que los dos anteriores sean seguros de usar.
¿Qué estrategia de verificación usas hoy en tu proyecto Java? ¿Round-trip, propiedades, algo diferente? Cuéntame en los comentarios.
Enjoy!
Joe