lex-analogical-reasoning 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 +7 -0
- data/Gemfile +11 -0
- data/lex-analogical-reasoning.gemspec +30 -0
- data/lib/legion/extensions/analogical_reasoning/client.rb +24 -0
- data/lib/legion/extensions/analogical_reasoning/helpers/analogy_engine.rb +205 -0
- data/lib/legion/extensions/analogical_reasoning/helpers/constants.rb +34 -0
- data/lib/legion/extensions/analogical_reasoning/helpers/structure_map.rb +109 -0
- data/lib/legion/extensions/analogical_reasoning/runners/analogical_reasoning.rb +102 -0
- data/lib/legion/extensions/analogical_reasoning/version.rb +9 -0
- data/lib/legion/extensions/analogical_reasoning.rb +16 -0
- data/spec/legion/extensions/analogical_reasoning/client_spec.rb +31 -0
- data/spec/legion/extensions/analogical_reasoning/helpers/analogy_engine_spec.rb +276 -0
- data/spec/legion/extensions/analogical_reasoning/helpers/structure_map_spec.rb +255 -0
- data/spec/legion/extensions/analogical_reasoning/runners/analogical_reasoning_spec.rb +213 -0
- data/spec/spec_helper.rb +20 -0
- metadata +75 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/analogical_reasoning/helpers/analogy_engine'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::AnalogicalReasoning::Helpers::AnalogyEngine do
|
|
6
|
+
subject(:engine) { described_class.new }
|
|
7
|
+
|
|
8
|
+
let(:base_analogy_params) do
|
|
9
|
+
{
|
|
10
|
+
source_domain: :solar_system,
|
|
11
|
+
target_domain: :atom,
|
|
12
|
+
mappings: { sun: :nucleus, planet: :electron },
|
|
13
|
+
mapping_type: :relational
|
|
14
|
+
}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
describe '#create_analogy' do
|
|
18
|
+
it 'creates and returns a StructureMap' do
|
|
19
|
+
analogy = engine.create_analogy(**base_analogy_params)
|
|
20
|
+
expect(analogy).to be_a(Legion::Extensions::AnalogicalReasoning::Helpers::StructureMap)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'assigns correct domains' do
|
|
24
|
+
analogy = engine.create_analogy(**base_analogy_params)
|
|
25
|
+
expect(analogy.source_domain).to eq(:solar_system)
|
|
26
|
+
expect(analogy.target_domain).to eq(:atom)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'records the analogy in history' do
|
|
30
|
+
engine.create_analogy(**base_analogy_params)
|
|
31
|
+
expect(engine.history.last[:event]).to eq(:created)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'stores the analogy for retrieval' do
|
|
35
|
+
analogy = engine.create_analogy(**base_analogy_params)
|
|
36
|
+
result = engine.apply_analogy(analogy_id: analogy.id, source_element: :sun)
|
|
37
|
+
expect(result[:found]).to be true
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
describe '#find_analogies' do
|
|
42
|
+
it 'finds analogies by source domain' do
|
|
43
|
+
engine.create_analogy(**base_analogy_params)
|
|
44
|
+
results = engine.find_analogies(domain: :solar_system)
|
|
45
|
+
expect(results.size).to eq(1)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'finds analogies by target domain' do
|
|
49
|
+
engine.create_analogy(**base_analogy_params)
|
|
50
|
+
results = engine.find_analogies(domain: :atom)
|
|
51
|
+
expect(results.size).to eq(1)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'returns empty array when domain not found' do
|
|
55
|
+
results = engine.find_analogies(domain: :unknown)
|
|
56
|
+
expect(results).to be_empty
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it 'finds across multiple analogies' do
|
|
60
|
+
engine.create_analogy(**base_analogy_params)
|
|
61
|
+
engine.create_analogy(
|
|
62
|
+
source_domain: :solar_system,
|
|
63
|
+
target_domain: :family,
|
|
64
|
+
mappings: { sun: :parent },
|
|
65
|
+
mapping_type: :relational
|
|
66
|
+
)
|
|
67
|
+
results = engine.find_analogies(domain: :solar_system)
|
|
68
|
+
expect(results.size).to eq(2)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
describe '#apply_analogy' do
|
|
73
|
+
let(:analogy) { engine.create_analogy(**base_analogy_params) }
|
|
74
|
+
|
|
75
|
+
it 'returns mapped element when found' do
|
|
76
|
+
result = engine.apply_analogy(analogy_id: analogy.id, source_element: :sun)
|
|
77
|
+
expect(result[:found]).to be true
|
|
78
|
+
expect(result[:mapped]).to be true
|
|
79
|
+
expect(result[:target_element]).to eq(:nucleus)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it 'returns mapped: false when source element has no mapping' do
|
|
83
|
+
result = engine.apply_analogy(analogy_id: analogy.id, source_element: :comet)
|
|
84
|
+
expect(result[:found]).to be true
|
|
85
|
+
expect(result[:mapped]).to be false
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it 'returns found: false for unknown analogy_id' do
|
|
89
|
+
result = engine.apply_analogy(analogy_id: 'nonexistent', source_element: :sun)
|
|
90
|
+
expect(result[:found]).to be false
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it 'includes confidence in result' do
|
|
94
|
+
result = engine.apply_analogy(analogy_id: analogy.id, source_element: :sun)
|
|
95
|
+
expect(result[:confidence]).to be_a(Float)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it 'records apply event in history' do
|
|
99
|
+
engine.apply_analogy(analogy_id: analogy.id, source_element: :sun)
|
|
100
|
+
applied = engine.history.select { |h| h[:event] == :applied }
|
|
101
|
+
expect(applied).not_to be_empty
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
describe '#evaluate_similarity' do
|
|
106
|
+
it 'returns 1.0 for identical concept hashes' do
|
|
107
|
+
concept = { color: :red, shape: :round }
|
|
108
|
+
score = engine.evaluate_similarity(source: concept, target: concept)
|
|
109
|
+
expect(score).to be > 0.8
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it 'returns 0.0 for empty hashes' do
|
|
113
|
+
expect(engine.evaluate_similarity(source: {}, target: {})).to eq(0.0)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it 'returns lower score for dissimilar concepts' do
|
|
117
|
+
source = { color: :red, shape: :round, size: :large }
|
|
118
|
+
target = { color: :blue, shape: :square, weight: :heavy }
|
|
119
|
+
score = engine.evaluate_similarity(source: source, target: target)
|
|
120
|
+
expect(score).to be < 0.5
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it 'returns higher score for structurally similar concepts' do
|
|
124
|
+
source = { role: :attractor, relation: :orbits, parent: :center }
|
|
125
|
+
target = { role: :attractor, relation: :orbits, parent: :core }
|
|
126
|
+
score = engine.evaluate_similarity(source: source, target: target)
|
|
127
|
+
expect(score).to be > 0.4
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
describe '#cross_domain_transfer' do
|
|
132
|
+
let(:analogy) { engine.create_analogy(**base_analogy_params) }
|
|
133
|
+
|
|
134
|
+
it 'maps source knowledge to target domain' do
|
|
135
|
+
result = engine.cross_domain_transfer(
|
|
136
|
+
analogy_id: analogy.id,
|
|
137
|
+
source_knowledge: { sun: 'massive gravity well' }
|
|
138
|
+
)
|
|
139
|
+
expect(result[:transferred]).to be true
|
|
140
|
+
expect(result[:result][:nucleus]).to eq('massive gravity well')
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it 'returns transferred: false for unknown analogy' do
|
|
144
|
+
result = engine.cross_domain_transfer(analogy_id: 'nope', source_knowledge: {})
|
|
145
|
+
expect(result[:transferred]).to be false
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it 'includes coverage ratio' do
|
|
149
|
+
result = engine.cross_domain_transfer(
|
|
150
|
+
analogy_id: analogy.id,
|
|
151
|
+
source_knowledge: { sun: 'center', comet: 'visitor' }
|
|
152
|
+
)
|
|
153
|
+
expect(result[:coverage]).to eq(0.5)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
it 'records transfer event in history' do
|
|
157
|
+
engine.cross_domain_transfer(analogy_id: analogy.id, source_knowledge: { sun: 'data' })
|
|
158
|
+
transferred = engine.history.select { |h| h[:event] == :transferred }
|
|
159
|
+
expect(transferred).not_to be_empty
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
describe '#reinforce_analogy' do
|
|
164
|
+
let(:analogy) { engine.create_analogy(**base_analogy_params) }
|
|
165
|
+
|
|
166
|
+
it 'strengthens analogy on success' do
|
|
167
|
+
initial = analogy.strength
|
|
168
|
+
engine.reinforce_analogy(analogy_id: analogy.id, success: true)
|
|
169
|
+
expect(analogy.strength).to be > initial
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
it 'weakens analogy on failure' do
|
|
173
|
+
initial = analogy.strength
|
|
174
|
+
engine.reinforce_analogy(analogy_id: analogy.id, success: false)
|
|
175
|
+
expect(analogy.strength).to be < initial
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
it 'returns not_found for unknown analogy' do
|
|
179
|
+
result = engine.reinforce_analogy(analogy_id: 'nope', success: true)
|
|
180
|
+
expect(result[:reinforced]).to be false
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
it 'returns reinforced: true on success' do
|
|
184
|
+
result = engine.reinforce_analogy(analogy_id: analogy.id, success: true)
|
|
185
|
+
expect(result[:reinforced]).to be true
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
describe '#productive_analogies' do
|
|
190
|
+
it 'returns only productive analogies' do
|
|
191
|
+
strong_analogy = engine.create_analogy(
|
|
192
|
+
source_domain: :water,
|
|
193
|
+
target_domain: :electricity,
|
|
194
|
+
mappings: { pressure: :voltage },
|
|
195
|
+
mapping_type: :relational,
|
|
196
|
+
strength: 0.9
|
|
197
|
+
)
|
|
198
|
+
engine.create_analogy(**base_analogy_params)
|
|
199
|
+
productive = engine.productive_analogies
|
|
200
|
+
expect(productive.map(&:id)).to include(strong_analogy.id)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
it 'returns empty array when no productive analogies' do
|
|
204
|
+
engine.create_analogy(**base_analogy_params)
|
|
205
|
+
expect(engine.productive_analogies).to be_empty
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
describe '#by_domain' do
|
|
210
|
+
it 'delegates to find_analogies' do
|
|
211
|
+
engine.create_analogy(**base_analogy_params)
|
|
212
|
+
results = engine.by_domain(domain: :solar_system)
|
|
213
|
+
expect(results.size).to eq(1)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
describe '#by_type' do
|
|
218
|
+
it 'filters analogies by mapping type' do
|
|
219
|
+
engine.create_analogy(**base_analogy_params)
|
|
220
|
+
engine.create_analogy(
|
|
221
|
+
source_domain: :water,
|
|
222
|
+
target_domain: :electricity,
|
|
223
|
+
mappings: { pressure: :voltage },
|
|
224
|
+
mapping_type: :attribute
|
|
225
|
+
)
|
|
226
|
+
results = engine.by_type(type: :relational)
|
|
227
|
+
expect(results.all? { |analogy| analogy.mapping_type == :relational }).to be true
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
describe '#decay_all' do
|
|
232
|
+
it 'reduces strength of all analogies' do
|
|
233
|
+
analogy = engine.create_analogy(**base_analogy_params)
|
|
234
|
+
initial = analogy.strength
|
|
235
|
+
engine.decay_all
|
|
236
|
+
expect(analogy.strength).to be < initial
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
describe '#prune_stale' do
|
|
241
|
+
it 'removes stale analogies and returns count' do
|
|
242
|
+
engine.create_analogy(
|
|
243
|
+
source_domain: :old, target_domain: :domain, mappings: {}, mapping_type: :attribute,
|
|
244
|
+
strength: 0.06
|
|
245
|
+
)
|
|
246
|
+
engine.create_analogy(**base_analogy_params)
|
|
247
|
+
|
|
248
|
+
pruned = engine.prune_stale
|
|
249
|
+
expect(pruned).to eq(1)
|
|
250
|
+
expect(engine.find_analogies(domain: :old)).to be_empty
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
it 'returns 0 when no stale analogies' do
|
|
254
|
+
engine.create_analogy(**base_analogy_params, strength: 0.9)
|
|
255
|
+
expect(engine.prune_stale).to eq(0)
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
describe '#to_h' do
|
|
260
|
+
it 'returns stats hash with expected keys' do
|
|
261
|
+
engine.create_analogy(**base_analogy_params)
|
|
262
|
+
stats = engine.to_h
|
|
263
|
+
expect(stats).to have_key(:total_analogies)
|
|
264
|
+
expect(stats).to have_key(:total_domains)
|
|
265
|
+
expect(stats).to have_key(:productive_count)
|
|
266
|
+
expect(stats).to have_key(:history_count)
|
|
267
|
+
expect(stats).to have_key(:domains)
|
|
268
|
+
expect(stats).to have_key(:analogy_states)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
it 'counts domains correctly' do
|
|
272
|
+
engine.create_analogy(**base_analogy_params)
|
|
273
|
+
expect(engine.to_h[:total_domains]).to eq(2)
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/analogical_reasoning/helpers/structure_map'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::AnalogicalReasoning::Helpers::StructureMap do
|
|
6
|
+
let(:relational_mappings) do
|
|
7
|
+
{
|
|
8
|
+
sun: { type: :relational, maps_to: :nucleus },
|
|
9
|
+
orbit: { type: :relational, maps_to: :electron_path }
|
|
10
|
+
}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
let(:surface_mappings) do
|
|
14
|
+
{
|
|
15
|
+
yellow: :gold,
|
|
16
|
+
hot: :energetic
|
|
17
|
+
}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
let(:structure_map) do
|
|
21
|
+
described_class.new(
|
|
22
|
+
source_domain: :solar_system,
|
|
23
|
+
target_domain: :atom,
|
|
24
|
+
mappings: relational_mappings,
|
|
25
|
+
mapping_type: :relational
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
describe '#initialize' do
|
|
30
|
+
it 'assigns required attributes' do
|
|
31
|
+
expect(structure_map.source_domain).to eq(:solar_system)
|
|
32
|
+
expect(structure_map.target_domain).to eq(:atom)
|
|
33
|
+
expect(structure_map.mapping_type).to eq(:relational)
|
|
34
|
+
expect(structure_map.mappings).to eq(relational_mappings)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'generates a UUID id' do
|
|
38
|
+
expect(structure_map.id).to match(/\A[0-9a-f-]{36}\z/)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it 'sets default strength' do
|
|
42
|
+
expect(structure_map.strength).to eq(Legion::Extensions::AnalogicalReasoning::Helpers::Constants::DEFAULT_STRENGTH)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it 'clamps custom strength to ceiling' do
|
|
46
|
+
map = described_class.new(
|
|
47
|
+
source_domain: :a,
|
|
48
|
+
target_domain: :b,
|
|
49
|
+
mappings: {},
|
|
50
|
+
mapping_type: :attribute,
|
|
51
|
+
strength: 2.0
|
|
52
|
+
)
|
|
53
|
+
expect(map.strength).to eq(Legion::Extensions::AnalogicalReasoning::Helpers::Constants::STRENGTH_CEILING)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it 'clamps custom strength to floor' do
|
|
57
|
+
map = described_class.new(
|
|
58
|
+
source_domain: :a,
|
|
59
|
+
target_domain: :b,
|
|
60
|
+
mappings: {},
|
|
61
|
+
mapping_type: :attribute,
|
|
62
|
+
strength: 0.0
|
|
63
|
+
)
|
|
64
|
+
expect(map.strength).to eq(Legion::Extensions::AnalogicalReasoning::Helpers::Constants::STRENGTH_FLOOR)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it 'sets times_used to 0' do
|
|
68
|
+
expect(structure_map.times_used).to eq(0)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
describe '#structural_score' do
|
|
73
|
+
it 'returns proportion of relational mappings' do
|
|
74
|
+
expect(structure_map.structural_score).to eq(1.0)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it 'returns 0.0 for empty mappings' do
|
|
78
|
+
map = described_class.new(
|
|
79
|
+
source_domain: :a, target_domain: :b, mappings: {}, mapping_type: :attribute
|
|
80
|
+
)
|
|
81
|
+
expect(map.structural_score).to eq(0.0)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it 'returns partial score for mixed mappings' do
|
|
85
|
+
mixed = relational_mappings.merge(surface_mappings)
|
|
86
|
+
map = described_class.new(
|
|
87
|
+
source_domain: :a, target_domain: :b, mappings: mixed, mapping_type: :relational
|
|
88
|
+
)
|
|
89
|
+
expect(map.structural_score).to eq(0.5)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
describe '#surface_score' do
|
|
94
|
+
it 'returns proportion of non-relational mappings' do
|
|
95
|
+
map = described_class.new(
|
|
96
|
+
source_domain: :a, target_domain: :b, mappings: surface_mappings, mapping_type: :attribute
|
|
97
|
+
)
|
|
98
|
+
expect(map.surface_score).to eq(1.0)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it 'returns 0.0 for empty mappings' do
|
|
102
|
+
map = described_class.new(
|
|
103
|
+
source_domain: :a, target_domain: :b, mappings: {}, mapping_type: :attribute
|
|
104
|
+
)
|
|
105
|
+
expect(map.surface_score).to eq(0.0)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
describe '#similarity_score' do
|
|
110
|
+
it 'weights structural and surface scores correctly' do
|
|
111
|
+
map = described_class.new(
|
|
112
|
+
source_domain: :a, target_domain: :b, mappings: relational_mappings, mapping_type: :relational
|
|
113
|
+
)
|
|
114
|
+
expected = (0.7 * 1.0) + (0.3 * 0.0)
|
|
115
|
+
expect(map.similarity_score).to be_within(0.001).of(expected)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
describe '#use!' do
|
|
120
|
+
it 'increments times_used' do
|
|
121
|
+
structure_map.use!
|
|
122
|
+
expect(structure_map.times_used).to eq(1)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it 'updates last_used_at' do
|
|
126
|
+
before = structure_map.last_used_at
|
|
127
|
+
sleep 0.01
|
|
128
|
+
structure_map.use!
|
|
129
|
+
expect(structure_map.last_used_at).to be >= before
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it 'boosts strength' do
|
|
133
|
+
initial = structure_map.strength
|
|
134
|
+
structure_map.use!
|
|
135
|
+
expect(structure_map.strength).to be > initial
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
it 'returns self' do
|
|
139
|
+
expect(structure_map.use!).to eq(structure_map)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
describe '#reinforce' do
|
|
144
|
+
it 'increases strength by reinforcement rate' do
|
|
145
|
+
initial = structure_map.strength
|
|
146
|
+
structure_map.reinforce
|
|
147
|
+
expect(structure_map.strength).to be_within(0.001).of(
|
|
148
|
+
(initial + Legion::Extensions::AnalogicalReasoning::Helpers::Constants::REINFORCEMENT_RATE)
|
|
149
|
+
.clamp(
|
|
150
|
+
Legion::Extensions::AnalogicalReasoning::Helpers::Constants::STRENGTH_FLOOR,
|
|
151
|
+
Legion::Extensions::AnalogicalReasoning::Helpers::Constants::STRENGTH_CEILING
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
it 'does not exceed strength ceiling' do
|
|
157
|
+
map = described_class.new(
|
|
158
|
+
source_domain: :a, target_domain: :b, mappings: {}, mapping_type: :attribute,
|
|
159
|
+
strength: Legion::Extensions::AnalogicalReasoning::Helpers::Constants::STRENGTH_CEILING
|
|
160
|
+
)
|
|
161
|
+
map.reinforce(amount: 0.5)
|
|
162
|
+
expect(map.strength).to eq(Legion::Extensions::AnalogicalReasoning::Helpers::Constants::STRENGTH_CEILING)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
describe '#weaken' do
|
|
167
|
+
it 'decreases strength' do
|
|
168
|
+
initial = structure_map.strength
|
|
169
|
+
structure_map.weaken
|
|
170
|
+
expect(structure_map.strength).to be < initial
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
it 'does not drop below strength floor' do
|
|
174
|
+
map = described_class.new(
|
|
175
|
+
source_domain: :a, target_domain: :b, mappings: {}, mapping_type: :attribute,
|
|
176
|
+
strength: Legion::Extensions::AnalogicalReasoning::Helpers::Constants::STRENGTH_FLOOR
|
|
177
|
+
)
|
|
178
|
+
map.weaken(amount: 0.5)
|
|
179
|
+
expect(map.strength).to eq(Legion::Extensions::AnalogicalReasoning::Helpers::Constants::STRENGTH_FLOOR)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
describe '#decay' do
|
|
184
|
+
it 'reduces strength by decay rate' do
|
|
185
|
+
initial = structure_map.strength
|
|
186
|
+
structure_map.decay
|
|
187
|
+
expect(structure_map.strength).to be_within(0.001).of(
|
|
188
|
+
(initial - Legion::Extensions::AnalogicalReasoning::Helpers::Constants::DECAY_RATE)
|
|
189
|
+
.clamp(
|
|
190
|
+
Legion::Extensions::AnalogicalReasoning::Helpers::Constants::STRENGTH_FLOOR,
|
|
191
|
+
Legion::Extensions::AnalogicalReasoning::Helpers::Constants::STRENGTH_CEILING
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
describe '#state' do
|
|
198
|
+
it 'returns :productive when strength is high' do
|
|
199
|
+
map = described_class.new(
|
|
200
|
+
source_domain: :a, target_domain: :b, mappings: {}, mapping_type: :attribute, strength: 0.9
|
|
201
|
+
)
|
|
202
|
+
expect(map.state).to eq(:productive)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
it 'returns :validated for mid-range strength' do
|
|
206
|
+
map = described_class.new(
|
|
207
|
+
source_domain: :a, target_domain: :b, mappings: {}, mapping_type: :attribute, strength: 0.6
|
|
208
|
+
)
|
|
209
|
+
expect(map.state).to eq(:validated)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
it 'returns :candidate for low-mid strength' do
|
|
213
|
+
map = described_class.new(
|
|
214
|
+
source_domain: :a, target_domain: :b, mappings: {}, mapping_type: :attribute, strength: 0.35
|
|
215
|
+
)
|
|
216
|
+
expect(map.state).to eq(:candidate)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
it 'returns :stale for very low strength' do
|
|
220
|
+
map = described_class.new(
|
|
221
|
+
source_domain: :a, target_domain: :b, mappings: {}, mapping_type: :attribute, strength: 0.1
|
|
222
|
+
)
|
|
223
|
+
expect(map.state).to eq(:stale)
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
describe '#productive?' do
|
|
228
|
+
it 'returns true when state is productive' do
|
|
229
|
+
map = described_class.new(
|
|
230
|
+
source_domain: :a, target_domain: :b, mappings: {}, mapping_type: :attribute, strength: 0.9
|
|
231
|
+
)
|
|
232
|
+
expect(map.productive?).to be true
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
it 'returns false when state is not productive' do
|
|
236
|
+
expect(structure_map.productive?).to be false
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
describe '#to_h' do
|
|
241
|
+
it 'includes all expected keys' do
|
|
242
|
+
hash = structure_map.to_h
|
|
243
|
+
expected_keys = %i[id source_domain target_domain mappings mapping_type strength
|
|
244
|
+
structural_score surface_score similarity_score state
|
|
245
|
+
times_used created_at last_used_at]
|
|
246
|
+
expected_keys.each { |key| expect(hash).to have_key(key) }
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
it 'reflects current state' do
|
|
250
|
+
hash = structure_map.to_h
|
|
251
|
+
expect(hash[:source_domain]).to eq(:solar_system)
|
|
252
|
+
expect(hash[:target_domain]).to eq(:atom)
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|