Los motores de reglas han operado sobre la misma premisa durante 60 anos. Esa premisa: el objeto de verificacion es un “hecho (fact)”.

Drools introduce objetos Java como “facts” en la working memory. Rego trata input como datos ya verdaderos. JSON Schema asume que la estructura del documento esta dada. Todos parten de la misma suposicion: los datos que entran son hechos.

Pero, cual es la razon de ser de un motor de reglas? Verificar si los datos cumplen las reglas. Llamar “ya verdadero” a algo que necesita verificacion es una contradiccion.

No son hechos, son afirmaciones

El objeto de verificacion no es un fact, sino un claim (afirmacion). Es una declaracion que puede ser verdadera o falsa. Su validez debe ser determinada por las reglas.

JWT ya sigue este principio. Llama a sub, exp, iss no “facts” sino “claims”. Son afirmaciones del emisor del token. Solo despues de verificar la firma, comprobar la expiracion y contrastar el issuer se puede confiar en ellas.

Esta estructura ya fue establecida en 1958.

El modelo de argumentacion de Toulmin

Stephen Toulmin analizo en 1958 la estructura de la argumentacion en 6 elementos:

  • Claim (afirmacion): El objeto de juicio. Lo que debe verificarse como verdadero o falso.
  • Ground (fundamento): Los datos de evidencia utilizados para el juicio.
  • Warrant (garantia): La regla que determina que el fundamento respalda la afirmacion.
  • Backing (respaldo): La justificacion de por que la regla es valida.
  • Qualifier (calificador): El grado de certeza del juicio.
  • Rebuttal (refutacion): Las condiciones excepcionales bajo las cuales la afirmacion no se sostiene.

La logica formal dice: “si las premisas son verdaderas, la conclusion tambien lo es”. Toulmin penso diferente. “Una afirmacion esta respaldada por fundamentos y reglas, pero si hay condiciones excepcionales, se revierte.” Toda argumentacion es refutable.

Los motores de reglas han estado del lado de la logica formal durante 60 anos. La entrada es un fact, el resultado es allow/deny, las excepciones son un mecanismo aparte. Toulmin estaba en el lado opuesto. La entrada es un claim, el resultado es un grado (degree), las excepciones estan integradas.

El problema: el libro de Toulmin estaba en la estanteria de filosofia. No era visible desde la estanteria de motores de reglas. Un eslabon perdido de 60 anos.

Asi que construimos un motor de reglas

toulmin es la implementacion del modelo de argumentacion de Toulmin como un motor de reglas en Go.

Los requisitos evolucionan

Veamos como if-else y toulmin responden a la misma evolucion.

// Lunes: "Solo usuarios autenticados, bloqueo por IP, red interna exenta de bloqueo"
g := toulmin.NewGraph("api:access")
auth    := g.Warrant(isAuthenticated, nil, 1.0)
blocked := g.Rebuttal(isIPBlocked, nil, 1.0)
exempt  := g.Defeater(isInternalIP, nil, 1.0)
g.Defeat(blocked, auth)
g.Defeat(exempt, blocked)

// Martes: "Agregar Rate limiting"
limited := g.Rebuttal(isRateLimited, nil, 1.0)
g.Defeat(limited, auth)

// Miercoles: "Usuarios premium exentos de Rate limit"
premium := g.Defeater(isPremiumUser, nil, 1.0)
g.Defeat(premium, limited)

// Jueves: "Durante incidentes, incluso premium tiene limite"
incident := g.Rebuttal(isIncidentMode, nil, 1.0)
g.Defeat(incident, premium)

2 lineas agregadas por dia, sin cambios en el codigo existente. La misma evolucion con if-else:

// Lunes
if user != nil {
    if blockedIPs[ip] {
        if strings.HasPrefix(ip, "10.") {
            allow = true
        }
    } else {
        allow = true
    }
}

// Jueves — 4 niveles de anidamiento, estructura incomprensible
if user != nil {
    if blockedIPs[ip] {
        if strings.HasPrefix(ip, "10.") {
            allow = true
        }
    } else if isRateLimited(ip) {
        if isPremium(user) {
            if !incidentMode {
                allow = true
            }
        }
    } else {
        allow = true
    }
}

toulmin: 2 lineas por requisito, estructura invariable. if-else: hay que reestructurar todo cada vez.

Las reglas son funciones Go

func(claim any, ground any, backing any) (bool, any)
  • ground = material de juicio que cambia con cada solicitud (usuario, IP, contexto)
  • backing = criterio de juicio fijado al declarar el grafo (umbrales, nombres de roles, configuracion)
  • Retorno = (resultado del juicio, evidencia). La evidencia es de tipo libre segun el dominio.
func CheckOneFileOneFunc(claim, ground, backing any) (bool, any) {
    g := ground.(*FileGround)
    if len(g.Funcs) > 1 {
        return true, &Evidence{Got: len(g.Funcs), Expected: 1}
    }
    return false, nil
}

No es necesario aprender un nuevo lenguaje como Rego. Basta con escribir funciones Go.

backing — misma funcion, diferentes criterios de juicio

backing transmite los criterios de juicio de la regla como valores en tiempo de ejecucion. Registrar la misma funcion con diferentes backing crea reglas separadas:

g := toulmin.NewGraph("access")
admin  := g.Warrant(isInRole, "admin", 1.0)
editor := g.Warrant(isInRole, "editor", 0.8)
g := toulmin.NewGraph("line-limit")
strict  := g.Warrant(CheckLineCount, &LineLimit{Max: 100}, 0.7)
relaxed := g.Warrant(CheckLineCount, &LineLimit{Max: 200}, 0.5)
g.Defeat(relaxed, strict)

Si backing es nil, significa que la regla no necesita criterios de juicio.

Las excepciones se declaran como grafo

Al declarar las relaciones entre reglas con la Graph Builder API, el motor las gestiona automaticamente. La funcion es el identificador. No se necesitan nombres de cadena.

g := toulmin.NewGraph("filefunc")
w := g.Warrant(CheckOneFileOneFunc, nil, 1.0)
d := g.Defeater(TestFileException, nil, 1.0)
g.Defeat(d, w)

results, _ := g.Evaluate(claim, ground)

La misma funcion puede reutilizarse en diferentes grafos con diferentes relaciones de derrota:

strictGraph := toulmin.NewGraph("strict")
strictGraph.Warrant(CheckOneFileOneFunc, nil, 1.0)
// Sin excepciones — archivos de test tampoco se permiten

lenientGraph := toulmin.NewGraph("lenient")
w := lenientGraph.Warrant(CheckOneFileOneFunc, nil, 1.0)
r1 := lenientGraph.Rebuttal(TestFileException, nil, 1.0)
r2 := lenientGraph.Rebuttal(GeneratedFileException, nil, 0.8)
lenientGraph.Defeat(r1, w)
lenientGraph.Defeat(r2, w)
// Excepcion para archivos de test y generados

Se rastrea el fundamento del juicio

EvaluateTrace rastrea no solo el verdict sino tambien que reglas se activaron y cuales derrotaron a cuales:

traced := g.EvaluateTrace(claim, ground)
// traced[0].Verdict: +0.6
// traced[0].Trace: [
//   {Name: "CheckOneFileOneFunc", Role: "warrant",  Activated: true,  Qualifier: 1.0},
//   {Name: "TestFileException",   Role: "rebuttal", Activated: true,  Qualifier: 1.0},
// ]

Cuando hay decenas de reglas, un humano puede leer “por que se obtuvo este verdict”.

El juicio se calcula con una sola formula

Se aplico el h-Categoriser de Amgoud (2013):

raw = w / (1 + Σ raw(attackers))
verdict = 2 × raw - 1
  • +1.0 — violacion confirmada
  • 0.0 — juicio indeterminado
  • -1.0 — refutacion confirmada

Cuando una regla se dispara, se convierte en warrant. Si una excepcion tambien se dispara, se convierte en attacker. La formula calcula la fuerza de ambos y produce el verdict. Si hay una excepcion de la excepcion? Se convierte en attacker del attacker, restaurando la regla original. Principio de compensacion (Compensation) — una propiedad que solo el h-Categoriser satisface.

Las reglas tienen tres niveles de fuerza

Se aplico la clasificacion de Nute (1994):

FuerzaSignificadoEjemplo
StrictImposible de anular“Sin autenticacion no se accede a la admin API”
DefeasiblePuede ser anulado por excepciones“Una funcion por archivo”
DefeaterSolo bloquea otras reglas sin hacer afirmaciones propias“Los archivos de test son excepcion”

Las reglas Strict rechazan aristas de ataque. Los Defeaters solo atacan y no emiten juicio propio. Esto expresa estructuralmente el nivel de aplicacion de las reglas.

Que lo diferencia de Rego

Regotoulmin
Escritura de reglasRequiere aprender Rego DSLFunciones Go
Manejo de excepcionesPatron default/else manualGrafo defeats declarativo
Juicioallow/deny binario[-1, +1] valor continuo
Justificacion de reglas# METADATA (ignorado por el motor)backing (parte de la estructura)
Fuerza de reglasNo existestrict/defeasible/defeater
Tamano del motorDecenas de miles de lineasCientos de lineas
VelocidadInterprete (parsing->AST->evaluacion)Llamada directa a funciones Go

Rego es amplio — tiene un ecosistema integrado con Kubernetes, Terraform, Envoy. toulmin es profundo — tiene lo que Rego no tiene (defeasibility, qualifier, backing).

La reubicacion del Qualifier

En el modelo original de Toulmin, el Qualifier esta adjunto al Claim. “Probablemente se le debe administrar penicilina a este paciente” — es un modificador modal que expresa el grado de certeza de la afirmacion.

El motor toulmin reubico el Qualifier del Claim a cada Rule. En un motor de reglas, el claim es solo el objeto de verificacion. “Este archivo tiene 3 funciones” — es una constatacion de hechos, no algo a lo que se le deba adjuntar un grado de certeza. Lo que determina la calidad del juicio es el grado de certeza de la regla:

  • “Una funcion por archivo” — qualifier 1.0 (regla estricta)
  • “Recomendado menos de 100 lineas” — qualifier 0.7 (regla flexible)

El qualifier de cada Rule se convierte en el peso inicial w(a) del h-Categoriser, y el verdict final cumple el rol que el Qualifier tenia en el modelo original de Toulmin: el grado de certeza del juicio.

Validacion empirica: conversion de 22 reglas de filefunc a Toulmin

filefunc es una herramienta de convencion de estructura de codigo para desarrollo nativo con LLM en Go. Las 22 reglas fueron convertidas completamente a warrants de Toulmin.

Clasificacion por fuerza

StrengthCantidadProporcionEjemplo
Strict1568%F1, F2, F3, F4, A1-A3, A6-A16
Defeasible418%Q1, Q2, Q3, C4
Defeater314%F5, F6, excepcion de archivos de test

La mayoria son strict — las convenciones de estructura de codigo por naturaleza minimizan las excepciones.

Resultados cuantitativos

ProyectoArchivos (antes->despues)LOC/archivo promedio (antes->despues)Violaciones SRP resueltasViolaciones depth resueltas
filefunc— (cumplimiento desde el inicio)25.100
fullend87->1,260244->25.466->0148->0
whyso12->99147.8->24.412->023->0

fullend paso de 87 archivos a 1,260. El numero de archivos exploto, pero el LOC promedio bajo de 244 a 25.4. Las 66 violaciones SRP y las 148 violaciones de depth se redujeron todas a 0.

Base teorica

No hay teoria original. Todo proviene de investigaciones existentes:

ElementoObra original
Estructura de 6 elementosToulmin (1958)
strict/defeasible/defeaterNute (1994)
h-CategoriserAmgoud & Ben-Naim (2013)

La originalidad esta en el descubrimiento de que estos se conectan. Lo que existia por separado durante 60 anos en filosofia (Toulmin), logica (Nute) y teoria de la argumentacion (Amgoud) se encuentra en un solo punto: el motor de reglas de software.

Calcular contratos

El estado de derecho funciona no porque los jueces sean inteligentes. Funciona porque la estructura obliga al juicio. Hay reglas, las excepciones estan declaradas y el veredicto se produce segun la evidencia.

toulmin traslado esta estructura al codigo.

  • Warrant = articulo de ley
  • Backing = proposito legislativo
  • Strength = norma imperativa vs norma dispositiva
  • Rebuttal = clausula de excepcion
  • Claim = caso
  • Ground = evidencia
  • h-Categoriser = veredicto

Se declara el contrato (warrant), se declaran las excepciones (rebuttal), se introduce la evidencia (ground) y el juicio (verdict) se calcula.

No es una persona quien juzga. Es una formula la que calcula.

Acc(a) = w(a) / (1 + Σ Acc(attackers))

Se puede definir el grafo en YAML

Sin codigo Go, se declara la estructura del grafo en YAML y se genera el codigo:

graph: filefunc
rules:
  - name: CheckOneFileOneFunc
    role: warrant
    qualifier: 1.0
  - name: TestFileException
    role: rebuttal
    qualifier: 1.0
defeats:
  - from: TestFileException
    to: CheckOneFileOneFunc
toulmin graph filefunc.yaml    # genera graph_gen.go

Solo hay que escribir las funciones de reglas en Go. La estructura del grafo la declara el YAML.

MIT License. github.com/park-jun-woo/toulmin