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
@@ -7,13 +7,15 @@ Master the art of authoring production rules. This guide covers best practices,
7
7
  Every rule consists of three parts:
8
8
 
9
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
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
17
19
  end
18
20
  end
19
21
  ```
@@ -45,9 +47,19 @@ Choose descriptive, actionable names:
45
47
  Control execution order when multiple rules match:
46
48
 
47
49
  ```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
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
51
63
  ```
52
64
 
53
65
  **Priority Guidelines:**
@@ -61,13 +73,19 @@ KBS::Rule.new("cleanup_task", priority: 10) # Fires last
61
73
  Patterns that must match for the rule to fire. Order matters for performance.
62
74
 
63
75
  ```ruby
64
- r.conditions = [
65
- # Most selective first (fewest matches)
66
- KBS::Condition.new(:critical_alert, { severity: "critical" }),
76
+ KBS.knowledge_base do
77
+ rule "example" do
78
+ # Most selective first (fewest matches)
79
+ on :critical_alert, severity: "critical"
67
80
 
68
- # Less selective last (more matches)
69
- KBS::Condition.new(:sensor, { id: :sensor_id? })
70
- ]
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
71
89
  ```
72
90
 
73
91
  ### 4. Action
@@ -75,16 +93,23 @@ r.conditions = [
75
93
  Code executed when all conditions match:
76
94
 
77
95
  ```ruby
78
- r.action = lambda do |facts, bindings|
79
- # Access matched facts
80
- alert = facts[0]
81
- sensor = facts[1]
96
+ KBS.knowledge_base do
97
+ rule "example" do
98
+ on :alert, message: :msg?
99
+ on :sensor, id: :sensor_id?
82
100
 
83
- # Access variable bindings
84
- sensor_id = bindings[:sensor_id?]
101
+ perform do |facts, bindings|
102
+ # Access matched facts
103
+ alert = facts[0]
104
+ sensor = facts[1]
85
105
 
86
- # Perform action
87
- notify_operator(sensor_id, alert[:message])
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
88
113
  end
89
114
  ```
90
115
 
@@ -96,17 +121,23 @@ end
96
121
 
97
122
  ```ruby
98
123
  # Bad: General condition first
99
- r.conditions = [
100
- KBS::Condition.new(:sensor, {}), # 1000 matches
101
- KBS::Condition.new(:critical_alert, {}) # 1 match
102
- ]
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
103
131
  # Creates 1000 partial matches, wastes memory
104
132
 
105
133
  # Good: Specific condition first
106
- r.conditions = [
107
- KBS::Condition.new(:critical_alert, {}), # 1 match
108
- KBS::Condition.new(:sensor, {}) # Joins with 1000
109
- ]
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
110
141
  # Creates 1 partial match, efficient joins
111
142
  ```
112
143
 
@@ -114,31 +145,31 @@ r.conditions = [
114
145
 
115
146
  ```ruby
116
147
  # Most selective (few facts)
117
- KBS::Condition.new(:emergency, { level: "critical" })
118
- KBS::Condition.new(:user, { role: "admin" })
148
+ on :emergency, level: "critical"
149
+ on :user, role: "admin"
119
150
 
120
151
  # Moderate selectivity
121
- KBS::Condition.new(:order, { status: "pending" })
122
- KBS::Condition.new(:stock, { exchange: "NYSE" })
152
+ on :order, status: "pending"
153
+ on :stock, exchange: "NYSE"
123
154
 
124
155
  # Least selective (many facts)
125
- KBS::Condition.new(:sensor, {})
126
- KBS::Condition.new(:log_entry, {})
156
+ on :sensor, {}
157
+ on :log_entry, {}
127
158
  ```
128
159
 
129
160
  ### Measuring Selectivity
130
161
 
131
162
  ```ruby
132
- def measure_selectivity(engine, type, pattern)
133
- engine.facts.count { |f|
163
+ def measure_selectivity(kb, type, pattern)
164
+ kb.engine.facts.count { |f|
134
165
  f.type == type &&
135
166
  pattern.all? { |k, v| f[k] == v }
136
167
  }
137
168
  end
138
169
 
139
170
  # Compare
140
- puts measure_selectivity(engine, :critical_alert, {}) # => 1
141
- puts measure_selectivity(engine, :sensor, {}) # => 1000
171
+ puts measure_selectivity(kb, :critical_alert, {}) # => 1
172
+ puts measure_selectivity(kb, :sensor, {}) # => 1000
142
173
 
143
174
  # Order: critical_alert first, sensor second
144
175
  ```
@@ -151,52 +182,58 @@ One action, one purpose:
151
182
 
152
183
  ```ruby
153
184
  # Good: Focused action
154
- r.action = lambda do |facts, bindings|
155
- send_email_alert(bindings[:email?], bindings[:message?])
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
156
192
  end
157
193
 
158
194
  # 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?])
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
164
205
  end
165
206
  ```
166
207
 
167
208
  Split complex actions into multiple rules:
168
209
 
169
210
  ```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
- ]
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)
175
215
 
176
- r.action = lambda do |facts, bindings|
177
- engine.add_fact(:high_temp_detected, { temp: bindings[:temp?] })
216
+ perform do |facts, bindings|
217
+ fact :high_temp_detected, temp: bindings[:temp?]
218
+ end
178
219
  end
179
- end
180
220
 
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
- ]
221
+ # Rule 2: Send alert
222
+ rule "send_temp_alert", priority: 40 do
223
+ on :high_temp_detected, temp: :temp?
186
224
 
187
- r.action = lambda do |facts, bindings|
188
- send_email("High temp: #{bindings[:temp?]}")
225
+ perform do |facts, bindings|
226
+ send_email("High temp: #{bindings[:temp?]}")
227
+ end
189
228
  end
190
- end
191
229
 
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
- ]
230
+ # Rule 3: Log event
231
+ rule "log_temp_event", priority: 30 do
232
+ on :high_temp_detected, temp: :temp?
197
233
 
198
- r.action = lambda do |facts, bindings|
199
- logger.info("Temperature spike: #{bindings[:temp?]}")
234
+ perform do |facts, bindings|
235
+ logger.info("Temperature spike: #{bindings[:temp?]}")
236
+ end
200
237
  end
201
238
  end
202
239
  ```
@@ -207,19 +244,31 @@ Actions should be deterministic and idempotent when possible:
207
244
 
208
245
  ```ruby
209
246
  # 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
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
214
255
 
215
- # Add new alert
216
- engine.add_fact(:alert, { id: bindings[:id?], message: "Alert!" })
256
+ # Add new alert
257
+ fact :alert, id: bindings[:id?], message: "Alert!"
258
+ end
259
+ end
217
260
  end
218
261
 
219
262
  # 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!" })
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
223
272
  end
224
273
  ```
225
274
 
@@ -228,17 +277,22 @@ end
228
277
  Protect against failures:
229
278
 
230
279
  ```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
- })
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
242
296
  end
243
297
  end
244
298
  ```
@@ -267,20 +321,22 @@ Use descriptive, consistent variable names:
267
321
  Connect facts through shared variables:
268
322
 
269
323
  ```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
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
284
340
  ```
285
341
 
286
342
  ### Computed Bindings
@@ -288,15 +344,22 @@ r.conditions = [
288
344
  Derive values in actions:
289
345
 
290
346
  ```ruby
291
- r.action = lambda do |facts, bindings|
292
- current = bindings[:current_temp?]
293
- max = bindings[:max_temp?]
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?]
294
355
 
295
- # Compute derived values
296
- diff = current - max
297
- percentage_over = ((current / max.to_f) - 1) * 100
356
+ # Compute derived values
357
+ diff = current - max
358
+ percentage_over = ((current / max.to_f) - 1) * 100
298
359
 
299
- puts "#{diff}°C over threshold (#{percentage_over.round(1)}%)"
360
+ puts "#{diff}°C over threshold (#{percentage_over.round(1)}%)"
361
+ end
362
+ end
300
363
  end
301
364
  ```
302
365
 
@@ -307,47 +370,40 @@ end
307
370
  Model state transitions:
308
371
 
309
372
  ```ruby
310
- # Transition: pending → processing
311
- KBS::Rule.new("start_processing") do |r|
312
- r.conditions = [
313
- KBS::Condition.new(:order, {
373
+ KBS.knowledge_base do
374
+ # Transition: pending → processing
375
+ rule "start_processing" do
376
+ on :order,
314
377
  id: :order_id?,
315
378
  status: "pending"
316
- })
317
- ]
318
379
 
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
- })
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
327
388
  end
328
- end
329
389
 
330
- # Transition: processing → completed
331
- KBS::Rule.new("complete_processing") do |r|
332
- r.conditions = [
333
- KBS::Condition.new(:order, {
390
+ # Transition: processing → completed
391
+ rule "complete_processing" do
392
+ on :order,
334
393
  id: :order_id?,
335
394
  status: "processing"
336
- }),
337
- KBS::Condition.new(:processing_done, {
395
+ on :processing_done,
338
396
  order_id: :order_id?
339
- })
340
- ]
341
397
 
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
- })
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
351
407
  end
352
408
  end
353
409
  ```
@@ -357,19 +413,19 @@ end
357
413
  Prevent duplicate actions:
358
414
 
359
415
  ```ruby
360
- KBS::Rule.new("send_alert_once") do |r|
361
- r.conditions = [
362
- KBS::Condition.new(:high_temp, { sensor_id: :id? }),
416
+ KBS.knowledge_base do
417
+ rule "send_alert_once" do
418
+ on :high_temp, sensor_id: :id?
363
419
 
364
420
  # Guard: Only fire if alert not already sent
365
- KBS::Condition.new(:alert_sent, { sensor_id: :id? }, negated: true)
366
- ]
421
+ without :alert_sent, sensor_id: :id?
367
422
 
368
- r.action = lambda do |facts, bindings|
369
- send_alert(bindings[:id?])
423
+ perform do |facts, bindings|
424
+ send_alert(bindings[:id?])
370
425
 
371
- # Record that we sent this alert
372
- engine.add_fact(:alert_sent, { sensor_id: bindings[:id?] })
426
+ # Record that we sent this alert
427
+ fact :alert_sent, sensor_id: bindings[:id?]
428
+ end
373
429
  end
374
430
  end
375
431
  ```
@@ -379,18 +435,18 @@ end
379
435
  Remove stale facts:
380
436
 
381
437
  ```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
- ]
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
+ }
390
445
 
391
- r.action = lambda do |facts, bindings|
392
- engine.remove_fact(facts[0])
393
- logger.info("Removed stale alert")
446
+ perform do |facts, bindings|
447
+ engine.remove_fact(facts[0])
448
+ logger.info("Removed stale alert")
449
+ end
394
450
  end
395
451
  end
396
452
  ```
@@ -400,20 +456,20 @@ end
400
456
  Compute over multiple facts:
401
457
 
402
458
  ```ruby
403
- KBS::Rule.new("compute_average_temp") do |r|
404
- r.conditions = [
405
- KBS::Condition.new(:compute_avg_requested, {})
406
- ]
459
+ KBS.knowledge_base do
460
+ rule "compute_average_temp" do
461
+ on :compute_avg_requested, {}
407
462
 
408
- r.action = lambda do |facts, bindings|
409
- temps = engine.facts
410
- .select { |f| f.type == :sensor }
411
- .map { |f| f[:temp] }
412
- .compact
463
+ perform do |facts, bindings|
464
+ temps = engine.facts
465
+ .select { |f| f.type == :sensor }
466
+ .map { |f| f[:temp] }
467
+ .compact
413
468
 
414
- avg = temps.sum / temps.size.to_f
469
+ avg = temps.sum / temps.size.to_f
415
470
 
416
- engine.add_fact(:average_temp, { value: avg })
471
+ fact :average_temp, value: avg
472
+ end
417
473
  end
418
474
  end
419
475
  ```
@@ -423,26 +479,23 @@ end
423
479
  React to time-based conditions:
424
480
 
425
481
  ```ruby
426
- KBS::Rule.new("detect_delayed_response") do |r|
427
- r.conditions = [
428
- KBS::Condition.new(:request, {
482
+ KBS.knowledge_base do
483
+ rule "detect_delayed_response" do
484
+ on :request,
429
485
  id: :req_id?,
430
486
  created_at: :created?
431
- }),
432
487
 
433
- KBS::Condition.new(:response, {
488
+ without :response,
434
489
  request_id: :req_id?
435
- }, negated: true),
436
490
 
437
- KBS::Condition.new(:request, {},
491
+ on :request, {},
438
492
  predicate: lambda { |f|
439
493
  (Time.now - f[:created_at]) > 300 # 5 minutes
440
494
  }
441
- )
442
- ]
443
495
 
444
- r.action = lambda do |facts, bindings|
445
- alert("Request #{bindings[:req_id?]} delayed!")
496
+ perform do |facts, bindings|
497
+ alert("Request #{bindings[:req_id?]} delayed!")
498
+ end
446
499
  end
447
500
  end
448
501
  ```
@@ -464,12 +517,14 @@ module Priority
464
517
  end
465
518
 
466
519
  # Use in rules
467
- KBS::Rule.new("emergency_shutdown", priority: Priority::CRITICAL) do |r|
468
- # ...
469
- end
520
+ KBS.knowledge_base do
521
+ rule "emergency_shutdown", priority: Priority::CRITICAL do
522
+ # ...
523
+ end
470
524
 
471
- KBS::Rule.new("process_order", priority: Priority::NORMAL) do |r|
472
- # ...
525
+ rule "process_order", priority: Priority::NORMAL do
526
+ # ...
527
+ end
473
528
  end
474
529
  ```
475
530
 
@@ -479,21 +534,37 @@ Avoid priority inversions where low-priority rules block high-priority rules:
479
534
 
480
535
  ```ruby
481
536
  # 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
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
486
544
 
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 }
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
492
551
  end
493
552
 
494
553
  # Fix: Make dependency higher priority
495
- KBS::Rule.new("compute_risk", priority: 110) do |r|
496
- # Now runs before emergency_check
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
497
568
  end
498
569
  ```
499
570
 
@@ -506,46 +577,67 @@ require 'minitest/autorun'
506
577
  require 'kbs'
507
578
 
508
579
  class TestTemperatureRules < Minitest::Test
509
- def setup
510
- @engine = KBS::Engine.new
580
+ def test_fires_when_temp_exceeds_threshold
581
+ alert_fired = false
511
582
 
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
- ]
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?
517
587
 
518
- r.action = lambda do |facts, bindings|
519
- @alert_fired = true if bindings[:temp?] > bindings[:max?]
588
+ perform do |facts, bindings|
589
+ alert_fired = true if bindings[:temp?] > bindings[:max?]
590
+ end
520
591
  end
592
+
593
+ fact :sensor, id: "bedroom", temp: 30
594
+ fact :threshold, id: "bedroom", max: 25
595
+ run
521
596
  end
522
597
 
523
- @engine.add_rule(@rule)
524
- @alert_fired = false
598
+ assert alert_fired, "Rule should fire when temp > threshold"
525
599
  end
526
600
 
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
601
+ def test_does_not_fire_when_temp_below_threshold
602
+ alert_fired = false
531
603
 
532
- assert @alert_fired, "Rule should fire when temp > threshold"
533
- end
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?
534
608
 
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
609
+ perform do |facts, bindings|
610
+ alert_fired = true if bindings[:temp?] > bindings[:max?]
611
+ end
612
+ end
539
613
 
540
- refute @alert_fired, "Rule should not fire when temp <= threshold"
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"
541
620
  end
542
621
 
543
622
  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
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
547
634
 
548
- refute @alert_fired, "Rule should not fire for different sensors"
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"
549
641
  end
550
642
  end
551
643
  ```
@@ -556,23 +648,42 @@ Test multiple rules working together:
556
648
 
557
649
  ```ruby
558
650
  def test_state_machine_workflow
559
- # Add state transition rules
560
- engine.add_rule(start_processing_rule)
561
- engine.add_rule(complete_processing_rule)
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
562
660
 
563
- # Add initial state
564
- engine.add_fact(:order, { id: 1, status: "pending" })
565
- engine.run
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
566
675
 
567
676
  # Should not transition yet
568
- assert_equal "pending", find_order(1)[:status]
677
+ order = kb.engine.facts.find { |f| f.type == :order && f[:id] == 1 }
678
+ assert_equal "pending", order[:status]
569
679
 
570
680
  # Trigger transition
571
- engine.add_fact(:processing_done, { order_id: 1 })
572
- engine.run
681
+ kb.fact :processing_done, order_id: 1
682
+ kb.run
573
683
 
574
- # Should transition to processing, then completed
575
- assert_equal "completed", find_order(1)[:status]
684
+ # Should transition to completed
685
+ order = kb.engine.facts.find { |f| f.type == :order && f[:id] == 1 }
686
+ assert_equal "completed", order[:status]
576
687
  end
577
688
  ```
578
689
 
@@ -582,16 +693,28 @@ Test rule invariants:
582
693
 
583
694
  ```ruby
584
695
  def test_no_duplicate_alerts
585
- # Add facts
586
- 100.times do |i|
587
- engine.add_fact(:high_temp, { sensor_id: i })
588
- end
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
589
711
 
590
- # Run engine multiple times
591
- 10.times { engine.run }
712
+ # Run engine multiple times
713
+ 10.times { run }
714
+ end
592
715
 
593
716
  # Property: At most one alert per sensor
594
- alert_counts = engine.facts
717
+ alert_counts = kb.engine.facts
595
718
  .select { |f| f.type == :alert_sent }
596
719
  .group_by { |f| f[:sensor_id] }
597
720
  .transform_values(&:count)
@@ -610,62 +733,81 @@ Negations are expensive:
610
733
 
611
734
  ```ruby
612
735
  # 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
- ]
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
618
744
 
619
745
  # Better: Combine into positive condition
620
- engine.add_fact(:conditions_met, {}) unless foo_exists? || bar_exists? || baz_exists?
746
+ KBS.knowledge_base do
747
+ rule "positive_logic" do
748
+ on :conditions_met, {}
749
+ perform { }
750
+ end
621
751
 
622
- r.conditions = [
623
- KBS::Condition.new(:conditions_met, {})
624
- ]
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
625
757
  ```
626
758
 
627
759
  ### Avoid Predicates for Simple Checks
628
760
 
629
761
  ```ruby
630
762
  # Expensive: Predicate disables network sharing
631
- KBS::Condition.new(:stock, {},
632
- predicate: lambda { |f| f[:symbol] == "AAPL" }
633
- )
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
634
769
 
635
770
  # Better: Use pattern matching
636
- KBS::Condition.new(:stock, { symbol: "AAPL" })
771
+ KBS.knowledge_base do
772
+ rule "with_pattern" do
773
+ on :stock, symbol: "AAPL"
774
+ perform { }
775
+ end
776
+ end
637
777
  ```
638
778
 
639
779
  ### Cache Computed Values
640
780
 
641
781
  ```ruby
642
782
  # 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)
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
647
791
  end
648
792
  end
649
793
 
650
794
  # 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
- ]
795
+ KBS.knowledge_base do
796
+ rule "update_average", priority: 100 do
797
+ on :sensor, temp: :temp? # Triggers when sensor added
655
798
 
656
- r.action = lambda do |facts, bindings|
657
- avg = compute_expensive_average(engine.facts)
658
- engine.add_fact(:cached_average, { value: avg })
799
+ perform do |facts, bindings|
800
+ avg = compute_expensive_average(engine.facts)
801
+ fact :cached_average, value: avg
802
+ end
659
803
  end
660
- end
661
804
 
662
- KBS::Rule.new("check_average", priority: 50) do |r|
663
- r.conditions = [
664
- KBS::Condition.new(:cached_average, { value: :avg? })
665
- ]
805
+ rule "check_average", priority: 50 do
806
+ on :cached_average, value: :avg?
666
807
 
667
- r.action = lambda do |facts, bindings|
668
- alert(bindings[:avg?]) if bindings[:avg?] > threshold
808
+ perform do |facts, bindings|
809
+ alert(bindings[:avg?]) if bindings[:avg?] > threshold
810
+ end
669
811
  end
670
812
  end
671
813
  ```
@@ -676,27 +818,27 @@ end
676
818
 
677
819
  ```ruby
678
820
  # Bad: Rule fires itself indefinitely
679
- KBS::Rule.new("infinite_loop") do |r|
680
- r.conditions = [
681
- KBS::Condition.new(:sensor, { temp: :temp? })
682
- ]
821
+ KBS.knowledge_base do
822
+ rule "infinite_loop" do
823
+ on :sensor, temp: :temp?
683
824
 
684
- r.action = lambda do |facts, bindings|
685
- # This triggers the rule again!
686
- engine.add_fact(:sensor, { temp: bindings[:temp?] + 1 })
825
+ perform do |facts, bindings|
826
+ # This triggers the rule again!
827
+ fact :sensor, temp: bindings[:temp?] + 1
828
+ end
687
829
  end
688
830
  end
689
831
 
690
832
  # 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, {})
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
700
842
  end
701
843
  end
702
844
  ```
@@ -707,39 +849,56 @@ end
707
849
  # Bad: Closure captures wrong variable
708
850
  rules = []
709
851
  %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
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!
715
857
  end
716
858
  end
717
859
  end
718
860
 
719
861
  # Fix: Force closure with parameter
720
862
  %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
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
726
869
  end
727
870
  end
728
871
  end
729
872
  ```
730
873
 
731
- ### 3. Forgetting to Call `engine.run`
874
+ ### 3. Forgetting to Call `run`
732
875
 
733
876
  ```ruby
734
877
  # Bad: Facts added but never matched
735
- engine.add_fact(:sensor, { temp: 30 })
736
- engine.add_fact(:threshold, { max: 25 })
737
- # Rules never fire!
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
738
889
 
739
890
  # 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
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
743
902
  ```
744
903
 
745
904
  ## Next Steps