dev-fran

Reglas como tests: escribís la regla una vez y corre en cada commit

Fran
ia · testing · architecture · claude
Reglas como tests: escribís la regla una vez y corre en cada commit

El patrón en un dibujo

Un test chequea un pedazo de código. Escribís el código, después escribís su test, uno por uno.

Un código también tiene reglas: «las entidades de un test reusan los mocks del slice», «cada caso de fallo afirma su código de error». Esas reglas vivían en un documento y en la cabeza de quien revisa. Ahora cada regla pasa a ser un test propio. Lo escribís una vez, y corre sobre cada archivo de su alcance, incluidos los que un agente escriba mañana.

Antes

Un código, un test

  • código A

    su test A

  • código B

    su test B

  • código C

    su test C

Cada archivo necesita su propio test, escrito a mano.

Ahora

Una regla, un test, todos los archivos

La regla

«Las entidades reusan los mocks del slice»

Su test

se escribe una vez

  • user.test.ts
  • order.test.ts
  • route.test.ts
  • el archivo que un agente escriba mañana

En cada commit, cada push y cada corrida de CI, en menos de 500 ms.

La regla se escribe una vez. El test que la hace cumplir llega a cada archivo de su alcance, incluidos los que se escriban mañana.

Esa es la idea entera. El resto del post cuenta cómo funciona, cómo se ve el test y por qué cambia lo que cuesta revisar código escrito por IA.

Por qué lo necesitaba

Mis agentes de IA commitean pasando por un gate de guardianes: subagentes revisores, uno por área del código, que leen el diff en stage y le ponen nota.

Los guardianes revisan bien, y se pagan en cada pasada. Un fix de una línea reabre la ronda, porque cualquier cambio en el área de un guardián anula su nota. Planes que tendrían que llevar minutos se llevaban una tarde.

Casi todo lo que chequea un guardián es un juicio puntual sobre un solo archivo. Esa pregunta pide un modelo que lea el archivo, y solo ese archivo.

Jev, en dos líneas

Jev es el modelo de evaluación de TypeSafe. Le das un contexto y un conjunto de preguntas de sí o no, y te devuelve la probabilidad del sí para cada una, al ritmo de un linter, cobrando solo la entrada.

Cómo funciona

Una corrida, por archivo

menos de 500 ms
  1. 1

    La regla

    Un archivo declarativo en el repo: qué mira y qué pregunta.

  2. 2

    Las piezas

    El test, el módulo que prueba, los mocks que podría usar. Se arman una vez por archivo.

  3. 3

    El alcance, en código

    Si el archivo no tiene lo que la pregunta juzga, se saltea con su razón.

  4. 4

    Una llamada a Jev

    Todas las preguntas de la regla juntas. Una probabilidad por pregunta.

  5. 5

    El veredicto

    Cada probabilidad contra su barrera. Un rojo nombra el archivo, la regla y la pregunta.

  6. 6

    El registro

    Guardado por hash del contenido. El gate del commit lo lee sin red.

El modelo aparece una sola vez, en el paso cuatro. Todo lo demás es código común.

Una regla es un archivo declarativo que vive en el repo. Dice qué mirar y qué preguntar:

export default {
  title: 'The test reuses the slice’s mocks and leaves them untouched',
  on: 'src/**/*.test.ts',
  // Las piezas de contexto que Jev recibe por cada archivo.
  state: ['file', 'subject', 'mocks'],
  questions: {
    reuses_slice_mocks: {
      // Por debajo de esta probabilidad el test falla. Sale de un rojo y un verde reales.
      barrier: 0.6,
      // El código decide a quién se le pregunta: sin mocks, no hay pregunta.
      applies: { mocks: true },
      instructions:
        'Does every entity-shaped object in `file` ' +
        'name an imported mock or spread one?',
    },
  },
}

El motor arma las piezas de cada archivo (el test, el módulo que prueba, los mocks que podría usar), saltea los archivos que quedan fuera del alcance de la pregunta y le manda a Jev una llamada por regla con todas sus preguntas.

Cómo se ve el test

$ run.sh --rule mocks

The test reuses the slice’s mocks

src/contexts/order/order.test.ts
src/contexts/user/user.test.ts
src/contexts/route/route.test.ts

reuses_slice_mocks · bajo la barrera

Does every object in `file` shaped like a domain entity either name an imported mock or spread one?

src/util/cache.test.tsn/a · no hay subject
barrera
Un describe por archivo, un test por regla, una fila por pregunta. El rojo llega con la pregunta exacta que falló.

Es una suite de Vitest común: un describe por archivo, un test por regla, una fila por pregunta. Un rojo nombra el archivo, la regla y la pregunta exacta que falló, así quien lo lea (una persona o un agente) sabe qué arreglar.

Se fabrica una vez, corre en todos lados

Se fabrica una vez

El test de la regla

Una suite de Vitest: un comando, un código de salida, filas en stdout

Corre en todos lados

  • Hook de pre-commit

    Frena el commit ante un rojo

  • Push

    Revisa la rama entera

  • CI/CD

    El mismo comando, el mismo código de salida

  • El agente, mientras escribe

    Chequea su propio test antes de pedir el commit

  • Una barrida del árbol

    Audita el código viejo contra una regla nueva

Mismo contenido y mismas preguntas: la respuesta sale del registro, sin llamar al modelo.

El test de la regla se fabrica una vez y corre en el hook de pre-commit, en el push, en CI/CD, en una barrida del árbol entero y adentro del propio loop del agente mientras escribe código. Cada corrida lleva menos de 500 ms.

Los resultados se guardan por hash del contenido. El gate del commit solo lee ese registro, así que funciona sin red y al instante, y el mismo archivo con las mismas preguntas se pregunta una sola vez.

Lo que ahorra: tiempo, tokens, rondas

Antes

Revisión con un agente guardián

Un subagente que razona por área, en cada commit

  1. git commit
  2. El gate rechaza: falta revisión
  3. Lanzar al guardiánagente
  4. Cargar brief, leer el diff, recorrer el repominutos · tokens
  5. Nota y plan de fixestokens
  6. Arreglar una línea

Cualquier cambio en su área anula la nota: la ronda empieza de nuevo.

Ahora

El test de la regla con Jev

Una pregunta de sí o no por regla, por archivo

  1. Editar el test
  2. Correr la reglamenos de 500 ms
  3. El rojo nombra archivo y preguntasolo tokens de entrada
  4. Arreglar una línea
  5. Verde, registrado por hash

El agente cierra este loop solo, antes de pedir el commit.

El primer loop se paga en cada vuelta. El segundo cuesta una corrida, y nada para el contenido que ya tiene respuesta.

La diferencia está en dónde se cierra el loop. Con un guardián, cada fix vuelve a pasar por un agente que razona. Con el test de la regla, el mismo agente que escribió el código corre la regla, lee el rojo, arregla la línea y la corre de nuevo, todo antes de pedir el commit.

  • Tiempo

    Con un guardián en cada commit

    Una ronda de minutos por pasada

    Con el test de la regla

    Menos de 500 ms por corrida

  • Tokens

    Con un guardián en cada commit

    Un agente que razona y lee su brief, el diff y el repo

    Con el test de la regla

    Una llamada por archivo que cobra solo la entrada, y cero para el contenido que ya tiene respuesta

  • Rondas

    Con un guardián en cada commit

    Cada fix en su área reabre la revisión

    Con el test de la regla

    El agente arregla y vuelve a chequear solo, antes del commit

  • Loops del harness

    Con un guardián en cada commit

    Un peaje fijo en cada commit

    Con el test de la regla

    Al guardián lo llama una fila en rojo, y esa fila es su brief

De dónde sale el ahorro: el agente caro queda para las preguntas que necesitan razonar.

Por qué funciona la arquitectura

En el repo

revisado en PR, como cualquier código

rules/<scope>/*.ts

qué mirar, qué preguntar

En el motor

fuera del repo, evoluciona solo

Runner de Vitest

un test por regla, por archivo

Jev, por el AI Gateway

una llamada por regla

Registro por hash

el gate lo lee sin red

  • 01

    La regla es dato

    El alcance vive en applies y unless. Una falsa alarma sobre una clase de archivos es un diff de una línea.

  • 02

    Se revisa como código

    Las reglas viven en el repo: cambiar una regla es una PR con su propia review.

  • 03

    Un motor, todos los scopes

    Tests, mocks, documentación: un scope nuevo es una carpeta de reglas, y el motor queda igual.

  • 04

    Preguntar y frenar van separados

    El gate lee un registro por hash del contenido: sin red, al instante, reproducible.

  • 05

    Vitest de regalo

    Códigos de salida, filtros, bail y reporters vienen incluidos, y CI también.

  • 06

    Se calibra antes de frenar

    observe imprime la respuesta al lado de su barrera y deja pasar.

La separación que lo vuelve barato de crecer: las reglas cambian en PR, el motor cambia por su cuenta.

La decisión de diseño que sostiene todo es la separación. Las reglas viven en el repo y se revisan como código; el motor que las corre vive afuera y evoluciona por su cuenta. Sumar una pregunta es una entrada, sumar un scope es una carpeta de reglas, y una falsa alarma se arregla con una línea de alcance.

Dónde está la línea

Lo detecta Jev

Un juicio sobre un archivo

  • ¿Un cambio en el código podría hacer fallar estas aserciones?
  • ¿Cada caso de fallo afirma su código de error?
  • ¿El test afirma las llamadas, además del valor?
  • ¿Cada entidad es un mock importado, o un spread de uno?

Lo hace mejor el código

Comparar, contar, cruzar referencias

  • expect.assertions(N) contra la cuenta real
  • ¿El beforeEach restaura cada mock que un caso pisa?
  • ¿Cada caso pasaría en cualquier orden?
  • ¿El test se conecta a una base real?
De la primera barrida sobre una suite de tests real. La columna izquierda encontró defectos reales que un agente revisor había dejado pasar.

Jev es fuerte en un juicio sobre un archivo. Todo lo que compara dos listas, cuenta o depende del orden es trabajo del código común. La regla práctica que salió de calibrar: el código enumera, Jev juzga de a una cosa.

Cómo empezar

  1. Listá lo que chequea tu revisor. Cada guardián, cada checklist de review.
  2. Repartí cada ítem en código (un grep, un chequeo de AST), un juicio sobre un archivo o razonamiento de verdad.
  3. Escribí el cajón del medio como reglas. Una pregunta de sí o no cada una, con el mecanismo visible en el texto.
  4. Calibrá cada pregunta con un rojo y un verde reales antes de dejar que frene algo.
  5. Enchufá la suite al hook y a CI, y guardá al agente revisor para el último cajón.

El agente revisor sigue estando, y se lo llama para las preguntas que necesitan razonar, con una fila en rojo como brief. Todas las demás reglas pasan a ser un test que escribís una vez.

Sobre mí

Escrito por Fran — quince años construyendo software y emprendiendo, ahora con IA en cada etapa. Estos artículos son las lecciones de ingeniería que voy sacando en el camino.