Image: AI generated
ظلت محركات القواعد لمدة 60 عامًا قائمة على نفس الفرضية: موضوع التحقق هو “حقيقة (fact)”.
يضع Drools كائنات Java كـ “fact” في working memory. ويتعامل Rego مع input كبيانات صحيحة بالفعل. ويفترض JSON Schema أن بنية المستند معطاة مسبقًا. جميعها تشترك في نفس الافتراض — البيانات الواردة هي حقائق.
لكن ما هو سبب وجود محرك القواعد أصلًا؟ إنه التحقق مما إذا كانت البيانات تستوفي القواعد. تسمية ما يحتاج إلى تحقق بـ “شيء صحيح بالفعل” هو تناقض.
ليست حقيقة بل ادعاء
موضوع التحقق ليس fact بل claim (ادعاء). تأكيد قد يكون صحيحًا أو خاطئًا. يجب أن يُحكم على صحته بواسطة القواعد.
JWT يتبع هذا المبدأ بالفعل. يسمي sub وexp وiss بـ “claims” وليس “facts”. إنها ادعاءات من مُصدر الرمز المميز. يجب التحقق من التوقيع، والتأكد من انتهاء الصلاحية، ومطابقة المُصدر قبل أن يمكن الوثوق بها.
هذه بنية تم تأسيسها عام 1958.
نموذج تولمن للحجاج
حلل Stephen Toulmin عام 1958 بنية الحجاج إلى 6 عناصر:
- 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 factory):
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() (nil spec).
الاستثناءات تُعلن كرسم بياني
أعلن العلاقات بين القواعد باستخدام 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)
يمكن إعادة استخدام نفس الدالة في رسوم بيانية مختلفة بعلاقات هزيمة مختلفة:
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 يقيس المحرك أيضًا زمن تنفيذ كل قاعدة. سجل التدقيق والتنقيح مدمجان في المحرك — لا حاجة إلى تسجيل منفصل.
الحكم يُحسب بمعادلة واحدة
تم تطبيق 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 |
| حجم المحرك | عشرات الآلاف من الأسطر | مئات الأسطر |
| السرعة | مفسر (تحليل→AST→تقييم) | استدعاء مباشر لدوال 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 إلى toulmin warrant.
تصنيف القوة
| القوة | العدد | النسبة | أمثلة |
|---|---|---|---|
| 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. جميع انتهاكات SRP البالغة 66 وانتهاكات depth البالغة 148 أصبحت صفرًا.
الأساس النظري
لا توجد نظرية أصيلة. جميعها أبحاث قائمة:
| العنصر | المرجع الأصلي |
|---|---|
| بنية العناصر الستة | 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: الإصدار الأول