lex-transfer-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: f75d8ed4ba9863fb0509ade3cc4f3c1ab7e4cdfdc61c63a39377c01929ca2bc1
4
+ data.tar.gz: 6a6fb03022582a040beebbffa0d28411788a9498091628366330ac7b538c90e0
5
+ SHA512:
6
+ metadata.gz: 117eb74b067523792cef8196ebec35d595b0762301c156ceb025ccf773cda5a99a3eec98df59e607dbbee7a98e6ec182f6876e4c6d39a4374fee01ddefb1d5a1
7
+ data.tar.gz: fb49e00c94a694769812012d05877f786377fee2c9db7c9951365e6b9cfd81a886cf26391bd17e483afc4766e54b462d8d4504700577213cfbf579765906f69b
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,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/legion/extensions/transfer_learning/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'lex-transfer-learning'
7
+ spec.version = Legion::Extensions::TransferLearning::VERSION
8
+ spec.authors = ['Esity']
9
+ spec.email = ['matthewdiverson@gmail.com']
10
+
11
+ spec.summary = 'LEX Transfer Learning'
12
+ spec.description = 'Domain knowledge transfer modeling for brain-modeled agentic AI'
13
+ spec.homepage = 'https://github.com/LegionIO/lex-transfer-learning'
14
+ spec.license = 'MIT'
15
+ spec.required_ruby_version = '>= 3.4'
16
+
17
+ spec.metadata['homepage_uri'] = spec.homepage
18
+ spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-transfer-learning'
19
+ spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-transfer-learning'
20
+ spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-transfer-learning'
21
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-transfer-learning/issues'
22
+ spec.metadata['rubygems_mfa_required'] = 'true'
23
+
24
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
25
+ Dir.glob('{lib,spec}/**/*') + %w[lex-transfer-learning.gemspec Gemfile]
26
+ end
27
+ spec.require_paths = ['lib']
28
+ spec.add_development_dependency 'legion-gaia'
29
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/transfer_learning/helpers/constants'
4
+ require 'legion/extensions/transfer_learning/helpers/domain_knowledge'
5
+ require 'legion/extensions/transfer_learning/helpers/transfer_engine'
6
+ require 'legion/extensions/transfer_learning/runners/transfer_learning'
7
+
8
+ module Legion
9
+ module Extensions
10
+ module TransferLearning
11
+ class Client
12
+ include Runners::TransferLearning
13
+
14
+ def initialize(**)
15
+ @transfer_engine = Helpers::TransferEngine.new
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :transfer_engine
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module TransferLearning
6
+ module Helpers
7
+ module Constants
8
+ MAX_DOMAINS = 200
9
+ POSITIVE_TRANSFER_THRESHOLD = 0.6
10
+ NEGATIVE_TRANSFER_THRESHOLD = 0.3
11
+ TRANSFER_BOOST = 0.15
12
+ INTERFERENCE_PENALTY = 0.1
13
+
14
+ TRANSFER_LABELS = {
15
+ positive: 'positive',
16
+ neutral: 'neutral',
17
+ negative: 'negative',
18
+ interference: 'interference'
19
+ }.freeze
20
+
21
+ DISTANCE_LABELS = {
22
+ near: 'near',
23
+ moderate: 'moderate',
24
+ far: 'far'
25
+ }.freeze
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module TransferLearning
8
+ module Helpers
9
+ class DomainKnowledge
10
+ attr_reader :id, :domain, :proficiency, :learn_count, :transfer_count
11
+
12
+ PROFICIENCY_LABELS = [
13
+ [0.0, 0.2, 'novice'],
14
+ [0.2, 0.4, 'beginner'],
15
+ [0.4, 0.6, 'intermediate'],
16
+ [0.6, 0.8, 'advanced'],
17
+ [0.8, 1.01, 'expert']
18
+ ].freeze
19
+
20
+ def initialize(domain:)
21
+ @id = SecureRandom.uuid
22
+ @domain = domain
23
+ @proficiency = 0.0
24
+ @learn_count = 0
25
+ @transfer_count = 0
26
+ end
27
+
28
+ def learn!(amount:)
29
+ @proficiency = (@proficiency + amount).clamp(0.0, 1.0).round(10)
30
+ @learn_count += 1
31
+ self
32
+ end
33
+
34
+ def record_transfer!
35
+ @transfer_count += 1
36
+ self
37
+ end
38
+
39
+ def apply_boost!(amount)
40
+ @proficiency = (@proficiency + amount).clamp(0.0, 1.0).round(10)
41
+ self
42
+ end
43
+
44
+ def apply_penalty!(amount)
45
+ @proficiency = (@proficiency - amount).clamp(0.0, 1.0).round(10)
46
+ self
47
+ end
48
+
49
+ def proficiency_label
50
+ PROFICIENCY_LABELS.each do |low, high, label|
51
+ return label if @proficiency >= low && @proficiency < high
52
+ end
53
+ 'novice'
54
+ end
55
+
56
+ def to_h
57
+ {
58
+ id: @id,
59
+ domain: @domain,
60
+ proficiency: @proficiency.round(10),
61
+ proficiency_label: proficiency_label,
62
+ learn_count: @learn_count,
63
+ transfer_count: @transfer_count
64
+ }
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module TransferLearning
6
+ module Helpers
7
+ class TransferEngine
8
+ include Constants
9
+
10
+ attr_reader :domains, :similarities, :transfer_history
11
+
12
+ def initialize
13
+ @domains = {}
14
+ @similarities = {}
15
+ @transfer_history = []
16
+ end
17
+
18
+ def set_similarity(domain_a:, domain_b:, similarity:)
19
+ sim = similarity.clamp(0.0, 1.0).round(10)
20
+ @similarities[similarity_key(domain_a, domain_b)] = sim
21
+ sim
22
+ end
23
+
24
+ def learn_domain(domain:, amount:)
25
+ check_domain_limit!
26
+ entry = get_or_create_domain(domain)
27
+ entry.learn!(amount: amount.clamp(0.0, 1.0))
28
+ entry.to_h
29
+ end
30
+
31
+ def attempt_transfer(from_domain:, to_domain:)
32
+ source = @domains[from_domain]
33
+ return { status: :source_not_found, from_domain: from_domain } unless source
34
+
35
+ check_domain_limit!
36
+ target = get_or_create_domain(to_domain)
37
+ sim = similarity_between(from_domain, to_domain)
38
+ type = transfer_type(sim)
39
+ distance = transfer_distance(sim)
40
+ effect = apply_transfer_effect!(target, source, type)
41
+ source.record_transfer!
42
+ record_history!(from_domain, to_domain, sim, type, distance, effect)
43
+ build_transfer_result(from_domain, to_domain, sim, type, distance, effect, target)
44
+ end
45
+
46
+ def transfer_effectiveness(from_domain:, to_domain:)
47
+ sim = similarity_between(from_domain, to_domain)
48
+ build_effectiveness(from_domain, to_domain, sim, transfer_type(sim),
49
+ @domains[from_domain], @domains[to_domain])
50
+ end
51
+
52
+ def most_transferable(target_domain:, limit: 5)
53
+ candidates = candidate_rows(target_domain)
54
+ candidates.select { |r| r[:type] == :positive }
55
+ .sort_by { |r| -r[:similarity] }
56
+ .first(limit)
57
+ end
58
+
59
+ def interference_risks(target_domain:)
60
+ candidate_rows(target_domain)
61
+ .select { |r| r[:type] == :interference }
62
+ .sort_by { |r| -r[:similarity] }
63
+ end
64
+
65
+ def transfer_report
66
+ {
67
+ total_transfers: @transfer_history.size,
68
+ positive_transfers: @transfer_history.count { |h| h[:type] == :positive },
69
+ negative_transfers: @transfer_history.count { |h| h[:type] == :negative },
70
+ neutral_transfers: @transfer_history.count { |h| h[:type] == :neutral },
71
+ interference_events: @transfer_history.count { |h| h[:type] == :interference },
72
+ domain_count: @domains.size,
73
+ similarity_pairs: @similarities.size
74
+ }
75
+ end
76
+
77
+ def to_h
78
+ {
79
+ domains: @domains.transform_values(&:to_h),
80
+ similarities: @similarities.dup,
81
+ transfer_history: @transfer_history.dup,
82
+ report: transfer_report
83
+ }
84
+ end
85
+
86
+ private
87
+
88
+ def get_or_create_domain(name)
89
+ @domains[name] ||= DomainKnowledge.new(domain: name)
90
+ end
91
+
92
+ def similarity_key(domain_a, domain_b)
93
+ [domain_a, domain_b].sort.join(':')
94
+ end
95
+
96
+ def similarity_between(domain_a, domain_b)
97
+ @similarities[similarity_key(domain_a, domain_b)] || 0.0
98
+ end
99
+
100
+ def transfer_type(similarity)
101
+ if similarity >= POSITIVE_TRANSFER_THRESHOLD then :positive
102
+ elsif similarity >= NEGATIVE_TRANSFER_THRESHOLD then :interference
103
+ elsif similarity > 0.0 then :negative
104
+ else :neutral
105
+ end
106
+ end
107
+
108
+ def transfer_distance(similarity)
109
+ if similarity >= POSITIVE_TRANSFER_THRESHOLD then :near
110
+ elsif similarity >= NEGATIVE_TRANSFER_THRESHOLD then :moderate
111
+ else :far
112
+ end
113
+ end
114
+
115
+ def apply_transfer_effect!(target, source, type)
116
+ case type
117
+ when :positive
118
+ scaled = (source.proficiency * TRANSFER_BOOST).round(10)
119
+ target.apply_boost!(scaled)
120
+ scaled
121
+ when :interference
122
+ penalty = (source.proficiency * INTERFERENCE_PENALTY).round(10)
123
+ target.apply_penalty!(penalty)
124
+ -penalty
125
+ else
126
+ 0.0
127
+ end
128
+ end
129
+
130
+ def record_history!(from_domain, to_domain, sim, type, distance, effect)
131
+ @transfer_history << {
132
+ from_domain: from_domain,
133
+ to_domain: to_domain,
134
+ similarity: sim.round(10),
135
+ type: type,
136
+ distance: distance,
137
+ effect: effect,
138
+ at: Time.now.utc
139
+ }
140
+ end
141
+
142
+ def check_domain_limit!
143
+ raise "domain limit of #{MAX_DOMAINS} reached" if @domains.size >= MAX_DOMAINS
144
+ end
145
+
146
+ def candidate_rows(target_domain)
147
+ @domains.reject { |name, _| name == target_domain }.map do |name, entry|
148
+ sim = similarity_between(name, target_domain)
149
+ type = transfer_type(sim)
150
+ { domain: name, proficiency: entry.proficiency.round(10), similarity: sim.round(10), type: type }
151
+ end
152
+ end
153
+
154
+ def build_transfer_result(from_domain, to_domain, sim, type, distance, effect, target)
155
+ {
156
+ status: :ok,
157
+ from_domain: from_domain,
158
+ to_domain: to_domain,
159
+ similarity: sim.round(10),
160
+ type: type,
161
+ distance: distance,
162
+ effect: effect,
163
+ proficiency: target.proficiency.round(10)
164
+ }
165
+ end
166
+
167
+ def build_effectiveness(from_domain, to_domain, sim, type, source, target)
168
+ {
169
+ from_domain: from_domain,
170
+ to_domain: to_domain,
171
+ similarity: sim.round(10),
172
+ type: type,
173
+ type_label: TRANSFER_LABELS[type],
174
+ distance: transfer_distance(sim),
175
+ distance_label: DISTANCE_LABELS[transfer_distance(sim)],
176
+ source_proficiency: source&.proficiency&.round(10) || 0.0,
177
+ target_proficiency: target&.proficiency&.round(10) || 0.0
178
+ }
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module TransferLearning
6
+ module Runners
7
+ module TransferLearning
8
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
9
+ Legion::Extensions::Helpers.const_defined?(:Lex)
10
+
11
+ def learn_domain(domain:, amount:, **)
12
+ result = transfer_engine.learn_domain(domain: domain, amount: amount)
13
+ Legion::Logging.info "[transfer_learning] learn: domain=#{domain} amount=#{amount} proficiency=#{result[:proficiency]}"
14
+ result
15
+ end
16
+
17
+ def attempt_transfer(from_domain:, to_domain:, **)
18
+ result = transfer_engine.attempt_transfer(from_domain: from_domain, to_domain: to_domain)
19
+ Legion::Logging.info "[transfer_learning] transfer: from=#{from_domain} to=#{to_domain} type=#{result[:type]} effect=#{result[:effect]}"
20
+ result
21
+ end
22
+
23
+ def set_similarity(domain_a:, domain_b:, similarity:, **)
24
+ sim = transfer_engine.set_similarity(domain_a: domain_a, domain_b: domain_b, similarity: similarity)
25
+ Legion::Logging.debug "[transfer_learning] similarity set: #{domain_a}<->#{domain_b} similarity=#{sim}"
26
+ { domain_a: domain_a, domain_b: domain_b, similarity: sim }
27
+ end
28
+
29
+ def transfer_effectiveness(from_domain:, to_domain:, **)
30
+ result = transfer_engine.transfer_effectiveness(from_domain: from_domain, to_domain: to_domain)
31
+ Legion::Logging.debug "[transfer_learning] effectiveness: from=#{from_domain} to=#{to_domain} type=#{result[:type]}"
32
+ result
33
+ end
34
+
35
+ def most_transferable(target_domain:, limit: 5, **)
36
+ candidates = transfer_engine.most_transferable(target_domain: target_domain, limit: limit)
37
+ Legion::Logging.debug "[transfer_learning] most_transferable: target=#{target_domain} found=#{candidates.size}"
38
+ { target_domain: target_domain, candidates: candidates, count: candidates.size }
39
+ end
40
+
41
+ def interference_risks(target_domain:, **)
42
+ risks = transfer_engine.interference_risks(target_domain: target_domain)
43
+ Legion::Logging.debug "[transfer_learning] interference_risks: target=#{target_domain} risks=#{risks.size}"
44
+ { target_domain: target_domain, risks: risks, count: risks.size }
45
+ end
46
+
47
+ def transfer_report(**)
48
+ report = transfer_engine.transfer_report
49
+ Legion::Logging.debug "[transfer_learning] report: domains=#{report[:domain_count]} transfers=#{report[:total_transfers]}"
50
+ report
51
+ end
52
+
53
+ def get_domain(domain:, **)
54
+ entry = transfer_engine.domains[domain]
55
+ if entry
56
+ Legion::Logging.debug "[transfer_learning] get_domain: domain=#{domain} proficiency=#{entry.proficiency}"
57
+ { found: true, domain: entry.to_h }
58
+ else
59
+ Legion::Logging.debug "[transfer_learning] get_domain: domain=#{domain} not found"
60
+ { found: false, domain: domain }
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ def transfer_engine
67
+ @transfer_engine ||= Helpers::TransferEngine.new
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module TransferLearning
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/transfer_learning/version'
4
+ require 'legion/extensions/transfer_learning/helpers/constants'
5
+ require 'legion/extensions/transfer_learning/helpers/domain_knowledge'
6
+ require 'legion/extensions/transfer_learning/helpers/transfer_engine'
7
+ require 'legion/extensions/transfer_learning/runners/transfer_learning'
8
+
9
+ module Legion
10
+ module Extensions
11
+ module TransferLearning
12
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/transfer_learning/client'
4
+
5
+ RSpec.describe Legion::Extensions::TransferLearning::Client do
6
+ let(:client) { described_class.new }
7
+
8
+ it 'responds to all runner methods' do
9
+ expect(client).to respond_to(:learn_domain)
10
+ expect(client).to respond_to(:attempt_transfer)
11
+ expect(client).to respond_to(:set_similarity)
12
+ expect(client).to respond_to(:transfer_effectiveness)
13
+ expect(client).to respond_to(:most_transferable)
14
+ expect(client).to respond_to(:interference_risks)
15
+ expect(client).to respond_to(:transfer_report)
16
+ expect(client).to respond_to(:get_domain)
17
+ end
18
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::TransferLearning::Helpers::Constants do
4
+ subject(:mod) { described_class }
5
+
6
+ it 'defines MAX_DOMAINS as 200' do
7
+ expect(mod::MAX_DOMAINS).to eq(200)
8
+ end
9
+
10
+ it 'defines POSITIVE_TRANSFER_THRESHOLD as 0.6' do
11
+ expect(mod::POSITIVE_TRANSFER_THRESHOLD).to eq(0.6)
12
+ end
13
+
14
+ it 'defines NEGATIVE_TRANSFER_THRESHOLD as 0.3' do
15
+ expect(mod::NEGATIVE_TRANSFER_THRESHOLD).to eq(0.3)
16
+ end
17
+
18
+ it 'defines TRANSFER_BOOST as 0.15' do
19
+ expect(mod::TRANSFER_BOOST).to eq(0.15)
20
+ end
21
+
22
+ it 'defines INTERFERENCE_PENALTY as 0.1' do
23
+ expect(mod::INTERFERENCE_PENALTY).to eq(0.1)
24
+ end
25
+
26
+ it 'defines all four TRANSFER_LABELS' do
27
+ expect(mod::TRANSFER_LABELS.keys).to contain_exactly(:positive, :neutral, :negative, :interference)
28
+ end
29
+
30
+ it 'defines all three DISTANCE_LABELS' do
31
+ expect(mod::DISTANCE_LABELS.keys).to contain_exactly(:near, :moderate, :far)
32
+ end
33
+
34
+ it 'freezes TRANSFER_LABELS' do
35
+ expect(mod::TRANSFER_LABELS).to be_frozen
36
+ end
37
+
38
+ it 'freezes DISTANCE_LABELS' do
39
+ expect(mod::DISTANCE_LABELS).to be_frozen
40
+ end
41
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::TransferLearning::Helpers::DomainKnowledge do
4
+ subject(:dk) { described_class.new(domain: :ruby) }
5
+
6
+ describe '#initialize' do
7
+ it 'assigns a uuid id' do
8
+ expect(dk.id).to match(/\A[0-9a-f-]{36}\z/)
9
+ end
10
+
11
+ it 'sets domain' do
12
+ expect(dk.domain).to eq(:ruby)
13
+ end
14
+
15
+ it 'starts proficiency at 0.0' do
16
+ expect(dk.proficiency).to eq(0.0)
17
+ end
18
+
19
+ it 'starts learn_count at 0' do
20
+ expect(dk.learn_count).to eq(0)
21
+ end
22
+
23
+ it 'starts transfer_count at 0' do
24
+ expect(dk.transfer_count).to eq(0)
25
+ end
26
+ end
27
+
28
+ describe '#learn!' do
29
+ it 'increases proficiency by amount' do
30
+ dk.learn!(amount: 0.3)
31
+ expect(dk.proficiency).to eq(0.3)
32
+ end
33
+
34
+ it 'increments learn_count' do
35
+ dk.learn!(amount: 0.1)
36
+ expect(dk.learn_count).to eq(1)
37
+ end
38
+
39
+ it 'clamps proficiency at 1.0' do
40
+ dk.learn!(amount: 1.5)
41
+ expect(dk.proficiency).to eq(1.0)
42
+ end
43
+
44
+ it 'clamps proficiency at 0.0 for negative amount' do
45
+ dk.learn!(amount: -0.5)
46
+ expect(dk.proficiency).to eq(0.0)
47
+ end
48
+
49
+ it 'returns self for chaining' do
50
+ expect(dk.learn!(amount: 0.1)).to be(dk)
51
+ end
52
+ end
53
+
54
+ describe '#record_transfer!' do
55
+ it 'increments transfer_count' do
56
+ dk.record_transfer!
57
+ expect(dk.transfer_count).to eq(1)
58
+ end
59
+
60
+ it 'returns self' do
61
+ expect(dk.record_transfer!).to be(dk)
62
+ end
63
+ end
64
+
65
+ describe '#apply_boost!' do
66
+ it 'increases proficiency' do
67
+ dk.learn!(amount: 0.5)
68
+ dk.apply_boost!(0.2)
69
+ expect(dk.proficiency).to be_within(0.0001).of(0.7)
70
+ end
71
+
72
+ it 'clamps at 1.0' do
73
+ dk.learn!(amount: 0.9)
74
+ dk.apply_boost!(0.5)
75
+ expect(dk.proficiency).to eq(1.0)
76
+ end
77
+ end
78
+
79
+ describe '#apply_penalty!' do
80
+ it 'decreases proficiency' do
81
+ dk.learn!(amount: 0.5)
82
+ dk.apply_penalty!(0.2)
83
+ expect(dk.proficiency).to be_within(0.0001).of(0.3)
84
+ end
85
+
86
+ it 'clamps at 0.0' do
87
+ dk.apply_penalty!(0.5)
88
+ expect(dk.proficiency).to eq(0.0)
89
+ end
90
+ end
91
+
92
+ describe '#proficiency_label' do
93
+ it 'returns novice for 0.0' do
94
+ expect(dk.proficiency_label).to eq('novice')
95
+ end
96
+
97
+ it 'returns beginner for 0.3' do
98
+ dk.learn!(amount: 0.3)
99
+ expect(dk.proficiency_label).to eq('beginner')
100
+ end
101
+
102
+ it 'returns intermediate for 0.5' do
103
+ dk.learn!(amount: 0.5)
104
+ expect(dk.proficiency_label).to eq('intermediate')
105
+ end
106
+
107
+ it 'returns advanced for 0.7' do
108
+ dk.learn!(amount: 0.7)
109
+ expect(dk.proficiency_label).to eq('advanced')
110
+ end
111
+
112
+ it 'returns expert for 0.9' do
113
+ dk.learn!(amount: 0.9)
114
+ expect(dk.proficiency_label).to eq('expert')
115
+ end
116
+ end
117
+
118
+ describe '#to_h' do
119
+ it 'returns a hash with expected keys' do
120
+ hash = dk.to_h
121
+ expect(hash.keys).to contain_exactly(:id, :domain, :proficiency, :proficiency_label, :learn_count, :transfer_count)
122
+ end
123
+
124
+ it 'reflects current proficiency' do
125
+ dk.learn!(amount: 0.4)
126
+ expect(dk.to_h[:proficiency]).to eq(0.4)
127
+ end
128
+
129
+ it 'includes proficiency_label' do
130
+ dk.learn!(amount: 0.4)
131
+ expect(dk.to_h[:proficiency_label]).to eq('intermediate')
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,299 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::TransferLearning::Helpers::TransferEngine do
4
+ subject(:engine) { described_class.new }
5
+
6
+ describe '#initialize' do
7
+ it 'starts with empty domains' do
8
+ expect(engine.domains).to be_empty
9
+ end
10
+
11
+ it 'starts with empty similarities' do
12
+ expect(engine.similarities).to be_empty
13
+ end
14
+
15
+ it 'starts with empty transfer_history' do
16
+ expect(engine.transfer_history).to be_empty
17
+ end
18
+ end
19
+
20
+ describe '#set_similarity' do
21
+ it 'stores similarity between two domains' do
22
+ engine.set_similarity(domain_a: :ruby, domain_b: :python, similarity: 0.7)
23
+ expect(engine.similarities['python:ruby']).to eq(0.7)
24
+ end
25
+
26
+ it 'is commutative (same key regardless of order)' do
27
+ engine.set_similarity(domain_a: :ruby, domain_b: :python, similarity: 0.7)
28
+ engine.set_similarity(domain_a: :python, domain_b: :ruby, similarity: 0.8)
29
+ key = 'python:ruby'
30
+ expect(engine.similarities[key]).to eq(0.8)
31
+ end
32
+
33
+ it 'clamps similarity to 0.0..1.0' do
34
+ engine.set_similarity(domain_a: :a, domain_b: :b, similarity: 1.5)
35
+ expect(engine.similarities['a:b']).to eq(1.0)
36
+ end
37
+
38
+ it 'clamps negative similarity to 0.0' do
39
+ engine.set_similarity(domain_a: :a, domain_b: :b, similarity: -0.3)
40
+ expect(engine.similarities['a:b']).to eq(0.0)
41
+ end
42
+
43
+ it 'returns the clamped similarity' do
44
+ result = engine.set_similarity(domain_a: :a, domain_b: :b, similarity: 0.5)
45
+ expect(result).to eq(0.5)
46
+ end
47
+ end
48
+
49
+ describe '#learn_domain' do
50
+ it 'creates and returns domain knowledge' do
51
+ result = engine.learn_domain(domain: :ruby, amount: 0.4)
52
+ expect(result[:domain]).to eq(:ruby)
53
+ expect(result[:proficiency]).to eq(0.4)
54
+ end
55
+
56
+ it 'accumulates proficiency on repeated calls' do
57
+ engine.learn_domain(domain: :ruby, amount: 0.3)
58
+ result = engine.learn_domain(domain: :ruby, amount: 0.3)
59
+ expect(result[:proficiency]).to be_within(0.0001).of(0.6)
60
+ end
61
+
62
+ it 'increments learn_count' do
63
+ engine.learn_domain(domain: :ruby, amount: 0.1)
64
+ result = engine.learn_domain(domain: :ruby, amount: 0.1)
65
+ expect(result[:learn_count]).to eq(2)
66
+ end
67
+
68
+ it 'clamps amount before applying' do
69
+ result = engine.learn_domain(domain: :ruby, amount: 5.0)
70
+ expect(result[:proficiency]).to eq(1.0)
71
+ end
72
+ end
73
+
74
+ describe '#attempt_transfer' do
75
+ before do
76
+ engine.learn_domain(domain: :ruby, amount: 0.8)
77
+ end
78
+
79
+ it 'returns source_not_found when source domain does not exist' do
80
+ result = engine.attempt_transfer(from_domain: :unknown, to_domain: :python)
81
+ expect(result[:status]).to eq(:source_not_found)
82
+ end
83
+
84
+ context 'with positive similarity' do
85
+ before { engine.set_similarity(domain_a: :ruby, domain_b: :python, similarity: 0.8) }
86
+
87
+ it 'returns status :ok' do
88
+ result = engine.attempt_transfer(from_domain: :ruby, to_domain: :python)
89
+ expect(result[:status]).to eq(:ok)
90
+ end
91
+
92
+ it 'classifies as positive transfer' do
93
+ result = engine.attempt_transfer(from_domain: :ruby, to_domain: :python)
94
+ expect(result[:type]).to eq(:positive)
95
+ end
96
+
97
+ it 'classifies distance as near' do
98
+ result = engine.attempt_transfer(from_domain: :ruby, to_domain: :python)
99
+ expect(result[:distance]).to eq(:near)
100
+ end
101
+
102
+ it 'boosts target proficiency' do
103
+ result = engine.attempt_transfer(from_domain: :ruby, to_domain: :python)
104
+ expect(result[:effect]).to be > 0
105
+ expect(result[:proficiency]).to be > 0
106
+ end
107
+
108
+ it 'increments source transfer_count' do
109
+ engine.attempt_transfer(from_domain: :ruby, to_domain: :python)
110
+ expect(engine.domains[:ruby].transfer_count).to eq(1)
111
+ end
112
+
113
+ it 'records transfer in history' do
114
+ engine.attempt_transfer(from_domain: :ruby, to_domain: :python)
115
+ expect(engine.transfer_history.size).to eq(1)
116
+ expect(engine.transfer_history.first[:type]).to eq(:positive)
117
+ end
118
+ end
119
+
120
+ context 'with interference similarity (0.3..0.6)' do
121
+ before { engine.set_similarity(domain_a: :ruby, domain_b: :cobol, similarity: 0.4) }
122
+
123
+ it 'classifies as interference' do
124
+ result = engine.attempt_transfer(from_domain: :ruby, to_domain: :cobol)
125
+ expect(result[:type]).to eq(:interference)
126
+ end
127
+
128
+ it 'applies penalty to target' do
129
+ engine.learn_domain(domain: :cobol, amount: 0.5)
130
+ result = engine.attempt_transfer(from_domain: :ruby, to_domain: :cobol)
131
+ expect(result[:effect]).to be < 0
132
+ end
133
+
134
+ it 'classifies distance as moderate' do
135
+ result = engine.attempt_transfer(from_domain: :ruby, to_domain: :cobol)
136
+ expect(result[:distance]).to eq(:moderate)
137
+ end
138
+ end
139
+
140
+ context 'with low similarity (0.0..0.3)' do
141
+ before { engine.set_similarity(domain_a: :ruby, domain_b: :sql, similarity: 0.1) }
142
+
143
+ it 'classifies as negative transfer' do
144
+ result = engine.attempt_transfer(from_domain: :ruby, to_domain: :sql)
145
+ expect(result[:type]).to eq(:negative)
146
+ end
147
+
148
+ it 'applies zero effect (negative = no net change)' do
149
+ result = engine.attempt_transfer(from_domain: :ruby, to_domain: :sql)
150
+ expect(result[:effect]).to eq(0.0)
151
+ end
152
+
153
+ it 'classifies distance as far' do
154
+ result = engine.attempt_transfer(from_domain: :ruby, to_domain: :sql)
155
+ expect(result[:distance]).to eq(:far)
156
+ end
157
+ end
158
+
159
+ context 'with zero similarity (unknown domains)' do
160
+ it 'classifies as neutral transfer' do
161
+ result = engine.attempt_transfer(from_domain: :ruby, to_domain: :haskell)
162
+ expect(result[:type]).to eq(:neutral)
163
+ end
164
+ end
165
+ end
166
+
167
+ describe '#transfer_effectiveness' do
168
+ before do
169
+ engine.learn_domain(domain: :ruby, amount: 0.7)
170
+ engine.learn_domain(domain: :python, amount: 0.3)
171
+ engine.set_similarity(domain_a: :ruby, domain_b: :python, similarity: 0.75)
172
+ end
173
+
174
+ it 'returns effectiveness hash' do
175
+ result = engine.transfer_effectiveness(from_domain: :ruby, to_domain: :python)
176
+ expect(result[:type]).to eq(:positive)
177
+ expect(result[:type_label]).to eq('positive')
178
+ expect(result[:distance]).to eq(:near)
179
+ expect(result[:similarity]).to eq(0.75)
180
+ end
181
+
182
+ it 'returns source and target proficiency' do
183
+ result = engine.transfer_effectiveness(from_domain: :ruby, to_domain: :python)
184
+ expect(result[:source_proficiency]).to eq(0.7)
185
+ expect(result[:target_proficiency]).to eq(0.3)
186
+ end
187
+
188
+ it 'handles unknown domains gracefully' do
189
+ result = engine.transfer_effectiveness(from_domain: :unknown_a, to_domain: :unknown_b)
190
+ expect(result[:source_proficiency]).to eq(0.0)
191
+ expect(result[:target_proficiency]).to eq(0.0)
192
+ end
193
+ end
194
+
195
+ describe '#most_transferable' do
196
+ before do
197
+ engine.learn_domain(domain: :target, amount: 0.2)
198
+ engine.learn_domain(domain: :high_sim, amount: 0.8)
199
+ engine.learn_domain(domain: :low_sim, amount: 0.8)
200
+ engine.learn_domain(domain: :no_sim, amount: 0.8)
201
+ engine.set_similarity(domain_a: :high_sim, domain_b: :target, similarity: 0.9)
202
+ engine.set_similarity(domain_a: :low_sim, domain_b: :target, similarity: 0.1)
203
+ end
204
+
205
+ it 'returns only positively transferable domains' do
206
+ result = engine.most_transferable(target_domain: :target)
207
+ expect(result.map { |r| r[:domain] }).to include(:high_sim)
208
+ expect(result.map { |r| r[:domain] }).not_to include(:low_sim)
209
+ expect(result.map { |r| r[:domain] }).not_to include(:no_sim)
210
+ end
211
+
212
+ it 'sorts by similarity descending' do
213
+ engine.learn_domain(domain: :mid_sim, amount: 0.8)
214
+ engine.set_similarity(domain_a: :mid_sim, domain_b: :target, similarity: 0.7)
215
+ result = engine.most_transferable(target_domain: :target)
216
+ sims = result.map { |r| r[:similarity] }
217
+ expect(sims).to eq(sims.sort.reverse)
218
+ end
219
+
220
+ it 'respects the limit parameter' do
221
+ 5.times do |i|
222
+ name = :"domain_#{i}"
223
+ engine.learn_domain(domain: name, amount: 0.5)
224
+ engine.set_similarity(domain_a: name, domain_b: :target, similarity: 0.7 + (i * 0.01))
225
+ end
226
+ result = engine.most_transferable(target_domain: :target, limit: 3)
227
+ expect(result.size).to be <= 3
228
+ end
229
+ end
230
+
231
+ describe '#interference_risks' do
232
+ before do
233
+ engine.learn_domain(domain: :target, amount: 0.3)
234
+ engine.learn_domain(domain: :risky, amount: 0.8)
235
+ engine.learn_domain(domain: :safe, amount: 0.8)
236
+ engine.set_similarity(domain_a: :risky, domain_b: :target, similarity: 0.45)
237
+ engine.set_similarity(domain_a: :safe, domain_b: :target, similarity: 0.8)
238
+ end
239
+
240
+ it 'returns only interference-type domains' do
241
+ result = engine.interference_risks(target_domain: :target)
242
+ domains = result.map { |r| r[:domain] }
243
+ expect(domains).to include(:risky)
244
+ expect(domains).not_to include(:safe)
245
+ end
246
+
247
+ it 'sorts by similarity descending' do
248
+ engine.learn_domain(domain: :also_risky, amount: 0.5)
249
+ engine.set_similarity(domain_a: :also_risky, domain_b: :target, similarity: 0.35)
250
+ result = engine.interference_risks(target_domain: :target)
251
+ sims = result.map { |r| r[:similarity] }
252
+ expect(sims).to eq(sims.sort.reverse)
253
+ end
254
+
255
+ it 'returns empty array when no interference risks' do
256
+ engine2 = described_class.new
257
+ engine2.learn_domain(domain: :t, amount: 0.3)
258
+ expect(engine2.interference_risks(target_domain: :t)).to be_empty
259
+ end
260
+ end
261
+
262
+ describe '#transfer_report' do
263
+ it 'returns zero counts for empty engine' do
264
+ report = engine.transfer_report
265
+ expect(report[:total_transfers]).to eq(0)
266
+ expect(report[:domain_count]).to eq(0)
267
+ end
268
+
269
+ it 'counts transfers correctly' do
270
+ engine.learn_domain(domain: :a, amount: 0.8)
271
+ engine.set_similarity(domain_a: :a, domain_b: :b, similarity: 0.7)
272
+ engine.attempt_transfer(from_domain: :a, to_domain: :b)
273
+ engine.attempt_transfer(from_domain: :a, to_domain: :b)
274
+ report = engine.transfer_report
275
+ expect(report[:total_transfers]).to eq(2)
276
+ expect(report[:positive_transfers]).to eq(2)
277
+ end
278
+
279
+ it 'counts interference events' do
280
+ engine.learn_domain(domain: :a, amount: 0.8)
281
+ engine.set_similarity(domain_a: :a, domain_b: :b, similarity: 0.4)
282
+ engine.attempt_transfer(from_domain: :a, to_domain: :b)
283
+ report = engine.transfer_report
284
+ expect(report[:interference_events]).to eq(1)
285
+ end
286
+ end
287
+
288
+ describe '#to_h' do
289
+ it 'returns a hash with expected keys' do
290
+ keys = engine.to_h.keys
291
+ expect(keys).to contain_exactly(:domains, :similarities, :transfer_history, :report)
292
+ end
293
+
294
+ it 'reflects current state' do
295
+ engine.learn_domain(domain: :ruby, amount: 0.5)
296
+ expect(engine.to_h[:domains].keys).to include(:ruby)
297
+ end
298
+ end
299
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/transfer_learning/client'
4
+
5
+ RSpec.describe Legion::Extensions::TransferLearning::Runners::TransferLearning do
6
+ let(:client) { Legion::Extensions::TransferLearning::Client.new }
7
+
8
+ describe '#learn_domain' do
9
+ it 'returns domain knowledge hash' do
10
+ result = client.learn_domain(domain: :ruby, amount: 0.5)
11
+ expect(result[:domain]).to eq(:ruby)
12
+ expect(result[:proficiency]).to eq(0.5)
13
+ end
14
+
15
+ it 'accumulates proficiency across calls' do
16
+ client.learn_domain(domain: :ruby, amount: 0.3)
17
+ result = client.learn_domain(domain: :ruby, amount: 0.3)
18
+ expect(result[:proficiency]).to be_within(0.0001).of(0.6)
19
+ end
20
+ end
21
+
22
+ describe '#set_similarity' do
23
+ it 'returns similarity hash' do
24
+ result = client.set_similarity(domain_a: :ruby, domain_b: :python, similarity: 0.7)
25
+ expect(result[:domain_a]).to eq(:ruby)
26
+ expect(result[:domain_b]).to eq(:python)
27
+ expect(result[:similarity]).to eq(0.7)
28
+ end
29
+ end
30
+
31
+ describe '#attempt_transfer' do
32
+ before do
33
+ client.learn_domain(domain: :ruby, amount: 0.8)
34
+ client.set_similarity(domain_a: :ruby, domain_b: :python, similarity: 0.8)
35
+ end
36
+
37
+ it 'returns ok status for known source' do
38
+ result = client.attempt_transfer(from_domain: :ruby, to_domain: :python)
39
+ expect(result[:status]).to eq(:ok)
40
+ end
41
+
42
+ it 'returns positive type for high similarity' do
43
+ result = client.attempt_transfer(from_domain: :ruby, to_domain: :python)
44
+ expect(result[:type]).to eq(:positive)
45
+ end
46
+
47
+ it 'returns source_not_found for unknown source' do
48
+ result = client.attempt_transfer(from_domain: :unknown, to_domain: :python)
49
+ expect(result[:status]).to eq(:source_not_found)
50
+ end
51
+
52
+ it 'includes proficiency in response' do
53
+ result = client.attempt_transfer(from_domain: :ruby, to_domain: :python)
54
+ expect(result).to have_key(:proficiency)
55
+ end
56
+ end
57
+
58
+ describe '#transfer_effectiveness' do
59
+ before do
60
+ client.learn_domain(domain: :a, amount: 0.6)
61
+ client.learn_domain(domain: :b, amount: 0.4)
62
+ client.set_similarity(domain_a: :a, domain_b: :b, similarity: 0.7)
63
+ end
64
+
65
+ it 'returns effectiveness details' do
66
+ result = client.transfer_effectiveness(from_domain: :a, to_domain: :b)
67
+ expect(result[:type]).to eq(:positive)
68
+ expect(result[:type_label]).to eq('positive')
69
+ expect(result[:distance_label]).to eq('near')
70
+ end
71
+ end
72
+
73
+ describe '#most_transferable' do
74
+ before do
75
+ client.learn_domain(domain: :target, amount: 0.2)
76
+ client.learn_domain(domain: :good_source, amount: 0.8)
77
+ client.set_similarity(domain_a: :good_source, domain_b: :target, similarity: 0.9)
78
+ end
79
+
80
+ it 'returns candidates hash' do
81
+ result = client.most_transferable(target_domain: :target)
82
+ expect(result[:target_domain]).to eq(:target)
83
+ expect(result[:candidates]).to be_an(Array)
84
+ expect(result[:count]).to eq(result[:candidates].size)
85
+ end
86
+
87
+ it 'includes positive transfer candidates' do
88
+ result = client.most_transferable(target_domain: :target)
89
+ expect(result[:candidates].map { |c| c[:domain] }).to include(:good_source)
90
+ end
91
+ end
92
+
93
+ describe '#interference_risks' do
94
+ before do
95
+ client.learn_domain(domain: :target, amount: 0.3)
96
+ client.learn_domain(domain: :risky_source, amount: 0.8)
97
+ client.set_similarity(domain_a: :risky_source, domain_b: :target, similarity: 0.4)
98
+ end
99
+
100
+ it 'returns risks hash' do
101
+ result = client.interference_risks(target_domain: :target)
102
+ expect(result[:target_domain]).to eq(:target)
103
+ expect(result[:risks]).to be_an(Array)
104
+ expect(result[:count]).to eq(result[:risks].size)
105
+ end
106
+
107
+ it 'identifies interference risks' do
108
+ result = client.interference_risks(target_domain: :target)
109
+ expect(result[:risks].map { |r| r[:domain] }).to include(:risky_source)
110
+ end
111
+ end
112
+
113
+ describe '#transfer_report' do
114
+ it 'returns a report hash' do
115
+ result = client.transfer_report
116
+ expect(result).to have_key(:total_transfers)
117
+ expect(result).to have_key(:domain_count)
118
+ end
119
+
120
+ it 'counts transfers after attempts' do
121
+ client.learn_domain(domain: :a, amount: 0.8)
122
+ client.set_similarity(domain_a: :a, domain_b: :b, similarity: 0.7)
123
+ client.attempt_transfer(from_domain: :a, to_domain: :b)
124
+ result = client.transfer_report
125
+ expect(result[:total_transfers]).to eq(1)
126
+ expect(result[:positive_transfers]).to eq(1)
127
+ end
128
+ end
129
+
130
+ describe '#get_domain' do
131
+ it 'returns found: false for unknown domain' do
132
+ result = client.get_domain(domain: :unknown)
133
+ expect(result[:found]).to be false
134
+ expect(result[:domain]).to eq(:unknown)
135
+ end
136
+
137
+ it 'returns found: true for known domain' do
138
+ client.learn_domain(domain: :ruby, amount: 0.5)
139
+ result = client.get_domain(domain: :ruby)
140
+ expect(result[:found]).to be true
141
+ expect(result[:domain][:domain]).to eq(:ruby)
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+
5
+ module Legion
6
+ module Logging
7
+ def self.debug(_msg); end
8
+ def self.info(_msg); end
9
+ def self.warn(_msg); end
10
+ def self.error(_msg); end
11
+ end
12
+ end
13
+
14
+ require 'legion/extensions/transfer_learning'
15
+
16
+ RSpec.configure do |config|
17
+ config.example_status_persistence_file_path = '.rspec_status'
18
+ config.disable_monkey_patching!
19
+ config.expect_with(:rspec) { |c| c.syntax = :expect }
20
+ end
metadata ADDED
@@ -0,0 +1,75 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lex-transfer-learning
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Esity
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: legion-gaia
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ description: Domain knowledge transfer modeling for brain-modeled agentic AI
27
+ email:
28
+ - matthewdiverson@gmail.com
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - Gemfile
34
+ - lex-transfer-learning.gemspec
35
+ - lib/legion/extensions/transfer_learning.rb
36
+ - lib/legion/extensions/transfer_learning/client.rb
37
+ - lib/legion/extensions/transfer_learning/helpers/constants.rb
38
+ - lib/legion/extensions/transfer_learning/helpers/domain_knowledge.rb
39
+ - lib/legion/extensions/transfer_learning/helpers/transfer_engine.rb
40
+ - lib/legion/extensions/transfer_learning/runners/transfer_learning.rb
41
+ - lib/legion/extensions/transfer_learning/version.rb
42
+ - spec/legion/extensions/transfer_learning/client_spec.rb
43
+ - spec/legion/extensions/transfer_learning/helpers/constants_spec.rb
44
+ - spec/legion/extensions/transfer_learning/helpers/domain_knowledge_spec.rb
45
+ - spec/legion/extensions/transfer_learning/helpers/transfer_engine_spec.rb
46
+ - spec/legion/extensions/transfer_learning/runners/transfer_learning_spec.rb
47
+ - spec/spec_helper.rb
48
+ homepage: https://github.com/LegionIO/lex-transfer-learning
49
+ licenses:
50
+ - MIT
51
+ metadata:
52
+ homepage_uri: https://github.com/LegionIO/lex-transfer-learning
53
+ source_code_uri: https://github.com/LegionIO/lex-transfer-learning
54
+ documentation_uri: https://github.com/LegionIO/lex-transfer-learning
55
+ changelog_uri: https://github.com/LegionIO/lex-transfer-learning
56
+ bug_tracker_uri: https://github.com/LegionIO/lex-transfer-learning/issues
57
+ rubygems_mfa_required: 'true'
58
+ rdoc_options: []
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: '3.4'
66
+ required_rubygems_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ requirements: []
72
+ rubygems_version: 3.6.9
73
+ specification_version: 4
74
+ summary: LEX Transfer Learning
75
+ test_files: []