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.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/deploy-github-pages.yml +52 -0
  3. data/CHANGELOG.md +68 -2
  4. data/README.md +235 -334
  5. data/docs/DOCUMENTATION_STATUS.md +158 -0
  6. data/docs/advanced/custom-persistence.md +775 -0
  7. data/docs/advanced/debugging.md +726 -0
  8. data/docs/advanced/index.md +8 -0
  9. data/docs/advanced/performance.md +832 -0
  10. data/docs/advanced/testing.md +691 -0
  11. data/docs/api/blackboard.md +1157 -0
  12. data/docs/api/engine.md +978 -0
  13. data/docs/api/facts.md +1212 -0
  14. data/docs/api/index.md +12 -0
  15. data/docs/api/rules.md +1034 -0
  16. data/docs/architecture/blackboard.md +553 -0
  17. data/docs/architecture/index.md +277 -0
  18. data/docs/architecture/network-structure.md +343 -0
  19. data/docs/architecture/rete-algorithm.md +737 -0
  20. data/docs/assets/css/custom.css +83 -0
  21. data/docs/assets/images/blackboard-architecture.svg +136 -0
  22. data/docs/assets/images/compiled-network.svg +101 -0
  23. data/docs/assets/images/fact-assertion-flow.svg +117 -0
  24. data/docs/assets/images/kbs.jpg +0 -0
  25. data/docs/assets/images/pattern-matching-trace.svg +136 -0
  26. data/docs/assets/images/rete-network-layers.svg +96 -0
  27. data/docs/assets/images/system-layers.svg +69 -0
  28. data/docs/assets/images/trading-signal-network.svg +139 -0
  29. data/docs/assets/js/mathjax.js +17 -0
  30. data/docs/examples/expert-systems.md +1031 -0
  31. data/docs/examples/index.md +9 -0
  32. data/docs/examples/multi-agent.md +1335 -0
  33. data/docs/examples/stock-trading.md +488 -0
  34. data/docs/guides/blackboard-memory.md +558 -0
  35. data/docs/guides/dsl.md +1321 -0
  36. data/docs/guides/facts.md +652 -0
  37. data/docs/guides/getting-started.md +383 -0
  38. data/docs/guides/index.md +23 -0
  39. data/docs/guides/negation.md +529 -0
  40. data/docs/guides/pattern-matching.md +561 -0
  41. data/docs/guides/persistence.md +451 -0
  42. data/docs/guides/variable-binding.md +491 -0
  43. data/docs/guides/writing-rules.md +755 -0
  44. data/docs/index.md +157 -0
  45. data/docs/installation.md +156 -0
  46. data/docs/quick-start.md +228 -0
  47. data/examples/README.md +2 -2
  48. data/examples/advanced_example.rb +2 -2
  49. data/examples/advanced_example_dsl.rb +224 -0
  50. data/examples/ai_enhanced_kbs.rb +1 -1
  51. data/examples/ai_enhanced_kbs_dsl.rb +538 -0
  52. data/examples/blackboard_demo_dsl.rb +50 -0
  53. data/examples/car_diagnostic.rb +1 -1
  54. data/examples/car_diagnostic_dsl.rb +54 -0
  55. data/examples/concurrent_inference_demo.rb +5 -5
  56. data/examples/concurrent_inference_demo_dsl.rb +363 -0
  57. data/examples/csv_trading_system.rb +1 -1
  58. data/examples/csv_trading_system_dsl.rb +525 -0
  59. data/examples/knowledge_base.db +0 -0
  60. data/examples/portfolio_rebalancing_system.rb +2 -2
  61. data/examples/portfolio_rebalancing_system_dsl.rb +613 -0
  62. data/examples/redis_trading_demo_dsl.rb +177 -0
  63. data/examples/run_all.rb +50 -0
  64. data/examples/run_all_dsl.rb +49 -0
  65. data/examples/stock_trading_advanced.rb +1 -1
  66. data/examples/stock_trading_advanced_dsl.rb +404 -0
  67. data/examples/temp.txt +7693 -0
  68. data/examples/temp_dsl.txt +8447 -0
  69. data/examples/timestamped_trading.rb +1 -1
  70. data/examples/timestamped_trading_dsl.rb +258 -0
  71. data/examples/trading_demo.rb +1 -1
  72. data/examples/trading_demo_dsl.rb +322 -0
  73. data/examples/working_demo.rb +1 -1
  74. data/examples/working_demo_dsl.rb +160 -0
  75. data/lib/kbs/blackboard/engine.rb +3 -3
  76. data/lib/kbs/blackboard/fact.rb +1 -1
  77. data/lib/kbs/condition.rb +1 -1
  78. data/lib/kbs/dsl/knowledge_base.rb +1 -1
  79. data/lib/kbs/dsl/variable.rb +1 -1
  80. data/lib/kbs/{rete_engine.rb → engine.rb} +1 -1
  81. data/lib/kbs/fact.rb +1 -1
  82. data/lib/kbs/version.rb +1 -1
  83. data/lib/kbs.rb +2 -2
  84. data/mkdocs.yml +181 -0
  85. metadata +66 -6
  86. data/examples/stock_trading_system.rb.bak +0 -563
@@ -0,0 +1,737 @@
1
+ # The RETE Algorithm in KBS
2
+
3
+ ## Overview
4
+
5
+ The RETE algorithm is a pattern matching algorithm for implementing production rule systems. Developed by Charles Forgy in 1979, RETE (Latin for "network") creates a discrimination network that efficiently matches rules against a working memory of facts. KBS implements the RETE algorithm with the critical **unlinking optimization** for improved performance.
6
+
7
+ ## Why RETE?
8
+
9
+ Traditional rule engines evaluate all rules against all facts on every cycle, resulting in O(R × F) complexity where R is the number of rules and F is the number of facts. RETE achieves near-constant time per working memory change by:
10
+
11
+ 1. **Sharing common patterns** across rules in a compiled network
12
+ 2. **Maintaining state** between cycles (incremental matching)
13
+ 3. **Processing only changes** rather than re-evaluating everything
14
+ 4. **Unlinking empty nodes** to skip unnecessary computation (RETE optimization)
15
+
16
+ ## Core Concepts
17
+
18
+ ### Facts
19
+
20
+ Facts are the fundamental units of knowledge in the system. Each fact has:
21
+
22
+ - **Type**: A symbol identifying the kind of fact (e.g., `:stock`, `:alert`, `:order`)
23
+ - **Attributes**: Key-value pairs containing the fact's data
24
+ - **ID**: A unique identifier (object_id for transient facts, UUID for persisted facts)
25
+
26
+ ```ruby
27
+ # Creating a fact
28
+ fact = engine.add_fact(:stock, symbol: "AAPL", price: 150.0, volume: 1000000)
29
+
30
+ # Fact structure
31
+ # => stock(symbol: AAPL, price: 150.0, volume: 1000000)
32
+ ```
33
+
34
+ **Implementation**: `lib/kbs/fact.rb:4`
35
+
36
+ ### Working Memory
37
+
38
+ Working memory is the collection of all facts currently known to the system. It implements the **Observer pattern** to notify the RETE network when facts are added or removed.
39
+
40
+ ```ruby
41
+ class WorkingMemory
42
+ def add_fact(fact)
43
+ @facts << fact
44
+ notify_observers(:add, fact) # Triggers RETE propagation
45
+ end
46
+
47
+ def remove_fact(fact)
48
+ @facts.delete(fact)
49
+ notify_observers(:remove, fact) # Triggers retraction
50
+ end
51
+ end
52
+ ```
53
+
54
+ **Implementation**: `lib/kbs/working_memory.rb:4`
55
+
56
+ ### Conditions and Patterns
57
+
58
+ A condition specifies a pattern that facts must match. Patterns can include:
59
+
60
+ - **Type matching**: `{ type: :stock }`
61
+ - **Literal values**: `{ symbol: "AAPL" }`
62
+ - **Variable bindings**: `{ price: :price? }` (variables start with `?`)
63
+ - **Predicates**: `{ price: ->(p) { p > 100 } }`
64
+ - **Negation**: `negated: true` (match when pattern is absent)
65
+
66
+ ```ruby
67
+ # Match any stock with symbol "AAPL"
68
+ Condition.new(:stock, { symbol: "AAPL" })
69
+
70
+ # Match stock and bind price to ?price variable
71
+ Condition.new(:stock, { symbol: "AAPL", price: :price? })
72
+
73
+ # Match when there is NO alert for "AAPL"
74
+ Condition.new(:alert, { symbol: "AAPL" }, negated: true)
75
+ ```
76
+
77
+ **Implementation**: `lib/kbs/condition.rb:4`
78
+
79
+ ### Rules
80
+
81
+ Rules are production rules consisting of:
82
+
83
+ - **Conditions** (IF part): Patterns to match in working memory
84
+ - **Action** (THEN part): Code to execute when all conditions match
85
+ - **Priority**: Optional integer for conflict resolution (higher fires first)
86
+
87
+ ```ruby
88
+ rule = Rule.new("high_price_alert") do |r|
89
+ r.conditions = [
90
+ Condition.new(:stock, { symbol: :symbol?, price: :price? }),
91
+ Condition.new(:threshold, { symbol: :symbol?, max: :max? })
92
+ ]
93
+
94
+ r.action = lambda do |facts, bindings|
95
+ if bindings[:price?] > bindings[:max?]
96
+ puts "Alert: #{bindings[:symbol?]} at #{bindings[:price?]}"
97
+ end
98
+ end
99
+ end
100
+ ```
101
+
102
+ **Implementation**: `lib/kbs/rule.rb:4`
103
+
104
+ ### Tokens
105
+
106
+ Tokens represent **partial matches** as they flow through the RETE network. A token is a linked list of facts that have matched conditions so far.
107
+
108
+ ```ruby
109
+ class Token
110
+ attr_accessor :parent, :fact, :node, :children
111
+
112
+ # Reconstruct the full chain of matched facts
113
+ def facts
114
+ facts = []
115
+ token = self
116
+ while token
117
+ facts.unshift(token.fact) if token.fact
118
+ token = token.parent
119
+ end
120
+ facts
121
+ end
122
+ end
123
+ ```
124
+
125
+ **Key insights**:
126
+ - The **root token** has `parent = nil`, `fact = nil` and represents "no conditions matched yet"
127
+ - Each join creates a **new token** linking to its parent token plus a new fact
128
+ - Tokens form a **tree structure** via the `children` array, enabling efficient retraction
129
+
130
+ **Implementation**: `lib/kbs/token.rb:4`
131
+
132
+ ## Network Architecture
133
+
134
+ The RETE network is a **directed acyclic graph (DAG)** consisting of three layers:
135
+
136
+ ![RETE Network Layers](../assets/images/rete-network-layers.svg)
137
+
138
+ *The three-layer RETE network architecture showing alpha memories (pattern matching), beta network (join processing), and production nodes (rule firing).*
139
+
140
+ ### Layer 1: Alpha Network
141
+
142
+ The **alpha network** performs **intra-condition** tests - matching individual facts against patterns. Each `AlphaMemory` node:
143
+
144
+ - Stores facts matching a specific pattern
145
+ - Is shared across all rules using the same pattern (network sharing)
146
+ - Propagates matches to successor join nodes
147
+
148
+ ```ruby
149
+ class AlphaMemory
150
+ attr_accessor :items, :successors, :pattern
151
+
152
+ def activate(fact)
153
+ return unless @linked
154
+ @items << fact
155
+ @successors.each { |s| s.right_activate(fact) }
156
+ end
157
+ end
158
+ ```
159
+
160
+ **Example**: If three rules all match `stock(symbol: "AAPL")`, they share one `AlphaMemory` node for that pattern.
161
+
162
+ **Implementation**: `lib/kbs/alpha_memory.rb:4`
163
+
164
+ ### Layer 2: Beta Network
165
+
166
+ The **beta network** performs **inter-condition** tests - joining facts from different conditions. It consists of:
167
+
168
+ #### Join Nodes
169
+
170
+ `JoinNode` combines tokens from **beta memory** (left input) with facts from **alpha memory** (right input):
171
+
172
+ ```ruby
173
+ class JoinNode
174
+ def left_activate(token)
175
+ return unless @left_linked && @right_linked
176
+
177
+ @alpha_memory.items.each do |fact|
178
+ if perform_join_tests(token, fact)
179
+ new_token = Token.new(token, fact, self)
180
+ @successors.each { |s| s.activate(new_token) }
181
+ end
182
+ end
183
+ end
184
+
185
+ def right_activate(fact)
186
+ return unless @left_linked && @right_linked
187
+
188
+ @beta_memory.tokens.each do |token|
189
+ if perform_join_tests(token, fact)
190
+ new_token = Token.new(token, fact, self)
191
+ @successors.each { |s| s.activate(new_token) }
192
+ end
193
+ end
194
+ end
195
+ end
196
+ ```
197
+
198
+ **Join tests** verify:
199
+ - Variable consistency (e.g., both conditions match same `:symbol?`)
200
+ - Cross-condition predicates (e.g., price1 > price2)
201
+
202
+ **Implementation**: `lib/kbs/join_node.rb:4`
203
+
204
+ #### Beta Memory
205
+
206
+ `BetaMemory` stores tokens (partial matches) and implements the **unlinking optimization**:
207
+
208
+ ```ruby
209
+ class BetaMemory
210
+ def add_token(token)
211
+ @tokens << token
212
+ unlink! if @tokens.empty? # Unlink when empty
213
+ relink! if @tokens.size == 1 # Relink when first token arrives
214
+ end
215
+
216
+ def remove_token(token)
217
+ @tokens.delete(token)
218
+ unlink! if @tokens.empty? # Unlink when last token removed
219
+ end
220
+ end
221
+ ```
222
+
223
+ **Implementation**: `lib/kbs/beta_memory.rb:4`
224
+
225
+ #### Negation Nodes
226
+
227
+ `NegationNode` implements **negated conditions** (e.g., "when there is NO matching fact"):
228
+
229
+ ```ruby
230
+ class NegationNode
231
+ def left_activate(token)
232
+ matches = @alpha_memory.items.select { |fact| perform_join_tests(token, fact) }
233
+
234
+ if matches.empty?
235
+ # No inhibiting facts found - propagate the token
236
+ new_token = Token.new(token, nil, self)
237
+ @successors.each { |s| s.activate(new_token) }
238
+ else
239
+ # Found inhibiting facts - block propagation
240
+ @tokens_with_matches[token] = matches
241
+ end
242
+ end
243
+
244
+ def right_deactivate(fact)
245
+ # When an inhibiting fact is removed, check if we can now propagate
246
+ @beta_memory.tokens.each do |token|
247
+ if @tokens_with_matches[token].include?(fact)
248
+ @tokens_with_matches[token].delete(fact)
249
+
250
+ if @tokens_with_matches[token].empty?
251
+ new_token = Token.new(token, nil, self)
252
+ @successors.each { |s| s.activate(new_token) }
253
+ end
254
+ end
255
+ end
256
+ end
257
+ end
258
+ ```
259
+
260
+ **Key insight**: Negation nodes propagate tokens with `fact = nil` since there's no actual fact to include.
261
+
262
+ **Implementation**: `lib/kbs/negation_node.rb:4`
263
+
264
+ ### Layer 3: Production Nodes
265
+
266
+ `ProductionNode` is the terminal node for each rule. When a token reaches a production node, all rule conditions have been satisfied:
267
+
268
+ ```ruby
269
+ class ProductionNode
270
+ def activate(token)
271
+ @tokens << token
272
+ # Don't fire immediately - wait for engine.run()
273
+ end
274
+
275
+ def fire_rule(token)
276
+ return if token.fired?
277
+ @rule.fire(token.facts)
278
+ token.mark_fired!
279
+ end
280
+ end
281
+ ```
282
+
283
+ **Why delay firing?** Negation nodes may need to **deactivate** tokens after they're created but before they fire. The two-phase approach (collect tokens, then fire) ensures correctness.
284
+
285
+ **Implementation**: `lib/kbs/production_node.rb:4`
286
+
287
+ ## The RETE Cycle
288
+
289
+ ### 1. Network Construction
290
+
291
+ When a rule is added via `engine.add_rule(rule)`, the network is built:
292
+
293
+ ```ruby
294
+ def build_network_for_rule(rule)
295
+ current_beta = @root_beta_memory
296
+
297
+ rule.conditions.each_with_index do |condition, index|
298
+ # Create or reuse alpha memory
299
+ pattern = condition.pattern.merge(type: condition.type)
300
+ alpha_memory = get_or_create_alpha_memory(pattern)
301
+
302
+ # Build join tests for variable consistency
303
+ tests = build_join_tests(condition, index)
304
+
305
+ # Create join or negation node
306
+ if condition.negated
307
+ negation_node = NegationNode.new(alpha_memory, current_beta, tests)
308
+ new_beta = BetaMemory.new
309
+ negation_node.successors << new_beta
310
+ current_beta = new_beta
311
+ else
312
+ join_node = JoinNode.new(alpha_memory, current_beta, tests)
313
+ new_beta = BetaMemory.new
314
+ join_node.successors << new_beta
315
+ current_beta = new_beta
316
+ end
317
+ end
318
+
319
+ # Terminal production node
320
+ production_node = ProductionNode.new(rule)
321
+ current_beta.successors << production_node
322
+ @production_nodes[rule.name] = production_node
323
+ end
324
+ ```
325
+
326
+ **Implementation**: `lib/kbs/rete_engine.rb:58`
327
+
328
+ ### 2. Fact Assertion
329
+
330
+ When `engine.add_fact(:stock, symbol: "AAPL", price: 150)` is called:
331
+
332
+ ![Fact Assertion Flow](../assets/images/fact-assertion-flow.svg)
333
+
334
+ *Step-by-step flow showing how a fact propagates through the RETE network from working memory to production nodes.*
335
+
336
+ ### 3. Pattern Matching Flow
337
+
338
+ Let's trace a fact through the network for this rule:
339
+
340
+ ```ruby
341
+ # Rule: Alert when AAPL stock exists but no alert exists
342
+ rule = Rule.new("no_alert") do |r|
343
+ r.conditions = [
344
+ Condition.new(:stock, { symbol: "AAPL" }),
345
+ Condition.new(:alert, { symbol: "AAPL" }, negated: true)
346
+ ]
347
+ r.action = ->(facts) { puts "No alert for AAPL!" }
348
+ end
349
+ ```
350
+
351
+ ![Pattern Matching Trace](../assets/images/pattern-matching-trace.svg)
352
+
353
+ *Complete trace showing how negation works: adding a stock fact fires the rule, adding an alert inhibits it, and removing the alert reactivates the rule.*
354
+
355
+ ### 4. Rule Execution
356
+
357
+ The final phase is `engine.run()`:
358
+
359
+ ```ruby
360
+ def run
361
+ @production_nodes.values.each do |node|
362
+ node.tokens.each do |token|
363
+ node.fire_rule(token)
364
+ end
365
+ end
366
+ end
367
+ ```
368
+
369
+ Each production node fires its accumulated tokens. The `fired?` flag prevents duplicate firing.
370
+
371
+ **Implementation**: `lib/kbs/rete_engine.rb:48`
372
+
373
+ ## RETE Optimization: Unlinking
374
+
375
+ ### The Problem
376
+
377
+ In basic RETE, join nodes always process activations even when one input is empty:
378
+
379
+ ```
380
+ BetaMemory (0 tokens) ──┐
381
+ ├──→ JoinNode ──→ (does useless work!)
382
+ AlphaMemory (100 facts) ┘
383
+ ```
384
+
385
+ If beta memory is empty, the join will produce zero results, wasting CPU cycles.
386
+
387
+ ### The Solution
388
+
389
+ RETE introduces **dynamic unlinking**: nodes automatically disconnect from the network when empty and reconnect when non-empty.
390
+
391
+ ```ruby
392
+ class BetaMemory
393
+ def add_token(token)
394
+ @tokens << token
395
+ relink! if @tokens.size == 1 # Reconnect when first token arrives
396
+ end
397
+
398
+ def remove_token(token)
399
+ @tokens.delete(token)
400
+ unlink! if @tokens.empty? # Disconnect when empty
401
+ end
402
+
403
+ def relink!
404
+ @linked = true
405
+ @successors.each { |s| s.left_relink! }
406
+ end
407
+
408
+ def unlink!
409
+ @linked = false
410
+ @successors.each { |s| s.left_unlink! }
411
+ end
412
+ end
413
+ ```
414
+
415
+ **Join node respects linking state**:
416
+
417
+ ```ruby
418
+ class JoinNode
419
+ def left_activate(token)
420
+ return unless @left_linked && @right_linked # Skip if unlinked!
421
+ # ... perform join ...
422
+ end
423
+
424
+ def right_activate(fact)
425
+ return unless @left_linked && @right_linked # Skip if unlinked!
426
+ # ... perform join ...
427
+ end
428
+ end
429
+ ```
430
+
431
+ ### Performance Impact
432
+
433
+ For rules with many conditions, unlinking can reduce RETE network activations by **90%+**:
434
+
435
+ - Empty alpha memories don't trigger join operations
436
+ - Empty beta memories don't process fact assertions
437
+ - Network "lights up" only the relevant paths
438
+
439
+ This is especially critical for:
440
+ - **Negated conditions** (often have empty alpha memories)
441
+ - **Rare patterns** (e.g., "critical alert" facts)
442
+ - **Complex rules** (many conditions = more opportunities for empty nodes)
443
+
444
+ ## Variable Binding
445
+
446
+ Variables (symbols starting with `?`) enable cross-condition constraints and action parameterization:
447
+
448
+ ### Extraction During Network Build
449
+
450
+ ```ruby
451
+ class Condition
452
+ def extract_variables(pattern)
453
+ vars = {}
454
+ pattern.each do |key, value|
455
+ if value.is_a?(Symbol) && value.to_s.start_with?('?')
456
+ vars[value] = key # { :symbol? => :symbol, :price? => :price }
457
+ end
458
+ end
459
+ vars
460
+ end
461
+ end
462
+ ```
463
+
464
+ **Implementation**: `lib/kbs/condition.rb:16`
465
+
466
+ ### Join Test Generation
467
+
468
+ Variables create **join tests** to ensure consistency:
469
+
470
+ ```ruby
471
+ # Rule with shared ?symbol variable
472
+ conditions = [
473
+ Condition.new(:stock, { symbol: :symbol?, price: :price? }),
474
+ Condition.new(:order, { symbol: :symbol?, quantity: 100 })
475
+ ]
476
+
477
+ # Generates join test:
478
+ {
479
+ token_field_index: 0, # Check first fact in token (stock)
480
+ token_field: :symbol, # Get its :symbol attribute
481
+ fact_field: :symbol, # Compare with order's :symbol attribute
482
+ operation: :eq # Must be equal
483
+ }
484
+ ```
485
+
486
+ **Implementation**: `lib/kbs/join_node.rb:89`
487
+
488
+ ### Action Binding
489
+
490
+ When a rule fires, bindings are extracted for the action:
491
+
492
+ ```ruby
493
+ def fire(facts)
494
+ bindings = extract_bindings(facts)
495
+ # bindings = { :symbol? => "AAPL", :price? => 150.0 }
496
+
497
+ @action.call(facts, bindings)
498
+ end
499
+
500
+ def extract_bindings(facts)
501
+ bindings = {}
502
+ @conditions.each_with_index do |condition, index|
503
+ next if condition.negated # Negated conditions have no fact
504
+ fact = facts[index]
505
+ condition.variable_bindings.each do |var, field|
506
+ bindings[var] = fact.attributes[field]
507
+ end
508
+ end
509
+ bindings
510
+ end
511
+ ```
512
+
513
+ **Implementation**: `lib/kbs/rule.rb:34`
514
+
515
+ ## Advanced Topics
516
+
517
+ ### Conflict Resolution
518
+
519
+ When multiple rules are activated simultaneously, KBS uses **priority** (higher values fire first):
520
+
521
+ ```ruby
522
+ rule1 = Rule.new("urgent", priority: 10) { ... }
523
+ rule2 = Rule.new("normal", priority: 0) { ... }
524
+
525
+ # rule1 fires before rule2
526
+ ```
527
+
528
+ For same-priority rules, firing order is deterministic but unspecified (depends on hash ordering).
529
+
530
+ ### Fact Retraction
531
+
532
+ Removing facts triggers **recursive token deletion**:
533
+
534
+ ```ruby
535
+ class JoinNode
536
+ def right_deactivate(fact)
537
+ tokens_to_remove = []
538
+
539
+ @beta_memory.tokens.each do |token|
540
+ # Find child tokens containing this fact
541
+ token.children.select { |child| child.fact == fact }.each do |child|
542
+ tokens_to_remove << child
543
+ @successors.each { |s| s.deactivate(child) } # Recursive!
544
+ end
545
+ end
546
+
547
+ tokens_to_remove.each { |token| token.parent.children.delete(token) }
548
+ end
549
+ end
550
+ ```
551
+
552
+ This ensures **truth maintenance**: when a premise is removed, all derived conclusions are also removed.
553
+
554
+ **Implementation**: `lib/kbs/join_node.rb:72`
555
+
556
+ ### Network Sharing
557
+
558
+ Alpha memories are **shared across rules** using pattern as the hash key:
559
+
560
+ ```ruby
561
+ def get_or_create_alpha_memory(pattern)
562
+ @alpha_memories[pattern] ||= AlphaMemory.new(pattern)
563
+ end
564
+ ```
565
+
566
+ If 10 rules all match `stock(symbol: "AAPL")`, they share one `AlphaMemory` node, reducing:
567
+ - Memory usage (one fact store instead of 10)
568
+ - Computation (one pattern match instead of 10)
569
+
570
+ **Implementation**: `lib/kbs/rete_engine.rb:104`
571
+
572
+ ### Incremental Matching
573
+
574
+ RETE is **incremental**: after the initial network build, only changes are processed. Adding a fact activates a small subgraph, not the entire network.
575
+
576
+ **Complexity**:
577
+ - Initial build: O(R × F) where R = rules, F = facts
578
+ - Per-fact addition: O(N) where N = activated nodes (typically << R × F)
579
+ - Per-fact removal: O(T) where T = tokens to remove
580
+
581
+ In practice, RETE can handle millions of facts with sub-millisecond updates.
582
+
583
+ ## Debugging RETE Networks
584
+
585
+ ### Visualizing Token Flow
586
+
587
+ Enable token tracing:
588
+
589
+ ```ruby
590
+ class Token
591
+ def to_s
592
+ "Token(#{facts.map(&:to_s).join(', ')})"
593
+ end
594
+ end
595
+
596
+ # In your rule action:
597
+ r.action = lambda do |facts, bindings|
598
+ puts "Fired with facts: #{facts.map(&:to_s).join(', ')}"
599
+ puts "Bindings: #{bindings.inspect}"
600
+ end
601
+ ```
602
+
603
+ ### Inspecting Network State
604
+
605
+ Check what's in memories:
606
+
607
+ ```ruby
608
+ # Alpha memory contents
609
+ engine.alpha_memories.each do |pattern, memory|
610
+ puts "Pattern #{pattern}: #{memory.items.size} facts"
611
+ memory.items.each { |f| puts " - #{f}" }
612
+ end
613
+
614
+ # Beta memory contents (requires introspection)
615
+ def walk_beta_network(beta)
616
+ puts "Beta memory: #{beta.tokens.size} tokens"
617
+ beta.tokens.each { |t| puts " - #{t}" }
618
+ beta.successors.each do |node|
619
+ if node.is_a?(BetaMemory)
620
+ walk_beta_network(node)
621
+ end
622
+ end
623
+ end
624
+ ```
625
+
626
+ ### Common Pitfalls
627
+
628
+ 1. **Forgetting to call `engine.run()`**: Tokens accumulate but rules don't fire
629
+ 2. **Pattern mismatches**: `{ type: :stock }` vs `Condition.new(:stock, {})` - the latter doesn't filter by type!
630
+ 3. **Variable binding errors**: Using `?symbol` (string) instead of `:symbol?` (symbol)
631
+ 4. **Negation timing**: Negated conditions only fire when facts are **absent**, not after they're removed (use `engine.run()` to re-evaluate)
632
+
633
+ ## Performance Characteristics
634
+
635
+ ### Time Complexity
636
+
637
+ | Operation | Complexity | Notes |
638
+ |-----------|-----------|-------|
639
+ | Add rule | O(C × F) | C = conditions, F = existing facts |
640
+ | Add fact | O(N) | N = activated nodes (avg << total nodes) |
641
+ | Remove fact | O(T) | T = tokens containing fact |
642
+ | Run rules | O(M) | M = matched tokens in production nodes |
643
+
644
+ ### Space Complexity
645
+
646
+ | Structure | Space | Notes |
647
+ |-----------|-------|-------|
648
+ | Alpha memories | O(F × P) | F = facts, P = unique patterns |
649
+ | Beta memories | O(T) | T = partial match tokens |
650
+ | Tokens | O(C × M) | C = conditions, M = complete matches |
651
+ | Network nodes | O(R × C) | R = rules, C = avg conditions per rule |
652
+
653
+ ### Optimization Strategies
654
+
655
+ 1. **Pattern specificity**: Put most selective conditions first to reduce beta memory size
656
+ 2. **Negation placement**: Place negated conditions last (they don't add facts to tokens)
657
+ 3. **Shared patterns**: Design rules to share common patterns
658
+ 4. **Fact pruning**: Remove obsolete facts to trigger unlinking
659
+ 5. **Priority tuning**: Use priority to fire expensive rules last
660
+
661
+ ## Comparison with Other Algorithms
662
+
663
+ ### Naive Match-All
664
+
665
+ ```ruby
666
+ # O(R × F) on every cycle
667
+ def naive_fire_rules
668
+ rules.each do |rule|
669
+ facts.each do |fact|
670
+ if rule.matches?(fact)
671
+ rule.fire(fact)
672
+ end
673
+ end
674
+ end
675
+ end
676
+ ```
677
+
678
+ **Problem**: Re-evaluates everything, no state preservation.
679
+
680
+ ### TREAT
681
+
682
+ TREAT eliminates alpha/beta network in favor of lazy evaluation:
683
+ - **Pros**: Simpler implementation, lower memory
684
+ - **Cons**: Slower for rules that fire frequently (no memoization)
685
+
686
+ RETE is better when rules fire often; TREAT is better for sparse firing.
687
+
688
+ ### Basic RETE vs RETE with Unlinking
689
+
690
+ Early RETE implementations lacked unlinking:
691
+ - **Without unlinking**: All nodes always active, many wasted join operations
692
+ - **With unlinking**: Nodes disconnect when empty, up to 10× faster
693
+
694
+ KBS implements RETE with unlinking optimization.
695
+
696
+ ## Implementation Files
697
+
698
+ | Component | File | Lines |
699
+ |-----------|------|-------|
700
+ | Core engine | `lib/kbs/rete_engine.rb` | ~110 |
701
+ | Working memory | `lib/kbs/working_memory.rb` | ~35 |
702
+ | Facts | `lib/kbs/fact.rb` | ~45 |
703
+ | Tokens | `lib/kbs/token.rb` | ~40 |
704
+ | Alpha memory | `lib/kbs/alpha_memory.rb` | ~40 |
705
+ | Beta memory | `lib/kbs/beta_memory.rb` | ~60 |
706
+ | Join nodes | `lib/kbs/join_node.rb` | ~120 |
707
+ | Negation nodes | `lib/kbs/negation_node.rb` | ~90 |
708
+ | Production nodes | `lib/kbs/production_node.rb` | ~30 |
709
+ | Conditions | `lib/kbs/condition.rb` | ~30 |
710
+ | Rules | `lib/kbs/rule.rb` | ~50 |
711
+
712
+ **Total**: ~650 lines of core RETE implementation.
713
+
714
+ ## Further Reading
715
+
716
+ ### Academic Papers
717
+
718
+ - Forgy, C. (1982). "Rete: A Fast Algorithm for the Many Pattern/Many Object Pattern Match Problem". *Artificial Intelligence*, 19(1), 17-37.
719
+ - Forgy, C. (1989). "Rete: A Fast Match Algorithm". *AI Expert*, 4(1), 34-40.
720
+
721
+ ### Textbooks
722
+
723
+ - Giarratano, J., & Riley, G. (2004). *Expert Systems: Principles and Programming* (4th ed.). Course Technology.
724
+ - Russell, S., & Norvig, P. (2020). *Artificial Intelligence: A Modern Approach* (4th ed.). Pearson. (Chapter on Rule-Based Systems)
725
+
726
+ ### Online Resources
727
+
728
+ - [RETE Algorithm Visualization](http://www.jessrules.com/docs/71/rete.html) - Jess documentation
729
+ - [Production Systems](https://en.wikipedia.org/wiki/Production_system_(computer_science)) - Wikipedia
730
+ - [Rule-Based Expert Systems](https://www.cs.toronto.edu/~hector/PublicKnowledgeBase.html) - University of Toronto
731
+
732
+ ## Next Steps
733
+
734
+ - **[DSL Guide](../guides/dsl.md)**: Learn how to write rules using KBS's Ruby DSL
735
+ - **[Blackboard Architecture](blackboard.md)**: Understand persistent memory and multi-agent systems
736
+ - **[Examples](../examples/index.md)**: See RETE in action with stock trading and expert systems
737
+ - **[Performance Tuning](../advanced/performance.md)**: Optimize your rule-based system