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