Les moteurs de regles reposent sur la meme premisse depuis 60 ans : l’objet de la verification est un “fait (fact)”.

Drools place des objets Java comme “facts” dans la working memory. Rego traite input comme des donnees deja vraies. JSON Schema considere la structure du document comme acquise. Tous partagent la meme hypothese : les donnees entrantes sont des faits.

Mais quelle est la raison d’etre d’un moteur de regles ? Verifier si les donnees satisfont les regles. Appeler “deja vrai” ce qui necessite une verification est une contradiction.

Ce ne sont pas des faits, mais des assertions

L’objet de la verification n’est pas un fact mais un claim (assertion). C’est une affirmation qui peut etre vraie ou fausse. Sa validite doit etre jugee par les regles.

JWT suit deja ce principe. sub, exp, iss ne sont pas appeles “facts” mais “claims”. Ce sont les assertions de l’emetteur du token. Ce n’est qu’apres avoir verifie la signature, controle l’expiration et compare l’issuer qu’on peut leur faire confiance.

Cette structure a ete formalisee des 1958.

Le modele d’argumentation de Toulmin

Stephen Toulmin a analyse en 1958 la structure de l’argumentation en 6 elements :

  • Claim (assertion) : l’objet du jugement. Ce qu’il faut verifier comme vrai ou faux.
  • Ground (donnees) : les preuves utilisees pour le jugement.
  • Warrant (garantie) : la regle qui etablit que les donnees soutiennent l’assertion.
  • Backing (fondement) : la justification de la validite de la regle.
  • Qualifier (qualificateur) : le degre de certitude du jugement.
  • Rebuttal (refutation) : les conditions d’exception ou l’assertion ne tient plus.

La logique formelle dit : “si les premisses sont vraies, la conclusion est vraie.” Toulmin voyait les choses differemment : “une assertion est soutenue par des donnees et des regles, mais peut etre renversee si des conditions d’exception s’appliquent.” Toute argumentation est refutable.

Pendant 60 ans, les moteurs de regles se sont ranges du cote de la logique formelle. L’entree est un fact, le resultat est allow/deny, les exceptions sont un mecanisme separe. Toulmin etait du cote oppose. L’entree est un claim, le resultat est un degre (degree), les exceptions sont integrees.

Le probleme : le livre de Toulmin etait range au rayon philosophie. Il etait invisible depuis le rayon des moteurs de regles. Un chainon manquant pendant 60 ans.

Alors j’ai construit un moteur de regles

toulmin est une implementation en Go du modele d’argumentation de Toulmin sous forme de moteur de regles.

Les exigences evoluent

Voyons comment if-else et toulmin reagissent a la meme evolution :

// Lundi: "Acces reserve aux utilisateurs authentifies, blocage IP, reseau interne exempt"
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)

// Mardi: "Ajout du Rate limiting"
limited := g.Rebuttal(isRateLimited, nil, 1.0)
g.Defeat(limited, auth)

// Mercredi: "Les utilisateurs premium sont exempts du Rate limit"
premium := g.Defeater(isPremiumUser, nil, 1.0)
g.Defeat(premium, limited)

// Jeudi: "Pendant un incident, meme les premium sont limites"
incident := g.Rebuttal(isIncidentMode, nil, 1.0)
g.Defeat(incident, premium)

2 lignes ajoutees par jour, aucune modification du code existant. La meme evolution en if-else :

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

// Jeudi — 4 niveaux d'imbrication, structure illisible
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 lignes par exigence, structure invariante. if-else : restructuration complete a chaque changement.

Les regles sont des fonctions Go

func(claim any, ground any, backing any) (bool, any)
  • ground = les elements de jugement qui changent a chaque requete (utilisateur, IP, contexte)
  • backing = les criteres de jugement fixes a la declaration du graphe (seuils, noms de roles, configuration)
  • Retour = (resultat du jugement, preuve). La preuve est de type libre selon le domaine.
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
}

Pas besoin d’apprendre un nouveau langage comme Rego. Il suffit d’ecrire des fonctions Go.

backing — meme fonction, criteres differents

backing transmet les criteres de jugement d’une regle sous forme de valeur a l’execution. Enregistrer la meme fonction avec des backing differents cree des regles distinctes :

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)

Quand backing est nil, cela signifie que la regle n’a pas besoin de criteres de jugement.

Les exceptions se declarent sous forme de graphe

L’API Graph Builder declare les relations entre regles, et le moteur s’occupe du reste. La fonction sert d’identifiant. Pas besoin de noms sous forme de chaines.

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 meme fonction peut etre reutilisee dans differents graphes avec des relations de defaite differentes :

strictGraph := toulmin.NewGraph("strict")
strictGraph.Warrant(CheckOneFileOneFunc, nil, 1.0)
// Pas d'exception — les fichiers de test non plus

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)
// Exception pour les fichiers de test et generes

Tracabilite des fondements du jugement

EvaluateTrace retrace non seulement le verdict mais aussi quelles regles ont ete activees et quelles regles en ont defait d’autres :

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},
// ]

Quand il y a des dizaines de regles, on peut lire de maniere comprehensible “pourquoi ce verdict a ete rendu”.

Le jugement se calcule par une seule formule

Application du h-Categoriser d’Amgoud (2013) :

raw = w / (1 + Σ raw(attackers))
verdict = 2 × raw - 1
  • +1.0 — violation confirmee
  • 0.0 — jugement indetermine
  • -1.0 — refutation confirmee

Quand une regle se declenche, elle devient un warrant. Quand une exception se declenche aussi, elle devient un attacker. La formule calcule le rapport de force entre les deux pour produire le verdict. Et s’il y a une exception a l’exception ? L’attacker de l’attacker restaure la regle originale. Principe de compensation — une propriete que seul le h-Categoriser satisfait.

Trois niveaux de force pour les regles

Application de la classification de Nute (1994) :

ForceSignificationExemple
StrictImpossible a neutraliser“Pas d’acces a l’API admin sans authentification”
DefeasibleNeutralisable par une exception“Une fonction par fichier”
DefeaterBloque d’autres regles sans emettre de jugement propre“Les fichiers de test font exception”

Les regles Strict refusent les aretes d’attaque. Les Defeater attaquent uniquement sans produire de jugement propre. Cela exprime structurellement le niveau d’application des regles.

En quoi est-ce different de Rego ?

Regotoulmin
Ecriture des reglesApprentissage du DSL Rego requisFonctions Go
Gestion des exceptionsPatterns default/else manuelsGraphe de defeats declaratif
Jugementallow/deny binaireValeur continue [-1, +1]
Justification des regles# METADATA (ignore par le moteur)backing (partie de la structure)
Force des reglesAbsentestrict/defeasible/defeater
Taille du moteurDizaines de milliers de lignesQuelques centaines de lignes
VitesseInterpreteur (parsing→AST→evaluation)Appel direct de fonctions Go

Rego est large — il dispose d’un ecosysteme d’integration avec Kubernetes, Terraform, Envoy. toulmin est profond — il possede ce que Rego n’a pas (defeasibility, qualifier, backing).

Le repositionnement du Qualifier

Dans le modele original de Toulmin, le Qualifier est attache au Claim. “Ce patient devrait probablement recevoir de la penicilline” — un modalisateur exprimant le degre de certitude de l’assertion.

Le moteur toulmin a repositionne le Qualifier du Claim vers chaque Rule. Dans un moteur de regles, le claim n’est que l’objet de verification. “Ce fichier contient 3 fonctions” — c’est un constat factuel, pas quelque chose qui appelle un degre de certitude. Ce qui determine la qualite du jugement, c’est le degre de certitude de la regle :

  • “Une fonction par fichier” — qualifier 1.0 (regle certaine)
  • “Recommandation : moins de 100 lignes” — qualifier 0.7 (regle flexible)

Le qualifier de chaque Rule devient le poids initial w(a) du h-Categoriser, et le verdict final assume le role que le Qualifier jouait dans le modele original de Toulmin — le degre de certitude du jugement.

Validation empirique : conversion des 22 regles de filefunc en Toulmin

filefunc est un outil de convention de structure de code pour le developpement Go natif LLM. Les 22 regles ont toutes ete converties en warrants Toulmin.

Classification par force

ForceNombreProportionExemples
Strict1568%F1, F2, F3, F4, A1-A3, A6-A16
Defeasible418%Q1, Q2, Q3, C4
Defeater314%F5, F6, exception fichiers de test

La majorite est strict — les conventions de structure de code minimisent par nature les exceptions.

Resultats quantitatifs

ProjetFichiers (avant→apres)LOC moyen/fichier (avant→apres)Violations SRP resoluesViolations depth resolues
filefunc— (conforme des le depart)25.100
fullend87→1 260244→25.466→0148→0
whyso12→99147.8→24.412→023→0

fullend est passe de 87 fichiers a 1 260. Le nombre de fichiers a explose, mais le LOC moyen est passe de 244 a 25.4. Les 66 violations SRP et 148 violations de profondeur sont toutes tombees a 0.

Fondements theoriques

Aucune theorie originale. Tout provient de recherches existantes :

ElementSource
Structure a 6 elementsToulmin (1958)
strict/defeasible/defeaterNute (1994)
h-CategoriserAmgoud & Ben-Naim (2013)

L’originalite reside dans la decouverte que ces elements se connectent. Ce qui existait separement depuis 60 ans en philosophie (Toulmin), en logique (Nute) et en theorie de l’argumentation (Amgoud) converge en un seul point : le moteur de regles logiciel.

Calculer les contrats

L’Etat de droit fonctionne non pas parce que les juges sont intelligents, mais parce que la structure impose le jugement. Il y a des regles, des exceptions declarees, et un verdict est produit selon les preuves.

toulmin transpose cette structure en code.

  • Warrant = article de loi
  • Backing = intention du legislateur
  • Strength = disposition imperative vs disposition supplective
  • Rebuttal = clause d’exception
  • Claim = affaire
  • Ground = preuves
  • h-Categoriser = verdict

On declare les contrats (warrant), on declare les exceptions (rebuttal), on injecte les preuves (ground), et le jugement (verdict) est calcule.

Ce n’est pas un humain qui juge. C’est une formule qui calcule.

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

Definir le graphe en YAML

On peut declarer la structure du graphe en YAML sans code Go et generer le code :

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    # genere graph_gen.go

Il suffit d’ecrire les fonctions de regles en Go. La structure du graphe est declaree en YAML.

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