Un modelo que nunca se envía: cómo uso Jev en driftwatch

driftwatch busca las partes de tu CLAUDE.md, tu AGENTS.md y tus skills que han dejado de ser verdad, y lo hace sin abrir una conexión, sin leer una API key y sin un LLM en el camino principal. Arranca en 192 ms. Esa frase es el producto: la razón para instalarlo en vez de pedirle a un agente que revise los documentos es que es determinista, rápido y ejecutable en CI sin que nadie firme un contrato con nadie.
npx @abr4xas/driftwatchY aun así llevo tres días seguidos usando un modelo para trabajar en él.
La resolución de esa contradicción resultó ser más interesante que el modelo, así que va primero: dónde puede estar un modelo en un proyecto que vende determinismo. Después lo concreto, que es cómo se le pregunta a Jev, qué funcionó y qué no.
Si no conoces Jev, lo cubrí entero en Jev, el modelo de TypeSafe que no escribe texto: los primitivos, la API y los precios. Aquí doy por sabido eso y voy al uso.
La tentación es real y es específica
driftwatch mide su precisión contra un corpus de 66 repositorios públicos fijados a un commit. Cada hallazgo que produce lo ha leído una persona, ha abierto el repositorio de verdad y ha dictado si es un problema real o un falso positivo. Eso está escrito en un documento, CLASSIFICATION.md, ronda a ronda, con la justificación de cada uno. Hoy son 341 documentos auditados y 37 hallazgos, 27 verdaderos y 10 falsos.
En ese documento hay tres clases de falso positivo marcadas con la misma nota: "no rule shape proposed yet". Un apodo de un crate. Un fichero que pertenece al proyecto de otro. Un bundle generado que la frase describe sin usar ninguna palabra que signifique "generado". Las tres son juicios semánticos que ninguna regla de prosa ha alcanzado, y ninguna se ha cerrado todavía.
Un clasificador calibrado las cerraría. Probablemente. Esa es la tentación, y la respuesta sigue siendo no.
El precio serían cuatro compromisos del producto y un binario de 192 ms convertido en un cliente HTTP con una credencial. No hay una versión pequeña de ese cambio: en el momento en que el camino principal llama a un modelo, la promesa de "offline, sin API key, determinista" deja de tener sentido, y esa promesa es el argumento contra todos los competidores que sí llaman a uno.
La regla, y por qué es aplicable
El modelo es un instrumento de investigación del corpus, nunca un componente de la herramienta.
Esa frase está escrita en el spec del proyecto y es la única razón por la que todo lo demás funciona. Es la que decide, en cada momento en que el modelo sería útil, si se usa o no.
Lo que hace que sea aplicable y no un buen deseo es que hay dos corpus, y esa fue la corrección que hizo posible todo el trabajo.
Al principio había uno: los 66 repositorios, con su veredicto humano por hallazgo. Bajo esa suposición, cualquier uso de un modelo aterriza sobre la medición, porque en un corpus único cada repositorio es o calibración o validación, y no hay ningún sitio donde un modelo pueda estar de pie sin estar juzgando algo.
El problema no era la adjudicación. Era pedirle a un solo artefacto dos trabajos con requisitos opuestos. Certificar precisión quiere pocos repositorios y un veredicto humano en cada uno. Encontrar reglas nuevas quiere muchísimos repositorios y ningún veredicto. Juntos se canibalizan. Separados, ninguno limita al otro.
Así que hay un segundo corpus: 2 533 repositorios clonados de GitHub, sin fijar, desechables, que no llevan ningún veredicto humano. Produce material para leer, clases de hallazgo y clases de descarte, y nunca una medición. La regla que lo gobierna cabe en una línea:
Ningún número calculado sobre el corpus de descubrimiento es una precisión.
El modelo vive entero en ese lado. Una regla que encuentra ahí sigue escribiéndose a mano, en código determinista, y sigue midiéndose de la única forma en que este proyecto mide algo: contra los 66 que ha leído una persona.
La regla se volvió una carpeta
Esto es lo que más me gusta del resultado, y llegó tarde. Los siete pases que llaman al modelo empezaron en scripts/ mezclados con los que no, distinguidos solo por el prefijo del nombre. El prefijo mentía: un script corpus-* importaba de un discovery-* y al revés.
Ahora el árbol es la regla:
scripts/
jev/ ask.ts · questions.ts · classify.ts · classes.ts
filter.ts · families.ts · claims.ts · findings.ts · review.ts
corpus/ el corpus de certificación
discovery/ el corpus de descubrimiento
lib/ lo que comparten
release/ la comprobación del tarball
Si gasta una credencial, vive bajo jev/. Nada en src/ importa nada de ahí, ai y zod son devDependencies, y el paquete publicado son 25 ficheros y 82 KB de código sin una línea con forma de modelo. La regla dejó de depender de que alguien la recuerde.
Un solo sitio donde se pregunta
Lo primero que hice fue lo que hace todo el mundo: pedirle a un modelo de chat, a través del gateway, un { same, why } en JSON. Funcionaba. También estaba pagando por un párrafo que nadie leía, porque la pregunta se hacía 137 veces sobre entradas casi idénticas y lo único que el código consumía era el booleano. El gateway me lo dijo en la cara cuando por fin pedí el modelo correcto:
Model 'typesafe-ai/jev' is an evaluation model, not a language model.
Use the evaluation generation API instead.
Después de siete pases tenía siete copias de la misma función: el id del modelo, el import perezoso del SDK, la comprobación de la credencial y el desempaquetado de la respuesta. Habían divergido, claro. Uno no comprobaba la clave en absoluto y descubría que faltaba en la primera petición, después de parsear un documento y de imprimir un resumen. Otro convertía la falta de clave en un código de salida donde los demás lanzaban.
Ahora hay un módulo, y su interfaz completa es esta:
export type Answered = {
probability(key: string): number // una pregunta booleana
chosen(key: string): Chosen // una pregunta de opción, con su confianza
}
export type Ask = (
state: Readonly<Record<string, unknown>>,
questions: Readonly<Record<string, Experimental_EvaluationQuestion>>,
) => Promise<Answered>
export async function openJev(): Promise<Ask>Lo que queda fuera es lo que un humano lee: las preguntas, el estado y todos los umbrales. Esos son el argumento del pase, y el pase es la cosa que se discute.
Un detalle que no esperaba que importase tanto: separar leer la respuesta de hacer la petición. La función que valida la forma es pura, así que hay tests de la parte que antes estaba copiada siete veces con siete mensajes de error distintos, sin credencial y sin red.
Las preguntas se escriben como afirmaciones
La documentación de TypeSafe da dos formas y esta es la que uso siempre. No "¿son estos dos documentos el mismo?", sino una afirmación con sus dos criterios escritos:
export const SAME_DOCUMENT = {
type: 'boolean',
instructions: 'Document B is document A, copied into another repository and possibly edited.',
criteria: {
true: 'One is derived from the other, or both from one source: the same document ' +
'installed in two repositories, with or without edits.',
false: 'Two documents written independently. They may describe the same tool, follow ' +
'the same convention, or share boilerplate, and still be two documents.',
},
}El criterio false es el que carga todo el peso
Esta es la única técnica de este post que usaría mañana en cualquier otro proyecto.
En los tres casos donde algo funcionó, el criterio true es casi decorativo y el false es el que hace el trabajo. La razón es siempre la misma: el candidato ya llegó hasta ahí porque se parece. La aritmética de antes ya filtró por parecido. Si no dices explícitamente qué cosas se parecen y aun así son distintas, la pregunta colapsa en la que el filtro ya respondió.
Con documentos, overlapOf ya estableció que dos comparten líneas, así que lo que se le pide a Jev es derivación y no similitud, y el false nombra el caso duro: dos proyectos escribiendo sus propias instrucciones para el mismo framework son dos documentos por mucho que la prosa coincida.
Con hallazgos, dos de este corpus se parecen en todo lo superficial, porque los dos son una ruta que un verificador no pudo resolver, casi siempre en Markdown y casi siempre del mismo check. Preguntar si son "similares" da que sí siempre. La taxonomía real está hecha de por qué la herramienta se equivocó, así que el false dice: un hueco que nunca se pensó que existiera no es un fichero producido por un build, y ninguno de los dos es una ruta del proyecto de otra persona.
Con palabras sueltas, la regla bare-word tira una palabra sin barra y sin extensión, que es la mayor parte de cualquier documento. Preguntado si la palabra "podría ser un fichero", casi cualquiera podría. Así que el false enumera las alternativas por su nombre: un comando o un subcomando, un paquete, un módulo, una variable, un valor de flag, un encabezado, el nombre de un producto, una palabra de prosa corriente, un placeholder, o una ruta del proyecto de otro.
Escribir el criterio false es, en la práctica, escribir la parte difícil de la especificación de la regla. Si no puedes escribirlo, todavía no sabes qué estás preguntando.
Dos preguntas, un estado, una petición
Un modelo de evaluación evalúa las preguntas en paralelo y no pueden verse las respuestas entre ellas. Eso permite hacer la pregunta especulativa gratis.
En el filtro de adquisición hay dos por documento. aboutThisRepo es la que se consume siempre. repoKind, una opción de cinco valores (repo de skills, producto con skills, plantilla, fork, dotfiles), es especulativa: se pregunta para cada documento y se consume por repositorio. Un segundo request costaría un round trip para aprender algo que el primero ya sabía.
El mismo patrón en el clasificador: isReal booleana y className como opción sobre las nueve clases que una persona nombró, más new. La respuesta de clase se consume solo cuando la primera dice que no es real.
Y una razón fuerte para que sean dos preguntas y no una opción con un valor de "verdadero positivo": son juicios distintos. La primera es sobre el repositorio, la segunda sobre una taxonomía. Plegarlas haría que "¿es esto real?" compitiera con nueve etiquetas de clase por la misma masa de probabilidad.
Los umbrales viven en el código
Jev nunca decide nada. Devuelve un número y el umbral está escrito en un fichero con un comentario que explica por qué es ese y no otro.
Fusionar dos documentos en una familia es una afirmación: saca una observación de todos los recuentos posteriores, así que una fusión errónea esconde evidencia en silencio, que es exactamente la forma de un falso positivo en la herramienta y se trata igual. Una separación errónea solo deja el sesgo donde ya estaba. Así que el umbral es estricto, 0.80, y la probabilidad de cada par se escribe al lado, porque un umbral que nadie puede volver a correr es un umbral con el que nadie puede discutir.
Calibrar una constante y luego correr aritmética
Creo que este patrón generaliza fuera de aquí.
Había 193 185 pares candidatos. Eso no es una pregunta que se le haga a un modelo: a ocho a la vez son horas de gateway y una factura que nadie dimensionó. Pero 137 pares ya juzgados decían que la respuesta era casi constante.
Así que una muestra estratificada de 600 pares, puesta a Jev banda por banda de solapamiento. Por encima de 0.85 contestó "un documento" 180 veces de 180. Por debajo de 0.65 caía a dos de cada tres.
El juicio del modelo lleva información en una parte del rango y ninguna en la otra, y la parte donde no lleva ninguna es el 79% de los pares. Así que el umbral es 0.85, por encima no se pregunta, y lo que corre es aritmética sobre un número que el modelo ayudó a elegir una vez, en tiempo de investigación. Mover esa constante significa volver a medir, no volver a discutir.
Una petición que falla no es un juicio
Siete pases escribieron esta regla en un comentario y luego la implementaron cada uno un poco distinto. Ahora está en un sitio.
Si una petición lanza, el ítem se descarta y no se puntúa como un no. Puntuarla dejaría que la red escribiera el resultado, halagando o condenando al modelo por algo que no hizo ninguno de los dos, y, peor, la corrida se leería como limpia en vez de como corta. El recuento de fallos vuelve con el resultado para que el pase pueda decirlo en voz alta.
Hay una distinción que el bucle original no podía hacer y que costó una tarde entender: lanzar significa que la petición falló, devolver nada significa que no había nada que preguntar. Un documento que ya no está en el disco pasaba por el mismo catch que un timeout del gateway.
La redacción es el instrumento
CLAIMS_A_PATH estaba duplicada byte a byte en dos pases, y uno de los dos tenía escrito en prosa que no debía divergir. Esa nota es precisamente el argumento para importarla en vez de copiarla: el 92% que reporto más abajo es el número que produjeron esas frases exactas. Una versión "limpiada" para que quede mejor en una línea de comandos sería otra medición citada bajo la anterior.
Funcionó: agrupar
Le pedí a Jev que clasificara los hallazgos ya adjudicados en las clases que una persona había dibujado. De los 10 hallazgos falsos, que se reparten en 9 clases distintas, acertó 9, con confianzas de 0.48 a 0.99:
| clase que dibujó una persona | Jev | confianza |
|---|---|---|
foreign-project |
✓ | 0.99 |
placeholder ×2 |
✓ ✓ | 0.99, 0.96 |
generated-bundle |
✓ | 0.90 |
comma-separated-globs |
✓ | 0.85 |
another-tools-layout |
✓ | 0.76 |
third-party-convention |
new |
0.76 |
crate-nickname |
✓ | 0.69 |
runtime-log |
✓ | 0.50 |
readers-project |
✓ | 0.48 |
La única que falla es la de definición más delgada, una convención de otra comunidad que este repositorio nunca adoptó y con un solo ejemplo, y la llama new, que no es una lectura irrazonable de esa etiqueta. Es la misma que falló la primera vez que corrí esto, hace rondas.
El control es lo que lo hace convincente. Le pedí también que clasificara los 27 hallazgos verdaderos, los que no pertenecen a ninguna clase de falso positivo. Contestó new para 26 de los 27. No está emparejando etiquetas con prosa: está negándose a aplicar la taxonomía donde la taxonomía no va.
La única excepción es un v2.rs de openai/codex al que le puso foreign-project con confianza 0.36, la más baja de toda la corrida, así que la señaló él mismo.
Y la confianza ordena el trabajo: las dos clases peor puntuadas, 0.48 y 0.50, son readers-project y runtime-log, exactamente las dos que el proyecto tenía ya marcadas como abiertas y ambiguas. La confianza tampoco sigue el tamaño de la clase: ocho de las nueve tienen un solo ejemplo y van de 0.48 a 0.99.
No funcionó, y era el punto: los veredictos
En la misma corrida le pregunté si cada hallazgo era un problema real.
| mín | mediana | máx | |
|---|---|---|---|
| los 27 verdaderos | 0.09 | 0.49 | 0.66 |
| los 10 falsos | 0.32 | 0.40 | 0.56 |
Sin umbral, el orden da AUC 0.646 contra 0.5 de una moneda. Las distribuciones se solapan casi enteras y ningún umbral sirve: en p < 0.6 atrapa los 10 falsos y tira 24 de los 27 verdaderos. Bajándolo a p < 0.5 conserva 13 verdaderos y ya se le escapan 2 falsos.
Bajo la regla de los dos corpus esto no cuesta nada, porque ninguna salida del modelo iba a ser un veredicto de todos modos. Pero vale la pena decirlo claro: lo que hace una persona cuando abre un repositorio y decide si una ruta falta de verdad no está reproducido aquí en absoluto. Lo que sí está reproducido es el paso siguiente, el que ordena un montón de cosas ya juzgadas en formas.
No funcionó, a secas: la primera vez que apunté a lo salvaje
Cogí el procedimiento de agrupación que había funcionado sobre 37 hallazgos y lo apunté a los 35 304 del corpus de descubrimiento. 4 950 pares, cero grupos, máximo 0.72.
El fallo era mío y era el muestreo: un hallazgo por repositorio repartido uniformemente es un conjunto construido para no tener nada en común. Le estaba haciendo una pregunta estricta a material elegido por su diversidad.
Bloqueando primero por check y por la forma sintáctica de lo reclamado, 5 310 pares dentro de tres vecindarios, salió exactamente un grupo de dos: dos reclamaciones en la misma línea del mismo fichero.
Lo que aprendí es que la pregunta mide casi-identidad, no clase compartida. Y la validación de esa mañana no podía haber mostrado otra cosa, porque la única fusión verdadera entre los 66 eran dos anclas literalmente idénticas en un fichero. Lo registré en vez de iterar sobre ello: el pase que funcionó preguntaba por una cosa contra su propio contexto, no por dos cosas la una contra la otra.
Y entonces encontró algo de verdad
Hay una decisión temprana del proyecto, ADR-0003, que dice que una palabra suelta como foo.ts sin barra "casi nunca" es una afirmación sobre una ruta concreta. Esa frase nunca había tenido un número detrás.
Así que 800 descartes de bare-word que no resuelven en ninguna parte, y una pregunta sobre si la frase pone la palabra como ruta de este repositorio.
| P(la frase reclama una ruta) | candidatos |
|---|---|
| 0.00 a 0.20 | 606 (76%) |
| 0.20 a 0.60 | 93 (12%) |
| 0.60 a 0.80 | 37 (5%) |
| 0.80 a 1.00 | 64 (8%) |
"Casi nunca" es 92%, medido sobre 2 533 repositorios en vez de argumentado.
Y el 8% tenía forma. 61 de los 64 llevan extensión, y 33 están en una frase que ya nombra un directorio:
`lib/` holds the HLS library, organized under `lib/hls/`
(core modules like `packager.ex`, `tracker.ex`)
`config/` — agent definitions (`agents.json`) and the skills list
packager.ex no es un nombre ambiguo ahí. El directorio está a dos palabras.
Eso es una regla candidata: determinista, estrecha, y no afloja la ADR, porque añade una condición que el propio documento suministra. La escribí. Los 66 la rechazaron.
| hallazgos | |
|---|---|
| antes | 37 |
| la regla como se propuso | 118 |
| estrechada todo lo posible | 104 |
La regla tal como la propuse añadía 81 hallazgos sobre repositorios que veinticuatro rondas de adjudicación habían dejado en 37. Estrecharla todo lo posible recuperó 14, y aun así quedaban 67 nuevos. Los dos fallos que quedan son estructurales, no de ajuste.
El primero: el directorio normalmente no lleva barra final. unjs/nitro escribe - `src/dev` — Development server logic (`app.ts`, `server.ts`). El directorio del que va la línea no tiene barra, así que una regla que busca un token terminado en / no lo ve, y encuentra src/config/ en el ítem de arriba, porque la ventana de prosa arrastra la entradilla a propósito. Tres hallazgos reclamando src/*.ts para ficheros que viven en src/dev/. Nadie escribió esa afirmación.
El segundo: remix-run/react-router escribe - `about.tsx` → `/about`. Eso es la convención de rutas, escrita para el app/routes/ de otro. Es una clase que el documento ya tenía nombrada, y el corpus de descubrimiento dice que aparece en el 19% de los documentos salvajes. Cualquier regla que alcance un nombre suelto en un tutorial alcanza esto, y no hay prueba sobre directorios que las separe: la frase nombra un directorio real en los dos casos.
Revertida. La puerta quedó cerrada con tests que llevan la evidencia dentro, para que la próxima persona a la que se le ocurra la idea encuentre la medición en vez de repetirla.
El balance honesto
El pipeline hizo exactamente lo que está diseñado para hacer: un modelo encontró una población que nadie habría encontrado a mano, una persona escribió la regla, y el corpus la mató antes de que se enviara. Nada de eso adjudicó nada.
Y hay que decir la otra mitad: a día de hoy todo este trabajo con Jev no ha producido ni una línea de src/. Una medición que dice que no es el pipeline funcionando y no el pipeline fallando, pero ese es el estado real, y un post que solo contara la parte de arriba estaría mintiendo por omisión.
Lo que sí produjo: un número donde había un adverbio, una taxonomía reproducida y validada con un control, 3 980 documentos colapsados en 3 570 familias donde el hash solo encontraba 20 duplicados entre repositorios, y el dato de que uno de cada cinco documentos de contexto no habla del repositorio en el que está, que sube al 39% en los repositorios que publican skills como producto.
Lo que me llevaría a otro proyecto
- Decide dónde el modelo no puede estar, y haz que sea una carpeta. Una regla en prosa es una regla que alguien olvida. Un directorio del que nada en
src/importa es una regla que se verifica sola. - Si vas a medir algo, el modelo no puede tocar la medición. Separa el artefacto que certifica del que suministra material. Mientras sean uno, cada uso de un modelo es una contaminación.
- Usa un modelo de evaluación si lo que quieres es un número. Pedirle JSON a un modelo de chat es pagar por prosa que nadie lee y después parsearla.
- Escribe el criterio
false. Es la parte difícil de la especificación de tu regla. Si no puedes escribirlo, no sabes qué estás preguntando. - Los umbrales van en tu código, con un comentario que diga por qué. El modelo no decide.
- Calibra una constante con el modelo y luego corre aritmética. Preguntar 193 000 veces es una factura, preguntar 600 veces para elegir un corte es una medición.
- Una petición que falla no es un juicio. Descártala y cuenta cuántas fueron.
- La redacción es el instrumento. Un número pertenece a las frases exactas que lo produjeron.
- Prepárate para que la respuesta sea no, y ten dónde escribirlo para que la próxima persona encuentre la medición en vez de repetir el intento.
Preguntas frecuentes
¿driftwatch usa un LLM?
No. El binario que instalas no abre ninguna conexión, no lee ninguna API key y no llama a ningún modelo. Todo el trabajo con Jev ocurre en scripts de investigación que no se publican con el paquete.
¿Para qué sirve un modelo de evaluación si no escribe el código?
Para encontrar poblaciones que nadie miraría a mano y para poner un número donde había un adverbio. En este proyecto midió que el "casi nunca" de una decisión temprana del proyecto era un 92%, sobre 2 533 repositorios.
¿Se puede usar Jev para decidir si algo es un falso positivo?
Aquí no funcionó. Sobre hallazgos ya adjudicados, la separación entre verdaderos y falsos dio AUC 0.646, y ningún umbral servía. Sí funcionó el paso siguiente, clasificar en una taxonomía cosas que una persona ya había juzgado.
¿Qué es la regla de los dos corpus?
Que el artefacto que certifica precisión y el que suministra material para investigar tienen que ser distintos. Mientras sean uno solo, cualquier uso de un modelo contamina la medición.
¿Cómo se escribe una pregunta para Jev?
Como una afirmación, no como una pregunta, y con los dos criterios escritos. El false es el que carga el peso, porque el candidato ya llegó hasta ahí porque se parece a algo.
Fuentes
¿Tienes un proyecto en mente?
Trabajo con Laravel, WordPress, SEO técnico y servidores MCP. El primer paso es una llamada de descubrimiento, sin costo ni compromiso, donde me cuentas qué necesitas y te digo con honestidad si puedo ayudarte.