lex-meta-learning 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8a759f0665914754bffed745ff2c13f7f2ec290e1c8093a35b725acdd8959578
4
+ data.tar.gz: 6a05f278f9a31fc145394fc9b5f599c997f0d0dcd03f7edb90bd1b6a07fe585f
5
+ SHA512:
6
+ metadata.gz: 9b9bddd16d9dac71aed816ad189205954f4568fb5b381f96dfd134db58a71fc3b2d210562d250a30352fcab2fc71a659eedcd95f7eaf35a05fd0427728173a24
7
+ data.tar.gz: 2784c2e50db877ab19b9c8234a317732394356917ad5e380b60a52fe49eaf2515074026ec3d9fe67882326413c94f2a869eca0f924e71f80e48bfd26fa7c68c3
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ gem 'rspec', '~> 3.13'
8
+ gem 'rubocop', '~> 1.75', require: false
9
+ gem 'rubocop-rspec', require: false
10
+
11
+ gem 'legion-gaia', path: '../../legion-gaia'
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/legion/extensions/meta_learning/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'lex-meta-learning'
7
+ spec.version = Legion::Extensions::MetaLearning::VERSION
8
+ spec.authors = ['Esity']
9
+ spec.email = ['matthewdiverson@gmail.com']
10
+
11
+ spec.summary = 'LEX Meta-Learning'
12
+ spec.description = 'Meta-learning engine for brain-modeled agentic AI — tracks learning efficiency, ' \
13
+ 'adapts learning rates, and selects strategies based on past performance'
14
+ spec.homepage = 'https://github.com/LegionIO/lex-meta-learning'
15
+ spec.license = 'MIT'
16
+ spec.required_ruby_version = '>= 3.4'
17
+
18
+ spec.metadata['homepage_uri'] = spec.homepage
19
+ spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-meta-learning'
20
+ spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-meta-learning'
21
+ spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-meta-learning'
22
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-meta-learning/issues'
23
+ spec.metadata['rubygems_mfa_required'] = 'true'
24
+
25
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
26
+ Dir.glob('{lib,spec}/**/*') + %w[lex-meta-learning.gemspec Gemfile]
27
+ end
28
+ spec.require_paths = ['lib']
29
+ spec.add_development_dependency 'legion-gaia'
30
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/meta_learning/helpers/constants'
4
+ require 'legion/extensions/meta_learning/helpers/learning_domain'
5
+ require 'legion/extensions/meta_learning/helpers/strategy'
6
+ require 'legion/extensions/meta_learning/helpers/meta_learning_engine'
7
+ require 'legion/extensions/meta_learning/runners/meta_learning'
8
+
9
+ module Legion
10
+ module Extensions
11
+ module MetaLearning
12
+ class Client
13
+ include Runners::MetaLearning
14
+
15
+ private
16
+
17
+ def engine
18
+ @engine ||= Helpers::MetaLearningEngine.new
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module MetaLearning
6
+ module Helpers
7
+ module Constants
8
+ MAX_DOMAINS = 100
9
+ MAX_STRATEGIES = 50
10
+ MAX_EPISODES = 1000
11
+
12
+ DEFAULT_LEARNING_RATE = 0.1
13
+ RATE_BOOST = 0.02
14
+ RATE_DECAY = 0.01
15
+ TRANSFER_BONUS = 0.05
16
+
17
+ PROFICIENCY_LABELS = {
18
+ (0.8..) => :expert,
19
+ (0.6...0.8) => :proficient,
20
+ (0.4...0.6) => :intermediate,
21
+ (0.2...0.4) => :novice,
22
+ (..0.2) => :beginner
23
+ }.freeze
24
+
25
+ EFFICIENCY_LABELS = {
26
+ (0.8..) => :highly_efficient,
27
+ (0.6...0.8) => :efficient,
28
+ (0.4...0.6) => :moderate,
29
+ (0.2...0.4) => :slow,
30
+ (..0.2) => :struggling
31
+ }.freeze
32
+
33
+ STRATEGY_TYPES = %i[
34
+ repetition elaboration analogy decomposition
35
+ pattern_matching trial_and_error observation
36
+ interleaving spaced_practice retrieval_practice
37
+ ].freeze
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module MetaLearning
8
+ module Helpers
9
+ class LearningDomain
10
+ include Constants
11
+
12
+ attr_accessor :preferred_strategy
13
+ attr_reader :id, :name, :proficiency, :learning_rate, :episodes_count, :successes, :failures, :related_domains, :created_at
14
+
15
+ def initialize(name:, learning_rate: DEFAULT_LEARNING_RATE, related_domains: [])
16
+ @id = SecureRandom.uuid
17
+ @name = name
18
+ @proficiency = 0.0
19
+ @learning_rate = learning_rate.clamp(0.001, 1.0)
20
+ @episodes_count = 0
21
+ @successes = 0
22
+ @failures = 0
23
+ @preferred_strategy = nil
24
+ @related_domains = Array(related_domains).dup
25
+ @created_at = Time.now.utc
26
+ end
27
+
28
+ def record_success!
29
+ @successes += 1
30
+ @episodes_count += 1
31
+ @proficiency = (@proficiency + @learning_rate).clamp(0.0, 1.0).round(10)
32
+ end
33
+
34
+ def record_failure!
35
+ @failures += 1
36
+ @episodes_count += 1
37
+ penalty = (@learning_rate * 0.5).round(10)
38
+ @proficiency = (@proficiency - penalty).clamp(0.0, 1.0).round(10)
39
+ end
40
+
41
+ def efficiency
42
+ total = @successes + @failures
43
+ return 0.0 if total.zero?
44
+
45
+ (@successes.to_f / total).round(10)
46
+ end
47
+
48
+ def efficiency_label
49
+ EFFICIENCY_LABELS.find { |range, _| range.cover?(efficiency) }&.last || :struggling
50
+ end
51
+
52
+ def proficiency_label
53
+ PROFICIENCY_LABELS.find { |range, _| range.cover?(@proficiency) }&.last || :beginner
54
+ end
55
+
56
+ def adapt_rate!(delta:)
57
+ @learning_rate = (@learning_rate + delta).clamp(0.001, 1.0).round(10)
58
+ end
59
+
60
+ def to_h
61
+ {
62
+ id: @id,
63
+ name: @name,
64
+ proficiency: @proficiency,
65
+ proficiency_label: proficiency_label,
66
+ learning_rate: @learning_rate,
67
+ episodes_count: @episodes_count,
68
+ successes: @successes,
69
+ failures: @failures,
70
+ efficiency: efficiency,
71
+ efficiency_label: efficiency_label,
72
+ preferred_strategy: @preferred_strategy,
73
+ related_domains: @related_domains,
74
+ created_at: @created_at
75
+ }
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module MetaLearning
6
+ module Helpers
7
+ class MetaLearningEngine
8
+ include Constants
9
+
10
+ attr_reader :domains, :strategies, :episodes
11
+
12
+ def initialize
13
+ @domains = {}
14
+ @strategies = {}
15
+ @episodes = []
16
+ end
17
+
18
+ def create_domain(name:, learning_rate: DEFAULT_LEARNING_RATE, related_domains: [])
19
+ return { error: :limit_reached } if @domains.size >= MAX_DOMAINS
20
+
21
+ domain = LearningDomain.new(name: name, learning_rate: learning_rate, related_domains: related_domains)
22
+ @domains[domain.id] = domain
23
+ domain
24
+ end
25
+
26
+ def create_strategy(name:, strategy_type:)
27
+ return { error: :limit_reached } if @strategies.size >= MAX_STRATEGIES
28
+ return { error: :invalid_strategy_type } unless STRATEGY_TYPES.include?(strategy_type)
29
+
30
+ strategy = Strategy.new(name: name, strategy_type: strategy_type)
31
+ @strategies[strategy.id] = strategy
32
+ strategy
33
+ end
34
+
35
+ def record_episode(domain_id:, success:, strategy_id: nil)
36
+ domain = @domains[domain_id]
37
+ return { error: :domain_not_found } unless domain
38
+
39
+ success ? domain.record_success! : domain.record_failure!
40
+
41
+ strategy = @strategies[strategy_id] if strategy_id
42
+ strategy&.use!(success: success, domain_name: domain.name)
43
+
44
+ if strategy && success
45
+ current_preferred_rate = preferred_strategy_rate_for(domain)
46
+ domain.preferred_strategy = strategy.name if strategy.success_rate > current_preferred_rate
47
+ end
48
+
49
+ episode = build_episode(domain, strategy_id, success)
50
+ @episodes << episode
51
+ @episodes.shift while @episodes.size > MAX_EPISODES
52
+
53
+ check_transfer_opportunities(domain)
54
+
55
+ episode
56
+ end
57
+
58
+ def recommend_strategy(domain_id:)
59
+ domain = @domains[domain_id]
60
+ return { error: :domain_not_found } unless domain
61
+
62
+ candidate = best_strategy_for_domain(domain)
63
+ return { recommendation: nil, reason: :no_data } if candidate.nil?
64
+
65
+ { recommendation: candidate.name, strategy_id: candidate.id, success_rate: candidate.success_rate }
66
+ end
67
+
68
+ def transfer_check(source_domain_id:, target_domain_id:)
69
+ source = @domains[source_domain_id]
70
+ target = @domains[target_domain_id]
71
+ return { error: :domain_not_found } unless source && target
72
+
73
+ eligible = source.proficiency >= 0.6 && target.related_domains.include?(source.name)
74
+ { eligible: eligible, source_proficiency: source.proficiency, target_domain: target.name }
75
+ end
76
+
77
+ def apply_transfer(source_domain_id:, target_domain_id:)
78
+ source = @domains[source_domain_id]
79
+ target = @domains[target_domain_id]
80
+ return { error: :domain_not_found } unless source && target
81
+
82
+ check = transfer_check(source_domain_id: source_domain_id, target_domain_id: target_domain_id)
83
+ return { applied: false, reason: :not_eligible } unless check[:eligible]
84
+
85
+ target.adapt_rate!(delta: TRANSFER_BONUS)
86
+ { applied: true, target_domain: target.name, new_learning_rate: target.learning_rate }
87
+ end
88
+
89
+ def domain_ranking(limit: 10)
90
+ @domains.values
91
+ .sort_by { |d| -d.proficiency }
92
+ .first(limit)
93
+ .map(&:to_h)
94
+ end
95
+
96
+ def strategy_ranking(limit: 10)
97
+ @strategies.values
98
+ .sort_by { |s| -s.success_rate }
99
+ .first(limit)
100
+ .map(&:to_h)
101
+ end
102
+
103
+ def overall_efficiency
104
+ return 0.0 if @domains.empty?
105
+
106
+ total = @domains.values.sum(&:efficiency)
107
+ (total / @domains.size).round(10)
108
+ end
109
+
110
+ def learning_curve(domain_id:)
111
+ domain = @domains[domain_id]
112
+ return { error: :domain_not_found } unless domain
113
+
114
+ domain_episodes = @episodes.select { |e| e[:domain_id] == domain_id }
115
+ { domain: domain.name, curve: domain_episodes }
116
+ end
117
+
118
+ def adapt_rates
119
+ adapted = []
120
+ @domains.each_value do |domain|
121
+ next if domain.episodes_count.zero?
122
+
123
+ if domain.efficiency >= 0.8
124
+ domain.adapt_rate!(delta: RATE_BOOST)
125
+ adapted << { domain: domain.name, direction: :boost, new_rate: domain.learning_rate }
126
+ elsif domain.efficiency < 0.2
127
+ domain.adapt_rate!(delta: -RATE_DECAY)
128
+ adapted << { domain: domain.name, direction: :decay, new_rate: domain.learning_rate }
129
+ end
130
+ end
131
+ { adapted: adapted, count: adapted.size }
132
+ end
133
+
134
+ def prune_stale_domains(min_episodes: 1)
135
+ before = @domains.size
136
+ @domains.reject! { |_, d| d.episodes_count < min_episodes }
137
+ pruned = before - @domains.size
138
+ { pruned: pruned, remaining: @domains.size }
139
+ end
140
+
141
+ def to_h
142
+ {
143
+ domain_count: @domains.size,
144
+ strategy_count: @strategies.size,
145
+ episode_count: @episodes.size,
146
+ overall_efficiency: overall_efficiency,
147
+ top_domain: @domains.values.max_by(&:proficiency)&.name,
148
+ top_strategy: @strategies.values.max_by(&:success_rate)&.name
149
+ }
150
+ end
151
+
152
+ private
153
+
154
+ def build_episode(domain, strategy_id, success)
155
+ {
156
+ id: SecureRandom.uuid,
157
+ domain_id: domain.id,
158
+ domain_name: domain.name,
159
+ strategy_id: strategy_id,
160
+ success: success,
161
+ proficiency: domain.proficiency,
162
+ recorded_at: Time.now.utc
163
+ }
164
+ end
165
+
166
+ def preferred_strategy_rate_for(domain)
167
+ return 0.0 unless domain.preferred_strategy
168
+
169
+ strategy = @strategies.values.find { |s| s.name == domain.preferred_strategy }
170
+ strategy&.success_rate || 0.0
171
+ end
172
+
173
+ def best_strategy_for_domain(domain)
174
+ direct = strategies_used_in_domain(domain.name)
175
+ return direct.max_by(&:success_rate) if direct.any?
176
+
177
+ related_strategies = domain.related_domains.flat_map { |rname| strategies_used_in_domain(rname) }.uniq
178
+ related_strategies.max_by(&:success_rate)
179
+ end
180
+
181
+ def strategies_used_in_domain(domain_name)
182
+ @strategies.values.select { |s| s.domains_used.include?(domain_name) }
183
+ end
184
+
185
+ def check_transfer_opportunities(domain)
186
+ @domains.each_value do |target|
187
+ next if target.id == domain.id
188
+ next unless target.related_domains.include?(domain.name)
189
+
190
+ check = transfer_check(source_domain_id: domain.id, target_domain_id: target.id)
191
+ apply_transfer(source_domain_id: domain.id, target_domain_id: target.id) if check[:eligible]
192
+ end
193
+ end
194
+ end
195
+ end
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module MetaLearning
8
+ module Helpers
9
+ class Strategy
10
+ include Constants
11
+
12
+ attr_reader :id, :name, :strategy_type, :usage_count,
13
+ :success_count, :domains_used, :created_at
14
+
15
+ def initialize(name:, strategy_type:)
16
+ @id = SecureRandom.uuid
17
+ @name = name
18
+ @strategy_type = strategy_type
19
+ @usage_count = 0
20
+ @success_count = 0
21
+ @domains_used = []
22
+ @created_at = Time.now.utc
23
+ end
24
+
25
+ def use!(success:, domain_name: nil)
26
+ @usage_count += 1
27
+ @success_count += 1 if success
28
+ @domains_used << domain_name if domain_name && !@domains_used.include?(domain_name)
29
+ end
30
+
31
+ def success_rate
32
+ return 0.0 if @usage_count.zero?
33
+
34
+ (@success_count.to_f / @usage_count).round(10)
35
+ end
36
+
37
+ def versatility
38
+ @domains_used.uniq.size
39
+ end
40
+
41
+ def to_h
42
+ {
43
+ id: @id,
44
+ name: @name,
45
+ strategy_type: @strategy_type,
46
+ usage_count: @usage_count,
47
+ success_count: @success_count,
48
+ success_rate: success_rate,
49
+ versatility: versatility,
50
+ domains_used: @domains_used,
51
+ created_at: @created_at
52
+ }
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module MetaLearning
6
+ module Runners
7
+ module MetaLearning
8
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
9
+ Legion::Extensions::Helpers.const_defined?(:Lex)
10
+
11
+ def create_learning_domain(name:, learning_rate: Helpers::Constants::DEFAULT_LEARNING_RATE,
12
+ related_domains: [], **)
13
+ result = engine.create_domain(name: name, learning_rate: learning_rate, related_domains: related_domains)
14
+ if result.is_a?(Hash) && result[:error]
15
+ Legion::Logging.warn "[meta_learning] create_domain failed: #{result[:error]}"
16
+ return result
17
+ end
18
+
19
+ Legion::Logging.debug "[meta_learning] domain created: #{result.name} id=#{result.id[0..7]}"
20
+ result.to_h
21
+ end
22
+
23
+ def register_learning_strategy(name:, strategy_type:, **)
24
+ result = engine.create_strategy(name: name, strategy_type: strategy_type)
25
+ if result.is_a?(Hash) && result[:error]
26
+ Legion::Logging.warn "[meta_learning] create_strategy failed: #{result[:error]}"
27
+ return result
28
+ end
29
+
30
+ Legion::Logging.debug "[meta_learning] strategy registered: #{result.name} type=#{result.strategy_type}"
31
+ result.to_h
32
+ end
33
+
34
+ def record_learning_episode(domain_id:, success:, strategy_id: nil, **)
35
+ result = engine.record_episode(domain_id: domain_id, strategy_id: strategy_id, success: success)
36
+ if result.is_a?(Hash) && result[:error]
37
+ Legion::Logging.warn "[meta_learning] record_episode failed: #{result[:error]}"
38
+ return result
39
+ end
40
+
41
+ Legion::Logging.debug "[meta_learning] episode recorded domain=#{result[:domain_name]} " \
42
+ "success=#{success} proficiency=#{result[:proficiency].round(4)}"
43
+ result
44
+ end
45
+
46
+ def recommend_learning_strategy(domain_id:, **)
47
+ result = engine.recommend_strategy(domain_id: domain_id)
48
+ Legion::Logging.debug "[meta_learning] strategy recommendation domain=#{domain_id[0..7]} " \
49
+ "recommendation=#{result[:recommendation]}"
50
+ result
51
+ end
52
+
53
+ def check_transfer_learning(source_domain_id:, target_domain_id:, **)
54
+ result = engine.transfer_check(source_domain_id: source_domain_id, target_domain_id: target_domain_id)
55
+ Legion::Logging.debug "[meta_learning] transfer check eligible=#{result[:eligible]}"
56
+ result
57
+ end
58
+
59
+ def apply_transfer_bonus(source_domain_id:, target_domain_id:, **)
60
+ result = engine.apply_transfer(source_domain_id: source_domain_id, target_domain_id: target_domain_id)
61
+ Legion::Logging.info "[meta_learning] transfer applied=#{result[:applied]}"
62
+ result
63
+ end
64
+
65
+ def learning_domain_ranking(limit: 10, **)
66
+ ranking = engine.domain_ranking(limit: limit)
67
+ Legion::Logging.debug "[meta_learning] domain ranking returned #{ranking.size} domains"
68
+ { ranking: ranking, count: ranking.size }
69
+ end
70
+
71
+ def learning_strategy_ranking(limit: 10, **)
72
+ ranking = engine.strategy_ranking(limit: limit)
73
+ Legion::Logging.debug "[meta_learning] strategy ranking returned #{ranking.size} strategies"
74
+ { ranking: ranking, count: ranking.size }
75
+ end
76
+
77
+ def learning_curve_report(domain_id:, **)
78
+ result = engine.learning_curve(domain_id: domain_id)
79
+ if result.is_a?(Hash) && result[:error]
80
+ Legion::Logging.warn "[meta_learning] learning_curve failed: #{result[:error]}"
81
+ return result
82
+ end
83
+
84
+ Legion::Logging.debug "[meta_learning] learning curve domain=#{result[:domain]} " \
85
+ "episodes=#{result[:curve].size}"
86
+ result
87
+ end
88
+
89
+ def update_meta_learning(**)
90
+ adapt_result = engine.adapt_rates
91
+ prune_result = engine.prune_stale_domains
92
+ stats = engine.to_h
93
+ Legion::Logging.info "[meta_learning] update: adapted=#{adapt_result[:count]} " \
94
+ "pruned=#{prune_result[:pruned]} domains=#{stats[:domain_count]}"
95
+ { adapt: adapt_result, prune: prune_result, stats: stats }
96
+ end
97
+
98
+ def meta_learning_stats(**)
99
+ stats = engine.to_h
100
+ Legion::Logging.debug "[meta_learning] stats domains=#{stats[:domain_count]} " \
101
+ "strategies=#{stats[:strategy_count]} efficiency=#{stats[:overall_efficiency]}"
102
+ stats
103
+ end
104
+
105
+ private
106
+
107
+ def engine
108
+ @engine ||= Helpers::MetaLearningEngine.new
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module MetaLearning
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/meta_learning/version'
4
+ require 'legion/extensions/meta_learning/helpers/constants'
5
+ require 'legion/extensions/meta_learning/helpers/learning_domain'
6
+ require 'legion/extensions/meta_learning/helpers/strategy'
7
+ require 'legion/extensions/meta_learning/helpers/meta_learning_engine'
8
+ require 'legion/extensions/meta_learning/runners/meta_learning'
9
+ require 'legion/extensions/meta_learning/client'
10
+
11
+ module Legion
12
+ module Extensions
13
+ module MetaLearning
14
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/meta_learning/client'
4
+
5
+ RSpec.describe Legion::Extensions::MetaLearning::Client do
6
+ it 'responds to all runner methods' do
7
+ client = described_class.new
8
+ expect(client).to respond_to(:create_learning_domain)
9
+ expect(client).to respond_to(:register_learning_strategy)
10
+ expect(client).to respond_to(:record_learning_episode)
11
+ expect(client).to respond_to(:recommend_learning_strategy)
12
+ expect(client).to respond_to(:check_transfer_learning)
13
+ expect(client).to respond_to(:apply_transfer_bonus)
14
+ expect(client).to respond_to(:learning_domain_ranking)
15
+ expect(client).to respond_to(:learning_strategy_ranking)
16
+ expect(client).to respond_to(:learning_curve_report)
17
+ expect(client).to respond_to(:update_meta_learning)
18
+ expect(client).to respond_to(:meta_learning_stats)
19
+ end
20
+
21
+ it 'maintains isolated state per instance' do
22
+ c1 = described_class.new
23
+ c2 = described_class.new
24
+ c1.create_learning_domain(name: 'ruby')
25
+ expect(c2.meta_learning_stats[:domain_count]).to eq(0)
26
+ end
27
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::MetaLearning::Helpers::Constants do
4
+ it 'defines MAX_DOMAINS as 100' do
5
+ expect(described_module::MAX_DOMAINS).to eq(100)
6
+ end
7
+
8
+ it 'defines MAX_STRATEGIES as 50' do
9
+ expect(described_module::MAX_STRATEGIES).to eq(50)
10
+ end
11
+
12
+ it 'defines MAX_EPISODES as 1000' do
13
+ expect(described_module::MAX_EPISODES).to eq(1000)
14
+ end
15
+
16
+ it 'defines DEFAULT_LEARNING_RATE as 0.1' do
17
+ expect(described_module::DEFAULT_LEARNING_RATE).to eq(0.1)
18
+ end
19
+
20
+ it 'defines STRATEGY_TYPES as an array of symbols' do
21
+ expect(described_module::STRATEGY_TYPES).to be_an(Array)
22
+ expect(described_module::STRATEGY_TYPES).to all(be_a(Symbol))
23
+ expect(described_module::STRATEGY_TYPES).to include(:repetition, :elaboration, :retrieval_practice)
24
+ end
25
+
26
+ it 'defines PROFICIENCY_LABELS covering full 0..1 range' do
27
+ labels = [0.0, 0.1, 0.25, 0.45, 0.65, 0.85].map do |v|
28
+ described_module::PROFICIENCY_LABELS.find { |range, _| range.cover?(v) }&.last
29
+ end
30
+ expect(labels).to all(be_a(Symbol))
31
+ end
32
+
33
+ it 'defines EFFICIENCY_LABELS covering full 0..1 range' do
34
+ labels = [0.0, 0.1, 0.25, 0.45, 0.65, 0.85].map do |v|
35
+ described_module::EFFICIENCY_LABELS.find { |range, _| range.cover?(v) }&.last
36
+ end
37
+ expect(labels).to all(be_a(Symbol))
38
+ end
39
+
40
+ def described_module
41
+ Legion::Extensions::MetaLearning::Helpers::Constants
42
+ end
43
+ end