kbs 0.0.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/deploy-github-pages.yml +52 -0
  3. data/CHANGELOG.md +68 -2
  4. data/README.md +291 -362
  5. data/docs/advanced/custom-persistence.md +775 -0
  6. data/docs/advanced/debugging.md +726 -0
  7. data/docs/advanced/index.md +8 -0
  8. data/docs/advanced/performance.md +865 -0
  9. data/docs/advanced/testing.md +827 -0
  10. data/docs/api/blackboard.md +1157 -0
  11. data/docs/api/engine.md +1047 -0
  12. data/docs/api/facts.md +1212 -0
  13. data/docs/api/index.md +12 -0
  14. data/docs/api/rules.md +1104 -0
  15. data/docs/architecture/blackboard.md +544 -0
  16. data/docs/architecture/index.md +277 -0
  17. data/docs/architecture/network-structure.md +343 -0
  18. data/docs/architecture/rete-algorithm.md +737 -0
  19. data/docs/assets/css/custom.css +83 -0
  20. data/docs/assets/images/blackboard-architecture.svg +136 -0
  21. data/docs/assets/images/compiled-network.svg +101 -0
  22. data/docs/assets/images/fact-assertion-flow.svg +117 -0
  23. data/docs/assets/images/fact-rule-relationship.svg +65 -0
  24. data/docs/assets/images/fact-structure.svg +42 -0
  25. data/docs/assets/images/inference-cycle.svg +47 -0
  26. data/docs/assets/images/kb-components.svg +43 -0
  27. data/docs/assets/images/kbs.jpg +0 -0
  28. data/docs/assets/images/pattern-matching-trace.svg +136 -0
  29. data/docs/assets/images/rete-network-layers.svg +96 -0
  30. data/docs/assets/images/rule-structure.svg +44 -0
  31. data/docs/assets/images/system-layers.svg +69 -0
  32. data/docs/assets/images/trading-signal-network.svg +139 -0
  33. data/docs/assets/js/mathjax.js +17 -0
  34. data/docs/examples/index.md +223 -0
  35. data/docs/guides/blackboard-memory.md +589 -0
  36. data/docs/guides/dsl.md +1321 -0
  37. data/docs/guides/facts.md +652 -0
  38. data/docs/guides/getting-started.md +385 -0
  39. data/docs/guides/index.md +23 -0
  40. data/docs/guides/negation.md +529 -0
  41. data/docs/guides/pattern-matching.md +561 -0
  42. data/docs/guides/persistence.md +451 -0
  43. data/docs/guides/variable-binding.md +491 -0
  44. data/docs/guides/writing-rules.md +914 -0
  45. data/docs/index.md +155 -0
  46. data/docs/installation.md +156 -0
  47. data/docs/quick-start.md +221 -0
  48. data/docs/what-is-a-fact.md +694 -0
  49. data/docs/what-is-a-knowledge-base.md +350 -0
  50. data/docs/what-is-a-rule.md +833 -0
  51. data/examples/.gitignore +1 -0
  52. data/examples/README.md +2 -2
  53. data/examples/advanced_example.rb +2 -2
  54. data/examples/advanced_example_dsl.rb +224 -0
  55. data/examples/ai_enhanced_kbs.rb +1 -1
  56. data/examples/ai_enhanced_kbs_dsl.rb +538 -0
  57. data/examples/blackboard_demo_dsl.rb +50 -0
  58. data/examples/car_diagnostic.rb +1 -1
  59. data/examples/car_diagnostic_dsl.rb +54 -0
  60. data/examples/concurrent_inference_demo.rb +5 -6
  61. data/examples/concurrent_inference_demo_dsl.rb +362 -0
  62. data/examples/csv_trading_system.rb +1 -1
  63. data/examples/csv_trading_system_dsl.rb +525 -0
  64. data/examples/iot_demo_using_dsl.rb +1 -1
  65. data/examples/portfolio_rebalancing_system.rb +2 -2
  66. data/examples/portfolio_rebalancing_system_dsl.rb +613 -0
  67. data/examples/redis_trading_demo_dsl.rb +177 -0
  68. data/examples/rule_source_demo.rb +123 -0
  69. data/examples/run_all.rb +50 -0
  70. data/examples/run_all_dsl.rb +49 -0
  71. data/examples/stock_trading_advanced.rb +1 -1
  72. data/examples/stock_trading_advanced_dsl.rb +404 -0
  73. data/examples/temp_dsl.txt +9392 -0
  74. data/examples/timestamped_trading.rb +1 -1
  75. data/examples/timestamped_trading_dsl.rb +258 -0
  76. data/examples/trading_demo.rb +1 -1
  77. data/examples/trading_demo_dsl.rb +322 -0
  78. data/examples/working_demo.rb +1 -1
  79. data/examples/working_demo_dsl.rb +160 -0
  80. data/lib/kbs/blackboard/engine.rb +3 -3
  81. data/lib/kbs/blackboard/fact.rb +1 -1
  82. data/lib/kbs/condition.rb +1 -1
  83. data/lib/kbs/decompiler.rb +204 -0
  84. data/lib/kbs/dsl/knowledge_base.rb +101 -2
  85. data/lib/kbs/dsl/variable.rb +1 -1
  86. data/lib/kbs/dsl.rb +3 -1
  87. data/lib/kbs/{rete_engine.rb → engine.rb} +42 -1
  88. data/lib/kbs/fact.rb +1 -1
  89. data/lib/kbs/version.rb +1 -1
  90. data/lib/kbs.rb +15 -13
  91. data/mkdocs.yml +181 -0
  92. metadata +74 -9
  93. data/examples/stock_trading_system.rb.bak +0 -563
data/docs/api/rules.md ADDED
@@ -0,0 +1,1104 @@
1
+ # Rules API Reference
2
+
3
+ Complete API reference for rule classes in KBS.
4
+
5
+ ## Table of Contents
6
+
7
+ - [KBS::Rule](#kbsrule) - Production rule with conditions and action
8
+ - [Rule Lifecycle](#rule-lifecycle)
9
+ - [Rule Patterns](#rule-patterns)
10
+ - [Best Practices](#best-practices)
11
+
12
+ ---
13
+
14
+ ## KBS::Rule
15
+
16
+ A production rule that fires when all conditions match.
17
+
18
+ **Structure**: A rule consists of:
19
+ 1. **Name** - Unique identifier
20
+ 2. **Priority** - Execution order (higher = more urgent)
21
+ 3. **Conditions** - Array of patterns to match
22
+ 4. **Action** - Lambda executed when all conditions match
23
+
24
+ ---
25
+
26
+ ### Constructor
27
+
28
+ #### `initialize(name, conditions: [], action: nil, priority: 0, &block)`
29
+
30
+ Creates a new rule.
31
+
32
+ **Parameters**:
33
+ - `name` (Symbol or String) - Unique rule identifier
34
+ - `conditions` (Array<KBS::Condition>, optional) - Conditions to match (default: `[]`)
35
+ - `action` (Proc, optional) - Action lambda to execute (default: `nil`)
36
+ - `priority` (Integer, optional) - Rule priority (default: `0`)
37
+ - `&block` (Block, optional) - Configuration block yielding self
38
+
39
+ **Returns**: `KBS::Rule` instance
40
+
41
+ **Example - Low-level API (Direct Construction)**:
42
+ ```ruby
43
+ # Minimal rule
44
+ rule = KBS::Rule.new(:high_temperature)
45
+
46
+ # Rule with all parameters
47
+ rule = KBS::Rule.new(
48
+ :high_temperature,
49
+ conditions: [
50
+ KBS::Condition.new(:temperature, value: ->(v) { v > 80 })
51
+ ],
52
+ action: ->(facts) { puts "High temperature detected!" },
53
+ priority: 10
54
+ )
55
+ ```
56
+
57
+ **Example - Low-level API (Block Configuration)**:
58
+ ```ruby
59
+ rule = KBS::Rule.new(:high_temperature) do |r|
60
+ r.conditions << KBS::Condition.new(:temperature, value: ->(v) { v > 80 })
61
+ r.action = ->(facts) { puts "High temperature: #{facts[0][:value]}" }
62
+ end
63
+ ```
64
+
65
+ **Using DSL (Recommended)**:
66
+ ```ruby
67
+ kb = KBS.knowledge_base do
68
+ rule "high_temperature", priority: 10 do
69
+ on :temperature, value: greater_than(80)
70
+ perform do |facts, bindings|
71
+ puts "High temperature: #{bindings[:value?]}"
72
+ end
73
+ end
74
+ end
75
+
76
+ # Add to engine
77
+ kb.rules.each { |r| engine.add_rule(r) }
78
+ ```
79
+
80
+ ---
81
+
82
+ ### Public Attributes
83
+
84
+ #### `name`
85
+
86
+ **Type**: `Symbol` or `String`
87
+
88
+ **Read-only**: Yes (via `attr_reader`)
89
+
90
+ **Description**: Unique rule identifier
91
+
92
+ **Example - Low-level API**:
93
+ ```ruby
94
+ rule = KBS::Rule.new(:high_temperature, priority: 10)
95
+ puts rule.name # => :high_temperature
96
+ ```
97
+
98
+ **Using DSL (Recommended)**:
99
+ ```ruby
100
+ kb = KBS.knowledge_base do
101
+ rule "high_temperature", priority: 10 do
102
+ on :temperature, value: greater_than(80)
103
+ perform { puts "Alert!" }
104
+ end
105
+ end
106
+
107
+ puts kb.rules.first.name # => "high_temperature"
108
+ ```
109
+
110
+ **Best Practice**: Use descriptive names that indicate the rule's purpose:
111
+ ```ruby
112
+ # Good
113
+ "high_temperature_alert"
114
+ "low_inventory_reorder"
115
+ "fraud_detection_high_risk"
116
+
117
+ # Less clear
118
+ "rule1"
119
+ "temp_rule"
120
+ "check"
121
+ ```
122
+
123
+ ---
124
+
125
+ #### `priority`
126
+
127
+ **Type**: `Integer`
128
+
129
+ **Read-only**: Yes (via `attr_reader`)
130
+
131
+ **Description**: Rule priority (higher = executes first in KBS::Blackboard::Engine)
132
+
133
+ **Default**: `0`
134
+
135
+ **Range**: Any integer (commonly 0-100)
136
+
137
+ **Example - Low-level API**:
138
+ ```ruby
139
+ rule = KBS::Rule.new(:critical_alert, priority: 100)
140
+ puts rule.priority # => 100
141
+ ```
142
+
143
+ **Using DSL (Recommended)**:
144
+ ```ruby
145
+ kb = KBS.knowledge_base do
146
+ rule "critical_alert", priority: 100 do
147
+ on :alert, level: "critical"
148
+ perform { puts "CRITICAL ALERT!" }
149
+ end
150
+ end
151
+ ```
152
+
153
+ **Priority Semantics**:
154
+ - **KBS::Engine**: Priority is stored but NOT used for execution order (rules fire in arbitrary order)
155
+ - **KBS::Blackboard::Engine**: Higher priority rules fire first within production nodes
156
+
157
+ **Common Priority Ranges**:
158
+ ```ruby
159
+ # Critical safety rules
160
+ priority: 100
161
+
162
+ # Important business rules
163
+ priority: 50
164
+
165
+ # Standard rules
166
+ priority: 10
167
+
168
+ # Cleanup/logging rules
169
+ priority: 0
170
+
171
+ # Background tasks
172
+ priority: -10
173
+ ```
174
+
175
+ **Example - Priority Ordering**:
176
+ ```ruby
177
+ kb = KBS.knowledge_base do
178
+ rule "log_temperature", priority: 0 do
179
+ on :temperature, value: :temp?
180
+ perform { |facts, b| puts "Logged: #{b[:temp?]}" }
181
+ end
182
+
183
+ rule "critical_alert", priority: 100 do
184
+ on :temperature, value: greater_than(100)
185
+ perform { puts "CRITICAL TEMPERATURE!" }
186
+ end
187
+
188
+ rule "high_alert", priority: 50 do
189
+ on :temperature, value: greater_than(80)
190
+ perform { puts "High temperature warning" }
191
+ end
192
+ end
193
+
194
+ engine = KBS::Blackboard::Engine.new
195
+ kb.rules.each { |r| engine.add_rule(r) }
196
+ engine.add_fact(:temperature, value: 110)
197
+ engine.run
198
+
199
+ # Output (in priority order):
200
+ # CRITICAL TEMPERATURE! (priority 100)
201
+ # High temperature warning (priority 50)
202
+ # Logged: 110 (priority 0)
203
+ ```
204
+
205
+ ---
206
+
207
+ #### `conditions`
208
+
209
+ **Type**: `Array<KBS::Condition>`
210
+
211
+ **Read/Write**: Yes (via `attr_accessor`)
212
+
213
+ **Description**: Array of conditions that must all match for rule to fire
214
+
215
+ **Example - Low-level API**:
216
+ ```ruby
217
+ rule = KBS::Rule.new(:temperature_alert)
218
+ rule.conditions << KBS::Condition.new(:temperature, value: ->(v) { v > 80 })
219
+ rule.conditions << KBS::Condition.new(:sensor, status: "active")
220
+
221
+ puts rule.conditions.size # => 2
222
+ ```
223
+
224
+ **Using DSL (Recommended)**:
225
+ ```ruby
226
+ kb = KBS.knowledge_base do
227
+ rule "temperature_alert" do
228
+ on :temperature, value: greater_than(80)
229
+ on :sensor, status: "active"
230
+ perform { puts "Alert!" }
231
+ end
232
+ end
233
+
234
+ puts kb.rules.first.conditions.size # => 2
235
+ ```
236
+
237
+ **Condition Order Matters** (for performance):
238
+
239
+ **Low-level API**:
240
+ ```ruby
241
+ # Good - Most selective condition first
242
+ rule.conditions = [
243
+ KBS::Condition.new(:sensor, id: 42), # Filters to 1 fact
244
+ KBS::Condition.new(:temperature, value: :temp?) # Then match temperature
245
+ ]
246
+
247
+ # Less optimal - Less selective first
248
+ rule.conditions = [
249
+ KBS::Condition.new(:temperature, value: :temp?), # Matches many facts
250
+ KBS::Condition.new(:sensor, id: 42) # Could have filtered first
251
+ ]
252
+ ```
253
+
254
+ **Using DSL (Recommended)**:
255
+ ```ruby
256
+ # Good - Most selective condition first
257
+ rule "sensor_alert" do
258
+ on :sensor, id: 42 # Filters to 1 fact
259
+ on :temperature, value: :temp? # Then match temperature
260
+ perform { |facts, b| puts b[:temp?] }
261
+ end
262
+
263
+ # Less optimal - Less selective first
264
+ rule "sensor_alert" do
265
+ on :temperature, value: :temp? # Matches many facts
266
+ on :sensor, id: 42 # Could have filtered first
267
+ perform { |facts, b| puts b[:temp?] }
268
+ end
269
+ ```
270
+
271
+ See [Performance Guide](../advanced/performance.md) for condition ordering strategies.
272
+
273
+ ---
274
+
275
+ #### `action`
276
+
277
+ **Type**: `Proc` (lambda or proc)
278
+
279
+ **Read/Write**: Yes (via `attr_accessor`)
280
+
281
+ **Description**: Lambda executed when all conditions match
282
+
283
+ **Signature**: `action.call(facts)` or `action.call(facts, bindings)` (both supported)
284
+
285
+ **Parameters**:
286
+ - `facts` (Array<KBS::Fact>) - Array of matched facts (parallel to conditions array)
287
+ - `bindings` (Hash, optional) - Variable bindings extracted from facts
288
+
289
+ **Example - Low-level API (Facts Parameter)**:
290
+ ```ruby
291
+ rule.action = ->(facts) do
292
+ temp_fact = facts[0] # First condition's matched fact
293
+ sensor_fact = facts[1] # Second condition's matched fact
294
+
295
+ puts "Temperature: #{temp_fact[:value]} from sensor #{sensor_fact[:id]}"
296
+ end
297
+ ```
298
+
299
+ **Example - Low-level API (Bindings Parameter)**:
300
+ ```ruby
301
+ # Rule with variable bindings
302
+ rule = KBS::Rule.new(:temperature_alert) do |r|
303
+ r.conditions << KBS::Condition.new(:temperature, value: :temp?, location: :loc?)
304
+ r.action = ->(facts, bindings) do
305
+ # bindings: {:temp? => 85, :loc? => "server_room"}
306
+ puts "#{bindings[:loc?]}: #{bindings[:temp?]}°F"
307
+ end
308
+ end
309
+ ```
310
+
311
+ **Using DSL (Recommended)**:
312
+ ```ruby
313
+ rule "temperature_alert" do
314
+ on :temperature, value: :temp?, location: :loc?
315
+ perform do |facts, bindings|
316
+ # Cleaner - DSL automatically provides bindings
317
+ puts "#{bindings[:loc?]}: #{bindings[:temp?]}°F"
318
+ end
319
+ end
320
+ ```
321
+
322
+ **Action Requirements**:
323
+ - Must be a Proc (lambda or proc)
324
+ - Should be idempotent if possible (safe to run multiple times)
325
+ - Should not modify facts directly (use `engine.add_fact` / `engine.remove_fact` instead)
326
+ - May add/remove facts (triggers new rule evaluation)
327
+
328
+ ---
329
+
330
+ ### Public Methods
331
+
332
+ #### `fire(facts)`
333
+
334
+ Executes the rule's action with matched facts.
335
+
336
+ **Parameters**:
337
+ - `facts` (Array<KBS::Fact>) - Matched facts (one per condition)
338
+
339
+ **Returns**: Result of action lambda, or `nil` if no action
340
+
341
+ **Side Effects**:
342
+ - Increments internal `@fired_count`
343
+ - Executes action lambda
344
+ - Action may modify external state, add/remove facts, etc.
345
+
346
+ **Example - Low-level API**:
347
+ ```ruby
348
+ rule = KBS::Rule.new(:log_temperature) do |r|
349
+ r.conditions << KBS::Condition.new(:temperature, value: :temp?)
350
+ r.action = ->(facts, bindings) do
351
+ puts "Temperature: #{bindings[:temp?]}"
352
+ end
353
+ end
354
+
355
+ fact = KBS::Fact.new(:temperature, value: 85)
356
+ rule.fire([fact])
357
+ # Output: Temperature: 85
358
+ ```
359
+
360
+ **Using DSL (Recommended)**:
361
+ ```ruby
362
+ kb = KBS.knowledge_base do
363
+ rule "log_temperature" do
364
+ on :temperature, value: :temp?
365
+ perform do |facts, bindings|
366
+ puts "Temperature: #{bindings[:temp?]}"
367
+ end
368
+ end
369
+
370
+ fact :temperature, value: 85
371
+ run # Fires the rule automatically
372
+ end
373
+ # Output: Temperature: 85
374
+ ```
375
+
376
+ **Note**: Typically called by the RETE engine, not user code. Users call `engine.run` which fires all activated rules.
377
+
378
+ ---
379
+
380
+ ## Rule Lifecycle
381
+
382
+ ### 1. Rule Creation
383
+
384
+ ```ruby
385
+ # Via DSL (recommended)
386
+ kb = KBS.knowledge_base do
387
+ rule "my_rule", priority: 10 do
388
+ on :temperature, value: :temp?
389
+ perform { |facts, b| puts b[:temp?] }
390
+ end
391
+ end
392
+
393
+ # Or programmatically
394
+ rule = KBS::Rule.new(
395
+ :my_rule,
396
+ conditions: [KBS::Condition.new(:temperature, value: :temp?)],
397
+ action: ->(facts) { puts facts[0][:value] },
398
+ priority: 10
399
+ )
400
+ ```
401
+
402
+ ---
403
+
404
+ ### 2. Rule Registration
405
+
406
+ ```ruby
407
+ engine.add_rule(rule)
408
+ # Internally:
409
+ # - Adds rule to @rules array
410
+ # - Compiles rule into RETE network
411
+ # - Creates alpha memories for condition patterns
412
+ # - Creates join nodes (or negation nodes)
413
+ # - Creates production node for rule
414
+ # - Activates existing facts through new network
415
+ ```
416
+
417
+ ---
418
+
419
+ ### 3. Rule Activation
420
+
421
+ ```ruby
422
+ engine.add_fact(:temperature, value: 85)
423
+ # Internally:
424
+ # - Fact activates matching alpha memories
425
+ # - Propagates through join nodes
426
+ # - Creates tokens in beta memories
427
+ # - Token reaches production node
428
+ # - Rule is "activated" (ready to fire)
429
+ ```
430
+
431
+ ---
432
+
433
+ ### 4. Rule Firing
434
+
435
+ ```ruby
436
+ engine.run
437
+ # Internally (KBS::Engine):
438
+ # - Iterates production nodes
439
+ # - For each token in production node:
440
+ # - Calls rule.fire(token.facts)
441
+ # - Executes action lambda
442
+
443
+ # Internally (KBS::Blackboard::Engine):
444
+ # - Same as above, but:
445
+ # - Logs rule firing to audit trail
446
+ # - Marks token as fired (prevents duplicate firing)
447
+ # - Records variable bindings
448
+ ```
449
+
450
+ ---
451
+
452
+ ### 5. Rule Re-firing
453
+
454
+ Rules can fire multiple times:
455
+
456
+ ```ruby
457
+ rule "log_temperature" do
458
+ on :temperature, value: :temp?
459
+ perform { |facts, b| puts "Temperature: #{b[:temp?]}" }
460
+ end
461
+
462
+ engine.add_fact(:temperature, value: 85)
463
+ engine.add_fact(:temperature, value: 90)
464
+ engine.add_fact(:temperature, value: 95)
465
+ engine.run
466
+
467
+ # Output:
468
+ # Temperature: 85
469
+ # Temperature: 90
470
+ # Temperature: 95
471
+ ```
472
+
473
+ Each fact creates a separate activation (token) that fires independently.
474
+
475
+ ---
476
+
477
+ ## Rule Patterns
478
+
479
+ ### 1. Simple Rule (One Condition)
480
+
481
+ Match single fact type:
482
+
483
+ ```ruby
484
+ rule "log_all_temperatures" do
485
+ on :temperature, value: :temp?
486
+ perform do |facts, bindings|
487
+ puts "Temperature: #{bindings[:temp?]}"
488
+ end
489
+ end
490
+ ```
491
+
492
+ ---
493
+
494
+ ### 2. Join Rule (Multiple Conditions)
495
+
496
+ Match multiple related facts:
497
+
498
+ ```ruby
499
+ rule "sensor_temperature_alert" do
500
+ on :sensor, id: :sensor_id?, status: "active"
501
+ on :temperature, sensor_id: :sensor_id?, value: greater_than(80)
502
+ perform do |facts, bindings|
503
+ puts "Sensor #{bindings[:sensor_id?]} reports high temperature"
504
+ end
505
+ end
506
+
507
+ # Matches when:
508
+ # - sensor fact with id=42, status="active" exists
509
+ # - temperature fact with sensor_id=42, value > 80 exists
510
+ ```
511
+
512
+ **Variable Binding**: `:sensor_id?` in first condition must equal `sensor_id` in second condition (join test).
513
+
514
+ ---
515
+
516
+ ### 3. Guard Rule (Negation)
517
+
518
+ Match when fact is absent:
519
+
520
+ ```ruby
521
+ rule "all_clear" do
522
+ on :system, status: "running"
523
+ negated :alert, level: "critical" # Fire when NO critical alerts exist
524
+ perform do
525
+ puts "All systems normal"
526
+ end
527
+ end
528
+ ```
529
+
530
+ ---
531
+
532
+ ### 4. State Machine Rule
533
+
534
+ Rules can implement state transitions:
535
+
536
+ ```ruby
537
+ rule "pending_to_processing" do
538
+ on :order, id: :order_id?, status: "pending"
539
+ on :worker, status: "available", id: :worker_id?
540
+ perform do |facts, bindings|
541
+ # Transition order to processing
542
+ order = find_order(bindings[:order_id?])
543
+ order.update(status: "processing", worker_id: bindings[:worker_id?])
544
+
545
+ # Update worker
546
+ worker = find_worker(bindings[:worker_id?])
547
+ worker.update(status: "busy")
548
+ end
549
+ end
550
+ ```
551
+
552
+ ---
553
+
554
+ ### 5. Cleanup Rule
555
+
556
+ Low-priority rules that clean up old facts:
557
+
558
+ ```ruby
559
+ rule "expire_old_temperatures", priority: 0 do
560
+ on :temperature, timestamp: less_than(Time.now - 3600)
561
+ perform do |facts, bindings|
562
+ fact = bindings[:matched_fact?]
563
+ fact.retract # Remove old temperature reading
564
+ end
565
+ end
566
+ ```
567
+
568
+ ---
569
+
570
+ ### 6. Aggregation Rule
571
+
572
+ Collect multiple facts and compute aggregate:
573
+
574
+ ```ruby
575
+ rule "daily_temperature_summary", priority: 5 do
576
+ on :trigger, event: "end_of_day"
577
+ perform do
578
+ temps = engine.working_memory.facts
579
+ .select { |f| f.type == :temperature }
580
+ .map { |f| f[:value] }
581
+
582
+ avg = temps.sum / temps.size.to_f
583
+ max = temps.max
584
+ min = temps.min
585
+
586
+ engine.add_fact(:daily_summary, avg: avg, max: max, min: min, date: Date.today)
587
+ end
588
+ end
589
+ ```
590
+
591
+ ---
592
+
593
+ ### 7. Conflict Resolution Rule
594
+
595
+ Higher priority rule overrides lower priority:
596
+
597
+ ```ruby
598
+ rule "high_risk_order", priority: 100 do
599
+ on :order, id: :order_id?, total: greater_than(10000)
600
+ perform do |facts, bindings|
601
+ puts "HIGH RISK: Order #{bindings[:order_id?]} requires manual review"
602
+ # This fires first due to priority
603
+ end
604
+ end
605
+
606
+ rule "auto_approve_order", priority: 10 do
607
+ on :order, id: :order_id?, status: "pending"
608
+ perform do |facts, bindings|
609
+ puts "Auto-approving order #{bindings[:order_id?]}"
610
+ # This fires later (if at all)
611
+ end
612
+ end
613
+ ```
614
+
615
+ ---
616
+
617
+ ### 8. Recursive Rule
618
+
619
+ Rule that adds facts triggering other rules:
620
+
621
+ ```ruby
622
+ rule "calculate_fibonacci" do
623
+ on :fib_request, n: :n?
624
+ negated :fib_result, n: :n? # Not already calculated
625
+ perform do |facts, bindings|
626
+ n = bindings[:n?]
627
+
628
+ if n <= 1
629
+ engine.add_fact(:fib_result, n: n, value: n)
630
+ else
631
+ # Request sub-problems
632
+ engine.add_fact(:fib_request, n: n - 1)
633
+ engine.add_fact(:fib_request, n: n - 2)
634
+
635
+ # Wait for sub-results in another rule...
636
+ end
637
+ end
638
+ end
639
+
640
+ rule "combine_fibonacci" do
641
+ on :fib_request, n: :n?
642
+ on :fib_result, n: :n_minus_1?, value: :val1?
643
+ on :fib_result, n: :n_minus_2?, value: :val2?
644
+ # ... (complex join test: ?n_minus_1 == ?n - 1, etc.)
645
+ perform do |facts, bindings|
646
+ result = bindings[:val1?] + bindings[:val2?]
647
+ engine.add_fact(:fib_result, n: bindings[:n?], value: result)
648
+ end
649
+ end
650
+ ```
651
+
652
+ ---
653
+
654
+ ## Best Practices
655
+
656
+ ### 1. Descriptive Rule Names
657
+
658
+ ```ruby
659
+ # Good
660
+ rule "high_temperature_alert"
661
+ rule "low_inventory_reorder"
662
+ rule "fraud_detection_suspicious_transaction"
663
+
664
+ # Bad
665
+ rule "rule1"
666
+ rule "temp"
667
+ rule "check"
668
+ ```
669
+
670
+ ---
671
+
672
+ ### 2. Order Conditions by Selectivity
673
+
674
+ Most selective (fewest matching facts) first:
675
+
676
+ ```ruby
677
+ # Good - sensor_id=42 filters to ~1 fact
678
+ rule "sensor_alert" do
679
+ on :sensor, id: 42, status: :status? # Very selective
680
+ on :temperature, sensor_id: 42, value: :temp? # Also selective
681
+ perform { ... }
682
+ end
683
+
684
+ # Bad - :temperature matches many facts
685
+ rule "sensor_alert" do
686
+ on :temperature, value: :temp? # Matches 1000s of facts
687
+ on :sensor, id: 42, status: :status? # Could have filtered first
688
+ perform { ... }
689
+ end
690
+ ```
691
+
692
+ **Why**: RETE builds network from first to last condition. Fewer intermediate tokens = faster.
693
+
694
+ ---
695
+
696
+ ### 3. Use Priority for Critical Rules
697
+
698
+ ```ruby
699
+ rule "critical_shutdown", priority: 1000 do
700
+ on :temperature, value: greater_than(120)
701
+ perform { shutdown_system! }
702
+ end
703
+
704
+ rule "log_temperature", priority: 0 do
705
+ on :temperature, value: :temp?
706
+ perform { |facts, b| log(b[:temp?]) }
707
+ end
708
+ ```
709
+
710
+ Critical safety rules should have high priority to fire before less important rules.
711
+
712
+ ---
713
+
714
+ ### 4. Keep Actions Idempotent
715
+
716
+ ```ruby
717
+ # Good - Idempotent (safe to run multiple times)
718
+ rule "alert_high_temp" do
719
+ on :temperature, value: greater_than(80)
720
+ perform do |facts, bindings|
721
+ # Check if alert already sent
722
+ unless alert_sent?(bindings[:temp?])
723
+ send_alert(bindings[:temp?])
724
+ mark_alert_sent(bindings[:temp?])
725
+ end
726
+ end
727
+ end
728
+
729
+ # Bad - Not idempotent (sends duplicate alerts)
730
+ rule "alert_high_temp" do
731
+ on :temperature, value: greater_than(80)
732
+ perform do |facts, bindings|
733
+ send_alert(bindings[:temp?]) # Sends every time rule fires
734
+ end
735
+ end
736
+ ```
737
+
738
+ ---
739
+
740
+ ### 5. Avoid Side Effects in Conditions
741
+
742
+ ```ruby
743
+ # Bad - Side effect in condition predicate
744
+ counter = 0
745
+ rule "count_temps" do
746
+ on :temperature, value: ->(v) { counter += 1; v > 80 } # BAD!
747
+ perform { puts "Count: #{counter}" }
748
+ end
749
+
750
+ # Good - Side effects in action only
751
+ counter = 0
752
+ rule "count_temps" do
753
+ on :temperature, value: greater_than(80)
754
+ perform { counter += 1; puts "Count: #{counter}" }
755
+ end
756
+ ```
757
+
758
+ **Why**: Predicates run during pattern matching (potentially multiple times). Side effects cause unpredictable behavior.
759
+
760
+ ---
761
+
762
+ ### 6. Use Variable Bindings for Joins
763
+
764
+ ```ruby
765
+ # Good - Variable binding creates join test
766
+ rule "order_inventory_check" do
767
+ on :order, product_id: :pid?, quantity: :qty?
768
+ on :inventory, product_id: :pid?, available: :available?
769
+ perform do |facts, bindings|
770
+ if bindings[:available?] < bindings[:qty?]
771
+ puts "Insufficient inventory for product #{bindings[:pid?]}"
772
+ end
773
+ end
774
+ end
775
+
776
+ # Bad - No join test (matches all combinations)
777
+ rule "order_inventory_check" do
778
+ on :order, product_id: :pid1?, quantity: :qty?
779
+ on :inventory, product_id: :pid2?, available: :available?
780
+ perform do |facts, bindings|
781
+ # No guarantee pid1 == pid2!
782
+ if bindings[:pid1?] == bindings[:pid2?] # Manual check in action (inefficient)
783
+ ...
784
+ end
785
+ end
786
+ end
787
+ ```
788
+
789
+ ---
790
+
791
+ ### 7. Document Complex Rules
792
+
793
+ ```ruby
794
+ # Good - Documented
795
+ rule "portfolio_rebalancing", priority: 50 do
796
+ # Triggers when portfolio drift exceeds threshold
797
+ # Conditions:
798
+ # 1. Portfolio exists and is active
799
+ # 2. Current allocation deviates > 5% from target
800
+ # Action:
801
+ # - Calculates rebalancing trades
802
+ # - Creates pending orders
803
+
804
+ on :portfolio, id: :portfolio_id?, status: "active"
805
+ on :drift_calculation, portfolio_id: :portfolio_id?, drift: greater_than(0.05)
806
+ perform do |facts, bindings|
807
+ # Implementation...
808
+ end
809
+ end
810
+ ```
811
+
812
+ ---
813
+
814
+ ### 8. Test Rules in Isolation
815
+
816
+ ```ruby
817
+ require 'minitest/autorun'
818
+
819
+ class TestHighTemperatureRule < Minitest::Test
820
+ def setup
821
+ @engine = KBS::Blackboard::Engine.new
822
+ @fired = false
823
+
824
+ @rule = KBS::Rule.new(:high_temp) do |r|
825
+ r.conditions << KBS::Condition.new(:temperature, value: ->(v) { v > 80 })
826
+ r.action = ->(facts) { @fired = true }
827
+ end
828
+
829
+ @engine.add_rule(@rule)
830
+ end
831
+
832
+ def test_fires_when_temperature_high
833
+ @engine.add_fact(:temperature, value: 85)
834
+ @engine.run
835
+ assert @fired
836
+ end
837
+
838
+ def test_does_not_fire_when_temperature_low
839
+ @engine.add_fact(:temperature, value: 75)
840
+ @engine.run
841
+ refute @fired
842
+ end
843
+ end
844
+ ```
845
+
846
+ ---
847
+
848
+ ### 9. Use Negation for Guards
849
+
850
+ ```ruby
851
+ # Good - Negation ensures system ready
852
+ rule "start_processing" do
853
+ on :work_item, status: "pending"
854
+ negated :system_error # Don't process if system has errors
855
+ perform { process_work_item }
856
+ end
857
+
858
+ # Alternative - Check in action (less efficient)
859
+ rule "start_processing" do
860
+ on :work_item, status: "pending"
861
+ perform do
862
+ unless system_has_errors?
863
+ process_work_item
864
+ end
865
+ end
866
+ end
867
+ ```
868
+
869
+ **Why**: Negation in condition prevents token creation. Action-based check still creates token (wastes memory).
870
+
871
+ ---
872
+
873
+ ### 10. Limit Fact Growth
874
+
875
+ ```ruby
876
+ # Good - Cleanup rule prevents unbounded growth
877
+ rule "expire_old_facts", priority: 0 do
878
+ on :temperature, timestamp: less_than(Time.now - 3600)
879
+ perform do |facts, bindings|
880
+ fact = bindings[:matched_fact?]
881
+ fact.retract
882
+ end
883
+ end
884
+
885
+ # Bad - No cleanup (memory leak)
886
+ loop do
887
+ engine.add_fact(:temperature, value: rand(100), timestamp: Time.now)
888
+ engine.run
889
+ sleep 1
890
+ # Facts accumulate forever!
891
+ end
892
+ ```
893
+
894
+ ---
895
+
896
+ ## Common Patterns Reference
897
+
898
+ ### Rule Priority Examples
899
+
900
+ ```ruby
901
+ # Emergency shutdown
902
+ priority: 1000
903
+
904
+ # Critical alerts
905
+ priority: 500
906
+
907
+ # Business logic
908
+ priority: 100
909
+
910
+ # Data validation
911
+ priority: 50
912
+
913
+ # Standard processing
914
+ priority: 10
915
+
916
+ # Logging/auditing
917
+ priority: 5
918
+
919
+ # Cleanup
920
+ priority: 0
921
+ ```
922
+
923
+ ---
924
+
925
+ ### Action Signatures
926
+
927
+ ```ruby
928
+ # 1. Facts only
929
+ action: ->(facts) do
930
+ temp_fact = facts[0]
931
+ puts temp_fact[:value]
932
+ end
933
+
934
+ # 2. Facts and bindings (recommended)
935
+ action: ->(facts, bindings) do
936
+ puts bindings[:temp?]
937
+ end
938
+
939
+ # 3. DSL style (cleanest)
940
+ perform do |facts, bindings|
941
+ puts bindings[:temp?]
942
+ end
943
+ ```
944
+
945
+ ---
946
+
947
+ ### Condition Patterns
948
+
949
+ ```ruby
950
+ # Literal matching
951
+ on :temperature, location: "server_room"
952
+
953
+ # Range check
954
+ on :temperature, value: between(70, 90)
955
+ on :temperature, value: greater_than(80)
956
+ on :temperature, value: less_than(100)
957
+
958
+ # Variable binding
959
+ on :temperature, location: :loc?, value: :temp?
960
+
961
+ # Predicate
962
+ on :temperature, value: ->(v) { v > 80 && v < 100 }
963
+
964
+ # Negation
965
+ negated :alert, level: "critical"
966
+
967
+ # Collection membership
968
+ on :order, status: one_of("pending", "processing", "completed")
969
+ ```
970
+
971
+ ---
972
+
973
+ ## Performance Considerations
974
+
975
+ ### Rule Compilation Cost
976
+
977
+ Adding a rule to the engine compiles it into the RETE network:
978
+
979
+ ```ruby
980
+ # Cost: O(C) where C = number of conditions
981
+ engine.add_rule(rule)
982
+ ```
983
+
984
+ **Optimization**: Add all rules before adding facts:
985
+
986
+ ```ruby
987
+ # Good
988
+ kb.rules.each { |r| engine.add_rule(r) } # Compile all rules first
989
+ facts.each { |f| engine.add_fact(f.type, f.attributes) } # Then add facts
990
+ engine.run
991
+
992
+ # Less optimal
993
+ facts.each do |f|
994
+ engine.add_fact(f.type, f.attributes)
995
+ kb.rules.each { |r| engine.add_rule(r) } # Recompiling for each fact!
996
+ engine.run
997
+ end
998
+ ```
999
+
1000
+ ---
1001
+
1002
+ ### Condition Ordering
1003
+
1004
+ Order conditions from most to least selective:
1005
+
1006
+ ```ruby
1007
+ # Assume:
1008
+ # - 10,000 temperature facts
1009
+ # - 100 sensor facts
1010
+ # - 10 sensors with id=42
1011
+
1012
+ # Good (selective first)
1013
+ rule "alert" do
1014
+ on :sensor, id: 42, status: :status? # Filters to 10 facts
1015
+ on :temperature, sensor_id: 42, value: :v? # Then filters to ~100 facts
1016
+ # Creates ~10 intermediate tokens
1017
+ end
1018
+
1019
+ # Bad (unselective first)
1020
+ rule "alert" do
1021
+ on :temperature, value: :v? # Matches 10,000 facts!
1022
+ on :sensor, id: 42, status: :status? # Then filters
1023
+ # Creates 10,000 intermediate tokens (slow, memory-intensive)
1024
+ end
1025
+ ```
1026
+
1027
+ ---
1028
+
1029
+ ### Action Complexity
1030
+
1031
+ Keep actions fast:
1032
+
1033
+ ```ruby
1034
+ # Good - Fast action
1035
+ perform do |facts, bindings|
1036
+ puts "Temperature: #{bindings[:temp?]}"
1037
+ end
1038
+
1039
+ # Bad - Slow action blocks engine
1040
+ perform do |facts, bindings|
1041
+ sleep 5 # Blocks engine for 5 seconds!
1042
+ send_email_alert(bindings[:temp?]) # Network I/O
1043
+ end
1044
+
1045
+ # Better - Offload slow work
1046
+ perform do |facts, bindings|
1047
+ # Post message for async worker
1048
+ engine.post_message("alert_system", "email_queue", bindings)
1049
+ end
1050
+ ```
1051
+
1052
+ ---
1053
+
1054
+ ## Debugging Rules
1055
+
1056
+ ### Why Didn't My Rule Fire?
1057
+
1058
+ ```ruby
1059
+ def debug_rule(engine, rule_name)
1060
+ rule = engine.rules.find { |r| r.name == rule_name }
1061
+ return "Rule not found" unless rule
1062
+
1063
+ puts "Rule: #{rule.name}"
1064
+ puts "Conditions (#{rule.conditions.size}):"
1065
+
1066
+ rule.conditions.each_with_index do |cond, i|
1067
+ matching_facts = engine.working_memory.facts.select { |f| f.matches?(cond.pattern.merge(type: cond.type)) }
1068
+
1069
+ puts " #{i + 1}. #{cond.type} #{cond.pattern}"
1070
+ puts " Negated: #{cond.negated}"
1071
+ puts " Matching facts: #{matching_facts.size}"
1072
+
1073
+ if matching_facts.empty?
1074
+ puts " ❌ NO MATCHING FACTS (rule can't fire)"
1075
+ else
1076
+ puts " ✓ #{matching_facts.size} facts match"
1077
+ matching_facts.first(3).each do |f|
1078
+ puts " - #{f}"
1079
+ end
1080
+ end
1081
+ end
1082
+
1083
+ # Check production node
1084
+ prod_node = engine.production_nodes[rule.name]
1085
+ if prod_node
1086
+ puts "Production node activations: #{prod_node.tokens.size}"
1087
+ else
1088
+ puts "Production node not found (rule not compiled?)"
1089
+ end
1090
+ end
1091
+
1092
+ debug_rule(engine, :high_temperature)
1093
+ ```
1094
+
1095
+ ---
1096
+
1097
+ ## See Also
1098
+
1099
+ - [Engine API](engine.md) - Registering and running rules
1100
+ - [Facts API](facts.md) - Understanding fact matching
1101
+ - [DSL Guide](../guides/dsl.md) - Declarative rule syntax
1102
+ - [Writing Rules Guide](../guides/writing-rules.md) - Best practices and patterns
1103
+ - [Performance Guide](../advanced/performance.md) - Optimization strategies
1104
+ - [Testing Guide](../advanced/testing.md) - Testing rules in isolation