ルールエンジンは60年間、同じ前提の上に立っていた。検証対象は「事実(fact)」であるという前提だ。

Droolsは Java オブジェクトを「fact」としてworking memoryに入れる。Regoはinputをすでに真であるデータとして扱う。JSON Schemaは文書構造が所与のものとみなす。すべて同じ仮定だ ── 入ってきたデータは事実である。

ところで、ルールエンジンの存在理由は何か? データがルールを満たすかどうかを検証することだ。検証が必要な対象を「すでに真であるもの」と呼ぶのは矛盾である。

事実ではなく主張である

検証対象はfactではなくclaim(主張)である。真かもしれないし偽かもしれない断言だ。ルールによって妥当性が判定されなければならない

JWTがすでにこの原則に従っている。subexpissを「facts」ではなく「claims」と呼ぶ。トークン発行者の主張だ。署名を検証し、有効期限を確認し、issuerを照合して初めて信頼できる。

これは1958年にすでに確立された構造だ。

Toulminの論証モデル

Stephen Toulminは1958年に論証の構造を6要素で分析した:

  • Claim(主張): 判定対象。真か偽か検証すべきもの。
  • Ground(根拠): 判定に使用される証拠データ。
  • Warrant(保証): 根拠が主張を裏付けると判断するルール。
  • Backing(裏付け): ルールがなぜ有効かに対する正当性。
  • Qualifier(限定): 判定の確信度。
  • Rebuttal(反駁): 主張が成立しない例外条件。

形式論理学は「前提が真なら結論も真」だ。Toulminは違った。「主張は根拠とルールによって裏付けられるが、例外条件があれば覆される。」すべての論証は反駁可能である。

ルールエンジンは60年間、形式論理学の側に立っていた。入力はfact、結果はallow/deny、例外は別のメカニズム。Toulminは反対側にいた。入力はclaim、結果は程度(degree)、例外は内蔵。

問題は ── Toulminの著書が哲学の書棚に置かれていたことだ。ルールエンジンの書棚からは見えなかった。60年間のミッシングリンク。

そこでルールエンジンを作った

toulminはToulminの論証モデルをGoルールエンジンとして実装したものだ。

要件は進化する

if-elseとtoulminが同じ進化にどう対応するか見てみよう。

// 月曜日: "認証済みユーザーのみアクセス、IPブロック適用、内部ネットワークはブロック免除"
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)

// 火曜日: "Rate limiting追加"
limited := g.Rebuttal(isRateLimited, nil, 1.0)
g.Defeat(limited, auth)

// 水曜日: "プレミアムユーザーはRate limit免除"
premium := g.Defeater(isPremiumUser, nil, 1.0)
g.Defeat(premium, limited)

// 木曜日: "障害対応中はプレミアムも制限"
incident := g.Rebuttal(isIncidentMode, nil, 1.0)
g.Defeat(incident, premium)

毎日2行追加、既存コードの変更なし。同じ進化をif-elseで:

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

// 木曜日 — 4重ネスト、構造把握不可
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行、構造不変。 if-else: 毎回構造全体を作り直す。

ルールはGo関数だ

func(claim any, ground any, backing any) (bool, any)
  • ground = リクエストごとに変わる判定材料(ユーザー、IP、コンテキスト)
  • backing = グラフ宣言時に固定される判定基準(閾値、ロール名、設定)
  • 返却 = (判定結果, 証拠)。証拠はドメインごとの自由型。
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
}

Regoのように新しい言語を学ぶ必要はない。Go関数を書けばよい。

backing — 同じ関数、異なる判定基準

backingはルールの判定基準をランタイム値で渡す。同じ関数を異なるbackingで登録すれば別々のルールになる:

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)

backingがnilならルールに判定基準が不要という意味だ。

例外はグラフで宣言する

Graph Builder APIでルール間の関係を宣言すればエンジンが自動的に処理する。関数が識別子だ。文字列名は不要。

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)

同じ関数を異なるグラフで異なる敗北関係として再利用できる:

strictGraph := toulmin.NewGraph("strict")
strictGraph.Warrant(CheckOneFileOneFunc, nil, 1.0)
// 例外なし — テストファイルも許可しない

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)
// テスト + 生成ファイル両方とも例外

判定根拠を追跡する

EvaluateTraceはverdictだけでなく、どのルールが活性化され、どのルールがどのルールを敗北させたかを追跡する:

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

ルールが数十個あるとき「なぜこのverdictが出たのか」を人間が読める。

判定は一つの数式で計算される

Amgoudのh-Categoriser(2013)を適用した:

raw = w / (1 + Σ raw(attackers))
verdict = 2 × raw - 1
  • +1.0 — 違反確定
  • 0.0 — 判定不能
  • -1.0 — 反駁確定

ルールが発火するとwarrantになる。例外も発火するとattackerになる。数式が両者の勢力を計算してverdictを算出する。例外の例外があれば? attackerのattackerとなり元のルールが復元される。補償原理(Compensation) ── h-Categoriserだけが満たす性質だ。

ルールには三つの強度がある

Nute(1994)の分類を適用した:

強度意味
Strict絶対に無力化不可「認証なしにadmin APIアクセス不可」
Defeasible例外により無力化可能「ファイルごとに関数一つ」
Defeater自身は主張せず他のルールのみ阻止「テストファイルは例外」

Strictルールは攻撃エッジを拒否する。Defeaterは攻撃のみ行い自身の判定はない。これがルールの強制レベルを構造的に表現する。

Regoと何が違うのか

Regotoulmin
ルール作成Rego DSL学習が必要Go関数
例外処理default/elseパターン手動defeatsグラフ宣言的
判定allow/deny二値[-1, +1]連続値
ルールの正当性# METADATA(エンジンが無視)backing(構造の一部)
ルール強度なしstrict/defeasible/defeater
エンジンサイズ数万行数百行
速度インタプリタ(パース→AST→評価)Go関数直接呼び出し

Regoは広い ── Kubernetes、Terraform、Envoy統合エコシステムがある。toulminは深い ── Regoにないもの(defeasibility、qualifier、backing)がある。

Qualifierの再配置

Toulminの元のモデルではQualifierはClaimに付属する。「おそらくこの患者にペニシリンを投与すべきだ」── 主張の確信度を表現する様相限定詞だ。

toulminエンジンはQualifierをClaimから各Ruleに再配置した。ルールエンジンにおいてclaimは検証対象に過ぎない。「このファイルに関数が3つある」── 事実確認であり、確信度が付くべき対象ではない。判定の質を決めるのはルールの確信度だ:

  • 「ファイルごとに関数一つ」 ── qualifier 1.0(確実なルール)
  • 「推奨100行以下」 ── qualifier 0.7(柔軟なルール)

各Ruleのqualifierがh-Categoriserの初期重みw(a)となり、最終verdictがToulmin元のモデルでQualifierが担っていた役割 ── 判定の確信度 ── を代替する。

実証:filefunc 22個ルールのToulmin変換

filefuncはLLMネイティブGo開発のためのコード構造コンベンションツールだ。22個のルールをすべてToulmin warrantに変換した。

強度分類

Strength割合
Strict1568%F1, F2, F3, F4, A1-A3, A6-A16
Defeasible418%Q1, Q2, Q3, C4
Defeater314%F5, F6, テストファイル例外

大部分がstrictだ ── コード構造コンベンションは本質的に例外を最小化する。

定量的結果

プロジェクトファイル数(前→後)平均LOC/ファイル(前→後)SRP違反解消depth違反解消
filefunc──(最初から準拠)25.100
fullend87→1,260244→25.466→0148→0
whyso12→99147.8→24.412→023→0

fullendは87個のファイルが1,260個に増えた。ファイル数は爆発したが平均LOCが244から25.4に下がった。SRP違反66件、depth違反148件がすべて0になった。

理論的基盤

独創的な理論はない。すべて既存研究だ:

要素原著
6要素構造Toulmin (1958)
strict/defeasible/defeaterNute (1994)
h-CategoriserAmgoud & Ben-Naim (2013)

独創性はこれらが繋がるという発見だ。60年間、哲学(Toulmin)、論理学(Nute)、論証理論(Amgoud)にそれぞれあったものが、ソフトウェアルールエンジンという一点で出会う。

契約を計算する

法治主義が機能する理由は裁判官が賢いからではない。構造が判定を強制するからだ。ルールがあり、例外が宣言されており、証拠に従って判決が算出される。

toulminはこの構造をコードに移した。

  • Warrant = 法律条項
  • Backing = 立法趣旨
  • Strength = 強行規定 vs 任意規定
  • Rebuttal = 例外条項
  • Claim = 事件
  • Ground = 証拠
  • h-Categoriser = 判決

契約(warrant)を宣言し、例外(rebuttal)を宣言し、証拠(ground)を代入すれば、判定(verdict)が計算される。

人間が判断するのではない。数式が計算する。

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

YAMLでグラフを定義できる

Goコードなしにグラフ構造をYAMLで宣言しコードを生成する:

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 生成

ルール関数だけGoで書けばよい。グラフ構造はYAMLが宣言する。

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