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,726 @@
|
|
|
1
|
+
# Debugging
|
|
2
|
+
|
|
3
|
+
Debug KBS applications using network visualization, token tracing, fact inspection, and rule execution logging. This guide provides tools and techniques to understand rule behavior and diagnose issues.
|
|
4
|
+
|
|
5
|
+
## Debugging Overview
|
|
6
|
+
|
|
7
|
+
Common debugging scenarios:
|
|
8
|
+
|
|
9
|
+
1. **Rules not firing** - Conditions don't match expected facts
|
|
10
|
+
2. **Unexpected rule firing** - Rules fire when they shouldn't
|
|
11
|
+
3. **Performance issues** - Slow rule execution
|
|
12
|
+
4. **Incorrect bindings** - Variables bound to wrong values
|
|
13
|
+
5. **Network structure** - Understanding compilation
|
|
14
|
+
|
|
15
|
+
## Enable Debug Output
|
|
16
|
+
|
|
17
|
+
### Basic Logging
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
require 'kbs'
|
|
21
|
+
|
|
22
|
+
engine = KBS::Engine.new
|
|
23
|
+
|
|
24
|
+
# Enable debug output
|
|
25
|
+
engine.instance_variable_set(:@debug, true)
|
|
26
|
+
|
|
27
|
+
# Or create debug wrapper
|
|
28
|
+
class DebugEngine < KBS::Engine
|
|
29
|
+
def add_fact(type, attributes = {})
|
|
30
|
+
fact = super
|
|
31
|
+
puts "[FACT ADDED] #{fact.type}: #{fact.attributes.inspect}"
|
|
32
|
+
fact
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def remove_fact(fact)
|
|
36
|
+
puts "[FACT REMOVED] #{fact.type}: #{fact.attributes.inspect}"
|
|
37
|
+
super
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Rule Execution Logging
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
class LoggingEngine < KBS::Engine
|
|
46
|
+
def initialize
|
|
47
|
+
super
|
|
48
|
+
@rule_log = []
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def run
|
|
52
|
+
puts "\n=== Engine Run Started ==="
|
|
53
|
+
puts "Facts: #{facts.size}"
|
|
54
|
+
puts "Rules: #{@rules.size}"
|
|
55
|
+
|
|
56
|
+
result = super
|
|
57
|
+
|
|
58
|
+
puts "\n=== Engine Run Completed ==="
|
|
59
|
+
puts "Rules fired: #{@rule_log.size}"
|
|
60
|
+
@rule_log.each_with_index do |entry, i|
|
|
61
|
+
puts " #{i + 1}. #{entry[:rule]} (#{entry[:timestamp]})"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
result
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
attr_reader :rule_log
|
|
68
|
+
end
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Fact Inspection
|
|
72
|
+
|
|
73
|
+
### Inspect Current Facts
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
def inspect_facts(engine)
|
|
77
|
+
puts "\n=== Current Facts ==="
|
|
78
|
+
|
|
79
|
+
# Group by type
|
|
80
|
+
facts_by_type = engine.facts.group_by(&:type)
|
|
81
|
+
|
|
82
|
+
facts_by_type.each do |type, facts|
|
|
83
|
+
puts "\n#{type} (#{facts.size}):"
|
|
84
|
+
facts.each_with_index do |fact, i|
|
|
85
|
+
puts " #{i + 1}. #{fact.attributes.inspect}"
|
|
86
|
+
if fact.is_a?(KBS::Blackboard::Fact)
|
|
87
|
+
puts " ID: #{fact.id}"
|
|
88
|
+
puts " Created: #{fact.created_at}"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
puts "\nTotal facts: #{engine.facts.size}"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Usage
|
|
97
|
+
inspect_facts(engine)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Query Fact History (Blackboard)
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
def inspect_fact_history(engine, fact_id)
|
|
104
|
+
return unless engine.is_a?(KBS::Blackboard::Engine)
|
|
105
|
+
|
|
106
|
+
puts "\n=== Fact History: #{fact_id} ==="
|
|
107
|
+
|
|
108
|
+
history = engine.fact_history(fact_id)
|
|
109
|
+
|
|
110
|
+
history.each do |entry|
|
|
111
|
+
puts "\n#{entry[:timestamp]}"
|
|
112
|
+
puts " Operation: #{entry[:operation]}"
|
|
113
|
+
puts " Attributes: #{entry[:attributes].inspect}"
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Find Facts by Criteria
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
def find_facts(engine, **criteria)
|
|
122
|
+
results = engine.facts.select do |fact|
|
|
123
|
+
criteria.all? do |key, value|
|
|
124
|
+
case key
|
|
125
|
+
when :type
|
|
126
|
+
fact.type == value
|
|
127
|
+
else
|
|
128
|
+
fact[key] == value
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
puts "\n=== Found #{results.size} facts ==="
|
|
134
|
+
results.each do |fact|
|
|
135
|
+
puts "#{fact.type}: #{fact.attributes.inspect}"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
results
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Usage
|
|
142
|
+
find_facts(engine, type: :sensor, location: "bedroom")
|
|
143
|
+
find_facts(engine, type: :alert, severity: "critical")
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Rule Debugging
|
|
147
|
+
|
|
148
|
+
### Trace Rule Execution
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
class RuleTracer
|
|
152
|
+
def initialize(engine)
|
|
153
|
+
@engine = engine
|
|
154
|
+
@traces = []
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def wrap_rules
|
|
158
|
+
@engine.instance_variable_get(:@rules).each do |rule|
|
|
159
|
+
wrap_rule(rule)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def wrap_rule(rule)
|
|
164
|
+
original_action = rule.action
|
|
165
|
+
|
|
166
|
+
rule.action = lambda do |facts, bindings|
|
|
167
|
+
trace = {
|
|
168
|
+
rule: rule.name,
|
|
169
|
+
timestamp: Time.now,
|
|
170
|
+
facts: facts.map { |f| { type: f.type, attrs: f.attributes } },
|
|
171
|
+
bindings: bindings.dup
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
puts "\n[RULE FIRING] #{rule.name}"
|
|
175
|
+
puts " Facts: #{facts.map(&:type).join(', ')}"
|
|
176
|
+
puts " Bindings: #{bindings.inspect}"
|
|
177
|
+
|
|
178
|
+
result = original_action.call(facts, bindings)
|
|
179
|
+
|
|
180
|
+
trace[:duration] = (Time.now - trace[:timestamp])
|
|
181
|
+
@traces << trace
|
|
182
|
+
|
|
183
|
+
puts " Duration: #{trace[:duration]}s"
|
|
184
|
+
|
|
185
|
+
result
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
attr_reader :traces
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Usage
|
|
193
|
+
tracer = RuleTracer.new(engine)
|
|
194
|
+
tracer.wrap_rules
|
|
195
|
+
engine.run
|
|
196
|
+
puts "\nTotal rule firings: #{tracer.traces.size}"
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Test Individual Conditions
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
def test_condition(engine, condition)
|
|
203
|
+
puts "\n=== Testing Condition ==="
|
|
204
|
+
puts "Type: #{condition.pattern[:type]}"
|
|
205
|
+
puts "Pattern: #{condition.pattern.inspect}"
|
|
206
|
+
|
|
207
|
+
# Find matching facts
|
|
208
|
+
matches = engine.facts.select do |fact|
|
|
209
|
+
fact.matches?(condition.pattern)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
puts "\nMatching facts: #{matches.size}"
|
|
213
|
+
matches.each do |fact|
|
|
214
|
+
puts " #{fact.attributes.inspect}"
|
|
215
|
+
|
|
216
|
+
# Test predicate if present
|
|
217
|
+
if condition.predicate
|
|
218
|
+
predicate_result = condition.predicate.call(fact)
|
|
219
|
+
puts " Predicate: #{predicate_result}"
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
matches
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Usage
|
|
227
|
+
condition = KBS::Condition.new(:sensor, {
|
|
228
|
+
type: "temperature",
|
|
229
|
+
value: :v?
|
|
230
|
+
}, predicate: lambda { |f| f[:value] > 25 })
|
|
231
|
+
|
|
232
|
+
test_condition(engine, condition)
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Why Did Rule Fire?
|
|
236
|
+
|
|
237
|
+
```ruby
|
|
238
|
+
def why_rule_fired(engine, rule_name)
|
|
239
|
+
rule = engine.instance_variable_get(:@rules).find { |r| r.name == rule_name }
|
|
240
|
+
|
|
241
|
+
return unless rule
|
|
242
|
+
|
|
243
|
+
puts "\n=== Why '#{rule_name}' Fired ==="
|
|
244
|
+
|
|
245
|
+
# Check each condition
|
|
246
|
+
rule.conditions.each_with_index do |condition, i|
|
|
247
|
+
puts "\nCondition #{i + 1}: #{condition.pattern[:type]}"
|
|
248
|
+
puts " Pattern: #{condition.pattern.inspect}"
|
|
249
|
+
puts " Negated: #{condition.negated?}"
|
|
250
|
+
|
|
251
|
+
matches = engine.facts.select { |f| f.matches?(condition.pattern) }
|
|
252
|
+
|
|
253
|
+
if condition.predicate
|
|
254
|
+
matches = matches.select { |f| condition.predicate.call(f) }
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
puts " Matches: #{matches.size} facts"
|
|
258
|
+
matches.each do |fact|
|
|
259
|
+
puts " - #{fact.attributes.inspect}"
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Why Didn't Rule Fire?
|
|
266
|
+
|
|
267
|
+
```ruby
|
|
268
|
+
def why_rule_didnt_fire(engine, rule_name)
|
|
269
|
+
rule = engine.instance_variable_get(:@rules).find { |r| r.name == rule_name }
|
|
270
|
+
|
|
271
|
+
return unless rule
|
|
272
|
+
|
|
273
|
+
puts "\n=== Why '#{rule_name}' Didn't Fire ==="
|
|
274
|
+
|
|
275
|
+
# Check each condition
|
|
276
|
+
failing_condition = nil
|
|
277
|
+
|
|
278
|
+
rule.conditions.each_with_index do |condition, i|
|
|
279
|
+
puts "\nCondition #{i + 1}: #{condition.pattern[:type]}"
|
|
280
|
+
|
|
281
|
+
matches = engine.facts.select { |f| f.matches?(condition.pattern) }
|
|
282
|
+
|
|
283
|
+
if condition.negated?
|
|
284
|
+
puts " Negated condition"
|
|
285
|
+
if matches.empty?
|
|
286
|
+
puts " ✓ PASSED (no matching facts)"
|
|
287
|
+
else
|
|
288
|
+
puts " ✗ FAILED (#{matches.size} matching facts found, but should be absent)"
|
|
289
|
+
failing_condition = i
|
|
290
|
+
matches.each do |fact|
|
|
291
|
+
puts " Blocking fact: #{fact.attributes.inspect}"
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
else
|
|
295
|
+
if matches.empty?
|
|
296
|
+
puts " ✗ FAILED (no matching facts)"
|
|
297
|
+
failing_condition = i
|
|
298
|
+
|
|
299
|
+
# Suggest similar facts
|
|
300
|
+
similar = engine.facts.select { |f| f.type == condition.pattern[:type] }
|
|
301
|
+
if similar.any?
|
|
302
|
+
puts " Similar facts (#{similar.size}):"
|
|
303
|
+
similar.first(3).each do |fact|
|
|
304
|
+
puts " - #{fact.attributes.inspect}"
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
else
|
|
308
|
+
# Check predicate
|
|
309
|
+
if condition.predicate
|
|
310
|
+
pred_matches = matches.select { |f| condition.predicate.call(f) }
|
|
311
|
+
if pred_matches.empty?
|
|
312
|
+
puts " ✗ FAILED (#{matches.size} facts match pattern, but predicate failed)"
|
|
313
|
+
failing_condition = i
|
|
314
|
+
matches.first(3).each do |fact|
|
|
315
|
+
puts " - #{fact.attributes.inspect} (predicate: false)"
|
|
316
|
+
end
|
|
317
|
+
else
|
|
318
|
+
puts " ✓ PASSED (#{pred_matches.size} facts)"
|
|
319
|
+
end
|
|
320
|
+
else
|
|
321
|
+
puts " ✓ PASSED (#{matches.size} facts)"
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
break if failing_condition
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
if failing_condition
|
|
330
|
+
puts "\n⚠️ Rule failed at condition #{failing_condition + 1}"
|
|
331
|
+
else
|
|
332
|
+
puts "\n✓ All conditions passed (rule should fire on next run)"
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# Usage
|
|
337
|
+
why_rule_didnt_fire(engine, "detect_high_temperature")
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
## Network Visualization
|
|
341
|
+
|
|
342
|
+
### Print Network Structure
|
|
343
|
+
|
|
344
|
+
```ruby
|
|
345
|
+
def visualize_network(engine)
|
|
346
|
+
puts "\n=== RETE Network Structure ==="
|
|
347
|
+
|
|
348
|
+
# Alpha network
|
|
349
|
+
puts "\nALPHA NETWORK:"
|
|
350
|
+
alpha_memories = []
|
|
351
|
+
|
|
352
|
+
engine.instance_eval do
|
|
353
|
+
@alpha_network.each do |pattern, memory|
|
|
354
|
+
puts " #{pattern.inspect}"
|
|
355
|
+
puts " Items: #{memory.items.size}"
|
|
356
|
+
alpha_memories << memory
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# Beta network
|
|
361
|
+
puts "\nBETA NETWORK:"
|
|
362
|
+
# Simplified - actual inspection depends on implementation
|
|
363
|
+
|
|
364
|
+
puts "\nSTATISTICS:"
|
|
365
|
+
puts " Alpha memories: #{alpha_memories.size}"
|
|
366
|
+
puts " Total facts: #{engine.facts.size}"
|
|
367
|
+
puts " Rules: #{engine.instance_variable_get(:@rules).size}"
|
|
368
|
+
end
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### Graphviz Export
|
|
372
|
+
|
|
373
|
+
```ruby
|
|
374
|
+
def export_to_graphviz(engine, filename = "network.dot")
|
|
375
|
+
File.open(filename, 'w') do |f|
|
|
376
|
+
f.puts "digraph RETE {"
|
|
377
|
+
f.puts " rankdir=TB;"
|
|
378
|
+
f.puts " node [shape=box];"
|
|
379
|
+
|
|
380
|
+
# Alpha nodes
|
|
381
|
+
f.puts "\n // Alpha Network"
|
|
382
|
+
engine.instance_eval do
|
|
383
|
+
@alpha_network.each_with_index do |(pattern, memory), i|
|
|
384
|
+
node_id = "alpha_#{i}"
|
|
385
|
+
label = "#{pattern[:type]}\\n#{memory.items.size} facts"
|
|
386
|
+
f.puts " #{node_id} [label=\"#{label}\", style=filled, fillcolor=lightblue];"
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# Production nodes
|
|
391
|
+
f.puts "\n // Production Nodes"
|
|
392
|
+
engine.instance_variable_get(:@rules).each_with_index do |rule, i|
|
|
393
|
+
node_id = "rule_#{i}"
|
|
394
|
+
label = "#{rule.name}\\n#{rule.priority}"
|
|
395
|
+
f.puts " #{node_id} [label=\"#{label}\", style=filled, fillcolor=lightgreen];"
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
# Edges (simplified)
|
|
399
|
+
# ...
|
|
400
|
+
|
|
401
|
+
f.puts "}"
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
puts "Network exported to #{filename}"
|
|
405
|
+
puts "Render with: dot -Tpng #{filename} -o network.png"
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# Usage
|
|
409
|
+
export_to_graphviz(engine)
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
## Token Tracing
|
|
413
|
+
|
|
414
|
+
### Trace Token Propagation
|
|
415
|
+
|
|
416
|
+
```ruby
|
|
417
|
+
class TokenTracer
|
|
418
|
+
def initialize
|
|
419
|
+
@trace = []
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def log_activation(node_type, node_id, token)
|
|
423
|
+
@trace << {
|
|
424
|
+
timestamp: Time.now,
|
|
425
|
+
node_type: node_type,
|
|
426
|
+
node_id: node_id,
|
|
427
|
+
token: token.inspect
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
puts "[#{node_type}] #{node_id}: #{token.inspect}"
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
def print_trace
|
|
434
|
+
puts "\n=== Token Trace ==="
|
|
435
|
+
@trace.each_with_index do |entry, i|
|
|
436
|
+
puts "\n#{i + 1}. [#{entry[:node_type]}] #{entry[:node_id]}"
|
|
437
|
+
puts " Time: #{entry[:timestamp]}"
|
|
438
|
+
puts " Token: #{entry[:token]}"
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
attr_reader :trace
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
# Usage: Instrument nodes
|
|
446
|
+
tracer = TokenTracer.new
|
|
447
|
+
|
|
448
|
+
# Wrap alpha activation
|
|
449
|
+
alpha_memory.define_singleton_method(:right_activate) do |fact|
|
|
450
|
+
tracer.log_activation("AlphaMemory", object_id, fact)
|
|
451
|
+
super(fact)
|
|
452
|
+
end
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
## Interactive Debugging
|
|
456
|
+
|
|
457
|
+
### Debug Console
|
|
458
|
+
|
|
459
|
+
```ruby
|
|
460
|
+
class DebugConsole
|
|
461
|
+
def initialize(engine)
|
|
462
|
+
@engine = engine
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def start
|
|
466
|
+
loop do
|
|
467
|
+
print "\nkbs> "
|
|
468
|
+
input = gets.chomp
|
|
469
|
+
|
|
470
|
+
break if input == "exit"
|
|
471
|
+
|
|
472
|
+
case input
|
|
473
|
+
when "facts"
|
|
474
|
+
inspect_facts(@engine)
|
|
475
|
+
when "rules"
|
|
476
|
+
list_rules
|
|
477
|
+
when "run"
|
|
478
|
+
@engine.run
|
|
479
|
+
puts "Engine ran successfully"
|
|
480
|
+
when /^add (\w+) (.+)$/
|
|
481
|
+
type = $1.to_sym
|
|
482
|
+
attrs = eval($2) # UNSAFE: eval user input (for demo only)
|
|
483
|
+
@engine.add_fact(type, attrs)
|
|
484
|
+
puts "Fact added"
|
|
485
|
+
when /^remove (\d+)$/
|
|
486
|
+
fact = @engine.facts[$1.to_i]
|
|
487
|
+
@engine.remove_fact(fact) if fact
|
|
488
|
+
puts "Fact removed"
|
|
489
|
+
when /^why (.+)$/
|
|
490
|
+
why_rule_didnt_fire(@engine, $1)
|
|
491
|
+
when "help"
|
|
492
|
+
print_help
|
|
493
|
+
else
|
|
494
|
+
puts "Unknown command: #{input}"
|
|
495
|
+
print_help
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
def list_rules
|
|
501
|
+
puts "\n=== Rules ==="
|
|
502
|
+
@engine.instance_variable_get(:@rules).each_with_index do |rule, i|
|
|
503
|
+
puts "#{i}. #{rule.name} (priority: #{rule.priority}, conditions: #{rule.conditions.size})"
|
|
504
|
+
end
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
def print_help
|
|
508
|
+
puts <<~HELP
|
|
509
|
+
|
|
510
|
+
Commands:
|
|
511
|
+
facts - List all facts
|
|
512
|
+
rules - List all rules
|
|
513
|
+
run - Run engine
|
|
514
|
+
add TYPE {ATTRS} - Add fact
|
|
515
|
+
remove INDEX - Remove fact
|
|
516
|
+
why RULE_NAME - Explain why rule didn't fire
|
|
517
|
+
exit - Exit console
|
|
518
|
+
help - Show this help
|
|
519
|
+
|
|
520
|
+
HELP
|
|
521
|
+
end
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
# Usage
|
|
525
|
+
console = DebugConsole.new(engine)
|
|
526
|
+
console.start
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
### Step-Through Debugger
|
|
530
|
+
|
|
531
|
+
```ruby
|
|
532
|
+
class StepDebugger
|
|
533
|
+
def initialize(engine)
|
|
534
|
+
@engine = engine
|
|
535
|
+
@breakpoints = []
|
|
536
|
+
@step_mode = false
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
def add_breakpoint(rule_name)
|
|
540
|
+
@breakpoints << rule_name
|
|
541
|
+
puts "Breakpoint added: #{rule_name}"
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
def enable_step_mode
|
|
545
|
+
@step_mode = true
|
|
546
|
+
|
|
547
|
+
@engine.instance_variable_get(:@rules).each do |rule|
|
|
548
|
+
wrap_rule_with_breakpoint(rule)
|
|
549
|
+
end
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
def wrap_rule_with_breakpoint(rule)
|
|
553
|
+
original_action = rule.action
|
|
554
|
+
|
|
555
|
+
rule.action = lambda do |facts, bindings|
|
|
556
|
+
if @breakpoints.include?(rule.name) || @step_mode
|
|
557
|
+
puts "\n🔴 BREAKPOINT: #{rule.name}"
|
|
558
|
+
puts "Facts: #{facts.map { |f| { type: f.type, attrs: f.attributes } }}"
|
|
559
|
+
puts "Bindings: #{bindings.inspect}"
|
|
560
|
+
|
|
561
|
+
print "Continue? [y/n/i(nspect)] "
|
|
562
|
+
response = gets.chomp
|
|
563
|
+
|
|
564
|
+
case response
|
|
565
|
+
when 'n'
|
|
566
|
+
puts "Skipping rule"
|
|
567
|
+
return
|
|
568
|
+
when 'i'
|
|
569
|
+
inspect_rule_context(facts, bindings)
|
|
570
|
+
end
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
original_action.call(facts, bindings)
|
|
574
|
+
end
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
def inspect_rule_context(facts, bindings)
|
|
578
|
+
puts "\n=== Rule Context ==="
|
|
579
|
+
puts "Facts (#{facts.size}):"
|
|
580
|
+
facts.each_with_index do |fact, i|
|
|
581
|
+
puts " #{i}. #{fact.type}: #{fact.attributes.inspect}"
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
puts "\nBindings:"
|
|
585
|
+
bindings.each do |var, value|
|
|
586
|
+
puts " #{var} => #{value.inspect}"
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
print "\nPress Enter to continue..."
|
|
590
|
+
gets
|
|
591
|
+
end
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
# Usage
|
|
595
|
+
debugger = StepDebugger.new(engine)
|
|
596
|
+
debugger.add_breakpoint("high_temperature_alert")
|
|
597
|
+
debugger.enable_step_mode
|
|
598
|
+
engine.run
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
## Common Debugging Patterns
|
|
602
|
+
|
|
603
|
+
### Verify Pattern Matching
|
|
604
|
+
|
|
605
|
+
```ruby
|
|
606
|
+
def verify_pattern_match(fact, pattern)
|
|
607
|
+
puts "\n=== Pattern Match Verification ==="
|
|
608
|
+
puts "Fact: #{fact.attributes.inspect}"
|
|
609
|
+
puts "Pattern: #{pattern.inspect}"
|
|
610
|
+
|
|
611
|
+
result = fact.matches?(pattern)
|
|
612
|
+
puts "Result: #{result}"
|
|
613
|
+
|
|
614
|
+
# Detail each attribute
|
|
615
|
+
pattern.each do |key, expected|
|
|
616
|
+
next if key == :type
|
|
617
|
+
|
|
618
|
+
actual = fact[key]
|
|
619
|
+
match = (expected == actual || expected.is_a?(Symbol))
|
|
620
|
+
|
|
621
|
+
puts "\n #{key}:"
|
|
622
|
+
puts " Expected: #{expected.inspect}"
|
|
623
|
+
puts " Actual: #{actual.inspect}"
|
|
624
|
+
puts " Match: #{match ? '✓' : '✗'}"
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
result
|
|
628
|
+
end
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
### Diagnose Join Issues
|
|
632
|
+
|
|
633
|
+
```ruby
|
|
634
|
+
def diagnose_join(engine, condition1, condition2)
|
|
635
|
+
puts "\n=== Join Diagnosis ==="
|
|
636
|
+
|
|
637
|
+
# Find matches for each condition
|
|
638
|
+
matches1 = engine.facts.select { |f| f.matches?(condition1.pattern) }
|
|
639
|
+
matches2 = engine.facts.select { |f| f.matches?(condition2.pattern) }
|
|
640
|
+
|
|
641
|
+
puts "\nCondition 1 matches: #{matches1.size}"
|
|
642
|
+
matches1.first(3).each { |f| puts " - #{f.attributes.inspect}" }
|
|
643
|
+
|
|
644
|
+
puts "\nCondition 2 matches: #{matches2.size}"
|
|
645
|
+
matches2.first(3).each { |f| puts " - #{f.attributes.inspect}" }
|
|
646
|
+
|
|
647
|
+
# Find join variables
|
|
648
|
+
vars1 = condition1.pattern.values.select { |v| v.is_a?(Symbol) && v.to_s.start_with?('?') }
|
|
649
|
+
vars2 = condition2.pattern.values.select { |v| v.is_a?(Symbol) && v.to_s.start_with?('?') }
|
|
650
|
+
join_vars = vars1 & vars2
|
|
651
|
+
|
|
652
|
+
puts "\nJoin variables: #{join_vars.inspect}"
|
|
653
|
+
|
|
654
|
+
if join_vars.empty?
|
|
655
|
+
puts "⚠️ No shared variables - conditions are independent"
|
|
656
|
+
else
|
|
657
|
+
# Check if any combinations match
|
|
658
|
+
combinations = 0
|
|
659
|
+
matches1.each do |f1|
|
|
660
|
+
matches2.each do |f2|
|
|
661
|
+
# Extract bindings
|
|
662
|
+
bindings1 = extract_bindings(f1, condition1.pattern)
|
|
663
|
+
bindings2 = extract_bindings(f2, condition2.pattern)
|
|
664
|
+
|
|
665
|
+
# Check join
|
|
666
|
+
if join_vars.all? { |v| bindings1[v] == bindings2[v] }
|
|
667
|
+
combinations += 1
|
|
668
|
+
end
|
|
669
|
+
end
|
|
670
|
+
end
|
|
671
|
+
|
|
672
|
+
puts "Valid combinations: #{combinations}"
|
|
673
|
+
end
|
|
674
|
+
end
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
### Track Memory Usage
|
|
678
|
+
|
|
679
|
+
```ruby
|
|
680
|
+
require 'objspace'
|
|
681
|
+
|
|
682
|
+
def track_memory_usage(engine)
|
|
683
|
+
puts "\n=== Memory Usage ==="
|
|
684
|
+
|
|
685
|
+
# Facts
|
|
686
|
+
fact_size = engine.facts.sum { |f| ObjectSpace.memsize_of(f) }
|
|
687
|
+
puts "Facts: #{(fact_size / 1024.0).round(2)} KB (#{engine.facts.size} facts)"
|
|
688
|
+
|
|
689
|
+
# Alpha memories
|
|
690
|
+
alpha_size = 0
|
|
691
|
+
engine.instance_eval do
|
|
692
|
+
@alpha_network.each do |_, memory|
|
|
693
|
+
alpha_size += ObjectSpace.memsize_of(memory)
|
|
694
|
+
alpha_size += memory.items.sum { |f| ObjectSpace.memsize_of(f) }
|
|
695
|
+
end
|
|
696
|
+
end
|
|
697
|
+
puts "Alpha network: #{(alpha_size / 1024.0).round(2)} KB"
|
|
698
|
+
|
|
699
|
+
total = fact_size + alpha_size
|
|
700
|
+
puts "\nTotal: #{(total / 1024.0).round(2)} KB"
|
|
701
|
+
end
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
## Debugging Checklist
|
|
705
|
+
|
|
706
|
+
- [ ] Verify facts are added with correct types and attributes
|
|
707
|
+
- [ ] Check condition patterns match fact structure
|
|
708
|
+
- [ ] Test predicates independently
|
|
709
|
+
- [ ] Ensure variables are bound correctly across conditions
|
|
710
|
+
- [ ] Check negated conditions for blocking facts
|
|
711
|
+
- [ ] Verify rule priorities
|
|
712
|
+
- [ ] Inspect network structure
|
|
713
|
+
- [ ] Trace rule execution
|
|
714
|
+
- [ ] Monitor memory usage
|
|
715
|
+
- [ ] Check for infinite loops
|
|
716
|
+
|
|
717
|
+
## Next Steps
|
|
718
|
+
|
|
719
|
+
- **[Testing Guide](testing.md)** - Write tests to prevent bugs
|
|
720
|
+
- **[Performance Guide](performance.md)** - Debug performance issues
|
|
721
|
+
- **[Architecture](../architecture/index.md)** - Understand network internals
|
|
722
|
+
- **[API Reference](../api/engine.md)** - Engine API documentation
|
|
723
|
+
|
|
724
|
+
---
|
|
725
|
+
|
|
726
|
+
*Good debugging is about asking the right questions. Use these tools to understand what your rules are doing.*
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Advanced Topics
|
|
2
|
+
|
|
3
|
+
techniques and optimizations.
|
|
4
|
+
|
|
5
|
+
- **[Performance Tuning](performance.md)** - Optimize for production
|
|
6
|
+
- **[Custom Persistence](custom-persistence.md)** - Build your own backend
|
|
7
|
+
- **[Debugging](debugging.md)** - Network inspection and tracing
|
|
8
|
+
- **[Testing Rules](testing.md)** - Test strategies for rule-based systems
|