Image: AI generated
מנועי כללים עמדו על אותה הנחה במשך 60 שנה. ההנחה שמושא האימות הוא “עובדה (fact)”.
Drools מכניס אובייקטי Java כ-“fact” ל-working memory. Rego מתייחס ל-input כנתון שכבר נכון. JSON Schema מניח שמבנה המסמך נתון מראש. כולם חולקים את אותה הנחה — הנתונים שנכנסו הם עובדות.
אבל מה הסיבה לקיומו של מנוע כללים? לאמת שהנתונים עומדים בכללים. לקרוא “כבר נכון” למשהו שצריך אימות — זו סתירה.
לא עובדה, אלא טענה
מושא האימות הוא לא fact אלא claim (טענה). הצהרה שיכולה להיות נכונה או שגויה. תקפותה צריכה להיקבע על ידי כללים.
JWT כבר עוקב אחרי עיקרון זה. sub, exp, iss נקראים לא “facts” אלא “claims”. אלו טענות של מנפיק ה-token. רק אחרי אימות החתימה, בדיקת התפוגה והשוואת ה-issuer אפשר לסמוך עליהן.
זהו מבנה שכבר גובש ב-1958.
מודל הטיעון של טולמין
Stephen Toulmin ניתח ב-1958 את מבנה הטיעון לשישה רכיבים:
- Claim (טענה): מושא ההכרעה. מה שצריך לאמת אם נכון או שגוי.
- Ground (בסיס): נתוני הראיות המשמשים להכרעה.
- Warrant (ערבות): הכלל שקובע שהבסיס תומך בטענה.
- Backing (גיבוי): ההצדקה מדוע הכלל תקף.
- Qualifier (מגביל): רמת הוודאות של ההכרעה.
- Rebuttal (הפרכה): תנאי חריג שבו הטענה אינה מתקיימת.
לוגיקה פורמלית אומרת “אם ההנחות נכונות, גם המסקנה נכונה”. טולמין היה שונה. “טענה נתמכת על ידי בסיס וכלל, אבל אם יש תנאי חריג — היא מתהפכת.” כל טיעון ניתן להפרכה.
מנועי כללים עמדו בצד הלוגיקה הפורמלית במשך 60 שנה. הקלט הוא fact, התוצאה היא allow/deny, חריגים מטופלים במנגנון נפרד. טולמין עמד בצד הנגדי. הקלט הוא claim, התוצאה היא דרגה (degree), חריגים מובנים.
הבעיה — הספר של טולמין עמד על מדף הפילוסופיה. הוא לא נראה במדף מנועי הכללים. חוליה חסרה של 60 שנה.
אז בניתי מנוע כללים
toulmin הוא מימוש מודל הטיעון של טולמין כמנוע כללים ב-Go.
דרישות מתפתחות
בואו נראה איך if-else ו-toulmin מגיבים לאותה התפתחות.
// יום שני: "רק משתמשים מאומתים, חסימת IP, רשת פנימית פטורה מחסימה"
g := toulmin.NewGraph("api:access")
auth := g.Rule(isAuthenticated)
blocked := g.Counter(isIPBlocked)
exempt := g.Except(isInternalIP)
blocked.Attacks(auth)
exempt.Attacks(blocked)
// יום שלישי: "הוספת Rate limiting"
limited := g.Counter(isRateLimited)
limited.Attacks(auth)
// יום רביעי: "משתמשי פרימיום פטורים מ-Rate limit"
premium := g.Except(isPremiumUser)
premium.Attacks(limited)
// יום חמישי: "בזמן תקרית גם פרימיום מוגבל"
incident := g.Counter(isIncidentMode)
incident.Attacks(premium)
כל יום שתי שורות נוספות, ללא שינוי בקוד קיים. אותה התפתחות ב-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: שתי שורות לדרישה, מבנה קבוע. if-else: כל פעם מפרקים את כל המבנה.
כללים הם פונקציות Go
func(ctx Context, specs Specs) (bool, any)
ctx= חומר ההכרעה שמשתנה בכל בקשה (משתמש, IP, הקשר). ניגשים אליו עםGet/Set.specs= קריטריון ההכרעה שנקבע בעת הצהרת הגרף באמצעות.With()(סף, שם תפקיד, הגדרה)- ערך מוחזר =
(תוצאת הכרעה, ראיה). הראיה היא טיפוס חופשי לפי תחום.
func CheckOneFileOneFunc(ctx toulmin.Context, specs toulmin.Specs) (bool, any) {
gf, _ := ctx.Get("file")
f := gf.(*FileGround)
if len(f.Funcs) > 1 {
return true, &Evidence{Got: len(f.Funcs), Expected: 1}
}
return false, nil
}
אין צורך ללמוד שפה חדשה כמו Rego. פשוט כותבים פונקציות Go. (גם הפורט ל-TypeScript rulecat משתמש באותה חתימה — npm install rulecat.)
spec — אותה פונקציה, קריטריון הכרעה שונה
spec מעביר את קריטריון ההכרעה של הכלל באמצעות הבנאי בעת ההצהרה. רישום אותה פונקציה עם spec שונה יוצר כלל נפרד — אין צורך במפעל closure:
g := toulmin.NewGraph("access")
admin := g.Rule(isInRole).With(&RoleSpec{Role: "admin"}) // ruleID = "isInRole#admin"
editor := g.Rule(isInRole).With(&RoleSpec{Role: "editor"}).Qualifier(0.8)
g := toulmin.NewGraph("line-limit")
strict := g.Rule(CheckLineCount).With(&LineLimit{Max: 100}).Qualifier(0.7)
relaxed := g.Rule(CheckLineCount).With(&LineLimit{Max: 200}).Qualifier(0.5)
relaxed.Attacks(strict)
ערך ה-spec חייב לממש את ממשק Spec (SpecName() string·Validate() error), והוא מאומת בעת הרישום. כלל שאינו זקוק ל-spec משמיט את .With() (spec בגודל nil).
חריגים מוצהרים כגרף
ה-Graph Builder API מצהיר על יחסים בין כללים והמנוע מטפל בשאר. הפונקציה היא המזהה. אין צורך בשמות מחרוזת.
g := toulmin.NewGraph("filefunc")
w := g.Rule(CheckOneFileOneFunc)
d := g.Except(TestFileException)
d.Attacks(w)
ctx := toulmin.NewContext()
ctx.Set("file", file)
results, _ := g.Evaluate(ctx)
ניתן לשימוש חוזר של אותה פונקציה בגרפים שונים עם יחסי defeat שונים:
strictGraph := toulmin.NewGraph("strict")
strictGraph.Rule(CheckOneFileOneFunc)
// ללא חריגים — גם קבצי בדיקה לא מותרים
lenientGraph := toulmin.NewGraph("lenient")
w := lenientGraph.Rule(CheckOneFileOneFunc)
r1 := lenientGraph.Except(TestFileException)
r2 := lenientGraph.Except(GeneratedFileException).Qualifier(0.8)
r1.Attacks(w)
r2.Attacks(w)
// קבצי בדיקה + קבצים שנוצרו אוטומטית — שניהם חריגים
מעקב אחר בסיס ההכרעה
אם מעבירים EvalOption{Trace: true}, המנוע עוקב לא רק אחר ה-verdict אלא גם אחר הכללים שהופעלו ואיזה כלל הביס איזה כלל. כל TraceEntry נושא את רכיבי טולמין כמות שהם — Name(Claim)·Ground(ctx)·Specs(Backing)·Verdict:
results, _ := g.Evaluate(ctx, toulmin.EvalOption{Trace: true})
// results[0].Verdict: +0.6
// results[0].Trace: [
// {Name: "CheckOneFileOneFunc", Role: "rule", Activated: true, Qualifier: 1.0},
// {Name: "TestFileException", Role: "except", Activated: true, Qualifier: 1.0},
// ]
כאשר יש עשרות כללים, אפשר לקרוא באופן קריא לאדם “למה יצא ה-verdict הזה”. אם מעבירים Duration: true, נמדד גם זמן הריצה של כל כלל. כך יומן ביקורת ו-debugging מובנים במנוע עצמו — אין צורך ב-logging נפרד.
ההכרעה מחושבת בנוסחה אחת
יישום h-Categoriser של Amgoud (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
| Rego | toulmin | |
|---|---|---|
| כתיבת כללים | צריך ללמוד Rego DSL | פונקציות Go |
| טיפול בחריגים | תבנית default/else ידנית | הצהרת גרף defeats |
| הכרעה | allow/deny בינארי | [-1, +1] ערך רציף |
| הצדקת הכלל | # METADATA (המנוע מתעלם) | spec (חלק מהמבנה) |
| עוצמת הכלל | אין | strict/defeasible/defeater |
| גודל המנוע | עשרות אלפי שורות | מאות שורות |
| מהירות | interpreter (parsing→AST→evaluation) | קריאה ישירה לפונקציות Go |
Rego רחב — יש לו אקוסיסטם אינטגרציה עם Kubernetes, Terraform, Envoy. toulmin עמוק — יש לו מה ש-Rego חסר (defeasibility, qualifier, backing).
מיקום מחדש של Qualifier
במודל המקורי של טולמין, Qualifier מוצמד ל-Claim. “כנראה צריך לתת למטופל הזה פניצילין” — מגביל מודאלי שמבטא את רמת הוודאות של הטענה.
מנוע toulmin העביר את ה-Qualifier מה-Claim לכל Rule בנפרד. במנוע כללים, claim הוא רק מושא אימות. “בקובץ הזה יש 3 פונקציות” — זו בדיקת עובדה, לא משהו שמתאים לו רמת ודאות. מה שקובע את איכות ההכרעה הוא רמת הוודאות של הכלל:
- “פונקציה אחת לקובץ” — qualifier 1.0 (כלל ודאי)
- “מומלץ עד 100 שורות” — qualifier 0.7 (כלל גמיש)
ה-qualifier של כל Rule הופך למשקל ההתחלתי w(a) של h-Categoriser, וה-verdict הסופי ממלא את התפקיד שה-Qualifier מילא במודל המקורי של טולמין — רמת הוודאות של ההכרעה.
הוכחה מעשית: המרת 22 כללי filefunc לטולמין
filefunc הוא כלי קונבנציית מבנה קוד לפיתוח Go מותאם ל-LLM. כל 22 הכללים הומרו ל-warrant-ים של טולמין.
סיווג עוצמה
| Strength | מספר | אחוז | דוגמה |
|---|---|---|---|
| Strict | 15 | 68% | F1, F2, F3, F4, A1-A3, A6-A16 |
| Defeasible | 4 | 18% | Q1, Q2, Q3, C4 |
| Defeater | 3 | 14% | F5, F6, חריג קובץ בדיקה |
הרוב הם strict — קונבנציית מבנה קוד מטבעה ממזערת חריגים.
תוצאות כמותיות
| פרויקט | מספר קבצים (לפני→אחרי) | ממוצע LOC/קובץ (לפני→אחרי) | פתרון הפרות SRP | פתרון הפרות depth |
|---|---|---|---|---|
| filefunc | — (עמד בדרישות מההתחלה) | 25.1 | 0 | 0 |
| yongol | 87→1,260 | 244→25.4 | 66→0 | 148→0 |
| whyso | 12→99 | 147.8→24.4 | 12→0 | 23→0 |
ב-yongol מספר הקבצים עלה מ-87 ל-1,260. מספר הקבצים התפוצץ, אבל ממוצע ה-LOC ירד מ-244 ל-25.4. 66 הפרות SRP ו-148 הפרות depth ירדו כולן לאפס.
בסיס תיאורטי
אין כאן תיאוריה מקורית. הכל מבוסס על מחקר קיים:
| רכיב | מקור |
|---|---|
| מבנה 6 רכיבים | Toulmin (1958) |
| strict/defeasible/defeater | Nute (1994) |
| h-Categoriser | Amgoud & Ben-Naim (2013) |
המקוריות היא בגילוי שאלה מתחברים. במשך 60 שנה הם היו בנפרד בפילוסופיה (Toulmin), לוגיקה (Nute), ותורת הטיעון (Amgoud), ונפגשו בנקודה אחת — מנוע כללים לתוכנה.
לחשב חוזים
שלטון החוק עובד לא בגלל שהשופט חכם. אלא בגלל שהמבנה כופה הכרעה. יש כללים, חריגים מוצהרים, והפסק דין מחושב לפי הראיות.
toulmin העביר את המבנה הזה לקוד.
- Warrant = סעיף חוק
- Backing = כוונת המחוקק
- Strength = הוראה קוגנטית מול הוראה דיספוזיטיבית
- Rebuttal = סעיף חריג
- Claim = מקרה
- Ground = ראיות
- h-Categoriser = פסק דין
מצהירים חוזה (warrant), מצהירים חריגים (rebuttal), מציבים ראיות (ground), וההכרעה (verdict) מחושבת.
לא אדם שופט. נוסחה מחשבת.
Acc(a) = w(a) / (1 + Σ Acc(attackers))
MIT License. github.com/park-jun-woo/toulmin
יומן שינויים
- 2026-06-18: עדכון API — גרף בתבנית בנאי (
Rule/Counter/Except·.With()·.Attacks()), חתימת כללfunc(ctx Context, specs Specs),Evaluate(ctx, EvalOption{Trace}), backing→spec, ושיקוף הפורט ל-TypeScript (rulecat) - 2026-03-22: מהדורה ראשונה