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.
@@ -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