kbs 0.0.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) 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 +291 -362
  5. data/docs/advanced/custom-persistence.md +775 -0
  6. data/docs/advanced/debugging.md +726 -0
  7. data/docs/advanced/index.md +8 -0
  8. data/docs/advanced/performance.md +865 -0
  9. data/docs/advanced/testing.md +827 -0
  10. data/docs/api/blackboard.md +1157 -0
  11. data/docs/api/engine.md +1047 -0
  12. data/docs/api/facts.md +1212 -0
  13. data/docs/api/index.md +12 -0
  14. data/docs/api/rules.md +1104 -0
  15. data/docs/architecture/blackboard.md +544 -0
  16. data/docs/architecture/index.md +277 -0
  17. data/docs/architecture/network-structure.md +343 -0
  18. data/docs/architecture/rete-algorithm.md +737 -0
  19. data/docs/assets/css/custom.css +83 -0
  20. data/docs/assets/images/blackboard-architecture.svg +136 -0
  21. data/docs/assets/images/compiled-network.svg +101 -0
  22. data/docs/assets/images/fact-assertion-flow.svg +117 -0
  23. data/docs/assets/images/fact-rule-relationship.svg +65 -0
  24. data/docs/assets/images/fact-structure.svg +42 -0
  25. data/docs/assets/images/inference-cycle.svg +47 -0
  26. data/docs/assets/images/kb-components.svg +43 -0
  27. data/docs/assets/images/kbs.jpg +0 -0
  28. data/docs/assets/images/pattern-matching-trace.svg +136 -0
  29. data/docs/assets/images/rete-network-layers.svg +96 -0
  30. data/docs/assets/images/rule-structure.svg +44 -0
  31. data/docs/assets/images/system-layers.svg +69 -0
  32. data/docs/assets/images/trading-signal-network.svg +139 -0
  33. data/docs/assets/js/mathjax.js +17 -0
  34. data/docs/examples/index.md +223 -0
  35. data/docs/guides/blackboard-memory.md +589 -0
  36. data/docs/guides/dsl.md +1321 -0
  37. data/docs/guides/facts.md +652 -0
  38. data/docs/guides/getting-started.md +385 -0
  39. data/docs/guides/index.md +23 -0
  40. data/docs/guides/negation.md +529 -0
  41. data/docs/guides/pattern-matching.md +561 -0
  42. data/docs/guides/persistence.md +451 -0
  43. data/docs/guides/variable-binding.md +491 -0
  44. data/docs/guides/writing-rules.md +914 -0
  45. data/docs/index.md +155 -0
  46. data/docs/installation.md +156 -0
  47. data/docs/quick-start.md +221 -0
  48. data/docs/what-is-a-fact.md +694 -0
  49. data/docs/what-is-a-knowledge-base.md +350 -0
  50. data/docs/what-is-a-rule.md +833 -0
  51. data/examples/.gitignore +1 -0
  52. data/examples/README.md +2 -2
  53. data/examples/advanced_example.rb +2 -2
  54. data/examples/advanced_example_dsl.rb +224 -0
  55. data/examples/ai_enhanced_kbs.rb +1 -1
  56. data/examples/ai_enhanced_kbs_dsl.rb +538 -0
  57. data/examples/blackboard_demo_dsl.rb +50 -0
  58. data/examples/car_diagnostic.rb +1 -1
  59. data/examples/car_diagnostic_dsl.rb +54 -0
  60. data/examples/concurrent_inference_demo.rb +5 -6
  61. data/examples/concurrent_inference_demo_dsl.rb +362 -0
  62. data/examples/csv_trading_system.rb +1 -1
  63. data/examples/csv_trading_system_dsl.rb +525 -0
  64. data/examples/iot_demo_using_dsl.rb +1 -1
  65. data/examples/portfolio_rebalancing_system.rb +2 -2
  66. data/examples/portfolio_rebalancing_system_dsl.rb +613 -0
  67. data/examples/redis_trading_demo_dsl.rb +177 -0
  68. data/examples/rule_source_demo.rb +123 -0
  69. data/examples/run_all.rb +50 -0
  70. data/examples/run_all_dsl.rb +49 -0
  71. data/examples/stock_trading_advanced.rb +1 -1
  72. data/examples/stock_trading_advanced_dsl.rb +404 -0
  73. data/examples/temp_dsl.txt +9392 -0
  74. data/examples/timestamped_trading.rb +1 -1
  75. data/examples/timestamped_trading_dsl.rb +258 -0
  76. data/examples/trading_demo.rb +1 -1
  77. data/examples/trading_demo_dsl.rb +322 -0
  78. data/examples/working_demo.rb +1 -1
  79. data/examples/working_demo_dsl.rb +160 -0
  80. data/lib/kbs/blackboard/engine.rb +3 -3
  81. data/lib/kbs/blackboard/fact.rb +1 -1
  82. data/lib/kbs/condition.rb +1 -1
  83. data/lib/kbs/decompiler.rb +204 -0
  84. data/lib/kbs/dsl/knowledge_base.rb +101 -2
  85. data/lib/kbs/dsl/variable.rb +1 -1
  86. data/lib/kbs/dsl.rb +3 -1
  87. data/lib/kbs/{rete_engine.rb → engine.rb} +42 -1
  88. data/lib/kbs/fact.rb +1 -1
  89. data/lib/kbs/version.rb +1 -1
  90. data/lib/kbs.rb +15 -13
  91. data/mkdocs.yml +181 -0
  92. metadata +74 -9
  93. data/examples/stock_trading_system.rb.bak +0 -563
@@ -0,0 +1,160 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/kbs'
4
+ include KBS::DSL::ConditionHelpers
5
+
6
+ # Define the knowledge base with rules
7
+ kb = KBS.knowledge_base do
8
+ # Rule 1: Simple stock momentum
9
+ rule "momentum_buy" do
10
+ on :stock, symbol: "AAPL"
11
+
12
+ perform do |facts, bindings|
13
+ stock = facts.find { |f| f.type == :stock }
14
+ puts "🚀 MOMENTUM SIGNAL: #{stock[:symbol]}"
15
+ puts " Price: $#{stock[:price]}"
16
+ puts " Volume: #{stock[:volume].to_s.reverse.scan(/\d{1,3}/).join(',').reverse}"
17
+ puts " Recommendation: BUY"
18
+ end
19
+ end
20
+
21
+ # Rule 2: High volume alert
22
+ rule "high_volume" do
23
+ on :stock do
24
+ volume greater_than(1000000)
25
+ end
26
+
27
+ perform do |facts, bindings|
28
+ stock = facts.find { |f| f.type == :stock }
29
+ puts "📊 HIGH VOLUME ALERT: #{stock[:symbol]}"
30
+ puts " Volume: #{stock[:volume].to_s.reverse.scan(/\d{1,3}/).join(',').reverse}"
31
+ puts " Above 1M shares traded"
32
+ end
33
+ end
34
+
35
+ # Rule 3: Price movement
36
+ rule "price_movement" do
37
+ on :stock do
38
+ price_change satisfies { |p| p && p.abs > 2 }
39
+ end
40
+
41
+ perform do |facts, bindings|
42
+ stock = facts.find { |f| f.type == :stock }
43
+ direction = stock[:price_change] > 0 ? "UP" : "DOWN"
44
+ puts "📈 SIGNIFICANT MOVE: #{stock[:symbol]} #{direction}"
45
+ puts " Change: #{stock[:price_change] > 0 ? '+' : ''}#{stock[:price_change]}%"
46
+ end
47
+ end
48
+
49
+ # Rule 4: RSI signals
50
+ rule "rsi_signal" do
51
+ on :stock do
52
+ rsi satisfies { |r| r && (r < 30 || r > 70) }
53
+ end
54
+
55
+ perform do |facts, bindings|
56
+ stock = facts.find { |f| f.type == :stock }
57
+ condition = stock[:rsi] < 30 ? "OVERSOLD" : "OVERBOUGHT"
58
+ action = stock[:rsi] < 30 ? "BUY" : "SELL"
59
+ puts "⚡ RSI SIGNAL: #{stock[:symbol]} #{condition}"
60
+ puts " RSI: #{stock[:rsi].round(1)}"
61
+ puts " Recommendation: #{action}"
62
+ end
63
+ end
64
+
65
+ # Rule 5: Multi-condition golden cross
66
+ rule "golden_cross_complete" do
67
+ on :stock, symbol: "AAPL"
68
+ on :ma_signal, type: "golden_cross"
69
+
70
+ perform do |facts, bindings|
71
+ stock = facts.find { |f| f.type == :stock }
72
+ signal = facts.find { |f| f.type == :ma_signal }
73
+ puts "🌟 GOLDEN CROSS CONFIRMED: #{stock[:symbol]}"
74
+ puts " 50-day MA crossed above 200-day MA"
75
+ puts " Price: $#{stock[:price]}"
76
+ puts " Recommendation: STRONG BUY"
77
+ end
78
+ end
79
+ end
80
+
81
+ def run_scenarios(kb)
82
+ puts "🏦 STOCK TRADING EXPERT SYSTEM"
83
+ puts "=" * 50
84
+
85
+ # Scenario 1: Apple momentum
86
+ puts "\n📊 SCENARIO 1: Apple with High Volume"
87
+ puts "-" * 30
88
+ kb.reset
89
+ kb.fact :stock, {
90
+ symbol: "AAPL",
91
+ price: 185.50,
92
+ volume: 1_500_000,
93
+ price_change: 3.2,
94
+ rsi: 68
95
+ }
96
+ kb.run
97
+
98
+ # Scenario 2: Google big move
99
+ puts "\n📊 SCENARIO 2: Google Big Price Move"
100
+ puts "-" * 30
101
+ kb.reset
102
+ kb.fact :stock, {
103
+ symbol: "GOOGL",
104
+ price: 142.80,
105
+ volume: 800_000,
106
+ price_change: -4.1,
107
+ rsi: 75
108
+ }
109
+ kb.run
110
+
111
+ # Scenario 3: Tesla oversold
112
+ puts "\n📊 SCENARIO 3: Tesla Oversold"
113
+ puts "-" * 30
114
+ kb.reset
115
+ kb.fact :stock, {
116
+ symbol: "TSLA",
117
+ price: 195.40,
118
+ volume: 2_200_000,
119
+ price_change: -1.8,
120
+ rsi: 25
121
+ }
122
+ kb.run
123
+
124
+ # Scenario 4: Apple Golden Cross
125
+ puts "\n📊 SCENARIO 4: Apple Golden Cross"
126
+ puts "-" * 30
127
+ kb.reset
128
+ kb.fact :stock, {
129
+ symbol: "AAPL",
130
+ price: 190.25,
131
+ volume: 1_100_000,
132
+ price_change: 2.1,
133
+ rsi: 55
134
+ }
135
+ kb.fact :ma_signal, {
136
+ symbol: "AAPL",
137
+ type: "golden_cross"
138
+ }
139
+ kb.run
140
+
141
+ # Scenario 5: Multiple signals
142
+ puts "\n📊 SCENARIO 5: NVIDIA Multiple Signals"
143
+ puts "-" * 30
144
+ kb.reset
145
+ kb.fact :stock, {
146
+ symbol: "NVDA",
147
+ price: 425.80,
148
+ volume: 3_500_000,
149
+ price_change: 8.7,
150
+ rsi: 78
151
+ }
152
+ kb.run
153
+
154
+ puts "\n" + "=" * 50
155
+ puts "DEMONSTRATION COMPLETE"
156
+ end
157
+
158
+ if __FILE__ == $0
159
+ run_scenarios(kb)
160
+ end
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../rete_engine'
3
+ require_relative '../engine'
4
4
  require_relative 'memory'
5
5
 
6
6
  module KBS
7
7
  module Blackboard
8
- # RETE engine integrated with Blackboard memory
9
- class Engine < ReteEngine
8
+ # KBS engine integrated with Blackboard memory
9
+ class Engine < KBS::Engine
10
10
  attr_reader :blackboard
11
11
 
12
12
  def initialize(db_path: ':memory:', store: nil)
@@ -39,7 +39,7 @@ module KBS
39
39
 
40
40
  if value.is_a?(Proc)
41
41
  return false unless @attributes[key] && value.call(@attributes[key])
42
- elsif value.is_a?(Symbol) && value.to_s.start_with?('?')
42
+ elsif value.is_a?(Symbol) && value.to_s.end_with?('?')
43
43
  next
44
44
  else
45
45
  return false unless @attributes[key] == value
data/lib/kbs/condition.rb CHANGED
@@ -16,7 +16,7 @@ module KBS
16
16
  def extract_variables(pattern)
17
17
  vars = {}
18
18
  pattern.each do |key, value|
19
- if value.is_a?(Symbol) && value.to_s.start_with?('?')
19
+ if value.is_a?(Symbol) && value.to_s.end_with?('?')
20
20
  vars[value] = key
21
21
  end
22
22
  end
@@ -0,0 +1,204 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KBS
4
+ # Reconstruct Ruby source from YARV bytecode.
5
+ #
6
+ # YARV bytecode is a stack machine. We simulate the stack,
7
+ # translating instructions back into Ruby expressions.
8
+ #
9
+ # Usage:
10
+ # KBS::Decompiler.new(some_proc).decompile # => "proc { |x| x + 1 }"
11
+ # KBS::Decompiler.new(some_lambda).decompile # => "->(x) { x + 1 }"
12
+ # KBS::Decompiler.new(some_proc).decompile_block # => "{ |x| x + 1 }"
13
+ #
14
+ class Decompiler
15
+ def initialize(proc_obj)
16
+ @iseq = RubyVM::InstructionSequence.of(proc_obj)
17
+ @arr = @iseq.to_a
18
+ @locals = @arr[10] # [:a, :b, ...]
19
+ @params = @arr[11] # {lead_num: 2, ...}
20
+ @body = @arr[13] # [1, :EVENT, [:instruction, ...], ...]
21
+ @children = []
22
+ @iseq.each_child { |c| @children << c }
23
+ @child_index = 0
24
+ @lambda = proc_obj.lambda?
25
+ end
26
+
27
+ def decompile
28
+ expr = decompile_body(@body)
29
+ params = build_params
30
+ if @lambda
31
+ "->(#{params}) { #{expr} }"
32
+ else
33
+ "proc { |#{params}| #{expr} }"
34
+ end
35
+ end
36
+
37
+ # Returns just the block literal: { |params| body }
38
+ # Useful when the surrounding keyword (perform, satisfies, etc.)
39
+ # is supplied by the caller.
40
+ def decompile_block
41
+ expr = decompile_body(@body)
42
+ params = build_params
43
+ if params.empty?
44
+ "{ #{expr} }"
45
+ else
46
+ "{ |#{params}| #{expr} }"
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def build_params
53
+ count = @params[:lead_num] || 0
54
+ @locals.first(count).join(", ")
55
+ end
56
+
57
+ def decompile_body(body)
58
+ stack = []
59
+ statements = []
60
+ instructions = body.select { |i| i.is_a?(Array) }
61
+
62
+ instructions.each do |inst|
63
+ op = inst[0]
64
+ case op
65
+ when :getlocal_WC_0
66
+ idx = inst[1]
67
+ name = slot_to_name(idx)
68
+ stack.push(name.to_s)
69
+
70
+ when :setlocal_WC_0
71
+ idx = inst[1]
72
+ name = slot_to_name(idx)
73
+ val = stack.pop
74
+ statements << "#{name} = #{val}"
75
+
76
+ when :putobject
77
+ stack.push(inst[1].inspect)
78
+
79
+ when :putobject_INT2FIX_0_
80
+ stack.push("0")
81
+
82
+ when :putobject_INT2FIX_1_
83
+ stack.push("1")
84
+
85
+ when :putself
86
+ stack.push("self")
87
+
88
+ when :putnil
89
+ stack.push("nil")
90
+
91
+ when :putchilledstring, :putstring
92
+ stack.push(inst[1].inspect)
93
+
94
+ when :opt_plus, :opt_minus, :opt_mult, :opt_div, :opt_mod,
95
+ :opt_eq, :opt_neq, :opt_lt, :opt_le, :opt_gt, :opt_ge,
96
+ :opt_ltlt
97
+ op_sym = inst[1][:mid]
98
+ b = stack.pop
99
+ a = stack.pop
100
+ stack.push("#{a} #{op_sym} #{b}")
101
+
102
+ when :opt_send_without_block
103
+ calldata = inst[1]
104
+ method = calldata[:mid]
105
+ argc = calldata[:orig_argc]
106
+ args = stack.pop(argc)
107
+ receiver = stack.pop
108
+ call_str = format_call(receiver, method, args)
109
+ stack.push(call_str)
110
+
111
+ when :send
112
+ calldata = inst[1]
113
+ method = calldata[:mid]
114
+ argc = calldata[:orig_argc]
115
+ args = stack.pop(argc)
116
+ receiver = stack.pop
117
+
118
+ child_iseq = @children[@child_index]
119
+ @child_index += 1
120
+ child_src = decompile_child(child_iseq)
121
+
122
+ base = format_call(receiver, method, args)
123
+ stack.push("#{base} { #{child_src} }")
124
+
125
+ when :newarray
126
+ count = inst[1]
127
+ items = stack.pop(count)
128
+ stack.push("[#{items.join(', ')}]")
129
+
130
+ when :newhash
131
+ count = inst[1]
132
+ pairs = stack.pop(count)
133
+ entries = pairs.each_slice(2).map { |k, v| "#{k} => #{v}" }
134
+ stack.push("{ #{entries.join(', ')} }")
135
+
136
+ when :branchunless
137
+ condition = stack.pop
138
+ remaining = instructions[instructions.index(inst) + 1..]
139
+ true_val = extract_value(remaining, 0)
140
+ false_val = extract_value(remaining, 1)
141
+ if true_val && false_val
142
+ stack.push("#{condition} ? #{true_val} : #{false_val}")
143
+ break
144
+ end
145
+
146
+ when :leave
147
+ # ignore
148
+
149
+ when :pop
150
+ val = stack.pop
151
+ statements << val if val
152
+
153
+ else
154
+ stack.push("/* #{op} */")
155
+ end
156
+ end
157
+
158
+ all = statements + [stack.last].compact
159
+ all.join("; ")
160
+ end
161
+
162
+ def format_call(receiver, method, args)
163
+ prefix = (receiver == "self") ? "" : "#{receiver}."
164
+ if args.empty?
165
+ "#{prefix}#{method}"
166
+ else
167
+ "#{prefix}#{method}(#{args.join(', ')})"
168
+ end
169
+ end
170
+
171
+ def decompile_child(child_iseq)
172
+ child_arr = child_iseq.to_a
173
+ child_locals = child_arr[10]
174
+ child_params = child_arr[11]
175
+ child_body = child_arr[13]
176
+ count = child_params[:lead_num] || 0
177
+ param_names = child_locals.first(count).join(", ")
178
+
179
+ saved = [@locals, @params, @body]
180
+ @locals, @params, @body = child_locals, child_params, child_body
181
+ expr = decompile_body(child_body)
182
+ @locals, @params, @body = saved
183
+
184
+ if count > 0
185
+ "|#{param_names}| #{expr}"
186
+ else
187
+ expr
188
+ end
189
+ end
190
+
191
+ def extract_value(instructions, index)
192
+ values = instructions.select { |i|
193
+ i.is_a?(Array) && (i[0] == :putchilledstring || i[0] == :putstring || i[0] == :putobject)
194
+ }
195
+ val = values[index]
196
+ val[1].inspect if val
197
+ end
198
+
199
+ def slot_to_name(slot_idx)
200
+ name_idx = @locals.size + 2 - slot_idx
201
+ @locals[name_idx] || "?local_#{slot_idx}"
202
+ end
203
+ end
204
+ end
@@ -6,12 +6,14 @@ module KBS
6
6
  attr_reader :engine, :rules
7
7
 
8
8
  def initialize
9
- @engine = ReteEngine.new
9
+ @engine = Engine.new
10
10
  @rules = {}
11
11
  @rule_builders = {}
12
+ @rule_source_locations = {}
12
13
  end
13
14
 
14
15
  def rule(name, &block)
16
+ @rule_source_locations[name] = block.source_location if block
15
17
  builder = RuleBuilder.new(name)
16
18
  builder.instance_eval(&block) if block_given?
17
19
  @rule_builders[name] = builder
@@ -42,7 +44,7 @@ module KBS
42
44
  end
43
45
 
44
46
  def reset
45
- @engine.working_memory.facts.clear
47
+ @engine.reset
46
48
  end
47
49
 
48
50
  def facts
@@ -65,6 +67,30 @@ module KBS
65
67
  puts "-" * 40
66
68
  end
67
69
 
70
+ def rule_source(name)
71
+ # Try file-based source first
72
+ if (location = @rule_source_locations[name])
73
+ file, line = location
74
+ if file && File.exist?(file)
75
+ source = extract_rule_source(file, line)
76
+ return source if source
77
+ end
78
+ end
79
+
80
+ # Fall back to reconstruction from internal state
81
+ reconstruct_rule_source(name)
82
+ end
83
+
84
+ def print_rule_source(name)
85
+ source = rule_source(name)
86
+ unless source
87
+ puts "No source available for rule '#{name}'"
88
+ return
89
+ end
90
+
91
+ puts source
92
+ end
93
+
68
94
  def print_rules
69
95
  puts "Knowledge Base Rules:"
70
96
  puts "-" * 40
@@ -81,6 +107,79 @@ module KBS
81
107
  end
82
108
  puts "-" * 40
83
109
  end
110
+
111
+ private
112
+
113
+ def reconstruct_rule_source(name)
114
+ builder = @rule_builders[name]
115
+ return nil unless builder
116
+
117
+ lines = []
118
+ lines << "rule #{name.inspect} do"
119
+ lines << " desc #{builder.description.inspect}" if builder.description
120
+ lines << " priority #{builder.priority}" if builder.priority != 0
121
+
122
+ builder.conditions.each do |cond|
123
+ keyword = cond.negated ? "without" : "on"
124
+ pattern_str = reconstruct_pattern(cond.pattern)
125
+ if pattern_str.empty?
126
+ lines << " #{keyword} #{cond.type.inspect}"
127
+ else
128
+ lines << " #{keyword} #{cond.type.inspect}, #{pattern_str}"
129
+ end
130
+ end
131
+
132
+ if builder.action_block
133
+ block_str = decompile_proc_block(builder.action_block)
134
+ lines << " perform #{block_str}"
135
+ end
136
+
137
+ lines << "end"
138
+ lines.join("\n")
139
+ end
140
+
141
+ def reconstruct_pattern(pattern)
142
+ return "" if pattern.empty?
143
+
144
+ pattern.map do |key, value|
145
+ val_str = if value.is_a?(Proc)
146
+ Decompiler.new(value).decompile
147
+ else
148
+ value.inspect
149
+ end
150
+ "#{key}: #{val_str}"
151
+ end.join(", ")
152
+ end
153
+
154
+ def decompile_proc_block(proc_obj)
155
+ Decompiler.new(proc_obj).decompile_block
156
+ rescue => e
157
+ "{ <decompilation failed: #{e.message}> }"
158
+ end
159
+
160
+ def extract_rule_source(file, start_line)
161
+ lines = File.readlines(file)
162
+ start_idx = start_line - 1
163
+ return nil if start_idx < 0 || start_idx >= lines.length
164
+
165
+ # source_location points to the block's `do` line, which is
166
+ # normally the same line as the `rule` call. Walk back up to
167
+ # 2 lines in case they are on separate lines.
168
+ rule_idx = start_idx.downto([start_idx - 2, 0].max).find do |i|
169
+ lines[i].match?(/\b(?:rule|defrule)\b/)
170
+ end || start_idx
171
+
172
+ base_indent = lines[rule_idx][/^\s*/].length
173
+ end_pattern = /^\s{#{base_indent}}end\b/
174
+
175
+ result = []
176
+ (rule_idx...lines.length).each do |i|
177
+ result << lines[i]
178
+ break if i > rule_idx && lines[i].match?(end_pattern)
179
+ end
180
+
181
+ result.join.chomp
182
+ end
84
183
  end
85
184
  end
86
185
  end
@@ -7,7 +7,7 @@ module KBS
7
7
 
8
8
  def initialize(name)
9
9
  name_str = name.to_s
10
- @name = name_str.start_with?('?') ? name_str.to_sym : "?#{name_str}".to_sym
10
+ @name = name_str.end_with?('?') ? name_str.to_sym : "#{name_str}?".to_sym
11
11
  end
12
12
 
13
13
  def to_sym
data/lib/kbs/dsl.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../kbs'
3
+ unless defined?(KBS::Engine)
4
+ require_relative '../kbs'
5
+ end
4
6
 
5
7
  # DSL components
6
8
  require_relative 'dsl/variable'
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module KBS
4
- class ReteEngine
4
+ class Engine
5
5
  attr_reader :working_memory, :rules, :alpha_memories, :production_nodes
6
6
 
7
7
  def initialize
@@ -53,6 +53,47 @@ module KBS
53
53
  end
54
54
  end
55
55
 
56
+ # Clear all transient RETE state while preserving the compiled rule network.
57
+ #
58
+ # The RETE network has four levels of transient state:
59
+ # 1. WorkingMemory — holds all asserted facts
60
+ # 2. AlphaMemory — holds facts matching each pattern
61
+ # 3. BetaMemory — holds tokens from condition joins
62
+ # 4. ProductionNode — holds tokens ready to fire
63
+ #
64
+ # Simply clearing working memory facts doesn't cascade reliably
65
+ # through intermediate beta memories. Stale tokens in beta memories
66
+ # cause false matches when new facts are asserted on the next cycle.
67
+ #
68
+ # This method clears all four levels directly while preserving the
69
+ # root beta memory's dummy token (needed for first-condition joins).
70
+ def reset
71
+ # 1. Clear working memory directly (bypass observer — we clear everything)
72
+ @working_memory.facts.clear
73
+
74
+ # 2. Clear alpha memories and their downstream beta memories
75
+ @alpha_memories.each_value do |alpha_mem|
76
+ alpha_mem.items.clear
77
+
78
+ # Each alpha memory successor is a JoinNode or NegationNode.
79
+ # Their successors are intermediate BetaMemory nodes.
80
+ # The root beta memory is never a join node successor, so
81
+ # its dummy token is preserved.
82
+ alpha_mem.successors.each do |join_node|
83
+ next unless join_node.respond_to?(:successors)
84
+ join_node.successors.each do |beta_or_prod|
85
+ beta_or_prod.tokens.clear if beta_or_prod.respond_to?(:tokens)
86
+ end
87
+ end
88
+ end
89
+
90
+ # 3. Clear production node tokens
91
+ @production_nodes.each_value { |node| node.tokens.clear }
92
+
93
+ # 4. Clear stale child references from the root dummy token
94
+ @root_beta_memory.tokens.each { |t| t.children.clear }
95
+ end
96
+
56
97
  private
57
98
 
58
99
  def build_network_for_rule(rule)
data/lib/kbs/fact.rb CHANGED
@@ -26,7 +26,7 @@ module KBS
26
26
 
27
27
  if value.is_a?(Proc)
28
28
  return false unless @attributes[key] && value.call(@attributes[key])
29
- elsif value.is_a?(Symbol) && value.to_s.start_with?('?')
29
+ elsif value.is_a?(Symbol) && value.to_s.end_with?('?')
30
30
  next
31
31
  else
32
32
  return false unless @attributes[key] == value
data/lib/kbs/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module KBS
4
- VERSION = "0.0.1"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/kbs.rb CHANGED
@@ -1,19 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "kbs/version"
3
+ require_relative 'kbs/version'
4
4
 
5
- # Core RETE II classes
6
- require_relative "kbs/fact"
7
- require_relative "kbs/working_memory"
8
- require_relative "kbs/token"
9
- require_relative "kbs/alpha_memory"
10
- require_relative "kbs/beta_memory"
11
- require_relative "kbs/join_node"
12
- require_relative "kbs/negation_node"
13
- require_relative "kbs/production_node"
14
- require_relative "kbs/condition"
15
- require_relative "kbs/rule"
16
- require_relative "kbs/rete_engine"
5
+ # Core RETE classes
6
+ require_relative 'kbs/fact'
7
+ require_relative 'kbs/working_memory'
8
+ require_relative 'kbs/token'
9
+ require_relative 'kbs/alpha_memory'
10
+ require_relative 'kbs/beta_memory'
11
+ require_relative 'kbs/join_node'
12
+ require_relative 'kbs/negation_node'
13
+ require_relative 'kbs/production_node'
14
+ require_relative 'kbs/condition'
15
+ require_relative 'kbs/rule'
16
+ require_relative 'kbs/engine'
17
+ require_relative 'kbs/decompiler'
18
+ require_relative 'kbs/dsl'
17
19
 
18
20
  module KBS
19
21
  class Error < StandardError; end