lex-default-mode-network 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-default-mode-network.gemspec +31 -0
- data/lib/legion/extensions/default_mode_network/actors/idle.rb +41 -0
- data/lib/legion/extensions/default_mode_network/client.rb +24 -0
- data/lib/legion/extensions/default_mode_network/helpers/constants.rb +49 -0
- data/lib/legion/extensions/default_mode_network/helpers/dmn_engine.rb +217 -0
- data/lib/legion/extensions/default_mode_network/helpers/wandering_thought.rb +56 -0
- data/lib/legion/extensions/default_mode_network/runners/default_mode_network.rb +118 -0
- data/lib/legion/extensions/default_mode_network/version.rb +9 -0
- data/lib/legion/extensions/default_mode_network.rb +17 -0
- data/spec/legion/extensions/default_mode_network/client_spec.rb +69 -0
- data/spec/legion/extensions/default_mode_network/helpers/constants_spec.rb +76 -0
- data/spec/legion/extensions/default_mode_network/helpers/dmn_engine_spec.rb +321 -0
- data/spec/legion/extensions/default_mode_network/helpers/wandering_thought_spec.rb +145 -0
- data/spec/legion/extensions/default_mode_network/runners/default_mode_network_spec.rb +269 -0
- data/spec/spec_helper.rb +20 -0
- metadata +78 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::DefaultModeNetwork::Helpers::Constants do
|
|
4
|
+
subject(:mod) { Class.new { include Legion::Extensions::DefaultModeNetwork::Helpers::Constants } }
|
|
5
|
+
|
|
6
|
+
describe 'idle thresholds' do
|
|
7
|
+
it 'IDLE_THRESHOLD is 30' do
|
|
8
|
+
expect(described_module::IDLE_THRESHOLD).to eq(30)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it 'DEEP_IDLE_THRESHOLD is 300' do
|
|
12
|
+
expect(described_module::DEEP_IDLE_THRESHOLD).to eq(300)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
describe 'thought limits' do
|
|
17
|
+
it 'MAX_WANDERING_THOUGHTS is 100' do
|
|
18
|
+
expect(described_module::MAX_WANDERING_THOUGHTS).to eq(100)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'MAX_THOUGHT_HISTORY is 200' do
|
|
22
|
+
expect(described_module::MAX_THOUGHT_HISTORY).to eq(200)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'MAX_ASSOCIATION_CHAIN is 5' do
|
|
26
|
+
expect(described_module::MAX_ASSOCIATION_CHAIN).to eq(5)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
describe 'salience parameters' do
|
|
31
|
+
it 'THOUGHT_SALIENCE_FLOOR is positive' do
|
|
32
|
+
expect(described_module::THOUGHT_SALIENCE_FLOOR).to be > 0
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'DEFAULT_SALIENCE is between floor and 1' do
|
|
36
|
+
expect(described_module::DEFAULT_SALIENCE).to be_between(
|
|
37
|
+
described_module::THOUGHT_SALIENCE_FLOOR, 1.0
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
describe 'ACTIVITY_LABELS' do
|
|
43
|
+
it 'maps all four modes' do
|
|
44
|
+
labels = described_module::ACTIVITY_LABELS
|
|
45
|
+
expect(labels[:active]).to eq(:task_focused)
|
|
46
|
+
expect(labels[:transitioning]).to eq(:shifting)
|
|
47
|
+
expect(labels[:idle]).to eq(:daydreaming)
|
|
48
|
+
expect(labels[:deep_idle]).to eq(:deep_reflection)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it 'is frozen' do
|
|
52
|
+
expect(described_module::ACTIVITY_LABELS).to be_frozen
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
describe 'SALIENCE_LABELS' do
|
|
57
|
+
it 'maps ranges to quality labels' do
|
|
58
|
+
labels = described_module::SALIENCE_LABELS
|
|
59
|
+
expect(labels.values).to include(:breakthrough, :significant, :notable, :passing, :fleeting)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it 'covers 0.9 as breakthrough' do
|
|
63
|
+
result = described_module::SALIENCE_LABELS.each { |range, lbl| break lbl if range.cover?(0.9) }
|
|
64
|
+
expect(result).to eq(:breakthrough)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it 'covers 0.1 as fleeting' do
|
|
68
|
+
result = described_module::SALIENCE_LABELS.each { |range, lbl| break lbl if range.cover?(0.1) }
|
|
69
|
+
expect(result).to eq(:fleeting)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def described_module
|
|
74
|
+
Legion::Extensions::DefaultModeNetwork::Helpers::Constants
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::DefaultModeNetwork::Helpers::DmnEngine do
|
|
4
|
+
subject(:engine) { described_class.new }
|
|
5
|
+
|
|
6
|
+
let(:const) { Legion::Extensions::DefaultModeNetwork::Helpers::Constants }
|
|
7
|
+
|
|
8
|
+
describe '#initialize' do
|
|
9
|
+
it 'starts in :active mode' do
|
|
10
|
+
expect(engine.mode).to eq(:active)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'has an empty thought list' do
|
|
14
|
+
expect(engine.thoughts).to be_empty
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'has an empty thought history' do
|
|
18
|
+
expect(engine.thought_history).to be_empty
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'records last_stimulus_at as recent Time' do
|
|
22
|
+
expect(engine.last_stimulus_at).to be_a(Time)
|
|
23
|
+
expect(Time.now.utc - engine.last_stimulus_at).to be < 1
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
describe '#register_stimulus' do
|
|
28
|
+
it 'resets idle timer and returns mode info' do
|
|
29
|
+
result = engine.register_stimulus(source: :api_call)
|
|
30
|
+
expect(result[:current_mode]).to eq(:active)
|
|
31
|
+
expect(result[:source]).to eq(:api_call)
|
|
32
|
+
expect(result).to have_key(:previous_mode)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'switches mode back to active' do
|
|
36
|
+
allow(engine).to receive(:idle_duration).and_return(60.0)
|
|
37
|
+
engine.tick_mode
|
|
38
|
+
engine.register_stimulus
|
|
39
|
+
expect(engine.mode).to eq(:active)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'updates last_stimulus_at' do
|
|
43
|
+
before = engine.last_stimulus_at
|
|
44
|
+
sleep(0.01)
|
|
45
|
+
engine.register_stimulus
|
|
46
|
+
expect(engine.last_stimulus_at).to be > before
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
describe '#tick_mode' do
|
|
51
|
+
it 'remains active when recently stimulated' do
|
|
52
|
+
result = engine.tick_mode
|
|
53
|
+
# idle_duration is near 0, mode stays active or becomes transitioning
|
|
54
|
+
expect(%i[active transitioning]).to include(result[:current_mode])
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'transitions to :idle after IDLE_THRESHOLD seconds' do
|
|
58
|
+
allow(engine).to receive(:idle_duration).and_return(const::IDLE_THRESHOLD.to_f + 1)
|
|
59
|
+
result = engine.tick_mode
|
|
60
|
+
expect(result[:current_mode]).to eq(:idle)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it 'transitions to :deep_idle after DEEP_IDLE_THRESHOLD seconds' do
|
|
64
|
+
allow(engine).to receive(:idle_duration).and_return(const::DEEP_IDLE_THRESHOLD.to_f + 1)
|
|
65
|
+
result = engine.tick_mode
|
|
66
|
+
expect(result[:current_mode]).to eq(:deep_idle)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it 'returns previous and current mode' do
|
|
70
|
+
result = engine.tick_mode
|
|
71
|
+
expect(result).to have_key(:previous_mode)
|
|
72
|
+
expect(result).to have_key(:current_mode)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it 'returns idle_duration in result' do
|
|
76
|
+
result = engine.tick_mode
|
|
77
|
+
expect(result[:idle_duration]).to be_a(Float)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
describe '#idle_duration' do
|
|
82
|
+
it 'returns elapsed seconds since last stimulus' do
|
|
83
|
+
expect(engine.idle_duration).to be >= 0
|
|
84
|
+
expect(engine.idle_duration).to be < 5
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
describe '#generate_thought' do
|
|
89
|
+
it 'returns a WanderingThought' do
|
|
90
|
+
thought = engine.generate_thought
|
|
91
|
+
expect(thought).to be_a(Legion::Extensions::DefaultModeNetwork::Helpers::WanderingThought)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it 'stores the generated thought' do
|
|
95
|
+
engine.generate_thought
|
|
96
|
+
expect(engine.thought_count).to eq(1)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it 'increments thought count each call' do
|
|
100
|
+
3.times { engine.generate_thought }
|
|
101
|
+
expect(engine.thought_count).to eq(3)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it 'generates valid thought types' do
|
|
105
|
+
valid_types = %i[self_referential social_replay spontaneous_plan wandering]
|
|
106
|
+
10.times do
|
|
107
|
+
t = engine.generate_thought
|
|
108
|
+
expect(valid_types).to include(t.thought_type)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
describe '#self_reflect' do
|
|
114
|
+
it 'creates a self_referential thought' do
|
|
115
|
+
thought = engine.self_reflect
|
|
116
|
+
expect(thought.thought_type).to eq(:self_referential)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it 'sets domain to :self' do
|
|
120
|
+
expect(engine.self_reflect.domain).to eq(:self)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it 'builds a non-empty association chain' do
|
|
124
|
+
expect(engine.self_reflect.association_chain).not_to be_empty
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it 'accepts a custom topic' do
|
|
128
|
+
thought = engine.self_reflect(topic: :values)
|
|
129
|
+
expect(thought.seed).to eq(:values)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it 'has positive salience' do
|
|
133
|
+
expect(engine.self_reflect.salience).to be > 0
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
describe '#social_replay' do
|
|
138
|
+
it 'creates a social_replay thought' do
|
|
139
|
+
thought = engine.social_replay(interaction: :recent_chat)
|
|
140
|
+
expect(thought.thought_type).to eq(:social_replay)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it 'sets domain to :social' do
|
|
144
|
+
expect(engine.social_replay.domain).to eq(:social)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
it 'uses provided interaction as seed' do
|
|
148
|
+
thought = engine.social_replay(interaction: :team_standup)
|
|
149
|
+
expect(thought.seed).to eq(:team_standup)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
it 'uses a default seed when interaction is nil' do
|
|
153
|
+
thought = engine.social_replay
|
|
154
|
+
expect(thought.seed).not_to be_nil
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
describe '#plan_spontaneously' do
|
|
159
|
+
it 'creates a spontaneous_plan thought' do
|
|
160
|
+
thought = engine.plan_spontaneously(goal: :improve_latency)
|
|
161
|
+
expect(thought.thought_type).to eq(:spontaneous_plan)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
it 'sets domain to :planning' do
|
|
165
|
+
expect(engine.plan_spontaneously.domain).to eq(:planning)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
it 'uses provided goal as seed' do
|
|
169
|
+
thought = engine.plan_spontaneously(goal: :reduce_errors)
|
|
170
|
+
expect(thought.seed).to eq(:reduce_errors)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
it 'generates higher salience than wandering (on average)' do
|
|
174
|
+
plans = 20.times.map { engine.plan_spontaneously }
|
|
175
|
+
wanders = 20.times.map { engine.wander }
|
|
176
|
+
expect(plans.sum(&:salience) / plans.size).to be > wanders.sum(&:salience) / wanders.size
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
describe '#wander' do
|
|
181
|
+
it 'creates a wandering thought' do
|
|
182
|
+
thought = engine.wander(seed: :curiosity)
|
|
183
|
+
expect(thought.thought_type).to eq(:wandering)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
it 'sets domain to :associative' do
|
|
187
|
+
expect(engine.wander.domain).to eq(:associative)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
it 'uses provided seed' do
|
|
191
|
+
thought = engine.wander(seed: :efficiency)
|
|
192
|
+
expect(thought.seed).to eq(:efficiency)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
it 'builds an association chain with the seed as first element' do
|
|
196
|
+
thought = engine.wander(seed: :pattern)
|
|
197
|
+
expect(thought.association_chain.first).to eq(:pattern)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
it 'chain does not exceed MAX_ASSOCIATION_CHAIN' do
|
|
201
|
+
thought = engine.wander(seed: :root)
|
|
202
|
+
expect(thought.association_chain.size).to be <= const::MAX_ASSOCIATION_CHAIN + 1
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
describe '#salient_thoughts' do
|
|
207
|
+
before do
|
|
208
|
+
engine.wander(seed: :a).tap { |t| t.salience = 0.8 }.tap { |t| engine.send(:store_thought, t) }
|
|
209
|
+
engine.wander(seed: :b).tap { |t| t.salience = 0.3 }.tap { |t| engine.send(:store_thought, t) }
|
|
210
|
+
engine.wander(seed: :c).tap { |t| t.salience = 0.9 }.tap { |t| engine.send(:store_thought, t) }
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
it 'returns thoughts sorted by descending salience' do
|
|
214
|
+
thoughts = engine.salient_thoughts(count: 3)
|
|
215
|
+
saliences = thoughts.map(&:salience)
|
|
216
|
+
expect(saliences).to eq(saliences.sort.reverse)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
it 'limits results by count' do
|
|
220
|
+
expect(engine.salient_thoughts(count: 2).size).to eq(2)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
it 'returns top thought with highest salience' do
|
|
224
|
+
expect(engine.salient_thoughts(count: 1).first.seed).to eq(:c)
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
describe '#thoughts_of_type' do
|
|
229
|
+
before do
|
|
230
|
+
engine.self_reflect.tap { |t| engine.send(:store_thought, t) }
|
|
231
|
+
engine.social_replay.tap { |t| engine.send(:store_thought, t) }
|
|
232
|
+
engine.wander.tap { |t| engine.send(:store_thought, t) }
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
it 'filters by thought_type' do
|
|
236
|
+
results = engine.thoughts_of_type(type: :self_referential)
|
|
237
|
+
expect(results.all? { |t| t.thought_type == :self_referential }).to be true
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
it 'returns empty array for unknown type' do
|
|
241
|
+
expect(engine.thoughts_of_type(type: :unknown_type)).to be_empty
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
it 'accepts string type and coerces to symbol' do
|
|
245
|
+
results = engine.thoughts_of_type(type: 'wandering')
|
|
246
|
+
expect(results).not_to be_empty
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
describe '#decay_all' do
|
|
251
|
+
it 'decays all stored thoughts' do
|
|
252
|
+
3.times { engine.generate_thought }
|
|
253
|
+
before_saliences = engine.thoughts.map(&:salience).dup
|
|
254
|
+
engine.decay_all
|
|
255
|
+
# All thoughts should have lower or equal salience
|
|
256
|
+
engine.thoughts.zip(before_saliences).each do |t, b|
|
|
257
|
+
expect(t.salience).to be <= b
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
it 'prunes thoughts that fade below floor' do
|
|
262
|
+
thought = engine.wander(seed: :test)
|
|
263
|
+
thought.salience = const::THOUGHT_SALIENCE_FLOOR + 0.001
|
|
264
|
+
engine.send(:store_thought, thought)
|
|
265
|
+
engine.decay_all
|
|
266
|
+
expect(engine.thoughts).not_to include(thought)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
it 'archives faded thoughts in thought_history' do
|
|
270
|
+
thought = engine.wander(seed: :archive_me)
|
|
271
|
+
thought.salience = const::THOUGHT_SALIENCE_FLOOR + 0.001
|
|
272
|
+
engine.send(:store_thought, thought)
|
|
273
|
+
engine.decay_all
|
|
274
|
+
ids = engine.thought_history.map { |h| h[:id] }
|
|
275
|
+
expect(ids).to include(thought.id)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
it 'returns the count of faded thoughts removed' do
|
|
279
|
+
thought = engine.wander(seed: :doomed)
|
|
280
|
+
thought.salience = const::THOUGHT_SALIENCE_FLOOR + 0.001
|
|
281
|
+
engine.send(:store_thought, thought)
|
|
282
|
+
removed = engine.decay_all
|
|
283
|
+
expect(removed).to eq(1)
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
describe '#thought_count' do
|
|
288
|
+
it 'returns 0 initially' do
|
|
289
|
+
expect(engine.thought_count).to eq(0)
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
it 'increments with each stored thought' do
|
|
293
|
+
5.times { engine.generate_thought }
|
|
294
|
+
expect(engine.thought_count).to eq(5)
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
describe 'thought pruning' do
|
|
299
|
+
it 'does not exceed MAX_WANDERING_THOUGHTS' do
|
|
300
|
+
(const::MAX_WANDERING_THOUGHTS + 10).times { engine.generate_thought }
|
|
301
|
+
expect(engine.thought_count).to be <= const::MAX_WANDERING_THOUGHTS
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
describe '#to_h' do
|
|
306
|
+
it 'returns a hash with expected keys' do
|
|
307
|
+
h = engine.to_h
|
|
308
|
+
expect(h).to include(:mode, :mode_label, :idle_duration, :thought_count, :history_count, :last_stimulus_at)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
it 'includes the mode label from ACTIVITY_LABELS' do
|
|
312
|
+
h = engine.to_h
|
|
313
|
+
expect(const::ACTIVITY_LABELS.values).to include(h[:mode_label])
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
it 'reflects current thought count' do
|
|
317
|
+
3.times { engine.generate_thought }
|
|
318
|
+
expect(engine.to_h[:thought_count]).to eq(3)
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::DefaultModeNetwork::Helpers::WanderingThought do
|
|
4
|
+
subject(:thought) do
|
|
5
|
+
described_class.new(
|
|
6
|
+
seed: :identity,
|
|
7
|
+
association_chain: %i[identity values purpose],
|
|
8
|
+
domain: :self,
|
|
9
|
+
thought_type: :self_referential,
|
|
10
|
+
salience: 0.6
|
|
11
|
+
)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
describe '#initialize' do
|
|
15
|
+
it 'assigns all fields' do
|
|
16
|
+
expect(thought.seed).to eq(:identity)
|
|
17
|
+
expect(thought.association_chain).to eq(%i[identity values purpose])
|
|
18
|
+
expect(thought.domain).to eq(:self)
|
|
19
|
+
expect(thought.thought_type).to eq(:self_referential)
|
|
20
|
+
expect(thought.salience).to eq(0.6)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'generates a UUID id' do
|
|
24
|
+
expect(thought.id).to match(/\A[0-9a-f-]{36}\z/)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'records created_at as a Time' do
|
|
28
|
+
expect(thought.created_at).to be_a(Time)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'clamps salience to 0..1' do
|
|
32
|
+
high = described_class.new(seed: :x, association_chain: [], domain: :d, thought_type: :wandering, salience: 5.0)
|
|
33
|
+
low = described_class.new(seed: :x, association_chain: [], domain: :d, thought_type: :wandering, salience: -1.0)
|
|
34
|
+
expect(high.salience).to eq(1.0)
|
|
35
|
+
expect(low.salience).to eq(0.0)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'coerces thought_type to symbol' do
|
|
39
|
+
t = described_class.new(seed: :x, association_chain: [], domain: :d, thought_type: 'wandering')
|
|
40
|
+
expect(t.thought_type).to eq(:wandering)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'wraps non-array association_chain in an array' do
|
|
44
|
+
t = described_class.new(seed: :x, association_chain: :single, domain: :d, thought_type: :wandering)
|
|
45
|
+
expect(t.association_chain).to eq([:single])
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
describe '#boost_salience' do
|
|
50
|
+
it 'increases salience by SALIENCE_ALPHA by default' do
|
|
51
|
+
before = thought.salience
|
|
52
|
+
thought.boost_salience
|
|
53
|
+
expect(thought.salience).to be_within(0.001).of(before + described_module::SALIENCE_ALPHA)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it 'accepts a custom amount' do
|
|
57
|
+
before = thought.salience
|
|
58
|
+
thought.boost_salience(0.2)
|
|
59
|
+
expect(thought.salience).to be_within(0.001).of(before + 0.2)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it 'caps at 1.0' do
|
|
63
|
+
thought.salience = 0.99
|
|
64
|
+
thought.boost_salience(0.5)
|
|
65
|
+
expect(thought.salience).to eq(1.0)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
describe '#decay' do
|
|
70
|
+
it 'reduces salience by THOUGHT_DECAY' do
|
|
71
|
+
before = thought.salience
|
|
72
|
+
thought.decay
|
|
73
|
+
expect(thought.salience).to be_within(0.001).of(before - described_module::THOUGHT_DECAY)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it 'does not drop below THOUGHT_SALIENCE_FLOOR' do
|
|
77
|
+
500.times { thought.decay }
|
|
78
|
+
expect(thought.salience).to be >= described_module::THOUGHT_SALIENCE_FLOOR
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
describe '#faded?' do
|
|
83
|
+
it 'returns false for a strong thought' do
|
|
84
|
+
expect(thought.faded?).to be false
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it 'returns true at the salience floor' do
|
|
88
|
+
thought.salience = described_module::THOUGHT_SALIENCE_FLOOR
|
|
89
|
+
expect(thought.faded?).to be true
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it 'returns false just above the floor' do
|
|
93
|
+
thought.salience = described_module::THOUGHT_SALIENCE_FLOOR + 0.01
|
|
94
|
+
expect(thought.faded?).to be false
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
describe '#label' do
|
|
99
|
+
it 'returns :significant for salience 0.7' do
|
|
100
|
+
thought.salience = 0.7
|
|
101
|
+
expect(thought.label).to eq(:significant)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it 'returns :breakthrough for salience 0.9' do
|
|
105
|
+
thought.salience = 0.9
|
|
106
|
+
expect(thought.label).to eq(:breakthrough)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it 'returns :fleeting for salience 0.1' do
|
|
110
|
+
thought.salience = 0.1
|
|
111
|
+
expect(thought.label).to eq(:fleeting)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it 'returns :notable for salience 0.5' do
|
|
115
|
+
thought.salience = 0.5
|
|
116
|
+
expect(thought.label).to eq(:notable)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it 'returns :passing for salience 0.3' do
|
|
120
|
+
thought.salience = 0.3
|
|
121
|
+
expect(thought.label).to eq(:passing)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
describe '#to_h' do
|
|
126
|
+
it 'returns a hash with all expected keys' do
|
|
127
|
+
h = thought.to_h
|
|
128
|
+
expect(h).to include(:id, :seed, :association_chain, :domain, :thought_type, :salience, :label, :created_at)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it 'includes the current label' do
|
|
132
|
+
thought.salience = 0.9
|
|
133
|
+
expect(thought.to_h[:label]).to eq(:breakthrough)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
it 'rounds salience to 4 decimal places' do
|
|
137
|
+
thought.salience = 0.123456789
|
|
138
|
+
expect(thought.to_h[:salience]).to eq(0.1235)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def described_module
|
|
143
|
+
Legion::Extensions::DefaultModeNetwork::Helpers::Constants
|
|
144
|
+
end
|
|
145
|
+
end
|