kbs 0.1.0 → 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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +85 -57
  3. data/docs/advanced/performance.md +109 -76
  4. data/docs/advanced/testing.md +399 -263
  5. data/docs/api/blackboard.md +1 -1
  6. data/docs/api/engine.md +77 -8
  7. data/docs/api/facts.md +3 -3
  8. data/docs/api/rules.md +110 -40
  9. data/docs/architecture/blackboard.md +108 -117
  10. data/docs/assets/images/fact-rule-relationship.svg +65 -0
  11. data/docs/assets/images/fact-structure.svg +42 -0
  12. data/docs/assets/images/inference-cycle.svg +47 -0
  13. data/docs/assets/images/kb-components.svg +43 -0
  14. data/docs/assets/images/rule-structure.svg +44 -0
  15. data/docs/assets/images/trading-signal-network.svg +1 -1
  16. data/docs/examples/index.md +219 -5
  17. data/docs/guides/blackboard-memory.md +89 -58
  18. data/docs/guides/dsl.md +24 -24
  19. data/docs/guides/getting-started.md +109 -107
  20. data/docs/guides/writing-rules.md +470 -311
  21. data/docs/index.md +16 -18
  22. data/docs/quick-start.md +92 -99
  23. data/docs/what-is-a-fact.md +694 -0
  24. data/docs/what-is-a-knowledge-base.md +350 -0
  25. data/docs/what-is-a-rule.md +833 -0
  26. data/examples/.gitignore +1 -0
  27. data/examples/advanced_example_dsl.rb +1 -1
  28. data/examples/ai_enhanced_kbs_dsl.rb +1 -1
  29. data/examples/car_diagnostic_dsl.rb +1 -1
  30. data/examples/concurrent_inference_demo.rb +0 -1
  31. data/examples/concurrent_inference_demo_dsl.rb +0 -1
  32. data/examples/csv_trading_system_dsl.rb +1 -1
  33. data/examples/iot_demo_using_dsl.rb +1 -1
  34. data/examples/portfolio_rebalancing_system_dsl.rb +1 -1
  35. data/examples/rule_source_demo.rb +123 -0
  36. data/examples/stock_trading_advanced_dsl.rb +1 -1
  37. data/examples/temp_dsl.txt +6214 -5269
  38. data/examples/timestamped_trading_dsl.rb +1 -1
  39. data/examples/trading_demo_dsl.rb +1 -1
  40. data/examples/working_demo_dsl.rb +1 -1
  41. data/lib/kbs/decompiler.rb +204 -0
  42. data/lib/kbs/dsl/knowledge_base.rb +100 -1
  43. data/lib/kbs/dsl.rb +3 -1
  44. data/lib/kbs/engine.rb +41 -0
  45. data/lib/kbs/version.rb +1 -1
  46. data/lib/kbs.rb +14 -12
  47. data/mkdocs.yml +30 -30
  48. metadata +15 -10
  49. data/docs/DOCUMENTATION_STATUS.md +0 -158
  50. data/docs/examples/expert-systems.md +0 -1031
  51. data/docs/examples/multi-agent.md +0 -1335
  52. data/docs/examples/stock-trading.md +0 -488
  53. data/examples/knowledge_base.db +0 -0
  54. data/examples/temp.txt +0 -7693
@@ -1154,4 +1154,4 @@ archive_old_audit(memory, Date.today - 30)
1154
1154
  - [Facts API](facts.md) - Persistent fact objects
1155
1155
  - [Custom Persistence](../advanced/custom-persistence.md) - Implementing custom stores
1156
1156
  - [Blackboard Guide](../guides/blackboard-memory.md) - Blackboard pattern overview
1157
- - [Multi-Agent Example](../examples/multi-agent.md) - Multi-agent coordination
1157
+ - [Blackboard Examples](../examples/index.md#advanced-features) - Multi-agent coordination and blackboard systems
data/docs/api/engine.md CHANGED
@@ -25,7 +25,7 @@ Creates a new in-memory RETE engine.
25
25
 
26
26
  **Returns**: `KBS::Engine` instance
27
27
 
28
- **Example**:
28
+ **Example - Low-level API**:
29
29
  ```ruby
30
30
  require 'kbs'
31
31
 
@@ -33,6 +33,19 @@ engine = KBS::Engine.new
33
33
  # Engine ready with empty working memory
34
34
  ```
35
35
 
36
+ **Using DSL (Recommended)**:
37
+ ```ruby
38
+ require 'kbs'
39
+
40
+ kb = KBS.knowledge_base do
41
+ # Engine automatically created
42
+ # Define rules and facts here
43
+ end
44
+
45
+ # Access engine if needed
46
+ engine = kb.engine
47
+ ```
48
+
36
49
  **Internal State Initialized**:
37
50
  - `@working_memory` - WorkingMemory instance
38
51
  - `@rules` - Array of registered rules
@@ -79,7 +92,7 @@ engine.add_rule(rule)
79
92
  kb = KBS.knowledge_base do
80
93
  rule "high_temperature", priority: 10 do
81
94
  on :temperature, location: "server_room", value: greater_than(80)
82
- perform do |bindings|
95
+ perform do |facts, bindings|
83
96
  puts "Alert: #{bindings[:location?]} is #{bindings[:value?]}°F"
84
97
  end
85
98
  end
@@ -112,7 +125,7 @@ Adds a fact to working memory and activates matching alpha memories.
112
125
  - Propagates through join nodes
113
126
  - May create new tokens in beta memories
114
127
 
115
- **Example**:
128
+ **Example - Low-level API**:
116
129
  ```ruby
117
130
  fact = engine.add_fact(:temperature, location: "server_room", value: 85)
118
131
  # => #<KBS::Fact:0x00... @type=:temperature @attributes={...}>
@@ -122,6 +135,14 @@ marker = engine.add_fact(:system_ready)
122
135
  # => #<KBS::Fact:0x00... @type=:system_ready @attributes={}>
123
136
  ```
124
137
 
138
+ **Using DSL (Recommended)**:
139
+ ```ruby
140
+ kb = KBS.knowledge_base do
141
+ fact :temperature, location: "server_room", value: 85
142
+ fact :system_ready
143
+ end
144
+ ```
145
+
125
146
  **Thread Safety**: Not thread-safe. Wrap in mutex if adding facts from multiple threads.
126
147
 
127
148
  **Performance**: O(A × P) where A is number of alpha memories, P is pattern matching cost
@@ -181,7 +202,7 @@ Executes all activated rules by firing production nodes.
181
202
  - Rule actions may add/remove facts
182
203
  - Rule actions may modify external state
183
204
 
184
- **Example**:
205
+ **Example - Low-level API**:
185
206
  ```ruby
186
207
  engine.add_fact(:temperature, value: 85)
187
208
  engine.add_fact(:sensor, status: "active")
@@ -192,6 +213,21 @@ engine.run # Execute all matching rules
192
213
  # Rules fire based on priority (highest first within each production)
193
214
  ```
194
215
 
216
+ **Using DSL (Recommended)**:
217
+ ```ruby
218
+ kb = KBS.knowledge_base do
219
+ rule "my_rule" do
220
+ on :temperature, value: greater_than(80)
221
+ perform { puts "High temperature!" }
222
+ end
223
+
224
+ fact :temperature, value: 85
225
+ fact :sensor, status: "active"
226
+
227
+ run # Execute all matching rules
228
+ end
229
+ ```
230
+
195
231
  **Execution Order**:
196
232
  - Production nodes fire in arbitrary order (dictionary order by rule name)
197
233
  - Within a production node, tokens fire in insertion order
@@ -392,6 +428,24 @@ engine = KBS::Blackboard::Engine.new(db_path: 'knowledge_base.db')
392
428
  # Facts persisted to knowledge_base.db
393
429
  ```
394
430
 
431
+ **Using DSL with Blackboard (Recommended)**:
432
+ ```ruby
433
+ engine = KBS::Blackboard::Engine.new(db_path: 'knowledge_base.db')
434
+
435
+ kb = KBS.knowledge_base(engine: engine) do
436
+ rule "persistent_rule" do
437
+ on :temperature, value: greater_than(80)
438
+ perform { puts "High temp alert!" }
439
+ end
440
+
441
+ fact :temperature, value: 85
442
+ run
443
+ end
444
+
445
+ # Facts persist across restarts
446
+ kb.close
447
+ ```
448
+
395
449
  **Example - Redis Persistence**:
396
450
  ```ruby
397
451
  require 'kbs/blackboard/persistence/redis_store'
@@ -434,7 +488,7 @@ Adds a persistent fact to the blackboard.
434
488
  - Activates alpha memories
435
489
  - Notifies observers
436
490
 
437
- **Example**:
491
+ **Example - Low-level API**:
438
492
  ```ruby
439
493
  fact = engine.add_fact(:temperature, location: "server_room", value: 85)
440
494
  puts fact.uuid # => "550e8400-e29b-41d4-a716-446655440000"
@@ -445,6 +499,21 @@ reloaded_facts = engine2.blackboard.get_facts_by_type(:temperature)
445
499
  puts reloaded_facts.first[:value] # => 85
446
500
  ```
447
501
 
502
+ **Using DSL (Recommended)**:
503
+ ```ruby
504
+ # Session 1
505
+ engine = KBS::Blackboard::Engine.new(db_path: 'knowledge_base.db')
506
+ kb = KBS.knowledge_base(engine: engine) do
507
+ fact :temperature, location: "server_room", value: 85
508
+ end
509
+ kb.close
510
+
511
+ # Session 2 - facts still available
512
+ engine2 = KBS::Blackboard::Engine.new(db_path: 'knowledge_base.db')
513
+ temps = engine2.blackboard.get_facts_by_type(:temperature)
514
+ puts temps.first[:value] # => 85
515
+ ```
516
+
448
517
  **Difference from KBS::Engine**: Returns `KBS::Blackboard::Fact` (has `.uuid`) instead of `KBS::Fact`.
449
518
 
450
519
  ---
@@ -639,7 +708,7 @@ engine = KBS::Blackboard::Engine.new(db_path: 'kb.db')
639
708
  kb = KBS.knowledge_base do
640
709
  rule "high_temp_alert", priority: 10 do
641
710
  on :temperature, value: greater_than(80)
642
- perform do |bindings|
711
+ perform do |facts, bindings|
643
712
  puts "Alert! Temperature: #{bindings[:value?]}"
644
713
  end
645
714
  end
@@ -894,7 +963,7 @@ old_facts.each { |f| engine.remove_fact(f) }
894
963
  ```ruby
895
964
  rule "expire_old_facts", priority: 0 do
896
965
  on :temperature, timestamp: ->(ts) { Time.now - ts > 3600 }
897
- perform do |bindings|
966
+ perform do |facts, bindings|
898
967
  fact = bindings[:matched_fact?]
899
968
  engine.remove_fact(fact)
900
969
  end
@@ -910,7 +979,7 @@ end
910
979
  ```ruby
911
980
  rule "risky_operation" do
912
981
  on :task, status: "pending"
913
- perform do |bindings|
982
+ perform do |facts, bindings|
914
983
  begin
915
984
  perform_risky_operation(bindings[:task_id?])
916
985
  rescue => e
data/docs/api/facts.md CHANGED
@@ -486,7 +486,7 @@ engine.remove_fact(fact)
486
486
  ```ruby
487
487
  rule "auto_expire_old_alerts" do
488
488
  on :alert, timestamp: ->(ts) { Time.now - ts > 3600 }
489
- perform do |bindings|
489
+ perform do |facts, bindings|
490
490
  # Fact can remove itself
491
491
  alert_fact = bindings[:matched_fact?]
492
492
  alert_fact.retract
@@ -1047,7 +1047,7 @@ You can't directly compare two attributes of the same fact in one condition. Use
1047
1047
  # Instead: Capture variables and check in action or use join test
1048
1048
  rule "large_order" do
1049
1049
  on :order, quantity: :qty?, price: :price?
1050
- perform do |bindings|
1050
+ perform do |facts, bindings|
1051
1051
  total = bindings[:qty?] * bindings[:price?]
1052
1052
  if total > 10000
1053
1053
  puts "Large order: $#{total}"
@@ -1111,7 +1111,7 @@ condition = KBS::Condition.new(
1111
1111
  # Better - Simple check first, complex check in action
1112
1112
  rule "complex_log_analysis" do
1113
1113
  on :log_entry, level: "ERROR", message: :msg? # Simple literal filter
1114
- perform do |bindings|
1114
+ perform do |facts, bindings|
1115
1115
  if bindings[:msg?] =~ /very.*complex.*regex.*pattern/
1116
1116
  # Expensive check runs only on ERROR logs
1117
1117
  end
data/docs/api/rules.md CHANGED
@@ -38,7 +38,7 @@ Creates a new rule.
38
38
 
39
39
  **Returns**: `KBS::Rule` instance
40
40
 
41
- **Example - Direct Construction**:
41
+ **Example - Low-level API (Direct Construction)**:
42
42
  ```ruby
43
43
  # Minimal rule
44
44
  rule = KBS::Rule.new(:high_temperature)
@@ -54,7 +54,7 @@ rule = KBS::Rule.new(
54
54
  )
55
55
  ```
56
56
 
57
- **Example - Block Configuration**:
57
+ **Example - Low-level API (Block Configuration)**:
58
58
  ```ruby
59
59
  rule = KBS::Rule.new(:high_temperature) do |r|
60
60
  r.conditions << KBS::Condition.new(:temperature, value: ->(v) { v > 80 })
@@ -62,12 +62,12 @@ rule = KBS::Rule.new(:high_temperature) do |r|
62
62
  end
63
63
  ```
64
64
 
65
- **Example - Using DSL** (recommended):
65
+ **Using DSL (Recommended)**:
66
66
  ```ruby
67
67
  kb = KBS.knowledge_base do
68
68
  rule "high_temperature", priority: 10 do
69
69
  on :temperature, value: greater_than(80)
70
- perform do |bindings|
70
+ perform do |facts, bindings|
71
71
  puts "High temperature: #{bindings[:value?]}"
72
72
  end
73
73
  end
@@ -89,23 +89,35 @@ kb.rules.each { |r| engine.add_rule(r) }
89
89
 
90
90
  **Description**: Unique rule identifier
91
91
 
92
- **Example**:
92
+ **Example - Low-level API**:
93
93
  ```ruby
94
94
  rule = KBS::Rule.new(:high_temperature, priority: 10)
95
95
  puts rule.name # => :high_temperature
96
96
  ```
97
97
 
98
+ **Using DSL (Recommended)**:
99
+ ```ruby
100
+ kb = KBS.knowledge_base do
101
+ rule "high_temperature", priority: 10 do
102
+ on :temperature, value: greater_than(80)
103
+ perform { puts "Alert!" }
104
+ end
105
+ end
106
+
107
+ puts kb.rules.first.name # => "high_temperature"
108
+ ```
109
+
98
110
  **Best Practice**: Use descriptive names that indicate the rule's purpose:
99
111
  ```ruby
100
112
  # Good
101
- :high_temperature_alert
102
- :low_inventory_reorder
103
- :fraud_detection_high_risk
113
+ "high_temperature_alert"
114
+ "low_inventory_reorder"
115
+ "fraud_detection_high_risk"
104
116
 
105
117
  # Less clear
106
- :rule1
107
- :temp_rule
108
- :check
118
+ "rule1"
119
+ "temp_rule"
120
+ "check"
109
121
  ```
110
122
 
111
123
  ---
@@ -122,12 +134,22 @@ puts rule.name # => :high_temperature
122
134
 
123
135
  **Range**: Any integer (commonly 0-100)
124
136
 
125
- **Example**:
137
+ **Example - Low-level API**:
126
138
  ```ruby
127
139
  rule = KBS::Rule.new(:critical_alert, priority: 100)
128
140
  puts rule.priority # => 100
129
141
  ```
130
142
 
143
+ **Using DSL (Recommended)**:
144
+ ```ruby
145
+ kb = KBS.knowledge_base do
146
+ rule "critical_alert", priority: 100 do
147
+ on :alert, level: "critical"
148
+ perform { puts "CRITICAL ALERT!" }
149
+ end
150
+ end
151
+ ```
152
+
131
153
  **Priority Semantics**:
132
154
  - **KBS::Engine**: Priority is stored but NOT used for execution order (rules fire in arbitrary order)
133
155
  - **KBS::Blackboard::Engine**: Higher priority rules fire first within production nodes
@@ -155,7 +177,7 @@ priority: -10
155
177
  kb = KBS.knowledge_base do
156
178
  rule "log_temperature", priority: 0 do
157
179
  on :temperature, value: :temp?
158
- perform { |b| puts "Logged: #{b[:temp?]}" }
180
+ perform { |facts, b| puts "Logged: #{b[:temp?]}" }
159
181
  end
160
182
 
161
183
  rule "critical_alert", priority: 100 do
@@ -190,7 +212,7 @@ engine.run
190
212
 
191
213
  **Description**: Array of conditions that must all match for rule to fire
192
214
 
193
- **Example**:
215
+ **Example - Low-level API**:
194
216
  ```ruby
195
217
  rule = KBS::Rule.new(:temperature_alert)
196
218
  rule.conditions << KBS::Condition.new(:temperature, value: ->(v) { v > 80 })
@@ -199,7 +221,22 @@ rule.conditions << KBS::Condition.new(:sensor, status: "active")
199
221
  puts rule.conditions.size # => 2
200
222
  ```
201
223
 
224
+ **Using DSL (Recommended)**:
225
+ ```ruby
226
+ kb = KBS.knowledge_base do
227
+ rule "temperature_alert" do
228
+ on :temperature, value: greater_than(80)
229
+ on :sensor, status: "active"
230
+ perform { puts "Alert!" }
231
+ end
232
+ end
233
+
234
+ puts kb.rules.first.conditions.size # => 2
235
+ ```
236
+
202
237
  **Condition Order Matters** (for performance):
238
+
239
+ **Low-level API**:
203
240
  ```ruby
204
241
  # Good - Most selective condition first
205
242
  rule.conditions = [
@@ -214,6 +251,23 @@ rule.conditions = [
214
251
  ]
215
252
  ```
216
253
 
254
+ **Using DSL (Recommended)**:
255
+ ```ruby
256
+ # Good - Most selective condition first
257
+ rule "sensor_alert" do
258
+ on :sensor, id: 42 # Filters to 1 fact
259
+ on :temperature, value: :temp? # Then match temperature
260
+ perform { |facts, b| puts b[:temp?] }
261
+ end
262
+
263
+ # Less optimal - Less selective first
264
+ rule "sensor_alert" do
265
+ on :temperature, value: :temp? # Matches many facts
266
+ on :sensor, id: 42 # Could have filtered first
267
+ perform { |facts, b| puts b[:temp?] }
268
+ end
269
+ ```
270
+
217
271
  See [Performance Guide](../advanced/performance.md) for condition ordering strategies.
218
272
 
219
273
  ---
@@ -232,7 +286,7 @@ See [Performance Guide](../advanced/performance.md) for condition ordering strat
232
286
  - `facts` (Array<KBS::Fact>) - Array of matched facts (parallel to conditions array)
233
287
  - `bindings` (Hash, optional) - Variable bindings extracted from facts
234
288
 
235
- **Example - Facts Parameter**:
289
+ **Example - Low-level API (Facts Parameter)**:
236
290
  ```ruby
237
291
  rule.action = ->(facts) do
238
292
  temp_fact = facts[0] # First condition's matched fact
@@ -242,7 +296,7 @@ rule.action = ->(facts) do
242
296
  end
243
297
  ```
244
298
 
245
- **Example - Bindings Parameter**:
299
+ **Example - Low-level API (Bindings Parameter)**:
246
300
  ```ruby
247
301
  # Rule with variable bindings
248
302
  rule = KBS::Rule.new(:temperature_alert) do |r|
@@ -254,11 +308,11 @@ rule = KBS::Rule.new(:temperature_alert) do |r|
254
308
  end
255
309
  ```
256
310
 
257
- **Example - DSL Preferred**:
311
+ **Using DSL (Recommended)**:
258
312
  ```ruby
259
313
  rule "temperature_alert" do
260
314
  on :temperature, value: :temp?, location: :loc?
261
- perform do |bindings|
315
+ perform do |facts, bindings|
262
316
  # Cleaner - DSL automatically provides bindings
263
317
  puts "#{bindings[:loc?]}: #{bindings[:temp?]}°F"
264
318
  end
@@ -289,7 +343,7 @@ Executes the rule's action with matched facts.
289
343
  - Executes action lambda
290
344
  - Action may modify external state, add/remove facts, etc.
291
345
 
292
- **Example**:
346
+ **Example - Low-level API**:
293
347
  ```ruby
294
348
  rule = KBS::Rule.new(:log_temperature) do |r|
295
349
  r.conditions << KBS::Condition.new(:temperature, value: :temp?)
@@ -303,6 +357,22 @@ rule.fire([fact])
303
357
  # Output: Temperature: 85
304
358
  ```
305
359
 
360
+ **Using DSL (Recommended)**:
361
+ ```ruby
362
+ kb = KBS.knowledge_base do
363
+ rule "log_temperature" do
364
+ on :temperature, value: :temp?
365
+ perform do |facts, bindings|
366
+ puts "Temperature: #{bindings[:temp?]}"
367
+ end
368
+ end
369
+
370
+ fact :temperature, value: 85
371
+ run # Fires the rule automatically
372
+ end
373
+ # Output: Temperature: 85
374
+ ```
375
+
306
376
  **Note**: Typically called by the RETE engine, not user code. Users call `engine.run` which fires all activated rules.
307
377
 
308
378
  ---
@@ -316,7 +386,7 @@ rule.fire([fact])
316
386
  kb = KBS.knowledge_base do
317
387
  rule "my_rule", priority: 10 do
318
388
  on :temperature, value: :temp?
319
- perform { |b| puts b[:temp?] }
389
+ perform { |facts, b| puts b[:temp?] }
320
390
  end
321
391
  end
322
392
 
@@ -386,7 +456,7 @@ Rules can fire multiple times:
386
456
  ```ruby
387
457
  rule "log_temperature" do
388
458
  on :temperature, value: :temp?
389
- perform { |b| puts "Temperature: #{b[:temp?]}" }
459
+ perform { |facts, b| puts "Temperature: #{b[:temp?]}" }
390
460
  end
391
461
 
392
462
  engine.add_fact(:temperature, value: 85)
@@ -413,7 +483,7 @@ Match single fact type:
413
483
  ```ruby
414
484
  rule "log_all_temperatures" do
415
485
  on :temperature, value: :temp?
416
- perform do |bindings|
486
+ perform do |facts, bindings|
417
487
  puts "Temperature: #{bindings[:temp?]}"
418
488
  end
419
489
  end
@@ -429,7 +499,7 @@ Match multiple related facts:
429
499
  rule "sensor_temperature_alert" do
430
500
  on :sensor, id: :sensor_id?, status: "active"
431
501
  on :temperature, sensor_id: :sensor_id?, value: greater_than(80)
432
- perform do |bindings|
502
+ perform do |facts, bindings|
433
503
  puts "Sensor #{bindings[:sensor_id?]} reports high temperature"
434
504
  end
435
505
  end
@@ -467,7 +537,7 @@ Rules can implement state transitions:
467
537
  rule "pending_to_processing" do
468
538
  on :order, id: :order_id?, status: "pending"
469
539
  on :worker, status: "available", id: :worker_id?
470
- perform do |bindings|
540
+ perform do |facts, bindings|
471
541
  # Transition order to processing
472
542
  order = find_order(bindings[:order_id?])
473
543
  order.update(status: "processing", worker_id: bindings[:worker_id?])
@@ -488,7 +558,7 @@ Low-priority rules that clean up old facts:
488
558
  ```ruby
489
559
  rule "expire_old_temperatures", priority: 0 do
490
560
  on :temperature, timestamp: less_than(Time.now - 3600)
491
- perform do |bindings|
561
+ perform do |facts, bindings|
492
562
  fact = bindings[:matched_fact?]
493
563
  fact.retract # Remove old temperature reading
494
564
  end
@@ -527,7 +597,7 @@ Higher priority rule overrides lower priority:
527
597
  ```ruby
528
598
  rule "high_risk_order", priority: 100 do
529
599
  on :order, id: :order_id?, total: greater_than(10000)
530
- perform do |bindings|
600
+ perform do |facts, bindings|
531
601
  puts "HIGH RISK: Order #{bindings[:order_id?]} requires manual review"
532
602
  # This fires first due to priority
533
603
  end
@@ -535,7 +605,7 @@ end
535
605
 
536
606
  rule "auto_approve_order", priority: 10 do
537
607
  on :order, id: :order_id?, status: "pending"
538
- perform do |bindings|
608
+ perform do |facts, bindings|
539
609
  puts "Auto-approving order #{bindings[:order_id?]}"
540
610
  # This fires later (if at all)
541
611
  end
@@ -552,7 +622,7 @@ Rule that adds facts triggering other rules:
552
622
  rule "calculate_fibonacci" do
553
623
  on :fib_request, n: :n?
554
624
  negated :fib_result, n: :n? # Not already calculated
555
- perform do |bindings|
625
+ perform do |facts, bindings|
556
626
  n = bindings[:n?]
557
627
 
558
628
  if n <= 1
@@ -572,7 +642,7 @@ rule "combine_fibonacci" do
572
642
  on :fib_result, n: :n_minus_1?, value: :val1?
573
643
  on :fib_result, n: :n_minus_2?, value: :val2?
574
644
  # ... (complex join test: ?n_minus_1 == ?n - 1, etc.)
575
- perform do |bindings|
645
+ perform do |facts, bindings|
576
646
  result = bindings[:val1?] + bindings[:val2?]
577
647
  engine.add_fact(:fib_result, n: bindings[:n?], value: result)
578
648
  end
@@ -633,7 +703,7 @@ end
633
703
 
634
704
  rule "log_temperature", priority: 0 do
635
705
  on :temperature, value: :temp?
636
- perform { |b| log(b[:temp?]) }
706
+ perform { |facts, b| log(b[:temp?]) }
637
707
  end
638
708
  ```
639
709
 
@@ -647,7 +717,7 @@ Critical safety rules should have high priority to fire before less important ru
647
717
  # Good - Idempotent (safe to run multiple times)
648
718
  rule "alert_high_temp" do
649
719
  on :temperature, value: greater_than(80)
650
- perform do |bindings|
720
+ perform do |facts, bindings|
651
721
  # Check if alert already sent
652
722
  unless alert_sent?(bindings[:temp?])
653
723
  send_alert(bindings[:temp?])
@@ -659,7 +729,7 @@ end
659
729
  # Bad - Not idempotent (sends duplicate alerts)
660
730
  rule "alert_high_temp" do
661
731
  on :temperature, value: greater_than(80)
662
- perform do |bindings|
732
+ perform do |facts, bindings|
663
733
  send_alert(bindings[:temp?]) # Sends every time rule fires
664
734
  end
665
735
  end
@@ -696,7 +766,7 @@ end
696
766
  rule "order_inventory_check" do
697
767
  on :order, product_id: :pid?, quantity: :qty?
698
768
  on :inventory, product_id: :pid?, available: :available?
699
- perform do |bindings|
769
+ perform do |facts, bindings|
700
770
  if bindings[:available?] < bindings[:qty?]
701
771
  puts "Insufficient inventory for product #{bindings[:pid?]}"
702
772
  end
@@ -707,7 +777,7 @@ end
707
777
  rule "order_inventory_check" do
708
778
  on :order, product_id: :pid1?, quantity: :qty?
709
779
  on :inventory, product_id: :pid2?, available: :available?
710
- perform do |bindings|
780
+ perform do |facts, bindings|
711
781
  # No guarantee pid1 == pid2!
712
782
  if bindings[:pid1?] == bindings[:pid2?] # Manual check in action (inefficient)
713
783
  ...
@@ -733,7 +803,7 @@ rule "portfolio_rebalancing", priority: 50 do
733
803
 
734
804
  on :portfolio, id: :portfolio_id?, status: "active"
735
805
  on :drift_calculation, portfolio_id: :portfolio_id?, drift: greater_than(0.05)
736
- perform do |bindings|
806
+ perform do |facts, bindings|
737
807
  # Implementation...
738
808
  end
739
809
  end
@@ -806,7 +876,7 @@ end
806
876
  # Good - Cleanup rule prevents unbounded growth
807
877
  rule "expire_old_facts", priority: 0 do
808
878
  on :temperature, timestamp: less_than(Time.now - 3600)
809
- perform do |bindings|
879
+ perform do |facts, bindings|
810
880
  fact = bindings[:matched_fact?]
811
881
  fact.retract
812
882
  end
@@ -867,7 +937,7 @@ action: ->(facts, bindings) do
867
937
  end
868
938
 
869
939
  # 3. DSL style (cleanest)
870
- perform do |bindings|
940
+ perform do |facts, bindings|
871
941
  puts bindings[:temp?]
872
942
  end
873
943
  ```
@@ -962,18 +1032,18 @@ Keep actions fast:
962
1032
 
963
1033
  ```ruby
964
1034
  # Good - Fast action
965
- perform do |bindings|
1035
+ perform do |facts, bindings|
966
1036
  puts "Temperature: #{bindings[:temp?]}"
967
1037
  end
968
1038
 
969
1039
  # Bad - Slow action blocks engine
970
- perform do |bindings|
1040
+ perform do |facts, bindings|
971
1041
  sleep 5 # Blocks engine for 5 seconds!
972
1042
  send_email_alert(bindings[:temp?]) # Network I/O
973
1043
  end
974
1044
 
975
1045
  # Better - Offload slow work
976
- perform do |bindings|
1046
+ perform do |facts, bindings|
977
1047
  # Post message for async worker
978
1048
  engine.post_message("alert_system", "email_queue", bindings)
979
1049
  end