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 confirmada0.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):
| Fuerza | Significado | Ejemplo |
|---|---|---|
| Strict | Imposible de anular | “Sin autenticacion no se accede a la admin API” |
| Defeasible | Puede ser anulado por excepciones | “Una funcion por archivo” |
| Defeater | Solo 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
| Rego | toulmin | |
|---|---|---|
| Escritura de reglas | Requiere aprender Rego DSL | Funciones Go |
| Manejo de excepciones | Patron default/else manual | Grafo defeats declarativo |
| Juicio | allow/deny binario | [-1, +1] valor continuo |
| Justificacion de reglas | # METADATA (ignorado por el motor) | backing (parte de la estructura) |
| Fuerza de reglas | No existe | strict/defeasible/defeater |
| Tamano del motor | Decenas de miles de lineas | Cientos de lineas |
| Velocidad | Interprete (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
| Strength | Cantidad | Proporcion | Ejemplo |
|---|---|---|---|
| Strict | 15 | 68% | F1, F2, F3, F4, A1-A3, A6-A16 |
| Defeasible | 4 | 18% | Q1, Q2, Q3, C4 |
| Defeater | 3 | 14% | F5, F6, excepcion de archivos de test |
La mayoria son strict — las convenciones de estructura de codigo por naturaleza minimizan las excepciones.
Resultados cuantitativos
| Proyecto | Archivos (antes->despues) | LOC/archivo promedio (antes->despues) | Violaciones SRP resueltas | Violaciones depth resueltas |
|---|---|---|---|---|
| filefunc | — (cumplimiento desde el inicio) | 25.1 | 0 | 0 |
| fullend | 87->1,260 | 244->25.4 | 66->0 | 148->0 |
| whyso | 12->99 | 147.8->24.4 | 12->0 | 23->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:
| Elemento | Obra original |
|---|---|
| Estructura de 6 elementos | Toulmin (1958) |
| strict/defeasible/defeater | Nute (1994) |
| h-Categoriser | Amgoud & 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