Saltar al contenido
oracle

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:

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

  1. Qué declara toda medida: umbral <comparador> <número> segun <origen> porque "<defensa>" y alcance "<punto ciego>".
  2. Las relaciones con sus campos y tipos derivados de la evidencia que existe.
  3. Con qué se escribe: operadores (agrupar, de, donde, resumen, unir), comparadores, lógicos, agregados y escalares declaradas.
  4. Las medidas que ya existen en el catálogo con lo que NO ven.
  5. 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.

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ó"
MacroPara quéCuántas la usan
ningunoninguna fila debe cumplir el predicado29
ninguno-requierelo mismo, declarando evidencia indispensable4
ninguno-parlo mismo sobre PARES de la misma relación0
peorel peor caso de una expresión no pasa de una tolerancia0

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:

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:

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é pasaQué dice
falta la defensa del umbralel umbral <= 0 no trae defensa
falta alcancehay que declarar qué NO ve
un campo mal escrito«>» sobre un valor ausente — mirá --relaciones
nunca se pone rojauna medida que no puede fallar no mide nada
nunca se pone verdeprobablemente 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:

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.