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.
- checksums.yaml +4 -4
- data/README.md +85 -57
- data/docs/advanced/performance.md +109 -76
- data/docs/advanced/testing.md +399 -263
- data/docs/api/blackboard.md +1 -1
- data/docs/api/engine.md +77 -8
- data/docs/api/facts.md +3 -3
- data/docs/api/rules.md +110 -40
- data/docs/architecture/blackboard.md +108 -117
- data/docs/assets/images/fact-rule-relationship.svg +65 -0
- data/docs/assets/images/fact-structure.svg +42 -0
- data/docs/assets/images/inference-cycle.svg +47 -0
- data/docs/assets/images/kb-components.svg +43 -0
- data/docs/assets/images/rule-structure.svg +44 -0
- data/docs/assets/images/trading-signal-network.svg +1 -1
- data/docs/examples/index.md +219 -5
- data/docs/guides/blackboard-memory.md +89 -58
- data/docs/guides/dsl.md +24 -24
- data/docs/guides/getting-started.md +109 -107
- data/docs/guides/writing-rules.md +470 -311
- data/docs/index.md +16 -18
- data/docs/quick-start.md +92 -99
- data/docs/what-is-a-fact.md +694 -0
- data/docs/what-is-a-knowledge-base.md +350 -0
- data/docs/what-is-a-rule.md +833 -0
- data/examples/.gitignore +1 -0
- data/examples/advanced_example_dsl.rb +1 -1
- data/examples/ai_enhanced_kbs_dsl.rb +1 -1
- data/examples/car_diagnostic_dsl.rb +1 -1
- data/examples/concurrent_inference_demo.rb +0 -1
- data/examples/concurrent_inference_demo_dsl.rb +0 -1
- data/examples/csv_trading_system_dsl.rb +1 -1
- data/examples/iot_demo_using_dsl.rb +1 -1
- data/examples/portfolio_rebalancing_system_dsl.rb +1 -1
- data/examples/rule_source_demo.rb +123 -0
- data/examples/stock_trading_advanced_dsl.rb +1 -1
- data/examples/temp_dsl.txt +6214 -5269
- data/examples/timestamped_trading_dsl.rb +1 -1
- data/examples/trading_demo_dsl.rb +1 -1
- data/examples/working_demo_dsl.rb +1 -1
- data/lib/kbs/decompiler.rb +204 -0
- data/lib/kbs/dsl/knowledge_base.rb +100 -1
- data/lib/kbs/dsl.rb +3 -1
- data/lib/kbs/engine.rb +41 -0
- data/lib/kbs/version.rb +1 -1
- data/lib/kbs.rb +14 -12
- data/mkdocs.yml +30 -30
- metadata +15 -10
- data/docs/DOCUMENTATION_STATUS.md +0 -158
- data/docs/examples/expert-systems.md +0 -1031
- data/docs/examples/multi-agent.md +0 -1335
- data/docs/examples/stock-trading.md +0 -488
- data/examples/knowledge_base.db +0 -0
- data/examples/temp.txt +0 -7693
data/docs/advanced/testing.md
CHANGED
|
@@ -35,16 +35,12 @@ require 'minitest/autorun'
|
|
|
35
35
|
require 'kbs'
|
|
36
36
|
|
|
37
37
|
class Minitest::Test
|
|
38
|
-
def
|
|
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(
|
|
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
|
|
62
|
-
|
|
63
|
-
@fired = false
|
|
57
|
+
def test_fires_when_temperature_high
|
|
58
|
+
fired = false
|
|
64
59
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
75
|
+
fact :sensor, type: "temperature", value: 35
|
|
76
|
+
run
|
|
77
|
+
end
|
|
89
78
|
|
|
90
|
-
assert
|
|
79
|
+
assert fired, "Rule should fire for high temperature"
|
|
91
80
|
|
|
92
|
-
alerts =
|
|
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
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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
|
|
122
|
-
|
|
123
|
-
@fired = false
|
|
143
|
+
def test_fires_when_both_conditions_met
|
|
144
|
+
fired = false
|
|
124
145
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
150
|
+
value: :temp?,
|
|
151
|
+
predicate: greater_than(30)
|
|
131
152
|
|
|
132
|
-
|
|
153
|
+
on :humidity,
|
|
133
154
|
location: :loc?,
|
|
134
|
-
value: :hum
|
|
135
|
-
|
|
136
|
-
]
|
|
155
|
+
value: :hum?,
|
|
156
|
+
predicate: less_than(40)
|
|
137
157
|
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
168
|
+
assert fired, "Rule should fire when both conditions met"
|
|
144
169
|
end
|
|
145
170
|
|
|
146
|
-
def
|
|
147
|
-
|
|
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
|
-
|
|
152
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
|
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
|
|
185
|
-
|
|
186
|
-
@fired = false
|
|
261
|
+
def test_fires_when_error_not_acknowledged
|
|
262
|
+
fired = false
|
|
187
263
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
195
|
-
|
|
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
|
-
|
|
278
|
+
assert fired, "Rule should fire when error not acknowledged"
|
|
200
279
|
end
|
|
201
280
|
|
|
202
|
-
def
|
|
203
|
-
|
|
204
|
-
@engine.run
|
|
281
|
+
def test_does_not_fire_when_error_acknowledged
|
|
282
|
+
fired = false
|
|
205
283
|
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
289
|
+
perform do |facts, bindings|
|
|
290
|
+
fired = true
|
|
291
|
+
end
|
|
292
|
+
end
|
|
213
293
|
|
|
214
|
-
|
|
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
|
|
226
|
-
|
|
227
|
-
@alerts = []
|
|
310
|
+
def test_cascading_rules
|
|
311
|
+
alerts = []
|
|
228
312
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
236
|
-
|
|
318
|
+
perform do |facts, bindings|
|
|
319
|
+
fact :temp_alert, severity: "high"
|
|
320
|
+
end
|
|
237
321
|
end
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
334
|
+
# Add high temperature
|
|
335
|
+
fact :sensor, value: 45
|
|
336
|
+
run
|
|
337
|
+
end
|
|
258
338
|
|
|
259
339
|
# Both rules should fire
|
|
260
|
-
assert
|
|
261
|
-
assert
|
|
262
|
-
assert_includes
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
|
272
|
-
refute
|
|
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
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
395
|
+
fact :trigger, {}
|
|
396
|
+
run
|
|
397
|
+
end
|
|
305
398
|
|
|
306
|
-
assert_equal [:high, :low],
|
|
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
|
|
432
|
+
def load_facts_into_kb(kb, facts)
|
|
340
433
|
facts.each do |fact_data|
|
|
341
|
-
|
|
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
|
-
|
|
351
|
-
|
|
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
|
-
|
|
354
|
-
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
-
|
|
384
|
-
|
|
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(
|
|
396
|
-
@
|
|
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
|
|
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
|
-
|
|
440
|
-
|
|
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(
|
|
542
|
+
tracker = CoverageTracker.new(kb)
|
|
443
543
|
tracker.wrap_rules
|
|
444
544
|
|
|
445
545
|
# Add facts and run
|
|
446
|
-
|
|
546
|
+
kb.fact :fact, {}
|
|
547
|
+
kb.run
|
|
447
548
|
|
|
448
549
|
tracker.report
|
|
449
550
|
|
|
450
551
|
# Assert all rules fired
|
|
451
|
-
|
|
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
|
-
|
|
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
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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
|
-
|
|
636
|
+
kb = KBS.knowledge_base
|
|
529
637
|
|
|
530
638
|
time = Benchmark.measure do
|
|
531
|
-
10_000.times { |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
|
-
|
|
550
|
-
|
|
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,
|
|
565
|
-
engine.update_fact(fact.id,
|
|
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
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
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
|
-
|
|
590
|
-
|
|
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
|
-
|
|
600
|
-
|
|
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
|
-
|
|
604
|
-
|
|
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
|
-
|
|
608
|
-
|
|
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
|
-
|
|
612
|
-
|
|
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
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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/
|
|
823
|
+
- **[Examples](../examples/index.md)** - See tested examples
|
|
688
824
|
|
|
689
825
|
---
|
|
690
826
|
|