Rule Engines standen 60 Jahre lang auf derselben Praemisse: Der Pruefgegenstand ist ein “Fakt (fact)”.
Drools legt Java-Objekte als “facts” in den Working Memory. Rego behandelt input als bereits wahre Daten. JSON Schema geht davon aus, dass die Dokumentstruktur gegeben ist. Alles dieselbe Annahme — eingehende Daten sind Fakten.
Aber wozu existiert eine Rule Engine ueberhaupt? Um zu verifizieren, ob Daten die Regeln erfuellen. Etwas, das verifiziert werden muss, als “bereits wahr” zu bezeichnen, ist ein Widerspruch.
Keine Fakten, sondern Behauptungen
Der Pruefgegenstand ist kein fact, sondern ein claim (Behauptung). Eine Aussage, die wahr oder falsch sein kann. Ihre Gueltigkeit muss durch Regeln bestimmt werden.
JWT folgt diesem Prinzip bereits. sub, exp, iss werden nicht “facts”, sondern “claims” genannt. Es sind Behauptungen des Token-Ausstellers. Erst nach Signaturpruefung, Ablaufkontrolle und Issuer-Abgleich kann man ihnen vertrauen.
Diese Struktur wurde bereits 1958 formuliert.
Toulmins Argumentationsmodell
Stephen Toulmin analysierte 1958 die Struktur der Argumentation in sechs Elementen:
- Claim (Behauptung): Der Pruefgegenstand. Das, was auf wahr oder falsch verifiziert werden muss.
- Ground (Grundlage): Die Beweisdaten, die zur Bewertung herangezogen werden.
- Warrant (Gewaehrleistung): Die Regel, die bestimmt, dass die Grundlage die Behauptung stuetzt.
- Backing (Stuetzung): Die Begruendung, warum die Regel gueltig ist.
- Qualifier (Qualifikator): Der Gewissheitsgrad der Bewertung.
- Rebuttal (Widerlegung): Die Ausnahmebedingung, unter der die Behauptung nicht gilt.
Formale Logik sagt: “Wenn die Praemissen wahr sind, ist auch die Schlussfolgerung wahr.” Toulmin war anders. “Behauptungen werden durch Grundlagen und Regeln gestuetzt, koennen aber durch Ausnahmebedingungen umgestossen werden.” Jede Argumentation ist widerlegbar.
Rule Engines standen 60 Jahre auf der Seite der formalen Logik. Input ist fact, Ergebnis ist allow/deny, Ausnahmen sind ein separater Mechanismus. Toulmin stand auf der anderen Seite. Input ist claim, Ergebnis ist ein Grad (degree), Ausnahmen sind eingebaut.
Das Problem war — Toulmins Buch stand im Philosophie-Regal. Im Rule-Engine-Regal war es nicht sichtbar. Ein Missing Link von 60 Jahren.
Also habe ich eine Rule Engine gebaut
toulmin ist Toulmins Argumentationsmodell, implementiert als Go Rule Engine.
Anforderungen entwickeln sich weiter
Sehen wir, wie if-else und toulmin auf dieselbe Evolution reagieren.
// Montag: "Nur authentifizierte Benutzer, IP-Sperre aktiv, internes Netz von Sperre befreit"
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)
// Dienstag: "Rate Limiting hinzufuegen"
limited := g.Rebuttal(isRateLimited, nil, 1.0)
g.Defeat(limited, auth)
// Mittwoch: "Premium-Benutzer sind vom Rate Limit befreit"
premium := g.Defeater(isPremiumUser, nil, 1.0)
g.Defeat(premium, limited)
// Donnerstag: "Waehrend der Stoerungsbehandlung auch Premium einschraenken"
incident := g.Rebuttal(isIncidentMode, nil, 1.0)
g.Defeat(incident, premium)
Taeglich 2 Zeilen hinzufuegen, kein bestehender Code wird geaendert. Dieselbe Evolution mit if-else:
// Montag
if user != nil {
if blockedIPs[ip] {
if strings.HasPrefix(ip, "10.") {
allow = true
}
} else {
allow = true
}
}
// Donnerstag — 4-fache Verschachtelung, Struktur nicht mehr erkennbar
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 Zeilen pro Anforderung, Struktur bleibt unveraendert. if-else: Jedes Mal muss die gesamte Struktur umgebaut werden.
Regeln sind Go-Funktionen
func(claim any, ground any, backing any) (bool, any)
ground= Bewertungsmaterial, das sich pro Anfrage aendert (Benutzer, IP, Kontext)backing= Bewertungskriterium, das bei der Graph-Deklaration fixiert wird (Schwellenwerte, Rollennamen, Konfiguration)- Rueckgabe =
(Bewertungsergebnis, Beweis). Der Beweis ist ein frei waehlbarer Domaenentyp.
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
}
Man muss keine neue Sprache wie Rego lernen. Man schreibt einfach Go-Funktionen.
backing — Gleiche Funktion, unterschiedliche Bewertungskriterien
backing uebergibt das Bewertungskriterium der Regel als Laufzeitwert. Dieselbe Funktion mit unterschiedlichem backing registriert ergibt separate Regeln:
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)
Wenn backing nil ist, bedeutet das, dass die Regel kein Bewertungskriterium benoetigt.
Ausnahmen werden als Graph deklariert
Mit der Graph Builder API deklariert man die Beziehungen zwischen Regeln, und die Engine erledigt den Rest. Funktionen sind die Bezeichner. Keine String-Namen noetig.
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)
Dieselbe Funktion kann in verschiedenen Graphen mit unterschiedlichen Defeat-Beziehungen wiederverwendet werden:
strictGraph := toulmin.NewGraph("strict")
strictGraph.Warrant(CheckOneFileOneFunc, nil, 1.0)
// Keine Ausnahmen — auch Testdateien nicht erlaubt
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)
// Test- + generierte Dateien beide als Ausnahme
Bewertungsgruende werden nachverfolgt
EvaluateTrace verfolgt nicht nur das Verdict, sondern auch welche Regeln aktiviert wurden und welche Regel welche andere Regel besiegt hat:
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},
// ]
Bei Dutzenden von Regeln kann ein Mensch nachvollziehen, “warum dieses Verdict zustande kam”.
Die Bewertung wird mit einer einzigen Formel berechnet
Amgouds h-Categoriser (2013) wurde angewendet:
raw = w / (1 + Σ raw(attackers))
verdict = 2 × raw - 1
+1.0— Verstoss bestaetigt0.0— Unentscheidbar-1.0— Widerlegung bestaetigt
Wenn eine Regel feuert, wird sie zum Warrant. Wenn eine Ausnahme ebenfalls feuert, wird sie zum Attacker. Die Formel berechnet die Kraefteverhaeltnisse und ergibt das Verdict. Was, wenn es eine Ausnahme von der Ausnahme gibt? Sie wird zum Attacker des Attackers und stellt die urspruengliche Regel wieder her. Kompensationsprinzip — eine Eigenschaft, die nur der h-Categoriser erfuellt.
Regeln haben drei Staerkegrade
Nutes (1994) Klassifikation wurde angewendet:
| Staerke | Bedeutung | Beispiel |
|---|---|---|
| Strict | Kann niemals unwirksam gemacht werden | “Kein Zugriff auf Admin-API ohne Authentifizierung” |
| Defeasible | Kann durch Ausnahmen unwirksam gemacht werden | “Eine Funktion pro Datei” |
| Defeater | Blockiert nur andere Regeln, ohne eigene Behauptung | “Testdateien sind Ausnahmen” |
Strict-Regeln verweigern Angriffskanten. Defeater greifen nur an und haben keine eigene Bewertung. Dies drueckt die Durchsetzungsebene von Regeln strukturell aus.
Was unterscheidet es von Rego
| Rego | toulmin | |
|---|---|---|
| Regelschreibung | Rego-DSL lernen erforderlich | Go-Funktionen |
| Ausnahmebehandlung | default/else-Muster manuell | Defeats-Graph deklarativ |
| Bewertung | allow/deny binaer | [-1, +1] kontinuierlicher Wert |
| Regelbegruendung | # METADATA (von Engine ignoriert) | backing (Teil der Struktur) |
| Regelstaerke | Nicht vorhanden | strict/defeasible/defeater |
| Engine-Groesse | Zehntausende Zeilen | Hunderte Zeilen |
| Geschwindigkeit | Interpreter (Parsing->AST->Auswertung) | Direkter Go-Funktionsaufruf |
Rego ist breit — mit Integrationen fuer Kubernetes, Terraform, Envoy. toulmin ist tief — es hat, was Rego nicht hat (Defeasibility, Qualifier, Backing).
Die Neupositionierung des Qualifiers
In Toulmins urspruenglichem Modell ist der Qualifier am Claim angebracht. “Vermutlich sollte diesem Patienten Penicillin verabreicht werden” — ein modaler Qualifikator, der den Gewissheitsgrad der Behauptung ausdrueckt.
Die toulmin Engine hat den Qualifier vom Claim auf jede einzelne Rule verschoben. In einer Rule Engine ist der Claim nur der Pruefgegenstand. “Diese Datei hat 3 Funktionen” — eine Tatsachenfeststellung, der kein Gewissheitsgrad zugeordnet werden sollte. Was die Qualitaet der Bewertung bestimmt, ist der Gewissheitsgrad der Regel:
- “Eine Funktion pro Datei” — Qualifier 1.0 (sichere Regel)
- “Empfohlen: unter 100 Zeilen” — Qualifier 0.7 (flexible Regel)
Der Qualifier jeder Rule wird zum Anfangsgewicht w(a) des h-Categorisers, und das endgueltige Verdict uebernimmt die Rolle, die der Qualifier in Toulmins urspruenglichem Modell hatte — den Gewissheitsgrad der Bewertung.
Empirische Validierung: Toulmin-Konvertierung der 22 filefunc-Regeln
filefunc ist ein Code-Struktur-Konventionstool fuer LLM-native Go-Entwicklung. Alle 22 Regeln wurden in Toulmin-Warrants konvertiert.
Staerke-Klassifikation
| Strength | Anzahl | Anteil | Beispiel |
|---|---|---|---|
| Strict | 15 | 68% | F1, F2, F3, F4, A1-A3, A6-A16 |
| Defeasible | 4 | 18% | Q1, Q2, Q3, C4 |
| Defeater | 3 | 14% | F5, F6, Testdatei-Ausnahme |
Die Mehrheit ist strict — Code-Struktur-Konventionen minimieren von Natur aus Ausnahmen.
Quantitative Ergebnisse
| Projekt | Dateien (vorher->nachher) | Durchschn. LOC/Datei (vorher->nachher) | SRP-Verstoesse behoben | Depth-Verstoesse behoben |
|---|---|---|---|---|
| filefunc | — (von Anfang an konform) | 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 wuchs von 87 auf 1.260 Dateien. Die Dateizahl explodierte, aber die durchschnittliche LOC fiel von 244 auf 25,4. 66 SRP-Verstoesse und 148 Depth-Verstoesse wurden alle auf 0 reduziert.
Theoretische Grundlage
Es gibt keine originaere Theorie. Alles basiert auf bestehender Forschung:
| Element | Originalwerk |
|---|---|
| 6-Elemente-Struktur | Toulmin (1958) |
| strict/defeasible/defeater | Nute (1994) |
| h-Categoriser | Amgoud & Ben-Naim (2013) |
Die Originalitaet liegt in der Entdeckung, dass diese Elemente sich verbinden lassen. Was 60 Jahre lang getrennt in Philosophie (Toulmin), Logik (Nute) und Argumentationstheorie (Amgoud) existierte, trifft sich in einem einzigen Punkt: der Software Rule Engine.
Vertraege berechnen
Rechtsstaatlichkeit funktioniert nicht, weil Richter klug sind. Sondern weil die Struktur die Bewertung erzwingt. Es gibt Regeln, Ausnahmen sind deklariert, und das Urteil wird anhand von Beweisen berechnet.
toulmin hat diese Struktur in Code uebertragen.
- Warrant = Gesetzesbestimmung
- Backing = Gesetzgebungsabsicht
- Strength = Zwingendes Recht vs. Dispositives Recht
- Rebuttal = Ausnahmeklausel
- Claim = Fall
- Ground = Beweis
- h-Categoriser = Urteil
Man deklariert den Vertrag (Warrant), deklariert die Ausnahme (Rebuttal), setzt den Beweis ein (Ground), und die Bewertung (Verdict) wird berechnet.
Kein Mensch urteilt. Die Formel berechnet.
Acc(a) = w(a) / (1 + Σ Acc(attackers))
Graphen koennen mit YAML definiert werden
Ohne Go-Code kann man die Graph-Struktur in YAML deklarieren und Code generieren:
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 # graph_gen.go generieren
Man schreibt nur die Regelfunktionen in Go. Die Graph-Struktur wird von YAML deklariert.
MIT License. github.com/park-jun-woo/toulmin