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

**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.

## **El costo de no verificar**

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.

## **El toolkit de 6 estrategias**

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.

![](https://cdn.hashnode.com/uploads/covers/64a79aba336591d2a1481aae/834339d3-1010-42e4-8e81-330344ccb29d.png align="center")

## **Estrategia 1 — Round-trip: la invariante más barata**

**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:

```java
// 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:

![](https://cdn.hashnode.com/uploads/covers/64a79aba336591d2a1481aae/4a6342c6-622a-4cbf-95e9-e0326458c0b9.png align="center")

**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
    

## **Estrategia 2 — Propiedades: prueba la regla, no el ejemplo**

**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](https://jqwik.net/), que funciona sobre JUnit 5:

```xml
<!-- pom.xml -->
<dependency>
    <groupId>net.jqwik</groupId>
    <artifactId>jqwik</artifactId>
    <version>1.9.2</version>
    <scope>test</scope>
</dependency>
```

```java
// 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.

## **Estrategia 3 — El patrón Haiku verify**

**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:

```text
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."

## **Estrategia 4 — Corpus real: no uses datos de juguete**

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:

```plaintext
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
```

```java
// 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.

## **La anatomía de una alucinación — historia real (adaptada a Java)**

Este es el patrón que más se repite en producción y que más fácilmente pasa la revisión humana.

### **Acto 1 — El DTO de aspecto perfecto**

Claude genera `PedidoDTO` para un endpoint de migración Spring Boot 2 → 3. El código es limpio, está bien comentado, compila sin warnings:

```java
// 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.

### **Acto 2 — La falla oculta**

La especificación OpenAPI decía:

```yaml
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.

### **Acto 3 — Lo que lo hubiera capturado en 30 segundos**

```java
@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?

## **Incorporar la verificación al pipeline Java**

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:

![](https://cdn.hashnode.com/uploads/covers/64a79aba336591d2a1481aae/afa1a1c0-b655-443e-a2d7-fff8354d3a2d.png align="center")

### **El checklist Pre-PR para código generado por IA**

Agrégalo como comentario en el PR template de tu repositorio:

```markdown
## 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"?
```

### **La línea para** [`CLAUDE.md`](http://CLAUDE.md)

```markdown
## 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.
```

## **Métricas de éxito**

| 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`](http://CLAUDE.md).

## **Acciones inmediatas**

### **Hoy (30 minutos)**

1.  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)
    
2.  Escribe un test round-trip para cada uno — si ya tienes `@SpringBootTest` es cuestión de agregar el test
    
3.  Agrega al [`CLAUDE.md`](http://CLAUDE.md) del proyecto: "Todo DTO nuevo requiere test round-trip"
    

### **Esta semana**

1.  Crea `src/test/resources/corpus/` con 5–10 JSONs reales anonimizados
    
2.  Integra el checklist Pre-PR al template del repositorio
    
3.  Implementa el patrón Haiku verify para los endpoints de mayor tráfico
    

### **Este mes**

1.  Añade jqwik para las invariantes de negocio críticas (montos, estados, referencias)
    
2.  Conecta los corpus tests al CI/CD — si falla el round-trip, el pipeline no avanza
    
3.  Meta: tasa de captura pre-producción >95%
    

* * *

## **La ecuación que cambia la perspectiva**

> "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*
