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