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