Documentación

Harness de evaluación

laya.evals convierte un conjunto de datos etiquetado en una puntuación repetible, y una línea base en una puerta de aprobado/suspenso, de modo que un cambio de calidad es un diff revisable en lugar de una comprobación manual.

Las matemáticas de las métricas y el analizador del conjunto de datos son Python puro más numpy y nunca importan torch, así que se ejecutan sin pesos. Ejecutar un conjunto de datos contra un checkpoint necesita el checkpoint y requiere su tiempo de carga normal.

Inicio rápido

# check the format without a model
laya-evals validate research/evals/fixture.jsonl

# score a labelled set on one checkpoint, with thresholds and a baseline
laya-evals run data.jsonl --model english --device cpu \
    --min-accuracy 0.8 --max-ece 0.05 --score-within 0.25 --slice language \
    --json report.json --markdown report.md

# compare a saved report to a baseline
laya-evals compare report.json --baseline baseline.json --tolerance choice_accuracy=0.02

laya eval ... es lo mismo a través de la CLI principal, así que laya eval validate data.jsonl también funciona.

Códigos de salida: 0 en caso de éxito, 1 cuando falla un umbral o una tolerancia de línea base, 2 ante un error de uso. run imprime las métricas generales y las particiones solicitadas en stdout, y escribe el informe completo y un resumen en Markdown cuando se dan --json / --markdown.

Atribuir los errores de la lista corta

Para un conjunto de elecciones etiquetado de alta cardinalidad, laya.evals_shortlist.evaluate_shortlist usa la ruta existente predict_shortlist y el harness de evaluación habitual. Responde a dos preguntas distintas: ¿la recuperación conservó la etiqueta de referencia, y Laya la eligió cuando estaba presente? Esta es una API de Python opcional para etiquetas choice; los informes normales de laya-evals run no cambian.

import laya
from laya.evals import Dataset
from laya.evals_shortlist import evaluate_shortlist
from laya.shortlist import embed_fn_from_agent

agent = laya.load()
dataset_path = "intents.jsonl"
dataset = Dataset.from_jsonl(dataset_path)
report = evaluate_shortlist(
    agent, dataset, embed_fn_from_agent(agent), k=20,
    checkpoint_id="my-checkpoint@revision", embedder_id="my-encoder@revision",
    dataset_path=dataset_path,
)
print(report.overall)
print(report.cases[0]["shortlist_status"])

Usa la misma función de embedding y el mismo checkpoint que el despliegue que estás midiendo. Los dos identificadores los proporciona quien llama y deben nombrar revisiones inmutables; el informe no puede inferir los pesos detrás de un callable arbitrario. dataset_path registra el SHA256 del archivo junto a la huella de preguntas ya existente. Cada caso conserva las etiquetas reales de la lista corta y uno de correct, retrieval_miss o decision_miss. shortlist_recall_at_k es la fracción de etiquetas de referencia conservadas. shortlist_accuracy_on_recalled son las decisiones correctas divididas entre los casos conservados; se omite cuando no se conservó ninguno. El choice_accuracy existente sigue siendo la precisión de extremo a extremo sobre todos los casos, incluidas las fallas de recuperación. Las métricas de la lista corta aparecen en las mismas particiones por idioma, modelo, pregunta y etiqueta. La latencia de la solicitud incluye el embedding y la llamada de decisión; el informe no aísla los tiempos por etapa. Con k >= n, la pregunta original pasa tal cual y el recall de la recuperación es 1 sin llamar al embedder.

Esto no reproduce los resultados de BANKING77 en issue #102: esos números dependen de su conjunto de datos, su checkpoint y su bi-encoder. Esta API hace repetible el mismo tipo de diagnóstico sobre un conjunto etiquetado propio de quien llama.

Evaluar una exportación ONNX

run --onnx PATH puntúa un modelo ONNX exportado a través de ONNXAgent en lugar del Router de torch, de modo que un despliegue ONNX (incluida una copia INT8 de scripts/export_onnx.py --quantize) queda sujeto a los mismos umbrales y líneas base que la ruta de torch:

python scripts/export_onnx.py --model convaiinnovations/laya --output laya.onnx --quantize
laya-evals run data.jsonl --onnx laya.int8.onnx --max-ece 0.05

--model nombra el checkpoint del que vino la exportación — un id del Hub o una ruta local, no un nombre corto de Router como english, ya que en esta ruta no hay Router (por defecto convaiinnovations/laya). Su configuración y su tokenizador se cargan desde ahí. El agente sirve un solo checkpoint, así que una fila del conjunto de datos cuyo campo model nombre otro distinto falla con un error claro en lugar de ser respondida silenciosamente por el modelo equivocado; --device no se aplica. --batch-size usa la API por lotes del agente cuando la tiene y recurre a una llamada por estado si no; --sort-by-length se reenvía a esa API por lotes; el respaldo por estado no tiene grupo que reordenar. Pasa --calibration PATH para cargar un mapa de calibración ajustado en ONNXAgent, de modo que las puertas de calibración como --max-ece evalúen contra probabilidades calibradas. El bloque config del informe registra la ruta onnx y la ruta calibration (cuando se establece).

Medido sobre research/evals/fixture.jsonl (12 filas etiquetadas, checkpoint inglés, CPU):

runner choice_acc noul_acc score_mae ece mean_conf p50 ms
torch Router 0.75 1.00 1.3418 0.1596 0.7304 116.8
--onnx fp32 0.75 1.00 1.3418 0.1596 0.7304 66.3
--onnx int8 0.75 1.00 1.3512 0.1658 0.7304 46.3

La exportación fp32 reproduce los números de torch exactamente, y la copia cuantizada mueve score_mae en 0.009 y ece en 0.006 — el tipo de deriva que compare --tolerance está pensado para controlar.

Formato del conjunto de datos

Un objeto JSON por línea (JSONL). Las líneas en blanco y las líneas que empiezan por # se ignoran.

campo obligatorio significado
state sí texto, correo, ticket o documento JSON sobre el que decidir
questions sí un diccionario de pregunta de Laya, exactamente como lo acepta Router.predict
expected sí la verdad de referencia indexada por id de pregunta: una etiqueta para choice, un número para score, true/false para noul
tags no cadenas por las que particionar
language no un código por el que particionar
model no fuerza un checkpoint para esta fila; --model lo anula. Una fila que no fuerza nada se etiqueta con el checkpoint con el que respondió el Router

research/evals/dataset.template.jsonl tiene un ejemplo comentado.

Métricas

Cada métrica se calcula por respuesta donde aplica y se agrega sobre el conjunto de datos:

métrica aplica a significado
choice_accuracy choice fracción cuya etiqueta elegida coincide
noul_accuracy noul fracción cuyo booleano (probabilidad >= 0.5) coincide
score_mae score error absoluto medio
score_within_<tol> score fracción dentro de una tolerancia absoluta
ece cualquier respuesta con una confianza error de calibración esperado, 15 bins, calculado sobre answer["answer_confidence"], la probabilidad calibrada que Laya informa en todos los tipos de respuesta
brier cualquier respuesta con una confianza y una etiqueta conocida puntuación de Brier de la confianza como P(correcta), mean((confidence - correct)**2); más bajo es mejor
aurc cualquier respuesta con una confianza y una etiqueta conocida área bajo la curva de riesgo–cobertura: un valor de riesgo por cada nivel de confianza distinto, cada uno ponderado por las respuestas que abarca ese nivel; más bajo es mejor, y premia una confianza que ordena correctas de incorrectas en lugar de solo estar calibrada
selective_accuracy@50, selective_accuracy@80 cualquier respuesta con una confianza y una etiqueta conocida precisión sobre las respuestas que acepta un umbral de confianza en el punto de cobertura del 50% / 80% – lo que compra abstenerse de la cola menos confiable. Un umbral no puede dividir un grupo de confianzas iguales, así que esto puede cubrir más que la fracción nombrada; consulta cortes de cobertura
mean_confidence cualquier respuesta con una confianza media del answer["answer_confidence"] informado
latency_p50_ms, latency_p95_ms por solicitud tiempo de reloj que esperó cada solicitud, informativo – consulta procesamiento por lotes
cost_per_decision_p50_ms, cost_per_decision_p95_ms por decisión el tiempo de reloj de una llamada dividido por las filas que llevó, informativo

Cortes de cobertura y empates

Ambas métricas de cobertura cortan en un umbral de confianza, y un umbral acepta toda respuesta en su propia confianza. Así que un corte nunca divide un grupo de respuestas que comparten una: cuando coverage * n cae dentro de un grupo así, se acepta cada miembro del grupo. El número de respuestas detrás de la cifra es, por lo tanto, el borde superior del grupo en lugar de la fracción nombrada – selective_accuracy@50 sobre una partición cuyas confianzas son todas iguales es la precisión de esa misma partición, no la mejor mitad de ella. El recuento que imprime la puerta (n= en el mensaje de fallo de una regla) es el tamaño de la partición, no el tamaño aceptado, así que un grupo muy amplio no es visible solo a partir del mensaje.

Los empates son el caso normal, no un caso límite: una temperatura ajustada puede dejar que un bucket informe una masa puntual, algo que laya.common.answer_confidence registra del choice:11+ que se distribuye, y un checkpoint real produjo un grupo de seis filas en exactamente 1.0 sobre doce respuestas. Cortar en un índice de fila hizo, en cambio, que ambas métricas dependieran del orden en que llegaban los datos – las mismas filas, barajadas, movieron selective_accuracy@50 entre 0.000 y 1.000.

aurc integra un valor de riesgo por cada nivel distinto, ponderado por las respuestas que abarca ese nivel, así que sigue siendo un área bajo la curva de riesgo–cobertura en lugar de un promedio de puntos de tamaño desigual.

Dos consecuencias que conviene anticipar:

  • Un número puede moverse en cualquier dirección, más de lo que podría reordenarlo. Donde un grupo cruza el corte, la lectura por umbral difiere de toda lectura por índice de fila de los mismos datos: medida sobre 400,001 conjuntos de datos con forma de empate, hasta 0.500 para selective_accuracy@50 y 0.351 para aurc. Sobre la forma de doce respuestas de arriba – seis correctas, todas con confianza 1.0 – aurc se mueve 0.327 (0.173 a 0.500). Una puerta que aprobaba puede fallar, y una que fallaba puede aprobar; el veredicto anterior dependía del orden de las filas, incluso para un límite absoluto min o max, que no lo rechaza nada porque lee una sola ejecución.
  • Regenera las líneas base confirmadas. config.coverage_metric_definition registra qué definición produjo un informe. Comparar una métrica de cobertura entre dos definiciones se rechaza en ambas puertas que restan una línea base – --baseline --tolerance (EvalReport.compare) y una regla relativa (max_drop / max_increase) bajo --gate-policy – y en cualquiera de los dos lados obsoleto, no solo en la línea base: un candidato producido por un laya más antiguo arrastra un artefacto de orden de filas que puede leerse mejor que la verdad, así que evaluarlo contra una línea base regenerada correctamente aprobaría una regresión que el informe bien puntuado suspende. Sin ese rechazo, una línea base obsoleta oculta una regresión real: una partición registrada en 0.033 bajo la definición antigua se lee como 0.517 bajo esta, así que un candidato que de verdad cayó 0.217 superaría un max_drop de 0.05. ece y brier no cortan y siguen siendo comparables.

Cuando no hay empates en los datos hay un nivel por respuesta, y ambas métricas son exactamente las que siempre han sido – idénticas bit a bit, no solo parecidas.

Añade ScoreWithin(0.25) a la lista del evaluador para obtener una métrica de tolerancia; el conjunto por defecto es choice_accuracy, noul_accuracy, score_mae, mean_confidence, más ece. Desde la CLI, lo mismo es una sola bandera: laya-evals run data.jsonl --score-within 0.25 informa score_within_0.25 junto a las predeterminadas, y la bandera se repite, así que --score-within 0.25 --score-within 0.5 informa de ambas.

Una métrica de tolerancia necesita una respuesta score con una etiqueta numérica, así que en un conjunto de datos sin ninguna no tiene valor: run nombra la métrica que no pudo calcular en lugar de publicar un cero silencioso, y una puerta --min / --max que nombre esa métrica falla como ausente. Las tolerancias que se pidieron en una ejecución se registran en el bloque config del informe, así que una línea base revisada dice qué columnas espera.

Procesamiento por lotes y tiempos

--batch-size N puntúa hasta N filas consecutivas que comparten checkpoint y esquema de preguntas en una sola llamada. Ambas métricas de tiempo provienen de las mismas mediciones y responden a preguntas distintas: cada fila de un lote vuelve cuando vuelve el lote, así que su latency es la llamada entera, mientras que su cost_per_decision es 1/N de ella. Por lo tanto, el procesamiento por lotes sube latency_* y baja cost_per_decision_* sobre un conjunto de decisiones sin cambios, y --max latency_p50_ms=... pregunta si las solicitudes se sirvieron rápido, no si la ejecución fue barata. Sin --batch-size las dos coinciden.

compare ignora cualquier métrica *_ms a menos que una tolerancia la nombre, así que estas nunca hacen fallar una línea base por ruido de tiempos. Lo que hizo realmente el harness – el tamaño de lote pedido, la forma de runner a la que se resolvió, cuántas filas compartieron una llamada y el fragmento más grande – se registra en config.timing del informe, porque la bandera por sí sola no dice si se procesó algo por lotes. Esos contadores registran las llamadas emitidas, no las llamadas que volvieron: con laya-evals run --on-error skip, un fragmento cuya llamada lanzó una excepción sigue contando en rows_grouped y max_chunk, junto a sus entradas en config.errored. La opción por defecto es --on-error fail, que vuelve a lanzar la excepción en lugar de publicar un informe cuyas métricas solo cubren las llamadas que volvieron. Las dos métricas *_ms cuentan solo las llamadas que volvieron, así que una llamada fallida nunca aporta una latencia que no midió.

Agrupar las filas dentro de un lote

--sort-by-length agrupa filas de tamaño similar en la misma pasada hacia adelante, de modo que cada pasada se rellena hasta un máximo más corto en lugar de hasta la fila más larga que contiene. Es la forma de las llamadas, no sus respuestas: los resultados vuelven en el mismo orden y puntúan igual, que es por lo que research/ puede informar 2.15x sobre 10,000 tickets sin que cambie ninguna decisión.

Tiene que haber más de una pasada para reordenar, así que solo surte efecto con un --batch-size N por debajo del número de filas que agrupa la ejecución. config.timing mantiene separadas las dos afirmaciones: sort_by_length es lo que dijo la línea de comandos, sort_by_length_sent es lo que llegó al runner. Una ejecución sin --batch-size pide algo que no puede ocurrir, y lo dice con sent: false; un runner cuyo predict_batch es anterior al parámetro se puntúa sin ordenar en lugar de lanzar TypeError a mitad de una ejecución larga.

La puerta de abstención en un umbral

--min-confidence T reenvía el umbral de abstención opcional del núcleo (#361) a cada llamada que hace la ejecución, así que Router y ONNXAgent marcan las respuestas cuyo answer_confidence cae por debajo de T con low_confidence: True antes de que el harness las vea. A diferencia de la agrupación, esto cambia las respuestas que puntúan: la misma ejecución con T=0 y T=0.7 es un experimento distinto, y un barrido de precision@coverage es una serie de estos, no una única línea base a la deriva.

El rango aceptado es el de laya.confidence.check_min_confidence del núcleo – [0.0, 1.0], finito, no un bool – en lugar de una copia local, así que un valor que la propia puerta rechazaría falla como error de uso (salida 2) antes de que se cargue ningún checkpoint. 0.0 es una petición legal: es el brazo de control de un barrido de precision@coverage, y una comprobación que lo descartara ocultaría el propio piso del barrido.

Un runner cuyo predict o (para una ejecución por lotes) cuyo predict_batch sea anterior a la puerta es rechazado con un EvalError con nombre, no puntuado sin el umbral. Descartar en silencio un control de puntuación es la clase de mentira que este harness existe para evitar: el informe publicaría una cifra de precision@coverage para una política que nunca se ejecutó. config.timing registra tanto la petición como el hecho: min_confidence es el umbral que se solicitó, min_confidence_sent dice si alguna llamada de esta ejecución lo llevó realmente.

Particiones

compare y run informan números generales y, para --slice language|model|qid|tag, las mismas métricas por valor de partición, de modo que una regresión en un idioma o en una pregunta es visible sin leer el agregado. La partición model contiene el checkpoint que respondió cada fila: la propia elección del Router por solicitud, o el model del runner para un runner que no enruta.

Puertas de partición opcionales

La puerta de línea base general puede aprobar mientras una partición más pequeña de idioma o de pregunta regresa mal. Para convertir una partición revisada en un requisito de CI, guarda una política JSON como gates.json:

{
  "version": 1,
  "rules": [
    {"slice": {"language": "zh"}, "metric": "choice_accuracy",
     "min_count": 50, "max_drop": 0.05},
    {"slice": {"qid": "intent"}, "metric": "ece",
     "min_count": 50, "max": 0.10}
  ]
}
laya-evals run data.jsonl --baseline baseline.json --tolerance choice_accuracy=0.02 \
    --gate-policy gates.json --json report.json
laya-evals compare report.json --baseline baseline.json \
    --tolerance choice_accuracy=0.02 --gate-policy gates.json

Cada regla selecciona exactamente un valor de language, model, qid o tag y nombra la métrica exactamente como aparece en el informe de particiones. Tiene un min_count positivo y exactamente un límite: min o max comprueba el valor del candidato; max_drop permite como máximo esa disminución respecto a la línea base; max_increase permite como máximo ese aumento. Los dos últimos requieren --baseline. El recuento es el número de respuestas puntuadas para esa métrica en la partición seleccionada, en ambos informes para una regla relativa. Para ece, es el número de respuestas con una confianza finita y un valor booleano correct. Una partición o métrica ausente, muy pocas respuestas puntuadas, o casos omitidos/con error hacen fallar la puerta opcional. Las reglas relativas también exigen que ambos informes lleven identidades de ejecución coincidentes, de modo que la evidencia ausente no pueda aparecer como aprobada. Una regresión medida informa de la partición, la métrica, los recuentos, los valores y el límite. Una sintaxis de política no válida sale con 2 antes de que se cargue un checkpoint; un fallo de calidad sale con 1. La política se registra en config.gate_policy de un informe run --json. compare --gate-policy aplica a las mediciones guardadas la política proporcionada en esa línea de comandos. Si difiere de la política registrada del informe, compare lo dice; una reverificación explícita bajo una política nueva no cambia la política con la que se hizo la ejecución original.

La comparación general habitual sigue aplicándose, incluida su tolerancia y su comportamiento de línea base heredada. Sin --gate-policy, el informe y la comparación de particiones se comportan como antes.

Identidad de la ejecución

run registra lo que midió en el bloque config del informe, así que el artefacto que lee un revisor es revisable por sí solo:

clave significado
schema la forma del informe, laya-evals-report/1, para que un consumidor pueda rechazar uno que no sepa leer
dataset la ruta tal como se escribió – un nombre, no un hash
dataset_sha256 el sha256 de los bytes del conjunto de datos que se analizaron
questions_sha256 una huella del esquema de preguntas: el id, el tipo, las instructions y los criteria de cada pregunta, sobre todo el conjunto de datos
laya_version el laya que calculó los números
coverage_metric_definition qué definición de aurc / selective_accuracy@* produjo este informe (consulta cortes de cobertura). Una regla de puerta relativa sobre cualquiera de ellas rechaza una línea base registrada bajo otra distinta, en lugar de restar números que no significan lo mismo
gate_policy la política opcional de puertas de partición aplicada por run --gate-policy
thresholds la puerta que aplicó esta ejecución: min, max y baseline_tolerance
revisions el commit del que se cargó cada checkpoint que respondió (consulta más abajo)

dataset es una ruta, y una ruta no es una identidad: un conjunto de datos se puede editar in situ, mover o volver a obtener con el mismo nombre, y una caché de CI puede entregar a dos ejecuciones el mismo nombre de archivo y bytes distintos. questions_sha256 cubre lo que se preguntó en lugar de cuántas filas había, así que añadir estados a un conjunto de preguntas sin cambios deja la huella intacta — dataset_sha256 sigue moviéndose, y añadir una fila es un cambio en los datos, no en la pregunta.

También cubre instructions, porque el texto de las instrucciones es el prompt. build_sequence renderiza "<type> question: <instructions>" en la cabeza tokenizada, Agent rechaza una pregunta sin instrucciones (“add the text the model should answer”), y la propia identidad de pregunta de Laya ya lo cuenta: Router._question_schema y la agrupación por lotes de este harness se basan en todo el diccionario de preguntas, y tests/test_router_batch.py fija que reformular solo instructions mueve una fila a su propio grupo de lote. Entonces, ¿una instrucción reformulada sigue comparando igual con una línea base? No — y ese es el punto. “Judge whether a refund is justified” y “Be conservative and only approve explicit refund requests” formulan preguntas distintas, y la puerta de métricas solo puede darse cuenta cuando la diferencia mueve un número más allá de la tolerancia que nombraste. Nombrar una opción choice es el mismo argumento: criteria es el espacio de decisión, y las comprobaciones metamórficas de research/eval/metamorphic.py existen porque renombrar una etiqueta cambia las respuestas.

Nada del texto de las instrucciones se normaliza salvo el único paso que aplica el propio motor: unas instructions no textuales se hashean como json.dumps(ins, ensure_ascii=False), igual que Agent._to_internal. Así que tanto los espacios en blanco como la redacción cuentan, y una reformulación que una persona considera una corrección de estilo se trata como un experimento nuevo. Esa es la opción honesta por defecto — la alternativa es una heurística de similitud interponiéndose entre una ejecución y su línea base, y ningún sistema de evaluación de uso común tiene una.

No se registra nada con marca temporal, así que un informe sigue siendo reproducible byte a byte para un runner fijo.

REPORT_SCHEMA, questions_fingerprint(dataset) y file_fingerprint(path) son públicos, así que un llamador que maneje laya.evals.evaluate directamente obtiene la misma identidad que una ejecución de la CLI.

Línea base y puerta de CI

  • Mantén juntos el conjunto de datos, un informe de línea base (la salida --json que hayas revisado) y las tolerancias, y confírmalos en el repositorio, de modo que un cambio sea un diff revisable. --tolerance METRIC=VALUE es la deriva absoluta máxima permitida para esa métrica.
  • laya-evals run ... --baseline baseline.json --tolerance ... sale con un código distinto de cero ante una deriva, así que se integra en CI sin cambios. laya.evals.EvalReport.compare y assert_regression exponen la misma lógica para los tests.

La puerta de métricas responde “¿se movieron los números”. No puede responder “¿eran estos los mismos números”, porque compare lee overall y solo overall — así que una línea base registrada contra un conjunto de datos daría por bueno un candidato puntuado sobre otro, con una aritmética idéntica. EvalReport.comparable_to cierra ese hueco: compara schema, dataset_sha256 y questions_sha256, y run --baseline y compare siguen imprimiendo todos los deltas, y luego fallan con una salida distinta de cero que nombra la clave y ambos valores:

FAIL: baseline is not comparable: dataset_sha256 (dataset bytes): baseline is <sha>, this run is <sha>

Una clave ausente en cualquiera de los dos lados es desconocida, no un conflicto, así que todo informe escrito antes de que existiera la identidad sigue comparando exactamente como lo hacía. Eso incluye la línea base de la puerta programada de abajo, que proviene de research/eval/ y no tiene ningún config.schema.

Dos superficies de CI usan esto:

  • un trabajo sin pesos en .github/workflows/ci.yml ejecuta tests/test_evals.py y tests/test_evals_api.py, así que las matemáticas de las métricas, el análisis del conjunto de datos y la CLI quedan cubiertos en cada PR sin descargar un checkpoint;
  • .github/workflows/evals.yml se ejecuta semanalmente, antes de una publicación y bajo demanda: evalúa el checkpoint inglés sobre la suite MASSIVE en inglés y lo compara con research/results/eval_english_51_languages.json con las tolerancias de research/evals/thresholds.json. Sube el informe como artefacto y no bloquea un PR.

El harness es determinista para una revisión de checkpoint fija, así que un informe es reproducible. run registra el conjunto de datos, el modelo y la unidad, más los hechos de tiempos de la ejecución, en el bloque config del informe, y revisions: el commit del que se cargó realmente cada checkpoint que respondió. --revision <SHA> fija ese commit para cada checkpoint que carga la ejecución, y --revision english=<SHA> fija uno (repetible) — que es la forma que quiere una ejecución con enrutamiento automático, ya que los tres checkpoints son tres repositorios y un commit no puede existir en todos ellos. Sin fijar, la ejecución toma la rama por defecto del checkpoint y el informe sigue diciendo qué commit respondió, así que una deriva de línea base se puede atribuir a los pesos o al código. laya/revisions.py publica SHAs de commit revisados en PINNED_REVISIONS para los llamadores que quieran optar por ellos. Con --onnx, solo se aplica un --revision <SHA> simple, a la descarga de configuración y tokenizador.

Añadir el conjunto etiquetado real

Suelta un JSONL en research/evals/ y una línea base revisada junto a él, y luego apunta un flujo de trabajo (o research/evals/check_regression.py) a ambos. El formato es el mismo que el del fixture; nada en el harness sabe de MASSIVE.