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
@@ -0,0 +1,914 @@
1
+ # Writing Rules
2
+
3
+ Master the art of authoring production rules. This guide covers best practices, patterns, and strategies for writing effective, maintainable, and performant rules in KBS.
4
+
5
+ ## Rule Anatomy
6
+
7
+ Every rule consists of three parts:
8
+
9
+ ```ruby
10
+ KBS.knowledge_base do
11
+ rule "rule_name", priority: 0 do
12
+ # 1. CONDITIONS - Pattern matching
13
+ on :fact_type, attr: value
14
+
15
+ # 2. ACTION - What to do when conditions match
16
+ perform do |facts, bindings|
17
+ # Execute logic
18
+ end
19
+ end
20
+ end
21
+ ```
22
+
23
+ ### 1. Rule Name
24
+
25
+ Choose descriptive, actionable names:
26
+
27
+ ```ruby
28
+ # Good: Clear intent
29
+ "send_high_temperature_alert"
30
+ "cancel_duplicate_orders"
31
+ "escalate_critical_issues"
32
+
33
+ # Bad: Vague or cryptic
34
+ "rule1"
35
+ "process"
36
+ "check_stuff"
37
+ ```
38
+
39
+ **Naming Conventions:**
40
+ - Use snake_case
41
+ - Start with verb (action-oriented)
42
+ - Be specific about what the rule does
43
+ - Include domain context
44
+
45
+ ### 2. Priority
46
+
47
+ Control execution order when multiple rules match:
48
+
49
+ ```ruby
50
+ KBS.knowledge_base do
51
+ rule "critical_safety_check", priority: 100 do # Fires first
52
+ # ...
53
+ end
54
+
55
+ rule "normal_processing", priority: 50 do
56
+ # ...
57
+ end
58
+
59
+ rule "cleanup_task", priority: 10 do # Fires last
60
+ # ...
61
+ end
62
+ end
63
+ ```
64
+
65
+ **Priority Guidelines:**
66
+ - **100+** - Safety checks, emergency shutdowns
67
+ - **50-99** - Business logic, processing
68
+ - **1-49** - Monitoring, logging, cleanup
69
+ - **0** - Default priority (no preference)
70
+
71
+ ### 3. Conditions
72
+
73
+ Patterns that must match for the rule to fire. Order matters for performance.
74
+
75
+ ```ruby
76
+ KBS.knowledge_base do
77
+ rule "example" do
78
+ # Most selective first (fewest matches)
79
+ on :critical_alert, severity: "critical"
80
+
81
+ # Less selective last (more matches)
82
+ on :sensor, id: :sensor_id?
83
+
84
+ perform do |facts, bindings|
85
+ # Action
86
+ end
87
+ end
88
+ end
89
+ ```
90
+
91
+ ### 4. Action
92
+
93
+ Code executed when all conditions match:
94
+
95
+ ```ruby
96
+ KBS.knowledge_base do
97
+ rule "example" do
98
+ on :alert, message: :msg?
99
+ on :sensor, id: :sensor_id?
100
+
101
+ perform do |facts, bindings|
102
+ # Access matched facts
103
+ alert = facts[0]
104
+ sensor = facts[1]
105
+
106
+ # Access variable bindings
107
+ sensor_id = bindings[:sensor_id?]
108
+
109
+ # Perform action
110
+ notify_operator(sensor_id, alert[:message])
111
+ end
112
+ end
113
+ end
114
+ ```
115
+
116
+ ## Condition Ordering
117
+
118
+ **Golden Rule**: Order conditions from most selective to least selective.
119
+
120
+ ### Why Order Matters
121
+
122
+ ```ruby
123
+ # Bad: General condition first
124
+ KBS.knowledge_base do
125
+ rule "inefficient" do
126
+ on :sensor, {} # 1000 matches
127
+ on :critical_alert, {} # 1 match
128
+ perform { }
129
+ end
130
+ end
131
+ # Creates 1000 partial matches, wastes memory
132
+
133
+ # Good: Specific condition first
134
+ KBS.knowledge_base do
135
+ rule "efficient" do
136
+ on :critical_alert, {} # 1 match
137
+ on :sensor, {} # Joins with 1000
138
+ perform { }
139
+ end
140
+ end
141
+ # Creates 1 partial match, efficient joins
142
+ ```
143
+
144
+ ### Selectivity Examples
145
+
146
+ ```ruby
147
+ # Most selective (few facts)
148
+ on :emergency, level: "critical"
149
+ on :user, role: "admin"
150
+
151
+ # Moderate selectivity
152
+ on :order, status: "pending"
153
+ on :stock, exchange: "NYSE"
154
+
155
+ # Least selective (many facts)
156
+ on :sensor, {}
157
+ on :log_entry, {}
158
+ ```
159
+
160
+ ### Measuring Selectivity
161
+
162
+ ```ruby
163
+ def measure_selectivity(kb, type, pattern)
164
+ kb.engine.facts.count { |f|
165
+ f.type == type &&
166
+ pattern.all? { |k, v| f[k] == v }
167
+ }
168
+ end
169
+
170
+ # Compare
171
+ puts measure_selectivity(kb, :critical_alert, {}) # => 1
172
+ puts measure_selectivity(kb, :sensor, {}) # => 1000
173
+
174
+ # Order: critical_alert first, sensor second
175
+ ```
176
+
177
+ ## Action Design
178
+
179
+ ### Single Responsibility
180
+
181
+ One action, one purpose:
182
+
183
+ ```ruby
184
+ # Good: Focused action
185
+ KBS.knowledge_base do
186
+ rule "send_email" do
187
+ on :alert, email: :email?, message: :message?
188
+ perform do |facts, bindings|
189
+ send_email_alert(bindings[:email?], bindings[:message?])
190
+ end
191
+ end
192
+ end
193
+
194
+ # Bad: Multiple responsibilities
195
+ KBS.knowledge_base do
196
+ rule "do_everything" do
197
+ on :trigger, email: :email?, id: :id?, data: :data?, msg: :msg?
198
+ perform do |facts, bindings|
199
+ send_email_alert(bindings[:email?])
200
+ update_database(bindings[:id?])
201
+ call_external_api(bindings[:data?])
202
+ write_log_file(bindings[:msg?])
203
+ end
204
+ end
205
+ end
206
+ ```
207
+
208
+ Split complex actions into multiple rules:
209
+
210
+ ```ruby
211
+ KBS.knowledge_base do
212
+ # Rule 1: Detect condition
213
+ rule "detect_high_temp", priority: 50 do
214
+ on :sensor, temp: :temp?, predicate: greater_than(30)
215
+
216
+ perform do |facts, bindings|
217
+ fact :high_temp_detected, temp: bindings[:temp?]
218
+ end
219
+ end
220
+
221
+ # Rule 2: Send alert
222
+ rule "send_temp_alert", priority: 40 do
223
+ on :high_temp_detected, temp: :temp?
224
+
225
+ perform do |facts, bindings|
226
+ send_email("High temp: #{bindings[:temp?]}")
227
+ end
228
+ end
229
+
230
+ # Rule 3: Log event
231
+ rule "log_temp_event", priority: 30 do
232
+ on :high_temp_detected, temp: :temp?
233
+
234
+ perform do |facts, bindings|
235
+ logger.info("Temperature spike: #{bindings[:temp?]}")
236
+ end
237
+ end
238
+ end
239
+ ```
240
+
241
+ ### Avoid Side Effects
242
+
243
+ Actions should be deterministic and idempotent when possible:
244
+
245
+ ```ruby
246
+ # Good: Idempotent (safe to run multiple times)
247
+ kb = KBS.knowledge_base do
248
+ rule "update_alert" do
249
+ on :trigger, id: :id?
250
+
251
+ perform do |facts, bindings|
252
+ # Remove old alert if exists
253
+ old = engine.facts.find { |f| f.type == :alert && f[:id] == bindings[:id?] }
254
+ engine.remove_fact(old) if old
255
+
256
+ # Add new alert
257
+ fact :alert, id: bindings[:id?], message: "Alert!"
258
+ end
259
+ end
260
+ end
261
+
262
+ # Bad: Non-idempotent (creates duplicates)
263
+ kb = KBS.knowledge_base do
264
+ rule "duplicate_alerts" do
265
+ on :trigger, id: :id?
266
+
267
+ perform do |facts, bindings|
268
+ # Always adds, even if alert already exists
269
+ fact :alert, id: bindings[:id?], message: "Alert!"
270
+ end
271
+ end
272
+ end
273
+ ```
274
+
275
+ ### Error Handling
276
+
277
+ Protect against failures:
278
+
279
+ ```ruby
280
+ KBS.knowledge_base do
281
+ rule "safe_email" do
282
+ on :alert, email: :email?, message: :message?
283
+
284
+ perform do |facts, bindings|
285
+ begin
286
+ send_email(bindings[:email?], bindings[:message?])
287
+ rescue Net::SMTPError => e
288
+ logger.error("Failed to send email: #{e.message}")
289
+ # Add failure fact for retry logic
290
+ fact :email_failure,
291
+ email: bindings[:email?],
292
+ error: e.message,
293
+ timestamp: Time.now
294
+ end
295
+ end
296
+ end
297
+ end
298
+ ```
299
+
300
+ ## Variable Binding Strategies
301
+
302
+ ### Consistent Naming
303
+
304
+ Use descriptive, consistent variable names:
305
+
306
+ ```ruby
307
+ # Good: Clear intent
308
+ :sensor_id?
309
+ :temperature_celsius?
310
+ :alert_threshold?
311
+ :user_email?
312
+
313
+ # Bad: Cryptic
314
+ :s?
315
+ :t?
316
+ :x?
317
+ ```
318
+
319
+ ### Join Patterns
320
+
321
+ Connect facts through shared variables:
322
+
323
+ ```ruby
324
+ KBS.knowledge_base do
325
+ # Pattern: Join sensor reading with threshold
326
+ rule "check_threshold" do
327
+ on :sensor,
328
+ id: :sensor_id?,
329
+ temp: :current_temp?
330
+
331
+ on :threshold,
332
+ sensor_id: :sensor_id?, # Same variable = join constraint
333
+ max_temp: :max_temp?
334
+
335
+ perform do |facts, bindings|
336
+ # Only matches when sensor_id is same in both facts
337
+ end
338
+ end
339
+ end
340
+ ```
341
+
342
+ ### Computed Bindings
343
+
344
+ Derive values in actions:
345
+
346
+ ```ruby
347
+ KBS.knowledge_base do
348
+ rule "calculate_diff" do
349
+ on :sensor, temp: :current_temp?
350
+ on :threshold, max_temp: :max_temp?
351
+
352
+ perform do |facts, bindings|
353
+ current = bindings[:current_temp?]
354
+ max = bindings[:max_temp?]
355
+
356
+ # Compute derived values
357
+ diff = current - max
358
+ percentage_over = ((current / max.to_f) - 1) * 100
359
+
360
+ puts "#{diff}°C over threshold (#{percentage_over.round(1)}%)"
361
+ end
362
+ end
363
+ end
364
+ ```
365
+
366
+ ## Rule Composition Patterns
367
+
368
+ ### State Machine
369
+
370
+ Model state transitions:
371
+
372
+ ```ruby
373
+ KBS.knowledge_base do
374
+ # Transition: pending → processing
375
+ rule "start_processing" do
376
+ on :order,
377
+ id: :order_id?,
378
+ status: "pending"
379
+
380
+ perform do |facts, bindings|
381
+ old_order = facts[0]
382
+ engine.remove_fact(old_order)
383
+ fact :order,
384
+ id: bindings[:order_id?],
385
+ status: "processing",
386
+ started_at: Time.now
387
+ end
388
+ end
389
+
390
+ # Transition: processing → completed
391
+ rule "complete_processing" do
392
+ on :order,
393
+ id: :order_id?,
394
+ status: "processing"
395
+ on :processing_done,
396
+ order_id: :order_id?
397
+
398
+ perform do |facts, bindings|
399
+ order = facts[0]
400
+ engine.remove_fact(order)
401
+ engine.remove_fact(facts[1]) # Remove trigger
402
+ fact :order,
403
+ id: bindings[:order_id?],
404
+ status: "completed",
405
+ completed_at: Time.now
406
+ end
407
+ end
408
+ end
409
+ ```
410
+
411
+ ### Guard Conditions
412
+
413
+ Prevent duplicate actions:
414
+
415
+ ```ruby
416
+ KBS.knowledge_base do
417
+ rule "send_alert_once" do
418
+ on :high_temp, sensor_id: :id?
419
+
420
+ # Guard: Only fire if alert not already sent
421
+ without :alert_sent, sensor_id: :id?
422
+
423
+ perform do |facts, bindings|
424
+ send_alert(bindings[:id?])
425
+
426
+ # Record that we sent this alert
427
+ fact :alert_sent, sensor_id: bindings[:id?]
428
+ end
429
+ end
430
+ end
431
+ ```
432
+
433
+ ### Cleanup Rules
434
+
435
+ Remove stale facts:
436
+
437
+ ```ruby
438
+ KBS.knowledge_base do
439
+ rule "cleanup_stale_alerts", priority: 1 do
440
+ on :alert,
441
+ timestamp: :time?,
442
+ predicate: lambda { |f|
443
+ (Time.now - f[:timestamp]) > 3600 # 1 hour old
444
+ }
445
+
446
+ perform do |facts, bindings|
447
+ engine.remove_fact(facts[0])
448
+ logger.info("Removed stale alert")
449
+ end
450
+ end
451
+ end
452
+ ```
453
+
454
+ ### Aggregation Rules
455
+
456
+ Compute over multiple facts:
457
+
458
+ ```ruby
459
+ KBS.knowledge_base do
460
+ rule "compute_average_temp" do
461
+ on :compute_avg_requested, {}
462
+
463
+ perform do |facts, bindings|
464
+ temps = engine.facts
465
+ .select { |f| f.type == :sensor }
466
+ .map { |f| f[:temp] }
467
+ .compact
468
+
469
+ avg = temps.sum / temps.size.to_f
470
+
471
+ fact :average_temp, value: avg
472
+ end
473
+ end
474
+ end
475
+ ```
476
+
477
+ ### Temporal Rules
478
+
479
+ React to time-based conditions:
480
+
481
+ ```ruby
482
+ KBS.knowledge_base do
483
+ rule "detect_delayed_response" do
484
+ on :request,
485
+ id: :req_id?,
486
+ created_at: :created?
487
+
488
+ without :response,
489
+ request_id: :req_id?
490
+
491
+ on :request, {},
492
+ predicate: lambda { |f|
493
+ (Time.now - f[:created_at]) > 300 # 5 minutes
494
+ }
495
+
496
+ perform do |facts, bindings|
497
+ alert("Request #{bindings[:req_id?]} delayed!")
498
+ end
499
+ end
500
+ end
501
+ ```
502
+
503
+ ## Priority Management
504
+
505
+ ### Priority Levels
506
+
507
+ Establish consistent priority levels for your domain:
508
+
509
+ ```ruby
510
+ # Define priority constants
511
+ module Priority
512
+ CRITICAL = 100 # Emergency, safety
513
+ HIGH = 75 # Important business logic
514
+ NORMAL = 50 # Standard processing
515
+ LOW = 25 # Cleanup, logging
516
+ MONITORING = 10 # Metrics, diagnostics
517
+ end
518
+
519
+ # Use in rules
520
+ KBS.knowledge_base do
521
+ rule "emergency_shutdown", priority: Priority::CRITICAL do
522
+ # ...
523
+ end
524
+
525
+ rule "process_order", priority: Priority::NORMAL do
526
+ # ...
527
+ end
528
+ end
529
+ ```
530
+
531
+ ### Priority Inversion
532
+
533
+ Avoid priority inversions where low-priority rules block high-priority rules:
534
+
535
+ ```ruby
536
+ # Bad: Low priority rule creates fact needed by high priority rule
537
+ KBS.knowledge_base do
538
+ rule "compute_risk", priority: 10 do
539
+ on :data, value: :v?
540
+ perform do |facts, bindings|
541
+ fact :risk_score, value: calculate_risk(bindings[:v?])
542
+ end
543
+ end
544
+
545
+ rule "emergency_check", priority: 100 do
546
+ on :risk_score, value: :risk? # Depends on low priority rule!
547
+ perform do |facts, bindings|
548
+ emergency_shutdown if bindings[:risk?] > 90
549
+ end
550
+ end
551
+ end
552
+
553
+ # Fix: Make dependency higher priority
554
+ KBS.knowledge_base do
555
+ rule "compute_risk", priority: 110 do # Now runs before emergency_check
556
+ on :data, value: :v?
557
+ perform do |facts, bindings|
558
+ fact :risk_score, value: calculate_risk(bindings[:v?])
559
+ end
560
+ end
561
+
562
+ rule "emergency_check", priority: 100 do
563
+ on :risk_score, value: :risk?
564
+ perform do |facts, bindings|
565
+ emergency_shutdown if bindings[:risk?] > 90
566
+ end
567
+ end
568
+ end
569
+ ```
570
+
571
+ ## Testing Strategies
572
+
573
+ ### Unit Test Rules in Isolation
574
+
575
+ ```ruby
576
+ require 'minitest/autorun'
577
+ require 'kbs'
578
+
579
+ class TestTemperatureRules < Minitest::Test
580
+ def test_fires_when_temp_exceeds_threshold
581
+ alert_fired = false
582
+
583
+ kb = KBS.knowledge_base do
584
+ rule "high_temp_alert" do
585
+ on :sensor, id: :id?, temp: :temp?
586
+ on :threshold, id: :id?, max: :max?
587
+
588
+ perform do |facts, bindings|
589
+ alert_fired = true if bindings[:temp?] > bindings[:max?]
590
+ end
591
+ end
592
+
593
+ fact :sensor, id: "bedroom", temp: 30
594
+ fact :threshold, id: "bedroom", max: 25
595
+ run
596
+ end
597
+
598
+ assert alert_fired, "Rule should fire when temp > threshold"
599
+ end
600
+
601
+ def test_does_not_fire_when_temp_below_threshold
602
+ alert_fired = false
603
+
604
+ kb = KBS.knowledge_base do
605
+ rule "high_temp_alert" do
606
+ on :sensor, id: :id?, temp: :temp?
607
+ on :threshold, id: :id?, max: :max?
608
+
609
+ perform do |facts, bindings|
610
+ alert_fired = true if bindings[:temp?] > bindings[:max?]
611
+ end
612
+ end
613
+
614
+ fact :sensor, id: "bedroom", temp: 20
615
+ fact :threshold, id: "bedroom", max: 25
616
+ run
617
+ end
618
+
619
+ refute alert_fired, "Rule should not fire when temp <= threshold"
620
+ end
621
+
622
+ def test_only_fires_for_matching_sensor
623
+ alert_fired = false
624
+
625
+ kb = KBS.knowledge_base do
626
+ rule "high_temp_alert" do
627
+ on :sensor, id: :id?, temp: :temp?
628
+ on :threshold, id: :id?, max: :max?
629
+
630
+ perform do |facts, bindings|
631
+ alert_fired = true if bindings[:temp?] > bindings[:max?]
632
+ end
633
+ end
634
+
635
+ fact :sensor, id: "bedroom", temp: 30
636
+ fact :threshold, id: "kitchen", max: 25
637
+ run
638
+ end
639
+
640
+ refute alert_fired, "Rule should not fire for different sensors"
641
+ end
642
+ end
643
+ ```
644
+
645
+ ### Integration Tests
646
+
647
+ Test multiple rules working together:
648
+
649
+ ```ruby
650
+ def test_state_machine_workflow
651
+ kb = KBS.knowledge_base do
652
+ # Add state transition rules
653
+ rule "start_processing" do
654
+ on :order, id: :id?, status: "pending"
655
+ perform do |facts, bindings|
656
+ engine.remove_fact(facts[0])
657
+ fact :order, id: bindings[:id?], status: "processing"
658
+ end
659
+ end
660
+
661
+ rule "complete_processing" do
662
+ on :order, id: :id?, status: "processing"
663
+ on :processing_done, order_id: :id?
664
+ perform do |facts, bindings|
665
+ engine.remove_fact(facts[0])
666
+ engine.remove_fact(facts[1])
667
+ fact :order, id: bindings[:id?], status: "completed"
668
+ end
669
+ end
670
+
671
+ # Add initial state
672
+ fact :order, id: 1, status: "pending"
673
+ run
674
+ end
675
+
676
+ # Should not transition yet
677
+ order = kb.engine.facts.find { |f| f.type == :order && f[:id] == 1 }
678
+ assert_equal "pending", order[:status]
679
+
680
+ # Trigger transition
681
+ kb.fact :processing_done, order_id: 1
682
+ kb.run
683
+
684
+ # Should transition to completed
685
+ order = kb.engine.facts.find { |f| f.type == :order && f[:id] == 1 }
686
+ assert_equal "completed", order[:status]
687
+ end
688
+ ```
689
+
690
+ ### Property-Based Testing
691
+
692
+ Test rule invariants:
693
+
694
+ ```ruby
695
+ def test_no_duplicate_alerts
696
+ kb = KBS.knowledge_base do
697
+ rule "send_alert_once" do
698
+ on :high_temp, sensor_id: :id?
699
+ without :alert_sent, sensor_id: :id?
700
+
701
+ perform do |facts, bindings|
702
+ send_alert(bindings[:id?])
703
+ fact :alert_sent, sensor_id: bindings[:id?]
704
+ end
705
+ end
706
+
707
+ # Add facts
708
+ 100.times do |i|
709
+ fact :high_temp, sensor_id: i
710
+ end
711
+
712
+ # Run engine multiple times
713
+ 10.times { run }
714
+ end
715
+
716
+ # Property: At most one alert per sensor
717
+ alert_counts = kb.engine.facts
718
+ .select { |f| f.type == :alert_sent }
719
+ .group_by { |f| f[:sensor_id] }
720
+ .transform_values(&:count)
721
+
722
+ alert_counts.each do |sensor_id, count|
723
+ assert_equal 1, count, "Sensor #{sensor_id} has #{count} alerts, expected 1"
724
+ end
725
+ end
726
+ ```
727
+
728
+ ## Performance Optimization
729
+
730
+ ### Minimize Negations
731
+
732
+ Negations are expensive:
733
+
734
+ ```ruby
735
+ # Expensive: 3 negations
736
+ KBS.knowledge_base do
737
+ rule "many_negations" do
738
+ without :foo, {}
739
+ without :bar, {}
740
+ without :baz, {}
741
+ perform { }
742
+ end
743
+ end
744
+
745
+ # Better: Combine into positive condition
746
+ KBS.knowledge_base do
747
+ rule "positive_logic" do
748
+ on :conditions_met, {}
749
+ perform { }
750
+ end
751
+
752
+ # Add conditions_met fact if foo, bar, baz don't exist
753
+ unless engine.facts.any? { |f| [:foo, :bar, :baz].include?(f.type) }
754
+ fact :conditions_met, {}
755
+ end
756
+ end
757
+ ```
758
+
759
+ ### Avoid Predicates for Simple Checks
760
+
761
+ ```ruby
762
+ # Expensive: Predicate disables network sharing
763
+ KBS.knowledge_base do
764
+ rule "with_predicate" do
765
+ on :stock, {}, predicate: lambda { |f| f[:symbol] == "AAPL" }
766
+ perform { }
767
+ end
768
+ end
769
+
770
+ # Better: Use pattern matching
771
+ KBS.knowledge_base do
772
+ rule "with_pattern" do
773
+ on :stock, symbol: "AAPL"
774
+ perform { }
775
+ end
776
+ end
777
+ ```
778
+
779
+ ### Cache Computed Values
780
+
781
+ ```ruby
782
+ # Bad: Recomputes every time rule fires
783
+ KBS.knowledge_base do
784
+ rule "check_average" do
785
+ on :sensor, temp: :temp?
786
+
787
+ perform do |facts, bindings|
788
+ avg = compute_expensive_average(engine.facts)
789
+ alert(avg) if avg > threshold
790
+ end
791
+ end
792
+ end
793
+
794
+ # Good: Cache as fact, recompute only when needed
795
+ KBS.knowledge_base do
796
+ rule "update_average", priority: 100 do
797
+ on :sensor, temp: :temp? # Triggers when sensor added
798
+
799
+ perform do |facts, bindings|
800
+ avg = compute_expensive_average(engine.facts)
801
+ fact :cached_average, value: avg
802
+ end
803
+ end
804
+
805
+ rule "check_average", priority: 50 do
806
+ on :cached_average, value: :avg?
807
+
808
+ perform do |facts, bindings|
809
+ alert(bindings[:avg?]) if bindings[:avg?] > threshold
810
+ end
811
+ end
812
+ end
813
+ ```
814
+
815
+ ## Common Pitfalls
816
+
817
+ ### 1. Infinite Loops
818
+
819
+ ```ruby
820
+ # Bad: Rule fires itself indefinitely
821
+ KBS.knowledge_base do
822
+ rule "infinite_loop" do
823
+ on :sensor, temp: :temp?
824
+
825
+ perform do |facts, bindings|
826
+ # This triggers the rule again!
827
+ fact :sensor, temp: bindings[:temp?] + 1
828
+ end
829
+ end
830
+ end
831
+
832
+ # Fix: Add termination condition
833
+ KBS.knowledge_base do
834
+ rule "limited_increment" do
835
+ on :sensor, temp: :temp?
836
+ without :increment_done, {}
837
+
838
+ perform do |facts, bindings|
839
+ fact :sensor, temp: bindings[:temp?] + 1
840
+ fact :increment_done, {}
841
+ end
842
+ end
843
+ end
844
+ ```
845
+
846
+ ### 2. Variable Scope Confusion
847
+
848
+ ```ruby
849
+ # Bad: Closure captures wrong variable
850
+ rules = []
851
+ %w[sensor1 sensor2 sensor3].each do |sensor|
852
+ # All rules reference same 'sensor' variable (last value!)
853
+ kb = KBS.knowledge_base do
854
+ rule "process_#{sensor}" do
855
+ on :reading, {}
856
+ perform { puts sensor } # Wrong!
857
+ end
858
+ end
859
+ end
860
+
861
+ # Fix: Force closure with parameter
862
+ %w[sensor1 sensor2 sensor3].each do |sensor_name|
863
+ captured_sensor = sensor_name # Force capture
864
+
865
+ kb = KBS.knowledge_base do
866
+ rule "process_#{captured_sensor}" do
867
+ on :reading, {}
868
+ perform { puts captured_sensor } # Correct
869
+ end
870
+ end
871
+ end
872
+ ```
873
+
874
+ ### 3. Forgetting to Call `run`
875
+
876
+ ```ruby
877
+ # Bad: Facts added but never matched
878
+ kb = KBS.knowledge_base do
879
+ rule "example" do
880
+ on :sensor, temp: :temp?
881
+ on :threshold, max: :max?
882
+ perform { }
883
+ end
884
+
885
+ fact :sensor, temp: 30
886
+ fact :threshold, max: 25
887
+ # Rules never fire!
888
+ end
889
+
890
+ # Good: Run after adding facts
891
+ kb = KBS.knowledge_base do
892
+ rule "example" do
893
+ on :sensor, temp: :temp?
894
+ on :threshold, max: :max?
895
+ perform { }
896
+ end
897
+
898
+ fact :sensor, temp: 30
899
+ fact :threshold, max: 25
900
+ run # Match and fire rules
901
+ end
902
+ ```
903
+
904
+ ## Next Steps
905
+
906
+ - **[Pattern Matching](pattern-matching.md)** - Deep dive into condition matching
907
+ - **[Variable Binding](variable-binding.md)** - Join tests and binding extraction
908
+ - **[Negation](negation.md)** - Negated condition behavior
909
+ - **[Performance Guide](../advanced/performance.md)** - Profiling and optimization
910
+ - **[Testing Guide](../advanced/testing.md)** - Comprehensive test strategies
911
+
912
+ ---
913
+
914
+ *Well-designed rules are self-documenting. If a rule is hard to understand, it's probably doing too much.*