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
@@ -35,16 +35,12 @@ require 'minitest/autorun'
35
35
  require 'kbs'
36
36
 
37
37
  class Minitest::Test
38
- def setup_engine
39
- KBS::Engine.new
40
- end
41
-
42
- def assert_rule_fired(engine, rule_name)
38
+ def assert_rule_fired(kb, rule_name)
43
39
  # Check if rule action was executed
44
40
  # Implementation depends on tracking mechanism
45
41
  end
46
42
 
47
- def refute_rule_fired(engine, rule_name)
43
+ def refute_rule_fired(kb, rule_name)
48
44
  # Check that rule did not fire
49
45
  end
50
46
  end
@@ -58,58 +54,84 @@ end
58
54
  require 'test_helper'
59
55
 
60
56
  class TestTemperatureRule < Minitest::Test
61
- def setup
62
- @engine = setup_engine
63
- @fired = false
57
+ def test_fires_when_temperature_high
58
+ fired = false
64
59
 
65
- # Create test rule
66
- @rule = KBS::Rule.new("high_temp_alert", priority: 100) do |r|
67
- r.conditions = [
68
- KBS::Condition.new(:sensor, {
60
+ kb = KBS.knowledge_base do
61
+ rule "high_temp_alert", priority: 100 do
62
+ on :sensor,
69
63
  type: "temperature",
70
- value: :temp?
71
- }, predicate: lambda { |f| f[:value] > 30 })
72
- ]
73
-
74
- r.action = lambda do |facts, bindings|
75
- @fired = true
76
- @engine.add_fact(:alert, {
77
- type: "high_temperature",
78
- temperature: bindings[:temp?]
79
- })
64
+ value: :temp?,
65
+ predicate: greater_than(30)
66
+
67
+ perform do |facts, bindings|
68
+ fired = true
69
+ fact :alert,
70
+ type: "high_temperature",
71
+ temperature: bindings[:temp?]
72
+ end
80
73
  end
81
- end
82
-
83
- @engine.add_rule(@rule)
84
- end
85
74
 
86
- def test_fires_when_temperature_high
87
- @engine.add_fact(:sensor, { type: "temperature", value: 35 })
88
- @engine.run
75
+ fact :sensor, type: "temperature", value: 35
76
+ run
77
+ end
89
78
 
90
- assert @fired, "Rule should fire for high temperature"
79
+ assert fired, "Rule should fire for high temperature"
91
80
 
92
- alerts = @engine.facts.select { |f| f.type == :alert }
81
+ alerts = kb.engine.facts.select { |f| f.type == :alert }
93
82
  assert_equal 1, alerts.size
94
83
  assert_equal 35, alerts.first[:temperature]
95
84
  end
96
85
 
97
86
  def test_does_not_fire_when_temperature_normal
98
- @engine.add_fact(:sensor, { type: "temperature", value: 25 })
99
- @engine.run
87
+ fired = false
88
+
89
+ kb = KBS.knowledge_base do
90
+ rule "high_temp_alert", priority: 100 do
91
+ on :sensor,
92
+ type: "temperature",
93
+ value: :temp?,
94
+ predicate: greater_than(30)
95
+
96
+ perform do |facts, bindings|
97
+ fired = true
98
+ fact :alert,
99
+ type: "high_temperature",
100
+ temperature: bindings[:temp?]
101
+ end
102
+ end
100
103
 
101
- refute @fired, "Rule should not fire for normal temperature"
104
+ fact :sensor, type: "temperature", value: 25
105
+ run
106
+ end
107
+
108
+ refute fired, "Rule should not fire for normal temperature"
102
109
 
103
- alerts = @engine.facts.select { |f| f.type == :alert }
110
+ alerts = kb.engine.facts.select { |f| f.type == :alert }
104
111
  assert_empty alerts
105
112
  end
106
113
 
107
114
  def test_threshold_boundary
108
- # Test at exact threshold
109
- @engine.add_fact(:sensor, { type: "temperature", value: 30 })
110
- @engine.run
115
+ fired = false
116
+
117
+ kb = KBS.knowledge_base do
118
+ rule "high_temp_alert" do
119
+ on :sensor,
120
+ type: "temperature",
121
+ value: :temp?,
122
+ predicate: greater_than(30)
111
123
 
112
- refute @fired, "Rule should not fire at exact threshold (>= not >)"
124
+ perform do |facts, bindings|
125
+ fired = true
126
+ end
127
+ end
128
+
129
+ # Test at exact threshold
130
+ fact :sensor, type: "temperature", value: 30
131
+ run
132
+ end
133
+
134
+ refute fired, "Rule should not fire at exact threshold (> not >=)"
113
135
  end
114
136
  end
115
137
  ```
@@ -118,61 +140,116 @@ end
118
140
 
119
141
  ```ruby
120
142
  class TestMultiConditionRule < Minitest::Test
121
- def setup
122
- @engine = setup_engine
123
- @fired = false
143
+ def test_fires_when_both_conditions_met
144
+ fired = false
124
145
 
125
- @rule = KBS::Rule.new("high_temp_and_low_humidity") do |r|
126
- r.conditions = [
127
- KBS::Condition.new(:temperature, {
146
+ kb = KBS.knowledge_base do
147
+ rule "high_temp_and_low_humidity" do
148
+ on :temperature,
128
149
  location: :loc?,
129
- value: :temp?
130
- }, predicate: lambda { |f| f[:value] > 30 }),
150
+ value: :temp?,
151
+ predicate: greater_than(30)
131
152
 
132
- KBS::Condition.new(:humidity, {
153
+ on :humidity,
133
154
  location: :loc?,
134
- value: :hum?
135
- }, predicate: lambda { |f| f[:value] < 40 })
136
- ]
155
+ value: :hum?,
156
+ predicate: less_than(40)
137
157
 
138
- r.action = lambda do |facts, bindings|
139
- @fired = true
158
+ perform do |facts, bindings|
159
+ fired = true
160
+ end
140
161
  end
162
+
163
+ fact :temperature, location: "room1", value: 35
164
+ fact :humidity, location: "room1", value: 30
165
+ run
141
166
  end
142
167
 
143
- @engine.add_rule(@rule)
168
+ assert fired, "Rule should fire when both conditions met"
144
169
  end
145
170
 
146
- def test_fires_when_both_conditions_met
147
- @engine.add_fact(:temperature, { location: "room1", value: 35 })
148
- @engine.add_fact(:humidity, { location: "room1", value: 30 })
149
- @engine.run
171
+ def test_does_not_fire_with_mismatched_locations
172
+ fired = false
150
173
 
151
- assert @fired, "Rule should fire when both conditions met"
152
- end
174
+ kb = KBS.knowledge_base do
175
+ rule "high_temp_and_low_humidity" do
176
+ on :temperature,
177
+ location: :loc?,
178
+ value: :temp?,
179
+ predicate: greater_than(30)
153
180
 
154
- def test_does_not_fire_with_mismatched_locations
155
- @engine.add_fact(:temperature, { location: "room1", value: 35 })
156
- @engine.add_fact(:humidity, { location: "room2", value: 30 })
157
- @engine.run
181
+ on :humidity,
182
+ location: :loc?,
183
+ value: :hum?,
184
+ predicate: less_than(40)
185
+
186
+ perform do |facts, bindings|
187
+ fired = true
188
+ end
189
+ end
158
190
 
159
- refute @fired, "Rule should not fire with different locations"
191
+ fact :temperature, location: "room1", value: 35
192
+ fact :humidity, location: "room2", value: 30
193
+ run
194
+ end
195
+
196
+ refute fired, "Rule should not fire with different locations"
160
197
  end
161
198
 
162
199
  def test_does_not_fire_when_only_temperature_high
163
- @engine.add_fact(:temperature, { location: "room1", value: 35 })
164
- # No humidity fact
165
- @engine.run
200
+ fired = false
201
+
202
+ kb = KBS.knowledge_base do
203
+ rule "high_temp_and_low_humidity" do
204
+ on :temperature,
205
+ location: :loc?,
206
+ value: :temp?,
207
+ predicate: greater_than(30)
208
+
209
+ on :humidity,
210
+ location: :loc?,
211
+ value: :hum?,
212
+ predicate: less_than(40)
213
+
214
+ perform do |facts, bindings|
215
+ fired = true
216
+ end
217
+ end
166
218
 
167
- refute @fired, "Rule should not fire without humidity fact"
219
+ fact :temperature, location: "room1", value: 35
220
+ # No humidity fact
221
+ run
222
+ end
223
+
224
+ refute fired, "Rule should not fire without humidity fact"
168
225
  end
169
226
 
170
227
  def test_does_not_fire_when_temperature_normal
171
- @engine.add_fact(:temperature, { location: "room1", value: 25 })
172
- @engine.add_fact(:humidity, { location: "room1", value: 30 })
173
- @engine.run
228
+ fired = false
229
+
230
+ kb = KBS.knowledge_base do
231
+ rule "high_temp_and_low_humidity" do
232
+ on :temperature,
233
+ location: :loc?,
234
+ value: :temp?,
235
+ predicate: greater_than(30)
236
+
237
+ on :humidity,
238
+ location: :loc?,
239
+ value: :hum?,
240
+ predicate: less_than(40)
241
+
242
+ perform do |facts, bindings|
243
+ fired = true
244
+ end
245
+ end
246
+
247
+ fact :temperature, location: "room1", value: 25
248
+ fact :humidity, location: "room1", value: 30
249
+ run
250
+ end
174
251
 
175
- refute @fired, "Rule should not fire with normal temperature"
252
+ refute fired, "Rule should not fire with normal temperature"
176
253
  end
177
254
  end
178
255
  ```
@@ -181,37 +258,45 @@ end
181
258
 
182
259
  ```ruby
183
260
  class TestNegationRule < Minitest::Test
184
- def setup
185
- @engine = setup_engine
186
- @fired = false
261
+ def test_fires_when_error_not_acknowledged
262
+ fired = false
187
263
 
188
- @rule = KBS::Rule.new("alert_if_no_acknowledgment") do |r|
189
- r.conditions = [
190
- KBS::Condition.new(:error, { id: :id? }),
191
- KBS::Condition.new(:acknowledged, { error_id: :id? }, negated: true)
192
- ]
264
+ kb = KBS.knowledge_base do
265
+ rule "alert_if_no_acknowledgment" do
266
+ on :error, id: :id?
267
+ without :acknowledged, error_id: :id?
193
268
 
194
- r.action = lambda do |facts, bindings|
195
- @fired = true
269
+ perform do |facts, bindings|
270
+ fired = true
271
+ end
196
272
  end
273
+
274
+ fact :error, id: 1
275
+ run
197
276
  end
198
277
 
199
- @engine.add_rule(@rule)
278
+ assert fired, "Rule should fire when error not acknowledged"
200
279
  end
201
280
 
202
- def test_fires_when_error_not_acknowledged
203
- @engine.add_fact(:error, { id: 1 })
204
- @engine.run
281
+ def test_does_not_fire_when_error_acknowledged
282
+ fired = false
205
283
 
206
- assert @fired, "Rule should fire when error not acknowledged"
207
- end
284
+ kb = KBS.knowledge_base do
285
+ rule "alert_if_no_acknowledgment" do
286
+ on :error, id: :id?
287
+ without :acknowledged, error_id: :id?
208
288
 
209
- def test_does_not_fire_when_error_acknowledged
210
- @engine.add_fact(:error, { id: 1 })
211
- @engine.add_fact(:acknowledged, { error_id: 1 })
212
- @engine.run
289
+ perform do |facts, bindings|
290
+ fired = true
291
+ end
292
+ end
213
293
 
214
- refute @fired, "Rule should not fire when error acknowledged"
294
+ fact :error, id: 1
295
+ fact :acknowledged, error_id: 1
296
+ run
297
+ end
298
+
299
+ refute fired, "Rule should not fire when error acknowledged"
215
300
  end
216
301
  end
217
302
  ```
@@ -222,54 +307,67 @@ end
222
307
 
223
308
  ```ruby
224
309
  class TestRuleInteractions < Minitest::Test
225
- def setup
226
- @engine = setup_engine
227
- @alerts = []
310
+ def test_cascading_rules
311
+ alerts = []
228
312
 
229
- # Rule 1: Detect high temperature
230
- @engine.add_rule(KBS::Rule.new("detect_high_temp") do |r|
231
- r.conditions = [
232
- KBS::Condition.new(:sensor, { value: :temp? }, predicate: lambda { |f| f[:value] > 30 })
233
- ]
313
+ kb = KBS.knowledge_base do
314
+ # Rule 1: Detect high temperature
315
+ rule "detect_high_temp" do
316
+ on :sensor, value: :temp?, predicate: greater_than(30)
234
317
 
235
- r.action = lambda do |facts, bindings|
236
- @engine.add_fact(:temp_alert, { severity: "high" })
318
+ perform do |facts, bindings|
319
+ fact :temp_alert, severity: "high"
320
+ end
237
321
  end
238
- end)
239
-
240
- # Rule 2: Escalate to critical
241
- @engine.add_rule(KBS::Rule.new("escalate_critical") do |r|
242
- r.conditions = [
243
- KBS::Condition.new(:temp_alert, { severity: "high" }),
244
- KBS::Condition.new(:sensor, { value: :temp? }, predicate: lambda { |f| f[:value] > 40 })
245
- ]
246
-
247
- r.action = lambda do |facts, bindings|
248
- @engine.add_fact(:critical_alert, { type: "temperature" })
249
- @alerts << :critical
322
+
323
+ # Rule 2: Escalate to critical
324
+ rule "escalate_critical" do
325
+ on :temp_alert, severity: "high"
326
+ on :sensor, value: :temp?, predicate: greater_than(40)
327
+
328
+ perform do |facts, bindings|
329
+ fact :critical_alert, type: "temperature"
330
+ alerts << :critical
331
+ end
250
332
  end
251
- end)
252
- end
253
333
 
254
- def test_cascading_rules
255
- # Add high temperature
256
- @engine.add_fact(:sensor, { value: 45 })
257
- @engine.run
334
+ # Add high temperature
335
+ fact :sensor, value: 45
336
+ run
337
+ end
258
338
 
259
339
  # Both rules should fire
260
- assert @engine.facts.any? { |f| f.type == :temp_alert }
261
- assert @engine.facts.any? { |f| f.type == :critical_alert }
262
- assert_includes @alerts, :critical
340
+ assert kb.engine.facts.any? { |f| f.type == :temp_alert }
341
+ assert kb.engine.facts.any? { |f| f.type == :critical_alert }
342
+ assert_includes alerts, :critical
263
343
  end
264
344
 
265
345
  def test_partial_cascade
266
- # Add moderately high temperature
267
- @engine.add_fact(:sensor, { value: 35 })
268
- @engine.run
346
+ alerts = []
347
+
348
+ kb = KBS.knowledge_base do
349
+ rule "detect_high_temp" do
350
+ on :sensor, value: :temp?, predicate: greater_than(30)
351
+ perform { fact :temp_alert, severity: "high" }
352
+ end
353
+
354
+ rule "escalate_critical" do
355
+ on :temp_alert, severity: "high"
356
+ on :sensor, value: :temp?, predicate: greater_than(40)
357
+ perform do |facts, bindings|
358
+ fact :critical_alert, type: "temperature"
359
+ alerts << :critical
360
+ end
361
+ end
362
+
363
+ # Add moderately high temperature
364
+ fact :sensor, value: 35
365
+ run
366
+ end
269
367
 
270
368
  # Only first rule fires
271
- assert @engine.facts.any? { |f| f.type == :temp_alert }
272
- refute @engine.facts.any? { |f| f.type == :critical_alert }
369
+ assert kb.engine.facts.any? { |f| f.type == :temp_alert }
370
+ refute kb.engine.facts.any? { |f| f.type == :critical_alert }
273
371
  end
274
372
  end
275
373
  ```
@@ -278,32 +376,27 @@ end
278
376
 
279
377
  ```ruby
280
378
  class TestRulePriority < Minitest::Test
281
- def setup
282
- @engine = setup_engine
283
- @execution_order = []
284
-
285
- # High priority rule
286
- @engine.add_rule(KBS::Rule.new("high_priority", priority: 100) do |r|
287
- r.conditions = [KBS::Condition.new(:trigger, {})]
288
- r.action = lambda do |facts, bindings|
289
- @execution_order << :high
379
+ def test_executes_in_priority_order
380
+ execution_order = []
381
+
382
+ kb = KBS.knowledge_base do
383
+ # High priority rule
384
+ rule "high_priority", priority: 100 do
385
+ on :trigger, {}
386
+ perform { execution_order << :high }
290
387
  end
291
- end)
292
388
 
293
- # Low priority rule
294
- @engine.add_rule(KBS::Rule.new("low_priority", priority: 10) do |r|
295
- r.conditions = [KBS::Condition.new(:trigger, {})]
296
- r.action = lambda do |facts, bindings|
297
- @execution_order << :low
389
+ # Low priority rule
390
+ rule "low_priority", priority: 10 do
391
+ on :trigger, {}
392
+ perform { execution_order << :low }
298
393
  end
299
- end)
300
- end
301
394
 
302
- def test_executes_in_priority_order
303
- @engine.add_fact(:trigger, {})
304
- @engine.run
395
+ fact :trigger, {}
396
+ run
397
+ end
305
398
 
306
- assert_equal [:high, :low], @execution_order
399
+ assert_equal [:high, :low], execution_order
307
400
  end
308
401
  end
309
402
  ```
@@ -336,9 +429,9 @@ module FactFixtures
336
429
  ]
337
430
  end
338
431
 
339
- def load_facts(engine, facts)
432
+ def load_facts_into_kb(kb, facts)
340
433
  facts.each do |fact_data|
341
- engine.add_fact(fact_data[:type], fact_data[:attributes])
434
+ kb.fact fact_data[:type], fact_data[:attributes]
342
435
  end
343
436
  end
344
437
  end
@@ -347,11 +440,15 @@ class TestWithFixtures < Minitest::Test
347
440
  include FactFixtures
348
441
 
349
442
  def test_with_high_temp_scenario
350
- engine = setup_engine
351
- # Add rules...
443
+ kb = KBS.knowledge_base do
444
+ rule "check_threshold" do
445
+ on :sensor, value: :v?, predicate: greater_than(30)
446
+ perform { }
447
+ end
448
+ end
352
449
 
353
- load_facts(engine, high_temp_scenario)
354
- engine.run
450
+ load_facts_into_kb(kb, high_temp_scenario)
451
+ kb.run
355
452
 
356
453
  # Assertions...
357
454
  end
@@ -362,26 +459,21 @@ end
362
459
 
363
460
  ```ruby
364
461
  module RuleFixtures
365
- def temperature_monitoring_rules
366
- [
367
- KBS::Rule.new("detect_high") do |r|
368
- r.conditions = [
369
- KBS::Condition.new(:sensor, { value: :v? }, predicate: lambda { |f| f[:value] > 30 })
370
- ]
371
- r.action = lambda { |facts, bindings| facts[0][:alerted] = true }
372
- end,
373
-
374
- KBS::Rule.new("detect_low") do |r|
375
- r.conditions = [
376
- KBS::Condition.new(:sensor, { value: :v? }, predicate: lambda { |f| f[:value] < 15 })
377
- ]
378
- r.action = lambda { |facts, bindings| facts[0][:alerted] = true }
462
+ # Note: Since DSL rules are defined in blocks,
463
+ # we provide factory methods instead of rule objects
464
+
465
+ def add_temperature_monitoring_rules(kb)
466
+ kb.instance_eval do
467
+ rule "detect_high" do
468
+ on :sensor, value: :v?, predicate: greater_than(30)
469
+ perform { |facts, bindings| facts[0][:alerted] = true }
379
470
  end
380
- ]
381
- end
382
471
 
383
- def load_rules(engine, rules)
384
- rules.each { |rule| engine.add_rule(rule) }
472
+ rule "detect_low" do
473
+ on :sensor, value: :v?, predicate: less_than(15)
474
+ perform { |facts, bindings| facts[0][:alerted] = true }
475
+ end
476
+ end
385
477
  end
386
478
  end
387
479
  ```
@@ -392,14 +484,13 @@ end
392
484
 
393
485
  ```ruby
394
486
  class CoverageTracker
395
- def initialize(engine)
396
- @engine = engine
487
+ def initialize(kb)
488
+ @kb = kb
397
489
  @rule_firings = Hash.new(0)
398
- @condition_matches = Hash.new(0)
399
490
  end
400
491
 
401
492
  def wrap_rules
402
- @engine.instance_variable_get(:@rules).each do |rule|
493
+ @kb.engine.instance_variable_get(:@rules).each do |rule|
403
494
  original_action = rule.action
404
495
 
405
496
  rule.action = lambda do |facts, bindings|
@@ -412,7 +503,7 @@ class CoverageTracker
412
503
  def report
413
504
  puts "\n=== Coverage Report ==="
414
505
 
415
- total_rules = @engine.instance_variable_get(:@rules).size
506
+ total_rules = @kb.engine.instance_variable_get(:@rules).size
416
507
  fired_rules = @rule_firings.keys.size
417
508
  coverage = (fired_rules.to_f / total_rules * 100).round(2)
418
509
 
@@ -423,32 +514,42 @@ class CoverageTracker
423
514
  puts " #{name}: #{count}"
424
515
  end
425
516
 
426
- untested = @engine.instance_variable_get(:@rules).map(&:name) - @rule_firings.keys
517
+ untested = @kb.engine.instance_variable_get(:@rules).map(&:name) - @rule_firings.keys
427
518
  if untested.any?
428
519
  puts "\nUntested Rules:"
429
520
  untested.each { |name| puts " - #{name}" }
430
521
  end
431
522
  end
432
523
 
433
- attr_reader :rule_firings, :condition_matches
524
+ attr_reader :rule_firings
434
525
  end
435
526
 
436
527
  # Usage
437
528
  class TestWithCoverage < Minitest::Test
438
529
  def test_coverage
439
- engine = setup_engine
440
- # Add rules...
530
+ kb = KBS.knowledge_base do
531
+ rule "rule1" do
532
+ on :fact, {}
533
+ perform { }
534
+ end
535
+
536
+ rule "rule2" do
537
+ on :other, {}
538
+ perform { }
539
+ end
540
+ end
441
541
 
442
- tracker = CoverageTracker.new(engine)
542
+ tracker = CoverageTracker.new(kb)
443
543
  tracker.wrap_rules
444
544
 
445
545
  # Add facts and run
446
- engine.run
546
+ kb.fact :fact, {}
547
+ kb.run
447
548
 
448
549
  tracker.report
449
550
 
450
551
  # Assert all rules fired
451
- assert_equal @engine.instance_variable_get(:@rules).size, tracker.rule_firings.size
552
+ # (or check specific coverage requirements)
452
553
  end
453
554
  end
454
555
  ```
@@ -457,41 +558,51 @@ end
457
558
 
458
559
  ```ruby
459
560
  def test_all_condition_paths
460
- engine = setup_engine
561
+ # Test path 1: All conditions pass
562
+ kb1 = KBS.knowledge_base do
563
+ rule "multi_path" do
564
+ on :a, {}
565
+ on :b, {}
566
+ without :c, {}
567
+ perform { }
568
+ end
461
569
 
462
- rule = KBS::Rule.new("multi_path") do |r|
463
- r.conditions = [
464
- KBS::Condition.new(:a, {}),
465
- KBS::Condition.new(:b, {}),
466
- KBS::Condition.new(:c, {}, negated: true)
467
- ]
468
- r.action = lambda { |facts, bindings| }
570
+ fact :a, {}
571
+ fact :b, {}
572
+ # c absent
573
+ run
469
574
  end
470
-
471
- engine.add_rule(rule)
472
-
473
- # Test path 1: All conditions pass
474
- engine.add_fact(:a, {})
475
- engine.add_fact(:b, {})
476
- # c absent
477
- engine.run
478
575
  # Assert...
479
576
 
480
577
  # Test path 2: Negation fails
481
- engine = setup_engine
482
- engine.add_rule(rule)
483
- engine.add_fact(:a, {})
484
- engine.add_fact(:b, {})
485
- engine.add_fact(:c, {}) # Blocks negation
486
- engine.run
578
+ kb2 = KBS.knowledge_base do
579
+ rule "multi_path" do
580
+ on :a, {}
581
+ on :b, {}
582
+ without :c, {}
583
+ perform { }
584
+ end
585
+
586
+ fact :a, {}
587
+ fact :b, {}
588
+ fact :c, {} # Blocks negation
589
+ run
590
+ end
487
591
  # Assert...
488
592
 
489
593
  # Test path 3: Positive condition missing
490
- engine = setup_engine
491
- engine.add_rule(rule)
492
- engine.add_fact(:a, {})
493
- # b missing
494
- engine.run
594
+ kb3 = KBS.knowledge_base do
595
+ rule "multi_path" do
596
+ on :a, {}
597
+ on :b, {}
598
+ without :c, {}
599
+ perform { }
600
+ end
601
+
602
+ fact :a, {}
603
+ # b missing
604
+ run
605
+ end
495
606
  # Assert...
496
607
  end
497
608
  ```
@@ -505,30 +616,27 @@ require 'benchmark'
505
616
 
506
617
  class PerformanceTest < Minitest::Test
507
618
  def test_rule_performance
508
- engine = setup_engine
509
-
510
- # Add rule
511
- engine.add_rule(KBS::Rule.new("perf_test") do |r|
512
- r.conditions = [
513
- KBS::Condition.new(:data, { value: :v? })
514
- ]
515
- r.action = lambda { |facts, bindings| }
516
- end)
517
-
518
- # Add many facts
519
- 1000.times { |i| engine.add_fact(:data, { value: i }) }
520
-
521
- # Benchmark
522
- time = Benchmark.measure { engine.run }
619
+ time = Benchmark.measure do
620
+ kb = KBS.knowledge_base do
621
+ rule "perf_test" do
622
+ on :data, value: :v?
623
+ perform { }
624
+ end
625
+
626
+ # Add many facts
627
+ 1000.times { |i| fact :data, value: i }
628
+ run
629
+ end
630
+ end
523
631
 
524
632
  assert time.real < 1.0, "Engine should complete in under 1 second"
525
633
  end
526
634
 
527
635
  def test_fact_addition_performance
528
- engine = setup_engine
636
+ kb = KBS.knowledge_base
529
637
 
530
638
  time = Benchmark.measure do
531
- 10_000.times { |i| engine.add_fact(:data, { value: i }) }
639
+ 10_000.times { |i| kb.fact :data, value: i }
532
640
  end
533
641
 
534
642
  rate = 10_000 / time.real
@@ -546,8 +654,10 @@ class TestBlackboardPersistence < Minitest::Test
546
654
  def test_facts_persist_across_sessions
547
655
  # Session 1: Add facts
548
656
  engine1 = KBS::Blackboard::Engine.new(db_path: 'test.db')
549
- engine1.add_fact(:sensor, { id: 1, value: 25 })
550
- engine1.close
657
+ kb1 = KBS.knowledge_base(engine: engine1) do
658
+ fact :sensor, id: 1, value: 25
659
+ end
660
+ kb1.close
551
661
 
552
662
  # Session 2: Load facts
553
663
  engine2 = KBS::Blackboard::Engine.new(db_path: 'test.db')
@@ -561,8 +671,8 @@ class TestBlackboardPersistence < Minitest::Test
561
671
  def test_audit_trail
562
672
  engine = KBS::Blackboard::Engine.new(db_path: ':memory:')
563
673
 
564
- fact = engine.add_fact(:data, { value: 1 })
565
- engine.update_fact(fact.id, { value: 2 })
674
+ fact = engine.add_fact(:data, value: 1)
675
+ engine.update_fact(fact.id, value: 2)
566
676
  engine.delete_fact(fact.id)
567
677
 
568
678
  history = engine.fact_history(fact.id)
@@ -581,13 +691,17 @@ end
581
691
 
582
692
  ```ruby
583
693
  def test_single_rule_only
584
- engine = setup_engine
585
-
586
- # Add ONLY the rule being tested
587
- engine.add_rule(my_test_rule)
694
+ kb = KBS.knowledge_base do
695
+ # Add ONLY the rule being tested
696
+ rule "my_test_rule" do
697
+ on :trigger, {}
698
+ perform { }
699
+ end
588
700
 
589
- # No other rules to interfere
590
- engine.run
701
+ # No other rules to interfere
702
+ fact :trigger, {}
703
+ run
704
+ end
591
705
  end
592
706
  ```
593
707
 
@@ -596,20 +710,44 @@ end
596
710
  ```ruby
597
711
  def test_edge_cases
598
712
  # Empty facts
599
- engine.run
600
- assert_empty engine.facts.select { |f| f.type == :alert }
713
+ kb = KBS.knowledge_base do
714
+ rule "check" do
715
+ on :sensor, value: :v?
716
+ perform { }
717
+ end
718
+ run
719
+ end
720
+ assert_empty kb.engine.facts.select { |f| f.type == :alert }
601
721
 
602
722
  # Exact threshold
603
- engine.add_fact(:sensor, { value: 30 })
604
- engine.run
723
+ kb = KBS.knowledge_base do
724
+ rule "check" do
725
+ on :sensor, value: :v?, predicate: greater_than(30)
726
+ perform { }
727
+ end
728
+ fact :sensor, value: 30
729
+ run
730
+ end
605
731
 
606
732
  # Just below threshold
607
- engine.add_fact(:sensor, { value: 29.99 })
608
- engine.run
733
+ kb = KBS.knowledge_base do
734
+ rule "check" do
735
+ on :sensor, value: :v?, predicate: greater_than(30)
736
+ perform { }
737
+ end
738
+ fact :sensor, value: 29.99
739
+ run
740
+ end
609
741
 
610
742
  # Just above threshold
611
- engine.add_fact(:sensor, { value: 30.01 })
612
- engine.run
743
+ kb = KBS.knowledge_base do
744
+ rule "check" do
745
+ on :sensor, value: :v?, predicate: greater_than(30)
746
+ perform { }
747
+ end
748
+ fact :sensor, value: 30.01
749
+ run
750
+ end
613
751
  end
614
752
  ```
615
753
 
@@ -617,20 +755,20 @@ end
617
755
 
618
756
  ```ruby
619
757
  def test_action_side_effects
620
- engine = setup_engine
621
758
  added_facts = []
622
759
 
623
- rule = KBS::Rule.new("test") do |r|
624
- r.conditions = [KBS::Condition.new(:trigger, {})]
625
- r.action = lambda do |facts, bindings|
626
- new_fact = engine.add_fact(:result, { value: 42 })
627
- added_facts << new_fact
760
+ kb = KBS.knowledge_base do
761
+ rule "test" do
762
+ on :trigger, {}
763
+ perform do |facts, bindings|
764
+ new_fact = fact :result, value: 42
765
+ added_facts << new_fact
766
+ end
628
767
  end
629
- end
630
768
 
631
- engine.add_rule(rule)
632
- engine.add_fact(:trigger, {})
633
- engine.run
769
+ fact :trigger, {}
770
+ run
771
+ end
634
772
 
635
773
  assert_equal 1, added_facts.size
636
774
  assert_equal 42, added_facts.first[:value]
@@ -654,12 +792,10 @@ end
654
792
  ```ruby
655
793
  class TestWithSetup < Minitest::Test
656
794
  def setup
657
- @engine = setup_engine
658
795
  @test_db = "test_#{SecureRandom.hex(8)}.db"
659
796
  end
660
797
 
661
798
  def teardown
662
- @engine.close if @engine.respond_to?(:close)
663
799
  File.delete(@test_db) if File.exist?(@test_db)
664
800
  end
665
801
  end
@@ -684,7 +820,7 @@ end
684
820
  - **[Debugging Guide](debugging.md)** - Debug failing tests
685
821
  - **[Performance Guide](performance.md)** - Optimize slow tests
686
822
  - **[Architecture](../architecture/index.md)** - Understand rule execution
687
- - **[Examples](../examples/stock-trading.md)** - See tested examples
823
+ - **[Examples](../examples/index.md)** - See tested examples
688
824
 
689
825
  ---
690
826