Saltar al contenido
oracle

Oracle — tutorial práctico: cómo se programa con Oracle#

Guía complementaria al estudio integral que genera oracle-estudio --archivo ORACLE-PARA-NOTEBOOKLM.md (filosofía, especificación completa, auditoría, historia). Este es distinto a propósito: es un tutorial — aprender haciendo, de lo más simple a lo más compuesto, con ejemplos reales tomados del propio repositorio y de un proyecto que ya lo usa en producción (Jam, un plugin de Unreal Engine). Si el estudio generado responde «¿por qué existe Oracle?», este documento responde «¿cómo escribo la primera medida, y la segunda, y la que necesita algo más complicado?».


0. Oracle en una frase#

La superficie es cómo se escribe; el JSON es cómo se guarda.

Oracle es un lenguaje de datos (no una biblioteca de funciones) para escribir medidas: reglas que toman hechos sobre lo que se construyó, calculan un número, lo comparan contra un umbral, y si el umbral se viola, señalan exactamente qué filas lo violaron. Las medidas y los casos se escriben en una superficie legible y se guardan como JSON —son datos, no código— y por eso se pueden inspeccionar, mutar, contar y medir con las mismas herramientas que mide cualquier otra cosa.

Si nunca escribiste una medida, la meta de este documento es que después de leerlo puedas escribir la tuya sin haber leído el evaluador.


1. El modelo mental: tres niveles, una sola representación#

L0   evidencia   los HECHOS crudos: pieza(id, x, y, ex, ey) · mutante(id, apunta_a, murio)
L1   medidas     enunciados SOBRE L0: "ninguna pieza se clava en otra"
L2   medidas     enunciados SOBRE L1: "toda medida tiene al menos un mutante que la fija"

Lo importante: L2 no necesita mecanismos nuevos. Una medida (L1) es un dato, así que el catálogo de medidas es una relación más (medida(id, umbral_op, umbral_valor, porque, alcance, …)), y se puede medir con el mismo álgebra que mide piezas o eventos. Ese es el sentido de «metalenguaje»: no hay una capa especial para «medir la medición».

                    ┌──────────────┐
   hechos  ───────► │   MEDIDA     │ ───────► veredicto (ok / no-ok, valor, testigos, alcance)
 (evidencia)         └──────────────┘
                    tubería → resumen → umbral

Una medida no produce "verdad": produce un veredicto acotado — un número, si pasó o no, las filas que lo explican (testigos), y una declaración explícita de qué NO mira (alcance). Ningún veredicto se presenta sin su alcance: por diseño, Oracle no permite escribir una medida que diga «todo bien» sin decir también qué no miró.


2. Anatomía de una medida#

Toda medida, en su forma completa (canónica), se escribe así en la superficie infija:

medida <id>:
    de <relacion> <alias>
    [unir <relacion2> <alias2>]
    [donde <predicado>]
    [agrupar:
        clave <nombre> = <expresion>
        agregado <nombre> = <agregado>(<expresion>)]
    resumen <agregado>(<expresion>)
    umbral <comparador> <valor> segun <origen> porque "<por qué ese número>"
    [requiere <relaciones>]
    ambito <ámbito>
    alcance "<qué NO ve esta medida>"
PiezaQué esObligatorio
iddominio.nombre, minúsculas ASCII, dígitos y _sí
de …la fuente: de dónde salen los datos y qué alias recibensí
unir …producto cartesiano con otra fuenteopcional
donde …el filtro de lo que ofende — acá se definen los testigosopcional
agrupar:agrupa filas por claves y calcula agregados intermediosopcional
resumencómo se colapsa la tubería a UN escalar — la medición en sísí
umbralcomparador + valor + origen (segun) + defensa en texto (porque) de por qué ese valorsí, con origen y defensa no vacía
requieredeclara qué relaciones de evidencia son indispensablesopcional (obligatorio en medidas de ausencia)
ambitojurisdicción de la medidasí
alcancequé NO mira esta medida, en textosí, no puede estar vacío

Tres reglas no son estilo, son validación dura:

  1. Una medida sin defensa del umbral no carga, y una medida sin alcance no carga. Fallan al leerse, no al usarse — antes de evaluar un solo hecho.
  2. Un umbral de igualdad (==) está prohibido. La regla universal meta.ningun_umbral_de_igualdad exige umbrales de orden (<=, >=, <, >).
  3. Si una medida declara requiere <relacion>, la relación no puede faltar ni venir vacía. Si no hay evidencia requerida, la evaluación devuelve SIN EVIDENCIA y no un verde espurio.

La forma canónica#

Una medida se escribe sólo en superficie (.oracle). Al leerla, Oracle la convierte en un árbol JSON homoicónico —la forma canónica, que es su AST— y trabaja sobre él; oracle medida expandir <archivo> lo muestra:

["medida", "<id>",
  ["desde", ["de", "<relacion>", "<alias>"],
            ["donde", <predicado>]],
  ["resumen", "<agregado>", <expresion>],
  ["umbral", "<comparador>", <valor>, "<por qué ese número>", "<origen>"],
  ["requiere", "<relacion>"],
  ["ambito", "<ámbito>"],
  ["alcance", "<qué NO ve esta medida>"]]

No se escribe a mano. Oracle lo sigue leyendo como formato de intercambio, y para migrar un proyecto que todavía tenga medidas, casos o relaciones en JSON:

oracle convertir <directorio> --a-superficie             # muestra qué se puede convertir
oracle convertir <directorio> --a-superficie --escribir  # convierte y retira cada JSON

Cada archivo se reemplaza sólo si releer la superficie nueva devuelve exactamente el mismo árbol canónico; los que no se pueden convertir quedan intactos y el resumen dice por qué.

El ejemplo más simple posible del propio catálogo de Oracle:

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 igual con el código roto"
    alcance "cuenta mutantes DECLARADOS que sobrevivieron. NO ve los mutadores que nadie escribió"

Léelo en voz alta y ya sabés leer el 90% de las medidas que vas a encontrar: «de la relación mutante, alias m, quedate con los que sobrevivieron (detecciones == 0 y rechazos == 0); contá cuántos quedaron; si son más de 0, rojo — porque un mutante vivo es un test que no discrimina; y esto no ve los mutantes que nadie llegó a escribir».

Un detalle que sorprende la primera vez: los testigos no se declaran aparte. Los testigos —las filas que se muestran cuando la medida da rojo, para que alguien pueda mirar el defecto— son exactamente las filas que sobrevivieron al último donde. No hay una segunda función que las calcule, porque escribir la misma condición dos veces es exactamente cómo se desincroniza (fue un defecto real del proyecto — ver §7).

Tampoco hay composición de medidas (DECISION-002): una medida no puede invocar el resultado de otra medida. Cada medida juzga hechos directos del dominio.


3. El álgebra: cinco operadores, y nada más#

Toda la sintaxis sale de combinar cinco operadores. Cada uno toma filas (o hace de fuente) y devuelve filas: esa clausura es lo que permite encadenarlos sin casos especiales.

OperadorSuperficieQué hace
dede <relacion> <alias>fuente: trae una relación y la etiqueta con un alias
dondedonde <predicado>filtra — acá se definen los testigos
unirunir <relacion> <alias>producto cartesiano con otra fuente
agruparagrupar:\n clave ...\n agregado ...agrupa filas y las resume a una fila por grupo
resumenresumen <agregado>(<expresion>)colapsa TODA la tubería a un único escalar

resumen no es un paso de la tubería: es lo último que se aplica para obtener el escalar de juicio.

3.1 de — la fuente#

de pieza a

Trae todos los hechos de la relación pieza y los etiqueta con el alias a. A partir de acá, cada fila de la tubería tiene acceso a los campos del hecho a.

3.2 donde — el filtro (y los testigos)#

donde a.volumen > 0

Se queda sólo con las filas donde el predicado da true. Es el único lugar donde se definen los testigos: lo que sobrevive acá es lo que se le muestra a un humano cuando la medida da rojo.

3.3 Acceso a los datos: siempre explícito#

Tres accesores leen datos de una fila:

AccesorSuperficieEn AST (JSON)Devuelve
Campoa.volumen["campo", "a", "volumen"]un campo de un hecho con alias a
Hechohecho(a)["hecho", "a"]el hecho ENTERO (para pasarlo a una escalar)
Columnareales["col", "reales"]una columna o agregado derivado por agrupar

a.volumen — "en la fila actual, tomá el hecho con alias a y devolvé su campo volumen". Comparar contra un campo que no existe es un error, no da false: así un nombre de campo mal escrito no se disfraza de verde silencioso.

3.4 Comparadores y lógicos#

==  !=  <  <=  >  >=       y   o   no

Reglas del álgebra que sorprenden si vienen de Python:

3.5 resumen y los agregados#

resumen contar(1)
resumen max(a.volumen)

Cinco agregados: contar, max, min, suma, promedio. contar es especial: no evalúa la expresión, sólo cuenta filas — por eso la convención es escribir resumen contar(1) (el 1 es un relleno que nunca se mira). Sobre cero filas, cualquier agregado da 0. suma y promedio aceptan números o booleanos (0/1); min y max exigen valores del mismo tipo y comparables.

3.6 unir — comparar filas entre sí#

de documento a
unir documento b
donde a.nombre == b.nombre y a.carpeta != b.carpeta

unir hace el producto cartesiano: cada fila resultante tiene AMBOS alias (a y b) disponibles. Es así como se comparan hechos entre sí — piezas que se tocan, documentos homónimos, las dos puntas de un relevo. Acá: «dos documentos con el mismo nombre en carpetas distintas» — el defecto real que motiva vault.nombre_unico_en_el_vault (un wikilink resuelve por nombre, y dos homónimos lo dejan ambiguo).

Un unir sobre la misma relación cuenta cada par dos veces ((a,b) y (b,a)) y también empareja cada fila consigo misma (a == b); normalmente hay que filtrar eso en el donde (por ejemplo exigiendo a.carpeta != b.carpeta o a.id != b.id).

3.7 agrupar — cómo se expresa la AUSENCIA sin usar null#

Este es el operador que más cuesta la primera vez, porque resuelve algo que en SQL pide un LEFT JOIN con nulos — y acá no hay nulos. La pregunta es «¿qué módulos no tienen NINGÚN importador real?» — una ausencia, no una presencia.

de modulo m
unir importa i
agrupar:
    clave modulo = m.nombre
    agregado reales = suma(i.b == m.nombre y i.es_test == false)
donde reales == 0

El truco: se agrupa sobre el PRODUCTO sin filtrar primero, y se agrega con suma sobre un predicado booleano. Como un booleano suma 0 o 1, un módulo sin ningún importador real da reales = 0 — y el grupo sigue existiendo, porque nunca se filtró antes de agrupar. Sin necesitar un concepto de nulo.

Forma general:

agrupar:
    clave <nombre_clave> = <expresion>
    agregado <nombre_agregado> = <agregado>(<expresion>)

Después de agrupar, las filas ya NO tienen los alias originales (m, i desaparecen: se consumieron en el resumen). Las claves y agregados derivados se leen directamente por su nombre o con nombre.


4. Las macros: la forma corta#

La mayoría de las medidas del catálogo de Oracle están escritas con una macro. Una macro es azúcar sintáctica que se expande a la forma canónica ANTES de construir la medida — el evaluador, la mutación y el inventario nunca se enteran de que hubo una macro. oracle expandir <archivo> te muestra la expansión.

MacroSuperficiePara qué
ningunoninguno <id>:\n de <rel> <alias>\n donde <pred>\n umbral <= 0 segun <origen> porque "..."\n ambito <ámbito>\n alcance "..."ninguna fila debe cumplir el predicado — el 80% de los casos
ninguno-parninguno-par <id>:\n de <rel> <a1>\n unir <rel> <a2>\n donde <pred>\n umbral <= 0 segun <origen> porque "..."\n ambito <ámbito>\n alcance "..."lo mismo, sobre PARES de la misma relación
peorpeor <id>:\n de <rel> <alias>\n donde <expr> > <tol>\n resumen max(<expr>)\n umbral <= <tol> segun <origen> porque "..."\n ambito <ámbito>\n alcance "..."el peor caso de una magnitud no puede pasar de una tolerancia

Las tres macros de la tabla admiten una relación vacía: sirve cuando la relación sólo registra infracciones y [] significa que no hubo ninguna. Si la relación es el universo de sujetos que debe examinarse, usá ninguno-requiere, ninguno-par-requiere o peor-requiere. Tienen los mismos parámetros y agregan requiere <relación> antes de evaluar: una relación ausente o vacía produce SIN EVIDENCIA. Un resumen sobre cero filas sigue dando 0; requiere es la guarda ante la ausencia de sujetos.

En oracle juzgar --con hechos.json, las medidas propias sin relación en la evidencia se informan y hacen fallar la corrida. Para evaluar deliberadamente sólo parte del catálogo, usá --parcial o seleccioná medidas con --medida <id>. Una sombra no perdona SIN EVIDENCIA, aunque su cota admita el valor numérico del veredicto.

ninguno — el caso común#

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ó"

Expande EXACTO a la forma canónica de §2. ninguno cubre todo lo que se reduce a «filtrás lo que ofende, contás, cero es el único número aceptable».

peor — cuando el número importa, no la cuenta#

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 y no produce juntas visibles en una pieza de 4 m"
    ambito universal
    alcance "desvío del PIVOTE respecto de la grilla. NO ve si el pivote está donde debería dentro de la malla"

Ejemplo del catálogo de geometría de Jam. La tolerancia (1.0) aparece en donde y en umbral. La macro exige que ambos valores coincidan; si alguien cambia uno solo, la invocación falla. El desajuste fue un defecto real del proyecto (caso 012 del corpus).

Expande a:

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 y no produce juntas visibles en una pieza de 4 m"
    ambito universal
    alcance "desvío del PIVOTE respecto de la grilla. NO ve si el pivote está donde debería dentro de la malla"

ninguno-par#

ninguno-par tareas.misma_persona_sobrecargada_el_mismo_dia:
    de tarea a
    unir tarea b
    donde a.dueno == b.dueno y a.vence == b.vence y a.id != b.id
    umbral <= 0 segun contrato porque "dos tareas del mismo día para la misma persona compiten por las mismas horas"
    ambito universal
    alcance "ve coincidencia de fecha y dueño. NO ve cuánto dura cada tarea ni si el día alcanza igual"

(Ejemplo ilustrativo, con la misma forma que vault.nombre_unico_en_el_vault del catálogo real de Jam.) El patrón general de ninguno-par: igualar el campo que define el conflicto (dueno + vence) y exigir que difieran en la identidad (id) — si no, cada tarea se empareja consigo misma y el predicado da siempre verdadero.

Las macros no son un embudo#

Si tu caso no encaja en ninguna macro, la forma canónica sigue siendo 100% válida. El ejemplo de colocacion.interpenetracion en §5.3 usa unir sobre DOS relaciones distintas (pieza y vecina) y no tiene macro que lo cubra — se escribe canónico y listo.


5. Seis ejemplos reales, de menor a mayor complejidad#

Todos están en producción hoy: los tres primeros en el propio catálogo de Oracle, los otros tres en el catálogo de geometría de Jam (un consumidor real e independiente).

5.1 Contar lo que ofende (el patrón más común)#

Ya lo viste en §2 (proceso.test_con_mutante_que_lo_mata). Receta: filtrás lo malo con donde, contás con resumen contar(1), umbral <= 0.

5.2 Medir una magnitud con una función de dominio (peor + escalar)#

peor snap.yaw:
    de pieza a
    donde desvio_de_paso(a.yaw, 90.0) > 0.5
    resumen max(desvio_de_paso(a.yaw, 90.0))
    umbral <= 0.5 segun convencion porque "medio grado en una pieza de 4 m da ~3 cm en la punta: el límite donde una junta se abre a la vista"
    ambito universal
    alcance "sólo el YAW contra su paso. NO ve pitch ni roll, ni si la pieza mira al lado correcto"

desvio_de_paso no es parte del álgebra: es una función escalar (UDF) que el proyecto Jam declaró — ver §6. El álgebra no sabe nada de grados ni de grillas; sólo sabe llamar funciones declaradas y comparar sus resultados.

5.3 Comparar filas entre sí con unir (forma canónica, sin macro)#

medida colocacion.interpenetracion:
    de pieza a
    unir vecina b
    donde no es_fondo(hecho(b)) y penetracion(hecho(a), hecho(b)) > 0
    resumen max(penetracion(hecho(a), hecho(b)))
    umbral <= 0 segun contrato porque "`penetracion` ya descuenta la tolerancia de contacto: tocarse da 0 y clavarse da >0"
    alcance "solape de AABB entre piezas de escala comparable. NO ve la malla real, oclusión visual, ni si la pieza quedó flotando"

Por qué NO es una macro: unir combina DOS relaciones distintas (pieza y vecina), no la misma consigo misma. ninguno-par no encaja, así que se escribe la forma canónica.

Fijate también es_fondo: sin ese filtro, cualquier pieza chica "interpenetraría" el fondo de escenografía (un SkySphere gigante) y la medida daría rojo siempre — un caso real de por qué el alcance y el filtro tienen que decir la verdad completa sobre qué se está comparando.

5.4 unir sin donde, resumiendo con min#

medida snap.comparte_cara:
    de pieza a
    unir objetivo b
    resumen min(solape_lateral_minimo(hecho(a), hecho(b)))
    umbral > 1.0 segun convencion porque "el solape lateral debe superar la tolerancia de 1 cm: tocar una arista o estar en diagonal no cuenta"
    alcance "solape de AABB en los dos ejes laterales. NO ve cuánto de la cara real de la malla coincide"

No todos los desde tienen donde: acá no hace falta filtrar, sólo unir y resumir directo con min. Fijate también el umbral > en vez de <= — el comparador lo elige la medida, no está fijo a <= 0.

5.5 agrupar en un caso real: que la traza de una simulación no tenga huecos#

medida simulacion.la_traza_no_tiene_huecos:
    de evento e
    agrupar:
        clave corrida = e.corrida
        agregado registrados = contar(1)
        agregado ultimo = max(e.t)
    donde registrados != ultimo + 1
    resumen contar(1)
    umbral <= 0 segun convencion porque "una traza con huecos describe otra corrida que la que ocurrió: si faltan pasos, cualquier cosa que se mida sobre ella habla de lo que se registró y no de lo que pasó"
    requiere evento
    alcance "compara cuántos eventos hay contra el instante final, asumiendo que el tiempo arranca en cero y avanza de a uno. NO ve trazas donde varios eventos comparten instante, ni sabe si el que falta es importante. Si evento viene vacío la medida NO concluye —lo declara en requiere, y sale SIN EVIDENCIA en vez de verde—."

mas es una escalar del núcleo (+1) — así se expresa aritmética sobre un campo ordinal (t), porque una relación es una bolsa sin orden: «consecutivo» se vuelve aritmética sobre el campo, no una propiedad implícita del almacenamiento.

5.6 L2: una medida sobre medidas#

ninguno meta.toda_medida_esta_fijada:
    de medida_en_uso m
    donde m.debe_tener_mutantes == true y (m.mutantes == 0 o m.mutantes_vivos != 0)
    umbral <= 0 segun contrato porque "una medida propia con cero mutantes pasa vacuamente igual que una cuyos mutantes sobreviven: en ambos casos el catálogo la contiene pero la mutación no demuestra que esté fijada"
    ambito universal
    alcance "exige al menos un mutante y ninguno vivo sólo cuando `debe_tener_mutantes` es verdadero. NO vuelve a exigirlos a medidas heredadas —responde su corpus de origen— ni a las evaluadas aparte, y NO ve los mutadores que nadie escribió. Si medida_en_uso viene vacía no hay medidas sin fijar y verde es correcto; además contiene una fila por medida cargada por construcción"

Ninguna sintaxis nueva: medida_en_uso es una relación como cualquier otra (la produce nucleo/marco.py a partir del catálogo real), y esta medida la mide con el mismo ninguno de siempre. Así es como el marco se verifica con sus propias herramientas.


6. Funciones escalares (UDF): cuando el álgebra no alcanza#

El álgebra no sabe geometría, ni de grillas, ni de nada de un dominio particular — a propósito. Lo que sí sabe hacer es llamar funciones declaradas, con nombre, aridad y unidad, para que aparezcan en el inventario y se puedan discutir igual que un umbral.

from oracle_metalenguaje import escalar

@escalar("volumen", "cm3", unidades_argumentos=("sin_unidad",))
def volumen(p: dict) -> float:
    return p["ex"] * p["ey"] * p["ez"]

@escalar("desvio_de_grilla", "cm", unidades_argumentos=("sin_unidad", "cm"))
def desvio_de_grilla(p: dict, grilla: float) -> float:
    """El peor desvío del PIVOTE respecto de la grilla, sobre los tres ejes."""
    return max(abs(v - round(v / grilla) * grilla) for v in (p["lx"], p["ly"], p["lz"]))

@escalar("penetracion", "cm", unidades_argumentos=("sin_unidad", "sin_unidad", "cm"))
def penetracion(a: dict, b: dict, tol: float = 1.0) -> float:
    """Profundidad efectiva en cm después de descontar la tolerancia de contacto."""
    solapes = []
    for (ca, ea), (cb, eb) in zip(_ejes(a), _ejes(b)):
        solape = (ea + eb) - abs(ca - cb)
        if solape <= tol:
            return 0.0
        solapes.append(solape)
    return min(solapes) - tol

(Estos tres son reales, del escalares.py del dominio de geometría de Jam.)

Reglas:


7. El corpus: cómo se escribe un caso#

La primera regla del repositorio: el caso del corpus se escribe ANTES que la medida. Dos motivos, no es prolijidad:

  1. una medida escrita primero se escribe para pasar, no para atrapar el defecto real;
  2. la herramienta puede decirte si tu medida está bien 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 igual. El caso del corpus es lo único que la detecta.

Toda la regla de las medidas aplica acá: la superficie (.caso) es cómo se escribe; el JSON es cómo se guarda — y el corpus carga ambos formatos por igual, sin paso de traducción.

Un caso en superficie tiene esta forma (ejemplo real, un falso_verde):

caso 001-verde-acumulativo:
    fecha: "2026-07-29"
    origen:
        repo: "Brianholl/jam"
        commit: "todos"
    procedencia: observada
    titulo: "«489 tests OK» reportado cada turno: un número que sube y nunca significa más"
    etiqueta: falso_verde
    sintoma:
        El agente cerró cada entrega con un conteo de tests en verde. El conteo crece monótonamente y no distingue haber cubierto algo nuevo de haber agregado tests a lo ya cubierto. Se lee como «está bien» y sólo dice «no se rompió lo de antes».
    como_se_detecto: persona
    medida: proceso.afirmacion_declara_alcance
    evidencia:
        afirmacion: id, texto, comando, alcance
            "a1", "459 tests OK", "unittest discover", ""
            "a2", "477 tests OK", "unittest discover", ""
            "a3", "489 tests OK", "unittest discover", ""
    leccion:
        Una afirmación de verde sin alcance declarado no es una afirmación verificable: es una cifra. El alcance es lo que la vuelve discutible.

Y uno verde_correcto — la otra polaridad, igual de necesaria:

caso 102-verificacion-vigente:
    fecha: "2026-07-29"
    origen:
        repo: "Brianholl/jam"
        commit: "sesión 2026-07-29"
    procedencia: observada
    titulo: "Después de recorrer el motor, los commits siguientes fueron sólo de documentación"
    etiqueta: verde_correcto
    sintoma:
        Se volvió a correr la verificación con motor y a partir de ahí sólo cambiaron documentos. `relevo.py` declaró la verificación vigente, y era cierto.
    como_se_detecto: observacion
    medida: proceso.verificacion_vigente
    evidencia:
        verificacion: que, commit, camino
            "motor", "80373ea", "editor headless"
        cambio: archivo, commiteado, es_codigo_vivo
            "RELEVO.md", true, false
            "Vault-kb/README.md", true, false
    leccion:
        La regla mira QUÉ cambió y no CUÁNTO: por eso los commits de documentación no invalidan una verificación, y eso es lo que la hace usable en vez de molesta.

Las etiquetas#

etiquetaSignifica
falso_verdealgo estaba mal y la medición dijo bien
falso_rojoalgo estaba bien y la medición dijo mal (pesa igual de grave: enseña a ignorar el verificador)
verde_correctoalgo estaba bien y la medición dijo bien — la otra polaridad, sin ella quitar_filtro sobrevive siempre
deuda_de_diseñoun defecto del propio lenguaje, no de una medida de dominio
medida_correcta_conclusion_erradala medida dio el veredicto correcto pero alguien sacó la conclusión causal equivocada de ella

¿Por qué necesito la otra polaridad (verde_correcto)?#

Es el error más común al empezar. Con contar y umbral <= 0, una medida sin la evidencia positiva siempre puede pasar vaciando la relación — quitarle el filtro a una medida sólo se nota si hay filas que NO ofenden y que deberían seguir dando verde. Sin verde_correcto, el corpus tiene "sólo defectos" y varias mutaciones (sobre todo quitar_filtro) sobreviven siempre. Es lo mismo que evaluar un clasificador únicamente con ejemplos positivos.

Casos sin medida: abierto, resuelto, limite_humano#

Un caso puede no tener una medida todavía ("medida": null o medida: null). Entonces declara estado_sin_medida:

caso 004-testigos-duplicados:
    fecha: "2026-07-29"
    origen:
        repo: "Brianholl/jam"
        commit: "535d476"
    procedencia: observada
    titulo: "La medición y sus testigos recorrían los datos dos veces, con dos definiciones"
    etiqueta: deuda_de_diseño
    sintoma:
        `INTERPENETRACION` declaraba `mide=penetracion_maxima` y `testigos=piezas_clavadas`: dos funciones que recorren lo mismo con la misma condición escrita dos veces. Nada garantiza que no se separen.
    como_se_detecto: persona
    medida: null
    estado_sin_medida: resuelto
    resuelto:
        2026-07-29, por construcción: los testigos son las filas que sobrevivieron a la única tubería de la medida; ya no existe una segunda función donde repetir la condición.
    evidencia:
        declaracion: medida, mide, testigos, condicion_repetida
            "colocacion.interpenetracion", "penetracion_maxima", "piezas_clavadas", true
    leccion:
        Si el lenguaje obliga a escribir dos veces la misma condición, el lenguaje está mal. Los testigos no son un cálculo aparte: son el filtro.

8. Un proyecto de punta a punta#

Armemos un proyecto mínimo desde cero: un gestor de tareas donde ninguna tarea vencida puede quedar sin persona asignada.

8.1 La carpeta#

mi-proyecto/
  oracle.json
  escalares.py
  catalogos/
    tareas/
      tareas.vencida_sin_dueno.oracle
  corpus/
    tareas/
      001-vencida-sin-nadie.caso
      002-vencida-con-dueno.caso

8.2 oracle.json#

{
  "esquema": "oracle.proyecto/v1",
  "perfiles": []
}

(Sin catalogo_base: true no se cargan las medidas universales de proceso/meta/simulacion — sólo tu catálogo. Las activás si además vas a medir TU PROCESO de construcción con un LLM.)

8.3 La escalar (opcional en este ejemplo, para mostrar el patrón)#

# escalares.py
from oracle_metalenguaje import escalar

@escalar("dias_de_atraso", "dias", unidades_argumentos=("sin_unidad",))
def dias_de_atraso(tarea: dict) -> int:
    return max(0, tarea["dias_vencida"])

8.4 La medida#

La escribís en la superficie infija (con la macro ninguno — el caso más común):

ninguno tareas.vencida_sin_dueno:
    de tarea t
    donde t.vencida == true y t.asignada == false
    umbral <= 0 segun contrato porque "una tarea vencida sin dueño no la va a hacer nadie: el atraso queda invisible hasta que alguien la busca a mano"
    ambito universal
    alcance "ve sólo el par vencida+sin-dueño. NO ve si la persona asignada realmente puede resolverla, ni cuán vencida está"

Y la guardás tal cual: el catálogo carga .oracle igual que .json, así que no hay paso de traducción.

mv tareas.vencida_sin_dueno.oracle catalogos/tareas/

8.5 El corpus — las dos polaridades#

caso 001-vencida-sin-nadie:
    fecha: "2026-07-31"
    origen:
        repo: "mi-proyecto"
        commit: "ejemplo"
    procedencia: construida
    titulo: "Una tarea vencida hace tres días y sin asignar"
    etiqueta: falso_verde
    sintoma:
        El tablero mostraba todo en orden porque nadie miraba las tareas sin dueño.
    como_se_detecto: persona
    medida: tareas.vencida_sin_dueno
    evidencia:
        tarea: id, vencida, asignada, dias_vencida
            "t1", true, false, 3
    leccion:
        Una tarea vencida sin dueño no aparece en ningún filtro habitual del tablero.
caso 002-vencida-con-dueno:
    fecha: "2026-07-31"
    origen:
        repo: "mi-proyecto"
        commit: "ejemplo"
    procedencia: construida
    titulo: "Vencida pero con alguien encima — no debe dar rojo"
    etiqueta: verde_correcto
    sintoma:
        Una tarea vencida CON dueño asignado no es el defecto que esta medida busca.
    como_se_detecto: observacion
    medida: tareas.vencida_sin_dueno
    evidencia:
        tarea: id, vencida, asignada, dias_vencida
            "t2", true, true, 1
    leccion:
        Sin este caso, quitarle el filtro `asignada` a la medida no lo notaría nadie.

(El id del caso y del archivo usan dueno en ASCII: los identificadores son nombres de archivo y por diseño rechazan caracteres no ASCII para evitar divergencias entre normalizaciones NFC/NFD).

8.6 El contexto del proyecto y correr todo#

Antes de verificar, podés ver todo lo que tu proyecto expone con un solo comando:

cd mi-proyecto
oracle contexto           # relaciones, campos, escalares, operadores y medidas disponibles
oracle contexto --compacto # la misma información en ~1.600 tokens (ideal para editores y LLMs)
oracle test               # secuencia completa: corpus, sintaxis, aceptación y mutación

Por qué oracle contexto complementa este tutorial en vez de acortarlo: este tutorial enseña a pensar una medida —los tres niveles, la clausura del álgebra, las macros, el rol de los testigos y las polaridades del corpus—. oracle contexto no explica nada de eso: da la fotografía viva y concreta del proyecto en el que estás trabajando. En vez de alternar entre oracle relaciones, oracle escalares y listados de catálogo, tenés el inventario activo en una sola salida.

oracle test tiene que confirmar: el caso 001 se pone ROJO con tareas.vencida_sin_dueno, y el 002 se pone VERDE. Si la mutación encuentra un mutante que sobrevive (por ejemplo, sacarle el y y dejar sólo vencida == true), es que falta un tercer caso que discrimine esa mutación específica — una tarea vencida CON dueño, que ya tenemos, o una NO vencida sin dueño, que faltaría agregar.

O, desde Python, como biblioteca:

from oracle_metalenguaje import Motor

motor = Motor.desde_proyecto("mi-proyecto")
informe = motor.evaluar({"tarea": [{"id": "t1", "vencida": True, "asignada": False}]})
print(informe.ok)     # False
print(informe.texto())

9. Los comandos: cuál usar y cuándo#

ComandoPara qué
oracle init [ruta]inicializa un proyecto con catalogos/, corpus/, diferencial/ y oracle.json
oracle caso nuevo <grupo/id>crea el andamio de un caso nuevo, ya en superficie (.caso)
oracle caso listarlista los casos del corpus, su etiqueta y qué medida reclaman
oracle caso generar <medida>fabrica evidencia discriminante para fijar mutaciones a partir de sobrevivientes
oracle nueva <dominio.nombre>crea el andamio de una medida, ya en superficie infija (.oracle)
oracle revisar <archivo>valida una medida suelta contra la evidencia
oracle medida probar <arch> --con <filas>corre una medida contra filas escritas a mano (--vigilar para re-probar al guardar)
oracle expandir <archivo>muestra la forma canónica a la que expande una macro
oracle contexto [--compacto]reúne relaciones, campos, escalares, operadores y medidas de TU proyecto (~1.600 tokens con --compacto)
oracle test [--rapido\|--todo]secuencia completa: corpus, sintaxis, aceptación, diferencial y mutación
oracle relacionesver qué hechos y campos existen HOY (derivados de evidencia real)
oracle escalaresver las funciones de dominio, operadores y agregados disponibles
oracle manual [tema]la referencia del lenguaje armada de sus fuentes (--html para el sitio, --man para páginas de manual; tema medidas para las 54 universales y sus alcances)
oracle biblioteca instaladasqué bibliotecas de políticas hay instaladas y cuáles usa este proyecto
oracle biblioteca verificar <ruta>certifica una biblioteca antes de confiar en ella
oracle biblioteca listar <ruta>muestra umbrales, orígenes (segun) y alcances de una biblioteca
oracle diagnosticoqué versión de Oracle corre y desde dónde, sin publicar nada del dominio
tools/sintaxis.py --verificarcomprueba ida y vuelta entre JSON y superficie en todo el catálogo, macros, corpus y bloques de documentación
python -m unittest discover -s tests -t . -qla suite de tests, sin dependencias externas

Todos aceptan --proyecto <ruta> (o $ORACLE_PROYECTO) y, si el proyecto declara escalares.py, exigen --confiar-escalares para ejecutarlo. Sin esa bandera, las inspecciones (--help, --relaciones, --nueva, --escalares sin UDF externas, contexto) son siempre seguras.

Heredar un catálogo sin quedar en rojo el primer día#

Cuando un proyecto adopta un catálogo que no escribió —el catálogo base de Oracle, o una biblioteca de políticas— suele salir rojo en cosas reales que nadie va a arreglar hoy. Apagar la medida sería volver al verde que no significa nada, así que hay una tercera opción: la sombra. Se declara en oracle.json y la medida se evalúa, se informa con la marca [EN SOMBRA], y no tumba la corrida:

{
  "sombra": {
    "meta.toda_medida_filtra_o_agrupa": {
      "desde": "2026-09-01",
      "porque": "tres medidas heredadas sin filtro; se arreglan de a una"
    }
  }
}

Los dos campos son obligatorios y hay medidas que los vigilan: una sombra sin fecha no se puede envejecer, una sin motivo no se puede discutir, y una sobre una medida que ya da verde no tiene nada que perdonar. La sombra además envejece:

Ninguna de esas medidas se puede poner en sombra a sí misma, que es lo que impide que la sombra se coma su propio control.


10. Errores frecuentes al escribir tu primera medida#

Lo que vesQué significaCómo se arregla
el umbral <= 0 no trae defensafalta el texto de porque en umbralagregá por qué ese número y no otro
hay que declarar qué NO vefalta alcance, o está vacíoescribí honestamente el punto ciego
«>» sobre un valor ausenteun campo mal escrito o que no existe en la evidencia--relaciones para ver los campos reales
«==» sobre un flotante…comparaste igualdad exacta de dos flotantesusá <=/>= con una tolerancia
medida que nunca se pone rojala condición del donde probablemente está invertidaescribí el caso del corpus primero; si no se pone rojo, la medida mide al revés
medida que nunca se pone verdefalta un caso verde_correcto, o el filtro es demasiado amplioagregá evidencia donde la medida DEBE dar verde
el id «x» está dos vecesdos archivos del catálogo declaran el mismo idcambiale el nombre a uno de los dos
la relación «x» no existe en la evidenciatu tubería pide una relación que la evidencia no traeuna relación vacía se declara [] explícitamente, nunca se omite

Lo que la herramienta no puede decirte: 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 validaciones automáticas — está bien formada, discrimina, y mide exactamente al revés de lo que pensabas. Por eso el caso del corpus va primero: es lo único que lee la intención.


11. Glosario rápido#

TérminoDefinición corta
superficie infijala forma humana y legible en que se escriben las medidas (.oracle) y los casos (.caso)
hechoun registro de campos escalares (sin objetos anidados)
relaciónuna bolsa nombrada de hechos del mismo tipo — el equivalente a una tabla
evidenciael mapa completo relación → lista de hechos que se le pasa a una medida
medidaun dato que describe cómo medir algo: tubería + resumen + umbral + alcance
testigoslas filas que sobrevivieron al último donde — se muestran cuando la medida da rojo
umbralel límite contra el que se compara el valor medido, con su defensa en texto (porque)
requieredeclaración explícita de relaciones necesarias para no emitir veredictos vacíos
alcancelo que la medida explícitamente NO mira — obligatorio, no puede estar vacío
escalar (UDF)una función de dominio declarada con @escalar, para lo que el álgebra no sabe hacer sola
macroazúcar sintáctica (ninguno, ninguno-requiere, ninguno-par, ninguno-par-requiere, peor, peor-requiere) que expande a la forma canónica
corpusla colección de casos reales (defectos y aciertos) que fija que las medidas midan lo que dicen medir
dominioel conjunto de medidas + escenario + implementación de referencia que verifica un proyecto externo (geometría, vault, etc.)
fixture diferencialevidencia versionada + veredicto de una implementación independiente, para comparar contra Oracle
L0 / L1 / L2evidencia / medidas sobre evidencia / medidas sobre medidas — la misma representación en los tres niveles

Para seguir#

Esta página se genera desde docs/tutorial-practico.md.