Escribir una medida#
Esto existe para que no haga falta pedirle permiso a nadie. Todo el argumento del repositorio es que quien ve un defecto pueda escribir la regla que lo atrapa; si para eso hay que saber cómo está hecho el evaluador, el único que puede escribir reglas es quien lo escribió — y ese es exactamente el problema que veníamos a resolver.
La superficie es cómo se escribe; el JSON es cómo se guarda. Este documento enseña a escribir medidas y casos directamente en su superficie de autoría (.oracle y .caso), que el sistema carga por igual sin paso de traducción.
Instalación#
Requiere Python 3.11 o posterior y no tiene dependencias de runtime. Desde el checkout de Oracle, la forma principal de instalar el comando es:
uv tool install .
oracle --help
Para probarlo sin instalar:
uvx --from . oracle --help
Si no tenés uv, hace falta un entorno virtual. Un pip install al Python del sistema falla en Arch, Debian 12+, Ubuntu 23.04+ y Fedora con externally-managed-environment, y saltearse esa protección rompe paquetes del sistema:
python -m venv .venv && . .venv/bin/activate
python -m pip install -e .
oracle --help
Con uv no hace falta nada de esto, y además deja oracle-lsp en el PATH, que es lo que el editor necesita para encontrarlo.
Después trabajás desde tu proyecto o lo pasás explícitamente:
oracle init <tu-proyecto>
oracle test --proyecto <tu-proyecto> --confiar-escalares
El orden importa: primero el caso, después la medida#
Escribí el caso del corpus antes que la medida. No es prolijidad:
- una medida escrita primero se escribe para pasar, no para atrapar;
- la herramienta puede decirte si tu medida está mal formada, pero no puede saber qué quisiste decir. Una condición invertida —que selecciona lo que está bien en vez de lo que ofende— pasa todas las comprobaciones automáticas. El caso es lo único que lo detecta.
# 1. el caso: la evidencia del defecto, y que se espera ROJO
# (el andamio ya nace en superficie .caso, o copiá uno que exista)
oracle caso proceso/001-lo-que-paso # crea corpus/proceso/001-lo-que-paso.caso
# 2. mirá con qué contás: el contexto de tu proyecto en un solo comando
oracle contexto # relaciones, campos, escalares, operadores y medidas existentes
oracle contexto --compacto # lo mismo en un quinto del texto (~1.600 tokens vs ~8.600)
# (o por separado si sólo querés una parte: oracle relaciones / oracle escalares)
# 3. la medida: el andamio ya nace en superficie infija, y el catálogo lo carga tal cual
oracle nueva colocacion.mi_regla # crea catalogos/colocacion/colocacion.mi_regla.oracle
oracle revisar catalogos/colocacion/colocacion.mi_regla.oracle
# 4. que todo siga cerrando
oracle test # corpus, sintaxis, aceptación, diferencial si hay fixtures, y mutación de medidas
oracle contexto: el inventario vivo de tu proyecto#
oracle contexto junta en una sola salida todo lo que hace falta para escribir una medida en el proyecto donde estás parado:
- Qué declara toda medida:
umbral <comparador> <número> segun <origen> porque "<defensa>"yalcance "<punto ciego>". - Las relaciones con sus campos y tipos derivados de la evidencia que existe.
- Con qué se escribe: operadores (
agrupar,de,donde,resumen,unir), comparadores, lógicos, agregados y escalares declaradas. - Las medidas que ya existen en el catálogo con lo que NO ven.
- La regla de orden: escribir el caso antes que la medida.
Con --compacto, la misma salida se emite en un quinto del texto (~1.600 tokens contra ~8.600 de correr los comandos que reemplaza).
Por qué complementa esta guía en vez de acortarla: este documento explica la semántica del álgebra, el modelo homoicónico en JSON, las macros, los comparadores prohibidos (como la igualdad flotante) y las reglas de diseño. oracle contexto no reemplaza esas explicaciones: entrega el inventario concreto y vivo del proyecto para no tener que buscar campos o funciones a mano mientras escribís.
Superficie de autoría e intercambio#
Escribí medidas en .oracle, casos en .caso y relaciones en .relacion. Oracle también lee JSON como formato de intercambio y para migrar proyectos anteriores. Si un mismo id aparece en superficie y en JSON, la carga señala ambos archivos: no elige uno en silencio.
oracle caso <grupo/NNN-descripcion>: crea el andamio.caso.oracle nueva <dominio.nombre>: crea el andamio.oracle.oracle convertir <medida.json>: convierte una medida anterior a la superficie.oracle medida expandir <medida.oracle>: muestra el árbol canónico para inspección.oracle convertir <directorio> --a-superficie: muestra qué medidas y casos JSON se pueden migrar; con--escribir, reemplaza cada origen sólo si la ida y vuelta conserva el árbol canónico. Informa las relaciones pendientes mientras no esté disponible la conversión por lote a.relacion.
En la evidencia de un .caso, usá tabla cuando todas las filas compartan campos cuyos nombres se puedan imprimir como cabecera. Usá fila {…} cuando las filas tengan campos distintos o un nombre no se pueda imprimir como cabecera; cada línea contiene un objeto con esa fila. Es el escape de la misma sintaxis .caso para conservar la evidencia sin pérdida.
El id tiene gramática cerrada y ASCII: dominio.nombre para medidas y NNN-descripcion para casos (minúsculas, dígitos y _/-). No es que el proyecto no sea en español —la prosa de porque y de alcance lo es entera—: es que el id es también un nombre de archivo, y en Unicode dueño puede ser dos secuencias de bytes distintas que se dibujan idénticas (NFC contra NFD). Dos ids que nadie puede distinguir mirando son una divergencia silenciosa, y eso se cierra por gramática.
Frontera de confianza#
Si el proyecto declara funciones en escalares.py, los comandos que cargan o evalúan su catálogo requieren --confiar-escalares. Esa bandera autoriza cargar código Python externo, pero Oracle lo ejecuta en un trabajador separado: el proceso principal sólo recibe metadatos y resultados JSON.
Lo que ese confinamiento sí detiene: leer el CONTENIDO de archivos fuera del proyecto, escribir fuera, abrir red, crear procesos y usar ctypes.
Lo que no detiene, y conviene saberlo antes de correr un escalares.py ajeno: los metadatos del sistema de archivos. Una UDF puede preguntar si existe cualquier ruta, leer tamaños, permisos y fechas, y devolver eso como resultado. No es un descuido — os.stat no emite ningún evento auditable en CPython, así que el mecanismo no puede verlo — y está declarado en el docstring de nucleo/aislamiento/escalares.py con un test que lo fija.
--confiar-escalares es opt-in por esto: la pregunta no es si el sandbox es perfecto, es si confiás en ese archivo. Si una UDF necesita más autoridad de la que el confinamiento da, no pertenece a una medida: generá ese dato antes y entregalo como evidencia.
--relaciones y --escalares sin la bandera son seguros: no ejecutan el archivo externo.
El id tiene una gramática cerrada: dominio.nombre, con segmentos en minúsculas ASCII, dígitos o _. No se aceptan rutas ni ..; el archivo se resuelve y confina debajo de catalogos/ antes de crear cualquier directorio.
La forma corta: las macros#
La mayoría de las medidas del catálogo están escritas como macro. Son azúcar que expande a la forma canónica —oracle expandir <archivo> te muestra en qué—, así que el evaluador, la mutación y el inventario no se enteran de que existen.
ninguno proceso.test_con_mutante_que_lo_mata:
de mutante m
donde m.detecciones_conductuales == 0 y m.rechazos_del_algebra == 0
umbral <= 0 segun contrato porque "un mutante que sobrevive es un test que no discrimina"
ambito universal
alcance "cuenta mutantes DECLARADOS. NO ve los que nadie escribió"
| Macro | Para qué | Cuántas la usan |
|---|---|---|
ninguno | ninguna fila debe cumplir el predicado | 29 |
ninguno-requiere | lo mismo, declarando evidencia indispensable | 4 |
ninguno-par | lo mismo sobre PARES de la misma relación | 0 |
peor | el peor caso de una expresión no pasa de una tolerancia | 0 |
peor exige la misma tolerancia en el filtro y en el umbral; la plantilla valida que coincidan:
peor snap.grilla:
de pieza a
donde desvio_de_grilla(hecho(a), 100.0) > 1.0
resumen max(desvio_de_grilla(hecho(a), 100.0))
umbral <= 1.0 segun convencion porque "por debajo de 1 cm el desvío no se ve"
ambito universal
alcance "desvío del PIVOTE. NO ve si el pivote está bien puesto dentro de la malla"
La invocación repite la tolerancia, y el lector rechaza valores distintos. Ese desajuste era el caso 012 del corpus.
Las macros no son un embudo: si tu caso no encaja, la forma canónica sigue siendo válida. colocacion.interpenetracion está escrita así porque une dos relaciones DISTINTAS.
La forma canónica#
medida dominio.nombre:
de relacion x
donde <lo que OFENDE>
resumen contar(1)
umbral <= 0 segun contrato porque "por qué ese número y no otro"
requiere relacion
alcance "qué NO ve esta medida"
Las piezas obligatorias están por una razón:
umbralconsegun— el número declara de dónde sale:medicion,contrato,convencionotanteo. La prosa deporquepuede quedar vacía, salvo en untanteo, donde sigue haciendo falta explicar qué se probó.alcance— un verde que no dice lo que no miró se lee como «está bien». Con esto, el informe termina enumerando sus propios puntos ciegos.requiere— declara qué relaciones de evidencia son indispensables para concluir. Si una relación requerida viene vacía o falta, la evaluación no emite un verde espurio sinoSIN EVIDENCIA.
Y una que no se declara: los testigos son las filas que sobrevivieron al donde. No los calculás aparte — si lo hicieras, tendrías la misma condición escrita dos veces y nada que las mantenga sincronizadas. Tampoco se permite componer medidas entre sí (DECISION-002): cada medida es una unidad de juicio aislada sobre evidencia directa.
La forma canónica: lo que Oracle guarda por dentro#
Una medida se escribe sólo en superficie, en un archivo .oracle. Al leerla, Oracle la convierte en un árbol JSON —la forma canónica— y trabaja sobre ese árbol. oracle medida expandir lo muestra; la medida de arriba queda así:
["medida", "dominio.nombre",
["desde", ["de", "relacion", "x"],
["donde", ["==", ["campo", "x", "activo"], false]]],
["resumen", "contar", 1],
["umbral", "<=", 0, "por qué ese número y no otro", "contrato"],
["requiere", "relacion"],
["alcance", "qué NO ve esta medida"]]
No hace falta escribirlo nunca: es la representación interna, como el bytecode de un lenguaje. Existe porque es homoicónica: el árbol es directamente la medida, y eso habilita lo que Oracle hace con ella:
- La mutación la debilita transformando el árbol, sin un parser en cada etapa.
- Las medidas pueden hablar de medidas: es el nivel L2. El catálogo se vuelve una relación (
medida_en_uso) y se juzga con la misma álgebra (por ejemplo, que ninguna use un umbral de igualdad o que todas declaren su defensa y alcance). - El diferencial guarda su huella, así que dos escrituras distintas de la misma medida son la misma medida.
Oracle sigue leyendo JSON —es el formato de intercambio—, pero una medida, un caso o una relación se escriben en superficie. Para migrar un proyecto viejo: oracle convertir <directorio> --a-superficie.
Tres ejemplos, de menor a mayor#
1. Contar lo que ofende#
medida proceso.test_con_mutante_que_lo_mata:
de mutante m
donde m.detecciones_conductuales == 0 y m.rechazos_del_algebra == 0
resumen contar(1)
umbral <= 0 segun contrato porque "un mutante que sobrevive es un test que no discrimina: pasa con el código roto"
alcance "cuenta mutantes DECLARADOS que sobrevivieron. NO ve los que nadie escribió"
El 90% de las medidas son así: filtrás lo malo, contás, y el umbral es <= 0 (un umbral == no se usa y está prohibido por meta.ningun_umbral_de_igualdad).
2. Medir una magnitud, no contar#
medida snap.grilla:
de pieza a
donde desvio_de_grilla(hecho(a), 100.0) > 1.0
resumen max(desvio_de_grilla(hecho(a), 100.0))
umbral <= 1.0 segun convencion porque "por debajo de 1 cm el desvío no se ve"
alcance "desvío del PIVOTE. NO ve si el pivote está bien puesto dentro de la malla"
Acá el valor es centímetros y no una cuenta, y eso dice más en el informe. Escrita a mano en forma canónica, la tolerancia aparece dos veces —en el donde y en el umbral— y nada las mantiene juntas: era el caso 012 del corpus. La macro peor exige que ambas apariciones coincidan.
3. Comparar filas entre sí#
medida vault.nombre_unico_en_el_vault:
de documento a
unir documento b
donde a.nombre == b.nombre y a.carpeta != b.carpeta
resumen contar(1)
umbral <= 0 segun contrato porque "un wikilink apunta por NOMBRE y no por ruta: dos homónimos dejan el enlace a cara o cruz"
alcance "NO ve nombres parecidos pero distintos, que confunden aunque no rompan un enlace"
unir hace el producto de una relación consigo misma. Es como se comparan cosas de a pares: piezas que se clavan, documentos homónimos, las dos puntas de un relevo.
Los errores que la herramienta sí te dice#
| Qué pasa | Qué dice |
|---|---|
| falta la defensa del umbral | el umbral <= 0 no trae defensa |
falta alcance | hay que declarar qué NO ve |
| un campo mal escrito | «>» sobre un valor ausente — mirá --relaciones |
| nunca se pone roja | una medida que no puede fallar no mide nada |
| nunca se pone verde | probablemente la condición esté invertida |
Comparar contra un campo que no existe es un error, no un False. Un False silencioso convertiría un nombre mal escrito en un verde, que es la peor falla posible acá.
Lo que NO te puede decir#
Si la condición dice lo que quisiste decir. Una medida que selecciona lo que está bien en vez de lo que ofende pasa todas las comprobaciones: está bien formada, discrimina, y mide exactamente al revés. La herramienta no lee intenciones.
Por eso el caso va primero. Y por eso tools/mutar.py existe: comprueba que el corpus fije tu medida, o sea que si alguien la escribiera distinta, algún caso lo notaría.
Cuando la medida no es tuya#
Dos cosas que aparecieron después de que este documento se escribiera, y que cambian qué hacés cuando el rojo viene de una medida que no escribiste vos:
oracle manuales la referencia del lenguaje armada de las declaraciones, no escrita aparte. Incluye los operadores, orígenes de umbral, etiquetas, relaciones explicadas y el temamedidas(oracle manual medidas), que lista las medidas universales con qué NO ve cada una. Ofrece tres vistas de la misma fuente: en terminal, en HTML para el sitio (--html), o en formato de páginas de manual (oracle manual --instalar-man <dir>dejaman oracle(1)yman oracle-segun(7)funcionando).- La sombra y su envejecimiento. Si heredás un catálogo y sale rojo en algo real que hoy no vas a arreglar, no apagues la medida: declarala en sombra en
oracle.json, condesdeyporque. Se sigue midiendo e informando como[EN SOMBRA], y no tumba la corrida. Los dos campos son obligatorios porque la sombra ahora envejece:meta.ninguna_sombra_envejece_sin_revisarse: si la sombra tiene más de 90 días, la medida falla.meta.toda_sombra_declara_una_fecha_real: si la fecha no se puede parsear o está en el futuro, falla.meta.ninguna_sombra_ya_en_verde: prohíbe tener en sombra medidas que ya dan verde.meta.ninguna_sombra_sobre_una_medida_que_no_existe: prohíbe sombras huérfanas. Ninguna de estas medidas puede ponerse en sombra a sí misma.
Si te falta un hecho#
Si lo que querés medir no está en --relaciones, no se agrega acá: se agrega en el sensor, que vive con el proyecto que produce los datos. El sensor produce hechos y no juzga; el álgebra juzga y no mira el mundo. Mezclarlos es cómo se llega a un verificador que nadie puede discutir.
Esta página se genera desde docs/03-escribir-una-medida.md.