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 confirmada
  • 0.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):

ForcaSignificadoExemplo
StrictImpossivel de neutralizar“Acesso a admin API sem autenticacao proibido”
DefeasiblePode ser neutralizado por excecao“Uma funcao por arquivo”
DefeaterBloqueia 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

Regotoulmin
Escrita de regrasRequer aprender Rego DSLFuncoes Go
Tratamento de excecoesPadrao default/else manualGrafo de defeats declarativo
JulgamentoBinario allow/denyValor continuo [-1, +1]
Justificativa da regra# METADATA (ignorado pelo motor)backing (parte da estrutura)
Forca da regraInexistentestrict/defeasible/defeater
Tamanho do motorDezenas de milhares de linhasCentenas de linhas
VelocidadeInterpretador (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

StrengthQtdProporcaoExemplo
Strict1568%F1, F2, F3, F4, A1-A3, A6-A16
Defeasible418%Q1, Q2, Q3, C4
Defeater314%F5, F6, excecao de arquivos de teste

A maioria e strict — convencoes de estrutura de codigo inerentemente minimizam excecoes.

Resultados quantitativos

ProjetoNum. arquivos (antes→depois)Media LOC/arquivo (antes→depois)Violacoes SRP resolvidasViolacoes depth resolvidas
filefunc— (em conformidade desde o inicio)25.100
fullend87→1.260244→25.466→0148→0
whyso12→99147.8→24.412→023→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:

ElementoAutoria original
Estrutura de 6 elementosToulmin (1958)
strict/defeasible/defeaterNute (1994)
h-CategoriserAmgoud & 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