kbs 0.1.0 → 0.2.1

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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +19 -0
  3. data/README.md +85 -57
  4. data/docs/advanced/performance.md +109 -76
  5. data/docs/advanced/testing.md +399 -263
  6. data/docs/api/blackboard.md +1 -1
  7. data/docs/api/engine.md +77 -8
  8. data/docs/api/facts.md +3 -3
  9. data/docs/api/rules.md +110 -40
  10. data/docs/architecture/blackboard.md +108 -117
  11. data/docs/assets/images/fact-rule-relationship.svg +65 -0
  12. data/docs/assets/images/fact-structure.svg +42 -0
  13. data/docs/assets/images/inference-cycle.svg +47 -0
  14. data/docs/assets/images/kb-components.svg +43 -0
  15. data/docs/assets/images/rule-structure.svg +44 -0
  16. data/docs/assets/images/trading-signal-network.svg +1 -1
  17. data/docs/examples/index.md +219 -5
  18. data/docs/guides/blackboard-memory.md +89 -58
  19. data/docs/guides/dsl.md +24 -24
  20. data/docs/guides/getting-started.md +109 -107
  21. data/docs/guides/writing-rules.md +470 -311
  22. data/docs/index.md +16 -18
  23. data/docs/quick-start.md +92 -99
  24. data/docs/what-is-a-fact.md +694 -0
  25. data/docs/what-is-a-knowledge-base.md +350 -0
  26. data/docs/what-is-a-rule.md +833 -0
  27. data/examples/.gitignore +1 -0
  28. data/examples/advanced_example_dsl.rb +1 -1
  29. data/examples/ai_enhanced_kbs_dsl.rb +1 -1
  30. data/examples/car_diagnostic_dsl.rb +1 -1
  31. data/examples/concurrent_inference_demo.rb +0 -1
  32. data/examples/concurrent_inference_demo_dsl.rb +0 -1
  33. data/examples/csv_trading_system_dsl.rb +1 -1
  34. data/examples/iot_demo_using_dsl.rb +1 -1
  35. data/examples/portfolio_rebalancing_system_dsl.rb +1 -1
  36. data/examples/rule_source_demo.rb +123 -0
  37. data/examples/stock_trading_advanced_dsl.rb +1 -1
  38. data/examples/temp_dsl.txt +6214 -5269
  39. data/examples/timestamped_trading_dsl.rb +1 -1
  40. data/examples/trading_demo_dsl.rb +1 -1
  41. data/examples/working_demo_dsl.rb +1 -1
  42. data/lib/kbs/decompiler.rb +204 -0
  43. data/lib/kbs/dsl/knowledge_base.rb +100 -1
  44. data/lib/kbs/dsl.rb +3 -1
  45. data/lib/kbs/engine.rb +41 -0
  46. data/lib/kbs/version.rb +1 -1
  47. data/lib/kbs.rb +14 -12
  48. data/mkdocs.yml +30 -30
  49. metadata +15 -10
  50. data/docs/DOCUMENTATION_STATUS.md +0 -158
  51. data/docs/examples/expert-systems.md +0 -1031
  52. data/docs/examples/multi-agent.md +0 -1335
  53. data/docs/examples/stock-trading.md +0 -488
  54. data/examples/knowledge_base.db +0 -0
  55. data/examples/temp.txt +0 -7693
@@ -0,0 +1,833 @@
1
+ # What is a Rule?
2
+
3
+ A **rule** is a declarative IF-THEN statement that defines what action to take when certain patterns of facts exist in the knowledge base. Rules are the "logic" that operates on facts (the "data").
4
+
5
+ ## Core Concept
6
+
7
+ Think of a rule as an **automated detector and responder**:
8
+
9
+ - **IF** these patterns exist in the knowledge base (conditions)
10
+ - **THEN** execute this action (perform block)
11
+
12
+ Unlike procedural code that you explicitly call, rules **automatically fire** when their conditions are satisfied.
13
+
14
+ ## Anatomy of a Rule
15
+
16
+ ### Basic Structure
17
+
18
+ ```ruby
19
+ rule "high_temperature_alert" do
20
+ # 1. METADATA (optional)
21
+ desc "Alert when server room temperature exceeds safe threshold"
22
+ priority 10
23
+
24
+ # 2. CONDITIONS (the IF part)
25
+ on :temperature, location: "server_room", value: greater_than(80)
26
+ on :sensor, location: "server_room", status: "active"
27
+
28
+ # 3. ACTION (the THEN part)
29
+ perform do |facts, bindings|
30
+ send_alert("High temperature: #{bindings[:value?]}°F")
31
+ end
32
+ end
33
+ ```
34
+
35
+ ### Visual Representation
36
+
37
+ ![Rule Structure](assets/images/rule-structure.svg)
38
+
39
+ *A rule consists of three parts: metadata (name, description, priority), conditions that must ALL match, and an action that executes when conditions are satisfied.*
40
+
41
+ ## How Rules Differ from Other Programming Constructs
42
+
43
+ | Aspect | Rule | Function/Method | IF Statement | Event Handler |
44
+ |--------|------|-----------------|--------------|---------------|
45
+ | **Invocation** | Automatic (pattern match) | Manual (explicit call) | Manual (in code flow) | Event-driven (explicit bind) |
46
+ | **When** | When patterns exist | When called | When executed | When event fires |
47
+ | **Conditions** | Declarative patterns | Imperative checks | Imperative checks | Event type |
48
+ | **Ordering** | By priority/RETE | Call sequence | Code sequence | Event sequence |
49
+ | **Scope** | All facts in KB | Parameters passed | Local variables | Event payload |
50
+
51
+ **Example Comparison:**
52
+
53
+ ```ruby
54
+ # Function - Manual invocation
55
+ def check_temperature(temp)
56
+ if temp > 80
57
+ send_alert("High temp: #{temp}")
58
+ end
59
+ end
60
+ check_temperature(85) # Must explicitly call
61
+
62
+ # IF Statement - Part of code flow
63
+ temperature = sensor.read
64
+ if temperature > 80 && sensor.active?
65
+ send_alert("High temp: #{temperature}")
66
+ end
67
+
68
+ # Event Handler - Event binding
69
+ sensor.on(:reading) do |temp|
70
+ if temp > 80
71
+ send_alert("High temp: #{temp}")
72
+ end
73
+ end
74
+
75
+ # Rule - Declarative, automatic
76
+ rule "high_temperature" do
77
+ on :temperature, value: greater_than(80)
78
+ on :sensor, status: "active"
79
+ perform do |facts, bindings|
80
+ send_alert("High temp: #{bindings[:value?]}")
81
+ end
82
+ end
83
+ # Fires automatically when facts match!
84
+ ```
85
+
86
+ ## Rule Lifecycle
87
+
88
+ ### 1. Definition
89
+
90
+ Rules are defined using the DSL:
91
+
92
+ ```ruby
93
+ kb = KBS.knowledge_base do
94
+ rule "golden_cross_signal" do
95
+ on :ma_50, value: :fast?
96
+ on :ma_200, value: :slow?
97
+ perform do |facts, bindings|
98
+ if bindings[:fast?] > bindings[:slow?]
99
+ puts "Buy signal: Golden cross detected"
100
+ end
101
+ end
102
+ end
103
+ end
104
+ ```
105
+
106
+ ### 2. Compilation
107
+
108
+ When added to an engine, rules are compiled into a RETE network:
109
+
110
+ ```ruby
111
+ engine.add_rule(rule)
112
+ # Rule compiled into discrimination network
113
+ # - Alpha nodes for each pattern
114
+ # - Join nodes to combine patterns
115
+ # - Production node for action
116
+ ```
117
+
118
+ ### 3. Activation
119
+
120
+ As facts are added, the rule's conditions are evaluated:
121
+
122
+ ```ruby
123
+ engine.add_fact(:ma_50, value: 52.3)
124
+ engine.add_fact(:ma_200, value: 51.8)
125
+ # Conditions now satisfied - rule activated
126
+ ```
127
+
128
+ ### 4. Firing
129
+
130
+ During `engine.run`, activated rules fire:
131
+
132
+ ```ruby
133
+ engine.run
134
+ # → "Buy signal: Golden cross detected"
135
+ ```
136
+
137
+ ### 5. Completion
138
+
139
+ Actions execute, potentially creating new facts:
140
+
141
+ ```ruby
142
+ perform do |facts, bindings|
143
+ # Can add derived facts
144
+ fact :signal, type: "golden_cross", timestamp: Time.now
145
+ # Can retract facts
146
+ retract old_signal
147
+ # Can call external code
148
+ execute_trade(bindings[:symbol?])
149
+ end
150
+ ```
151
+
152
+ ## Rule Components in Detail
153
+
154
+ ### Metadata
155
+
156
+ Optional information about the rule:
157
+
158
+ ```ruby
159
+ rule "fraud_detection" do
160
+ desc "Flag transactions with suspicious patterns"
161
+ priority 100 # Higher priority = fires first (blackboard only)
162
+ # ... conditions and action
163
+ end
164
+ ```
165
+
166
+ **Name** - Unique identifier
167
+
168
+ - Should be descriptive and actionable
169
+ - Use snake_case
170
+ - Example: `"reorder_low_inventory"`, `"escalate_critical_alert"`
171
+
172
+ **Description**—Human-readable explanation
173
+
174
+ - Documents the rule's purpose
175
+ - Helpful for debugging and maintenance
176
+ - Example: `"Reorders products when inventory falls below minimum threshold"`
177
+
178
+ **Priority**—Execution order (0-100 typical)
179
+
180
+ - Only affects `KBS::Blackboard::Engine`
181
+ - Higher numbers fire first
182
+ - Default: 0
183
+
184
+ ### Conditions (The IF Part)
185
+
186
+ Patterns that must ALL match for the rule to fire:
187
+
188
+ ```ruby
189
+ rule "order_fulfillment" do
190
+ # Condition 1: Must have pending order
191
+ on :order, status: "pending", product_id: :pid?, quantity: :qty?
192
+
193
+ # Condition 2: Must have inventory for same product
194
+ on :inventory, product_id: :pid?, available: :avail?
195
+ # ^^^^^^
196
+ # Join test - must match!
197
+
198
+ # Condition 3: Must NOT have existing shipment
199
+ without :shipment, order_id: :oid?
200
+
201
+ perform do |facts, bindings|
202
+ # Fires when ALL conditions satisfied
203
+ end
204
+ end
205
+ ```
206
+
207
+ **Condition Types:**
208
+
209
+ 1. **Positive** - Pattern must exist: `on :temperature, value: > 80`
210
+ 2. **Negative** - Pattern must NOT exist: `without :alert`
211
+ 3. **Join** - Variables link conditions: `:pid?` in both conditions above
212
+
213
+ ### Action (The THEN Part)
214
+
215
+ Code executed when all conditions match:
216
+
217
+ ```ruby
218
+ perform do |facts, bindings|
219
+ # facts - Array of matched facts (in condition order)
220
+ # bindings - Hash of variable captures {:pid? => 123, :qty? => 5}
221
+
222
+ # Can access facts
223
+ order = facts[0]
224
+ inventory = facts[1]
225
+
226
+ # Can access bindings
227
+ product_id = bindings[:pid?]
228
+ quantity = bindings[:qty?]
229
+ available = bindings[:avail?]
230
+
231
+ # Can make decisions
232
+ if available >= quantity
233
+ ship_order(order)
234
+ else
235
+ backorder(order)
236
+ end
237
+
238
+ # Can add facts
239
+ fact :shipment, order_id: order[:id], shipped_at: Time.now
240
+
241
+ # Can retract facts
242
+ retract order
243
+
244
+ # Can call external code
245
+ notify_customer(order[:customer_id])
246
+ end
247
+ ```
248
+
249
+ ## How Rules Work: The Inference Cycle
250
+
251
+ Rules participate in an automatic reasoning loop:
252
+
253
+ ![Inference Cycle](assets/images/inference-cycle.svg)
254
+
255
+ *Rules execute within a continuous inference cycle: facts are added, the RETE network matches patterns, activated rules fire and potentially create new facts, triggering another cycle. Inference completes when no new facts are generated.*
256
+
257
+ **Example:**
258
+
259
+ ```ruby
260
+ kb = KBS.knowledge_base do
261
+ # Rule 1: Detect high temperature
262
+ rule "detect_high_temp" do
263
+ on :temperature, value: greater_than(80), sensor_id: :sid?
264
+ without :alert, sensor_id: :sid?
265
+ perform do |facts, bindings|
266
+ # Add alert fact (triggers Rule 2)
267
+ fact :alert, sensor_id: bindings[:sid?], level: "high"
268
+ end
269
+ end
270
+
271
+ # Rule 2: Escalate alerts
272
+ rule "escalate_alert" do
273
+ on :alert, level: "high", sensor_id: :sid?
274
+ on :sensor, id: :sid?, critical: true
275
+ perform do |facts, bindings|
276
+ notify_ops(bindings[:sid?])
277
+ end
278
+ end
279
+
280
+ # Add facts
281
+ fact :temperature, value: 85, sensor_id: 42
282
+ fact :sensor, id: 42, critical: true
283
+
284
+ # Run inference
285
+ run
286
+ # → Rule 1 fires, creates :alert fact
287
+ # → Rule 2 fires (activated by new alert), notifies ops
288
+ end
289
+ ```
290
+
291
+ ## Types of Rules
292
+
293
+ ### 1. Detection Rules
294
+
295
+ Identify patterns and generate alerts:
296
+
297
+ ```ruby
298
+ rule "detect_fraud" do
299
+ on :transaction, amount: greater_than(10_000)
300
+ on :account, new_account: true
301
+ perform { flag_for_review }
302
+ end
303
+ ```
304
+
305
+ ### 2. Derivation Rules
306
+
307
+ Infer new facts from existing facts:
308
+
309
+ ```ruby
310
+ rule "derive_momentum" do
311
+ on :price, current: :curr?, previous: :prev?
312
+ perform do |facts, bindings|
313
+ change_pct = ((bindings[:curr?] - bindings[:prev?]) / bindings[:prev?]) * 100
314
+ fact :momentum, change_pct: change_pct
315
+ end
316
+ end
317
+ ```
318
+
319
+ ### 3. Reaction Rules
320
+
321
+ Take action when conditions arise:
322
+
323
+ ```ruby
324
+ rule "reorder_inventory" do
325
+ on :inventory, product_id: :pid?, quantity: less_than(10)
326
+ perform do |facts, bindings|
327
+ create_purchase_order(bindings[:pid?], quantity: 100)
328
+ end
329
+ end
330
+ ```
331
+
332
+ ### 4. State Machine Rules
333
+
334
+ Manage transitions between states:
335
+
336
+ ```ruby
337
+ rule "pending_to_processing" do
338
+ on :order, id: :oid?, status: "pending"
339
+ on :worker, status: "available", id: :wid?
340
+ perform do |facts, bindings|
341
+ order = query(:order, id: bindings[:oid?]).first
342
+ retract order
343
+ fact :order, id: bindings[:oid?], status: "processing", worker_id: bindings[:wid?]
344
+ end
345
+ end
346
+ ```
347
+
348
+ ### 5. Guard Rules
349
+
350
+ Prevent invalid states:
351
+
352
+ ```ruby
353
+ rule "prevent_duplicate_orders" do
354
+ on :order, customer_id: :cid?, product_id: :pid?, status: "pending"
355
+ on :order, customer_id: :cid?, product_id: :pid?, status: "processing"
356
+ perform do |facts, bindings|
357
+ cancel_duplicate_order(facts[0])
358
+ end
359
+ end
360
+ ```
361
+
362
+ ### 6. Cleanup Rules
363
+
364
+ Remove obsolete facts:
365
+
366
+ ```ruby
367
+ rule "expire_old_alerts" do
368
+ on :alert, timestamp: ->(ts) { Time.now - ts > 3600 }
369
+ perform do |facts, bindings|
370
+ retract facts[0]
371
+ end
372
+ end
373
+ ```
374
+
375
+ ## Rule Patterns and Best Practices
376
+
377
+ ### Pattern: Rule Chaining
378
+
379
+ Rules can trigger other rules:
380
+
381
+ ```ruby
382
+ # Rule 1 creates fact that activates Rule 2
383
+ rule "detect_anomaly" do
384
+ on :sensor, value: :val?
385
+ perform { fact :anomaly, value: bindings[:val?] }
386
+ end
387
+
388
+ rule "escalate_anomaly" do
389
+ on :anomaly, value: greater_than(100)
390
+ perform { send_alert }
391
+ end
392
+ ```
393
+
394
+ ### Pattern: Multi-Condition Filtering
395
+
396
+ Combine multiple conditions to narrow matches:
397
+
398
+ ```ruby
399
+ rule "qualified_lead" do
400
+ on :customer, revenue: greater_than(100_000)
401
+ on :interaction, customer_id: :cid?, type: "demo_request"
402
+ on :product_fit, customer_id: :cid?, score: greater_than(80)
403
+ without :opportunity, customer_id: :cid?
404
+ perform { create_opportunity }
405
+ end
406
+ ```
407
+
408
+ ### Pattern: Exception Handling
409
+
410
+ Use negation to ensure preconditions:
411
+
412
+ ```ruby
413
+ rule "process_payment" do
414
+ on :order, status: "confirmed"
415
+ without :payment, order_id: :oid? # No payment yet
416
+ without :error, order_id: :oid? # No errors
417
+ perform { charge_customer }
418
+ end
419
+ ```
420
+
421
+ ### Pattern: Temporal Rules
422
+
423
+ Time-aware reasoning:
424
+
425
+ ```ruby
426
+ rule "stale_data_warning" do
427
+ on :reading, timestamp: ->(ts) { Time.now - ts > 300 }, sensor_id: :sid?
428
+ perform do |facts, bindings|
429
+ alert("Stale data from sensor #{bindings[:sid?]}")
430
+ end
431
+ end
432
+ ```
433
+
434
+ ### Pattern: Aggregation
435
+
436
+ Collect and analyze multiple facts:
437
+
438
+ ```ruby
439
+ rule "daily_summary" do
440
+ on :trigger, event: "end_of_day"
441
+ perform do
442
+ temps = query(:temperature).map { |f| f[:value] }
443
+ avg = temps.sum / temps.size.to_f
444
+ fact :summary, avg_temp: avg, date: Date.today
445
+ end
446
+ end
447
+ ```
448
+
449
+ ## Rule Ordering and Priority
450
+
451
+ ### Priority in KBS::Blackboard::Engine
452
+
453
+ Controls which rules fire first when multiple are activated:
454
+
455
+ ```ruby
456
+ rule "critical_shutdown", priority: 100 do
457
+ on :temperature, value: greater_than(120)
458
+ perform { emergency_shutdown! }
459
+ end
460
+
461
+ rule "send_warning", priority: 50 do
462
+ on :temperature, value: greater_than(80)
463
+ perform { send_warning_email }
464
+ end
465
+
466
+ rule "log_reading", priority: 10 do
467
+ on :temperature, value: :val?
468
+ perform { log(bindings[:val?]) }
469
+ end
470
+
471
+ # With temp = 125, fires in order:
472
+ # 1. critical_shutdown (priority 100)
473
+ # 2. send_warning (priority 50)
474
+ # 3. log_reading (priority 10)
475
+ ```
476
+
477
+ ### Priority in KBS::Engine
478
+
479
+ Priority is stored but **not used** for ordering - rules fire in arbitrary order.
480
+
481
+ ### When Priority Matters
482
+
483
+ **Use priority for:**
484
+
485
+ - Critical safety checks (priority 100)
486
+ - System integrity rules (priority 75)
487
+ - Business logic (priority 50)
488
+ - Logging and monitoring (priority 10)
489
+
490
+ **Don't rely on priority for:**
491
+
492
+ - Sequencing actions (use fact dependencies instead)
493
+ - Enforcing order between independent rules
494
+ - Complex orchestration (use state machines)
495
+
496
+ ## Rules vs. Queries
497
+
498
+ Rules are **reactive** (fire automatically), queries are **proactive** (you call them):
499
+
500
+ ```ruby
501
+ # Rule - Automatic
502
+ rule "alert_on_high_temp" do
503
+ on :temperature, value: greater_than(80)
504
+ perform { send_alert } # Fires automatically
505
+ end
506
+
507
+ # Query - Manual
508
+ temps = query(:temperature, value: greater_than(80))
509
+ temps.each { |t| send_alert } # You must iterate
510
+ ```
511
+
512
+ **When to use rules:**
513
+
514
+ - Continuous monitoring
515
+ - Event-driven reactions
516
+ - Complex multi-condition patterns
517
+ - Automatic inference
518
+
519
+ **When to use queries:**
520
+
521
+ - One-time lookups
522
+ - Reporting and analysis
523
+ - Interactive exploration
524
+ - When you need explicit control
525
+
526
+ ## Performance Considerations
527
+
528
+ ### Rule Count
529
+
530
+ - 10-100 rules: Excellent
531
+ - 100-1,000 rules: Very good (network sharing helps)
532
+ - 1,000+ rules: Good (consider grouping by domain)
533
+
534
+ ### Condition Count
535
+
536
+ ```ruby
537
+ # Fast - 1-2 conditions
538
+ rule "simple" do
539
+ on :stock, symbol: "AAPL"
540
+ perform { ... }
541
+ end
542
+
543
+ # Typical - 2-4 conditions
544
+ rule "moderate" do
545
+ on :order, status: "pending"
546
+ on :inventory, available: greater_than(0)
547
+ on :customer, verified: true
548
+ perform { ... }
549
+ end
550
+
551
+ # Slower - 5+ conditions (but still efficient with RETE)
552
+ rule "complex" do
553
+ on :order, ...
554
+ on :customer, ...
555
+ on :inventory, ...
556
+ on :pricing, ...
557
+ on :shipping, ...
558
+ perform { ... }
559
+ end
560
+ ```
561
+
562
+ ### Condition Ordering Impact
563
+
564
+ **Huge impact** - order by selectivity:
565
+
566
+ ```ruby
567
+ # Bad - general first (creates many partial matches)
568
+ on :sensor # 1000 facts
569
+ on :alert, level: "critical" # 1 fact
570
+ # → 1000 tokens created
571
+
572
+ # Good - specific first (creates few partial matches)
573
+ on :alert, level: "critical" # 1 fact
574
+ on :sensor # 1000 facts
575
+ # → 1 token created
576
+ ```
577
+
578
+ ### Action Complexity
579
+
580
+ Keep actions lightweight:
581
+
582
+ ```ruby
583
+ # Good - fast action
584
+ perform do |facts, bindings|
585
+ fact :alert, level: "high"
586
+ end
587
+
588
+ # Acceptable - moderate work
589
+ perform do |facts, bindings|
590
+ send_notification(bindings[:user_id?])
591
+ end
592
+
593
+ # Avoid - heavy work in action
594
+ perform do |facts, bindings|
595
+ # Don't do this in action:
596
+ complex_calculation()
597
+ database_batch_update()
598
+ api_call_with_retry()
599
+ # Instead, add a fact to trigger async processing
600
+ fact :work_item, type: "heavy_task", data: bindings
601
+ end
602
+ ```
603
+
604
+ ## Common Pitfalls
605
+
606
+ ### 1. Forgetting "All Conditions Must Match"
607
+
608
+ ```ruby
609
+ # This rule NEVER fires if there's no :inventory fact
610
+ rule "process_order" do
611
+ on :order, status: "pending"
612
+ on :inventory, available: greater_than(0) # What if no inventory fact?
613
+ perform { ship_order }
614
+ end
615
+
616
+ # Fix: Use negation or optional patterns
617
+ rule "process_order" do
618
+ on :order, status: "pending"
619
+ without :inventory, available: less_than(1) # OK if no inventory fact
620
+ perform { ship_order }
621
+ end
622
+ ```
623
+
624
+ ### 2. Expecting Sequential Execution
625
+
626
+ ```ruby
627
+ # Rules don't execute in definition order
628
+ rule "step1" do ... end
629
+ rule "step2" do ... end # NOT guaranteed to fire after step1
630
+
631
+ # Use fact dependencies instead
632
+ rule "step1" do
633
+ perform { fact :step1_complete }
634
+ end
635
+
636
+ rule "step2" do
637
+ on :step1_complete # Depends on step1
638
+ perform { ... }
639
+ end
640
+ ```
641
+
642
+ ### 3. Infinite Loops
643
+
644
+ ```ruby
645
+ # Bad - creates infinite loop
646
+ rule "loop" do
647
+ on :counter, value: :val?
648
+ perform do |facts, bindings|
649
+ # Retracts and re-adds fact → rule fires again → infinite loop!
650
+ retract facts[0]
651
+ fact :counter, value: bindings[:val?] + 1
652
+ end
653
+ end
654
+
655
+ # Fix: Add termination condition
656
+ rule "loop" do
657
+ on :counter, value: less_than(10)
658
+ perform do |facts, bindings|
659
+ retract facts[0]
660
+ fact :counter, value: bindings[:val?] + 1
661
+ end
662
+ end
663
+ ```
664
+
665
+ ### 4. Side Effects in Conditions
666
+
667
+ ```ruby
668
+ # Wrong - side effects in predicate
669
+ counter = 0
670
+ on :stock, price: ->(p) {
671
+ counter += 1 # Bad! Runs many times
672
+ p > 100
673
+ }
674
+
675
+ # Right - side effects in action
676
+ on :stock, price: greater_than(100)
677
+ perform { counter += 1 }
678
+ ```
679
+
680
+ ### 5. Modifying Facts Instead of Retracting
681
+
682
+ ```ruby
683
+ # Wrong - changes don't trigger rules
684
+ fact = engine.facts.first
685
+ fact[:status] = "processed" # No rules fire
686
+
687
+ # Right - retract and re-add
688
+ retract old_fact
689
+ fact :order, status: "processed" # Rules fire
690
+ ```
691
+
692
+ ## Testing Rules
693
+
694
+ ### Unit Testing
695
+
696
+ Test rules in isolation:
697
+
698
+ ```ruby
699
+ def test_high_temp_alert
700
+ kb = KBS.knowledge_base do
701
+ rule "alert" do
702
+ on :temperature, value: greater_than(80)
703
+ perform { fact :alert, level: "high" }
704
+ end
705
+
706
+ fact :temperature, value: 85
707
+ run
708
+ end
709
+
710
+ alerts = kb.query(:alert)
711
+ assert_equal 1, alerts.size
712
+ assert_equal "high", alerts.first[:level]
713
+ end
714
+ ```
715
+
716
+ ### Integration Testing
717
+
718
+ Test rule interactions:
719
+
720
+ ```ruby
721
+ def test_alert_escalation
722
+ kb = KBS.knowledge_base do
723
+ rule "create_alert" do
724
+ on :temperature, value: greater_than(80)
725
+ perform { fact :alert, level: "high" }
726
+ end
727
+
728
+ rule "escalate_alert" do
729
+ on :alert, level: "high"
730
+ on :sensor, critical: true
731
+ perform { fact :escalation, priority: "urgent" }
732
+ end
733
+
734
+ fact :temperature, value: 85
735
+ fact :sensor, critical: true
736
+ run
737
+ end
738
+
739
+ assert kb.query(:alert).any?
740
+ assert kb.query(:escalation).any?
741
+ end
742
+ ```
743
+
744
+ ## Rule Design Principles
745
+
746
+ ### 1. Single Responsibility
747
+
748
+ One rule, one purpose:
749
+
750
+ ```ruby
751
+ # Good—focused
752
+ rule "reorder_low_inventory" do
753
+ on :inventory, quantity: less_than(10)
754
+ perform { create_purchase_order }
755
+ end
756
+
757
+ # Bad—does too much
758
+ rule "inventory_management" do
759
+ on :inventory
760
+ perform do
761
+ check_quantity
762
+ update_forecasts
763
+ notify_suppliers
764
+ generate_reports
765
+ end
766
+ end
767
+ ```
768
+
769
+ ### 2. Declarative Over Imperative
770
+
771
+ Express what, not how:
772
+
773
+ ```ruby
774
+ # Good—declarative
775
+ rule "qualified_customer" do
776
+ on :customer, revenue: greater_than(100_000)
777
+ on :engagement, score: greater_than(80)
778
+ perform { create_opportunity }
779
+ end
780
+
781
+ # Less ideal—imperative
782
+ rule "check_customer" do
783
+ on :customer
784
+ perform do |facts|
785
+ if facts[0][:revenue] > 100_000
786
+ engagement = query(:engagement, customer_id: facts[0][:id]).first
787
+ if engagement && engagement[:score] > 80
788
+ create_opportunity
789
+ end
790
+ end
791
+ end
792
+ end
793
+ ```
794
+
795
+ ### 3. Explicit Over Implicit
796
+
797
+ Make conditions explicit:
798
+
799
+ ```ruby
800
+ # Good—clear dependencies
801
+ rule "ship_order" do
802
+ on :order, status: "paid"
803
+ on :inventory, available: greater_than(0)
804
+ without :shipment # Explicit: no existing shipment
805
+ perform { ship }
806
+ end
807
+
808
+ # Bad—hidden assumptions
809
+ rule "ship_order" do
810
+ on :order, status: "paid"
811
+ perform { ship } # Implicitly assumes inventory exists
812
+ end
813
+ ```
814
+
815
+ ## Further Reading
816
+
817
+ - **[Writing Rules Guide](guides/writing-rules.md)** - Detailed best practices
818
+ - **[Rules API Reference](api/rules.md)** - Complete method documentation
819
+ - **[DSL Reference](guides/dsl.md)** - Rule definition syntax
820
+ - **[Pattern Matching](guides/pattern-matching.md)** - Condition patterns
821
+ - **[RETE Algorithm](architecture/rete-algorithm.md)** - How rules are compiled and executed
822
+
823
+ ## Summary
824
+
825
+ A **rule** is:
826
+
827
+ - A **declarative IF-THEN statement** that automatically fires when patterns match
828
+ - Composed of **conditions** (patterns to match) and **action** (code to execute)
829
+ - **Automatically activated** by the RETE engine when facts satisfy conditions
830
+ - The "logic" that operates on facts (the "data") in a knowledge base
831
+ - Available with optional **priority** for execution ordering (blackboard only)
832
+
833
+ Think of rules as **automated sentinels** that continuously watch for specific patterns and react instantly when those patterns appear.