Motores de regras se apoiam na mesma premissa ha 60 anos. A premissa de que o objeto de verificacao e um “fato (fact)”.
O Drools coloca objetos Java como “facts” na working memory. O Rego trata o input como dados ja verdadeiros. O JSON Schema assume que a estrutura do documento e um dado. Todos partem da mesma suposicao — os dados recebidos sao fatos.
Mas qual e a razao de existir de um motor de regras? E verificar se os dados satisfazem as regras. Chamar de “ja verdadeiro” algo que precisa ser verificado e uma contradicao.
Nao e fato, e afirmacao
O objeto de verificacao nao e um fact, mas sim um claim (afirmacao). E uma declaracao que pode ser verdadeira ou falsa. Sua validade precisa ser julgada por regras.
O JWT ja segue esse principio. Chama sub, exp, iss nao de “facts”, mas de “claims”. Sao afirmacoes do emissor do token. So se pode confiar neles apos verificar a assinatura, confirmar a expiracao e comparar o issuer.
Essa estrutura ja foi estabelecida em 1958.
O modelo de argumentacao de Toulmin
Stephen Toulmin analisou a estrutura da argumentacao em 6 elementos em 1958:
- Claim (afirmacao): O objeto de julgamento. Aquilo cuja veracidade ou falsidade precisa ser verificada.
- Ground (fundamento): Os dados de evidencia usados no julgamento.
- Warrant (garantia): A regra que determina que o fundamento sustenta a afirmacao.
- Backing (respaldo): A justificativa de por que a regra e valida.
- Qualifier (qualificador): O grau de certeza do julgamento.
- Rebuttal (refutacao): Condicoes de excecao em que a afirmacao nao se sustenta.
A logica formal diz “se as premissas sao verdadeiras, a conclusao tambem e”. Toulmin foi diferente. “A afirmacao e sustentada por fundamentos e regras, mas se ha condicoes de excecao, ela e revertida.” Toda argumentacao e refutavel.
Motores de regras estiveram do lado da logica formal por 60 anos. A entrada e um fact, o resultado e allow/deny, excecoes sao mecanismos separados. Toulmin estava do lado oposto. A entrada e um claim, o resultado e um grau (degree), excecoes sao integradas.
O problema era que o livro de Toulmin estava na prateleira de filosofia. Nao era visivel na prateleira de motores de regras. Um elo perdido de 60 anos.
Entao construi um motor de regras
toulmin e a implementacao do modelo de argumentacao de Toulmin como um motor de regras em Go.
Requisitos evoluem
Veja como if-else e toulmin respondem a mesma evolucao.
// Segunda-feira: "Somente usuarios autenticados, bloqueio de IP aplicado, rede interna isenta do bloqueio"
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)
// Terca-feira: "Adicionar Rate limiting"
limited := g.Rebuttal(isRateLimited, nil, 1.0)
g.Defeat(limited, auth)
// Quarta-feira: "Usuarios premium isentos do Rate limit"
premium := g.Defeater(isPremiumUser, nil, 1.0)
g.Defeat(premium, limited)
// Quinta-feira: "Durante resposta a incidentes, ate premium e limitado"
incident := g.Rebuttal(isIncidentMode, nil, 1.0)
g.Defeat(incident, premium)
Duas linhas adicionadas por dia, sem alteracao no codigo existente. A mesma evolucao com if-else:
// Segunda-feira
if user != nil {
if blockedIPs[ip] {
if strings.HasPrefix(ip, "10.") {
allow = true
}
} else {
allow = true
}
}
// Quinta-feira — 4 niveis de aninhamento, estrutura incompreensivel
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 linhas por requisito, estrutura invariavel. if-else: reescreve toda a estrutura a cada vez.
Regras sao funcoes Go
func(claim any, ground any, backing any) (bool, any)
ground= material de julgamento que muda a cada requisicao (usuario, IP, contexto)backing= criterio de julgamento fixado na declaracao do grafo (limiares, nomes de roles, configuracoes)- Retorno =
(resultado do julgamento, evidencia). Evidencia e um tipo livre por 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
}
Nao precisa aprender uma nova linguagem como Rego. Basta escrever funcoes Go.
backing — mesma funcao, criterios de julgamento diferentes
O backing passa o criterio de julgamento da regra como valor em runtime. Registrar a mesma funcao com backings diferentes cria regras distintas:
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)
Se o backing for nil, significa que a regra nao precisa de criterio de julgamento.
Excecoes sao declaradas como grafo
Declare as relacoes entre regras com a Graph Builder API e o motor cuida do resto. A funcao e o identificador. Nao e necessario nome em string.
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)
A mesma funcao pode ser reutilizada em grafos diferentes com relacoes de derrota diferentes:
strictGraph := toulmin.NewGraph("strict")
strictGraph.Warrant(CheckOneFileOneFunc, nil, 1.0)
// Sem excecao — arquivos de teste tambem nao sao permitidos
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)
// Arquivos de teste + gerados sao excecao
Rastreamento do fundamento do julgamento
EvaluateTrace rastreia nao apenas o verdict, mas quais regras foram ativadas e qual regra derrotou qual:
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},
// ]
Quando ha dezenas de regras, e possivel ler de forma compreensivel “por que esse verdict foi gerado”.
O julgamento e calculado por uma unica formula
Aplicou-se o h-Categoriser de Amgoud (2013):
raw = w / (1 + Σ raw(attackers))
verdict = 2 × raw - 1
+1.0— violacao confirmada0.0— julgamento inconclusivo-1.0— refutacao confirmada
Quando uma regra dispara, torna-se um warrant. Se uma excecao tambem dispara, torna-se um attacker. A formula calcula a forca de ambos e produz o verdict. E se ha excecao da excecao? Torna-se um attacker do attacker, restaurando a regra original. Principio de compensacao — uma propriedade que somente o h-Categoriser satisfaz.
Regras tem tres niveis de forca
Aplicou-se a classificacao de Nute (1994):
| Forca | Significado | Exemplo |
|---|---|---|
| Strict | Impossivel de neutralizar | “Acesso a admin API sem autenticacao proibido” |
| Defeasible | Pode ser neutralizado por excecao | “Uma funcao por arquivo” |
| Defeater | Bloqueia outras regras sem fazer afirmacao propria | “Arquivos de teste sao excecao” |
Regras Strict rejeitam arestas de ataque. Defeater apenas ataca sem ter seu proprio julgamento. Isso expressa estruturalmente o nivel de imposicao das regras.
Qual a diferenca para o Rego
| Rego | toulmin | |
|---|---|---|
| Escrita de regras | Requer aprender Rego DSL | Funcoes Go |
| Tratamento de excecoes | Padrao default/else manual | Grafo de defeats declarativo |
| Julgamento | Binario allow/deny | Valor continuo [-1, +1] |
| Justificativa da regra | # METADATA (ignorado pelo motor) | backing (parte da estrutura) |
| Forca da regra | Inexistente | strict/defeasible/defeater |
| Tamanho do motor | Dezenas de milhares de linhas | Centenas de linhas |
| Velocidade | Interpretador (parsing→AST→avaliacao) | Chamada direta de funcoes Go |
Rego e amplo — possui ecossistema de integracao com Kubernetes, Terraform, Envoy. toulmin e profundo — possui o que Rego nao tem (defeasibility, qualifier, backing).
O reposicionamento do Qualifier
No modelo original de Toulmin, o Qualifier e anexado ao Claim. “Provavelmente devemos administrar penicilina a este paciente” — e um qualificador modal que expressa o grau de certeza da afirmacao.
O motor toulmin reposicionou o Qualifier do Claim para cada Rule. Em um motor de regras, o claim e apenas o objeto de verificacao. “Este arquivo tem 3 funcoes” — e uma verificacao factual, nao algo a que se atribui grau de certeza. O que determina a qualidade do julgamento e a certeza da regra:
- “Uma funcao por arquivo” — qualifier 1.0 (regra certa)
- “Recomendado menos de 100 linhas” — qualifier 0.7 (regra flexivel)
O qualifier de cada Rule torna-se o peso inicial w(a) do h-Categoriser, e o verdict final substitui o papel que o Qualifier desempenhava no modelo original de Toulmin — o grau de certeza do julgamento.
Validacao empirica: conversao das 22 regras do filefunc para Toulmin
filefunc e uma ferramenta de convencao de estrutura de codigo para desenvolvimento Go nativo para LLM. Todas as 22 regras foram convertidas em warrants Toulmin.
Classificacao por forca
| Strength | Qtd | Proporcao | Exemplo |
|---|---|---|---|
| Strict | 15 | 68% | F1, F2, F3, F4, A1-A3, A6-A16 |
| Defeasible | 4 | 18% | Q1, Q2, Q3, C4 |
| Defeater | 3 | 14% | F5, F6, excecao de arquivos de teste |
A maioria e strict — convencoes de estrutura de codigo inerentemente minimizam excecoes.
Resultados quantitativos
| Projeto | Num. arquivos (antes→depois) | Media LOC/arquivo (antes→depois) | Violacoes SRP resolvidas | Violacoes depth resolvidas |
|---|---|---|---|---|
| filefunc | — (em conformidade desde o 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 |
O fullend passou de 87 para 1.260 arquivos. O numero de arquivos explodiu, mas a media de LOC caiu de 244 para 25.4. Todas as 66 violacoes de SRP e 148 violacoes de depth foram reduzidas a zero.
Fundamentacao teorica
Nao ha teoria original. Tudo e pesquisa existente:
| Elemento | Autoria original |
|---|---|
| Estrutura de 6 elementos | Toulmin (1958) |
| strict/defeasible/defeater | Nute (1994) |
| h-Categoriser | Amgoud & Ben-Naim (2013) |
A originalidade esta na descoberta de que eles se conectam. O que existia separadamente por 60 anos em filosofia (Toulmin), logica (Nute) e teoria da argumentacao (Amgoud) se encontra em um unico ponto: o motor de regras de software.
Calculando contratos
O estado de direito funciona nao porque os juizes sao inteligentes. Funciona porque a estrutura impoe o julgamento. Ha regras, excecoes sao declaradas e o veredito e produzido conforme as evidencias.
toulmin transpoe essa estrutura para codigo.
- Warrant = dispositivo legal
- Backing = intencao legislativa
- Strength = norma cogente vs norma dispositiva
- Rebuttal = clausula de excecao
- Claim = caso
- Ground = evidencia
- h-Categoriser = sentenca
Declare o contrato (warrant), declare as excecoes (rebuttal), insira as evidencias (ground) e o julgamento (verdict) e calculado.
Nao e uma pessoa que julga. E a formula que calcula.
Acc(a) = w(a) / (1 + Σ Acc(attackers))
Definindo grafos com YAML
E possivel declarar a estrutura do grafo em YAML sem codigo Go e gerar o 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 # gera graph_gen.go
Basta escrever as funcoes de regra em Go. A estrutura do grafo e declarada pelo YAML.
MIT License. github.com/park-jun-woo/toulmin