lex-cognitive-tide 0.1.1
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 +13 -0
- data/LICENSE +21 -0
- data/README.md +41 -0
- data/lex-cognitive-tide.gemspec +30 -0
- data/lib/legion/extensions/cognitive_tide/actors/tide_cycle.rb +41 -0
- data/lib/legion/extensions/cognitive_tide/client.rb +25 -0
- data/lib/legion/extensions/cognitive_tide/helpers/constants.rb +36 -0
- data/lib/legion/extensions/cognitive_tide/helpers/oscillator.rb +69 -0
- data/lib/legion/extensions/cognitive_tide/helpers/tidal_pool.rb +83 -0
- data/lib/legion/extensions/cognitive_tide/helpers/tide_engine.rb +167 -0
- data/lib/legion/extensions/cognitive_tide/runners/cognitive_tide.rb +118 -0
- data/lib/legion/extensions/cognitive_tide/version.rb +9 -0
- data/lib/legion/extensions/cognitive_tide.rb +17 -0
- data/spec/legion/extensions/cognitive_tide/actors/tide_cycle_spec.rb +46 -0
- data/spec/legion/extensions/cognitive_tide/client_spec.rb +90 -0
- data/spec/legion/extensions/cognitive_tide/helpers/constants_spec.rb +100 -0
- data/spec/legion/extensions/cognitive_tide/helpers/oscillator_spec.rb +153 -0
- data/spec/legion/extensions/cognitive_tide/helpers/tidal_pool_spec.rb +193 -0
- data/spec/legion/extensions/cognitive_tide/helpers/tide_engine_spec.rb +325 -0
- data/spec/legion/extensions/cognitive_tide/runners/cognitive_tide_spec.rb +249 -0
- data/spec/spec_helper.rb +27 -0
- metadata +82 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::CognitiveTide::Helpers::TideEngine do
|
|
4
|
+
let(:engine) { described_class.new }
|
|
5
|
+
|
|
6
|
+
def add_primary(eng = engine, amplitude: 1.0)
|
|
7
|
+
eng.add_oscillator(oscillator_type: :primary, period: 86_400, amplitude: amplitude)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def add_secondary(eng = engine, amplitude: 0.5)
|
|
11
|
+
eng.add_oscillator(oscillator_type: :secondary, period: 43_200, amplitude: amplitude)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
describe '#initialize' do
|
|
15
|
+
it 'starts with no oscillators' do
|
|
16
|
+
expect(engine.oscillators).to be_empty
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'starts with no pools' do
|
|
20
|
+
expect(engine.pools).to be_empty
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe '#add_oscillator' do
|
|
25
|
+
it 'returns an Oscillator instance' do
|
|
26
|
+
osc = add_primary
|
|
27
|
+
expect(osc).to be_a(Legion::Extensions::CognitiveTide::Helpers::Oscillator)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'appends to oscillators' do
|
|
31
|
+
add_primary
|
|
32
|
+
expect(engine.oscillators.size).to eq(1)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'allows multiple oscillators' do
|
|
36
|
+
add_primary
|
|
37
|
+
add_secondary
|
|
38
|
+
expect(engine.oscillators.size).to eq(2)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it 'raises ArgumentError for unknown oscillator_type' do
|
|
42
|
+
expect { engine.add_oscillator(oscillator_type: :invalid, period: 3600) }
|
|
43
|
+
.to raise_error(ArgumentError)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
describe '#composite_tide_level' do
|
|
48
|
+
it 'returns 0.0 with no oscillators' do
|
|
49
|
+
expect(engine.composite_tide_level).to eq(0.0)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'returns a value in [0, 1]' do
|
|
53
|
+
add_primary
|
|
54
|
+
add_secondary
|
|
55
|
+
level = engine.composite_tide_level
|
|
56
|
+
expect(level).to be >= 0.0
|
|
57
|
+
expect(level).to be <= 1.0
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'is deterministic for the same time' do
|
|
61
|
+
t = Time.utc(2026, 1, 1, 12, 0, 0)
|
|
62
|
+
allow(Time).to receive(:now).and_return(t)
|
|
63
|
+
add_primary
|
|
64
|
+
expect(engine.composite_tide_level).to eq(engine.composite_tide_level)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it 'returns 0.0 when all amplitudes are 0' do
|
|
68
|
+
engine.add_oscillator(oscillator_type: :primary, period: 3600, amplitude: 0.0)
|
|
69
|
+
expect(engine.composite_tide_level).to eq(0.0)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it 'respects amplitude weighting with a single oscillator' do
|
|
73
|
+
eng = described_class.new
|
|
74
|
+
eng.add_oscillator(oscillator_type: :primary, period: 3600, amplitude: 1.0, phase_offset: 0.0)
|
|
75
|
+
level = eng.composite_tide_level
|
|
76
|
+
expect(level).to be >= 0.0
|
|
77
|
+
expect(level).to be <= 1.0
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
describe '#current_phase' do
|
|
82
|
+
it 'returns one of the four tide phases' do
|
|
83
|
+
add_primary
|
|
84
|
+
expect(Legion::Extensions::CognitiveTide::Helpers::Constants::TIDE_PHASES)
|
|
85
|
+
.to include(engine.current_phase)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it 'returns :high_tide when level >= 0.65' do
|
|
89
|
+
# Force high level: sin at peak with phase_offset = PI/2, at t=0
|
|
90
|
+
t = Time.utc(2026, 1, 1)
|
|
91
|
+
allow(Time).to receive(:now).and_return(t)
|
|
92
|
+
# phase_offset = PI/2 means sin = 1.0 at t=0
|
|
93
|
+
engine.add_oscillator(oscillator_type: :primary, period: 86_400, amplitude: 1.0,
|
|
94
|
+
phase_offset: Math::PI / 2.0)
|
|
95
|
+
expect(engine.current_phase).to eq(:high_tide)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it 'returns :low_tide when level <= 0.35' do
|
|
99
|
+
t = Time.utc(2026, 1, 1)
|
|
100
|
+
allow(Time).to receive(:now).and_return(t)
|
|
101
|
+
# phase_offset = -PI/2 means sin = -1.0 at t=0 → value_at = 0.0
|
|
102
|
+
engine.add_oscillator(oscillator_type: :primary, period: 86_400, amplitude: 1.0,
|
|
103
|
+
phase_offset: -Math::PI / 2.0)
|
|
104
|
+
expect(engine.current_phase).to eq(:low_tide)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
describe '#create_pool' do
|
|
109
|
+
it 'creates a pool for a domain' do
|
|
110
|
+
pool = engine.create_pool(domain: 'philosophy')
|
|
111
|
+
expect(pool).to be_a(Legion::Extensions::CognitiveTide::Helpers::TidalPool)
|
|
112
|
+
expect(pool.domain).to eq('philosophy')
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it 'appends to pools' do
|
|
116
|
+
engine.create_pool(domain: 'philosophy')
|
|
117
|
+
expect(engine.pools.size).to eq(1)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it 'returns existing pool if domain already exists' do
|
|
121
|
+
p1 = engine.create_pool(domain: 'ideas')
|
|
122
|
+
p2 = engine.create_pool(domain: 'ideas')
|
|
123
|
+
expect(p1.id).to eq(p2.id)
|
|
124
|
+
expect(engine.pools.size).to eq(1)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it 'returns nil when MAX_POOLS is reached' do
|
|
128
|
+
max = Legion::Extensions::CognitiveTide::Helpers::Constants::MAX_POOLS
|
|
129
|
+
max.times { |i| engine.create_pool(domain: "domain_#{i}") }
|
|
130
|
+
result = engine.create_pool(domain: 'overflow')
|
|
131
|
+
expect(result).to be_nil
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it 'does not add a pool when at capacity' do
|
|
135
|
+
max = Legion::Extensions::CognitiveTide::Helpers::Constants::MAX_POOLS
|
|
136
|
+
max.times { |i| engine.create_pool(domain: "domain_#{i}") }
|
|
137
|
+
engine.create_pool(domain: 'overflow')
|
|
138
|
+
expect(engine.pools.size).to eq(max)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
describe '#deposit_to_pool' do
|
|
143
|
+
it 'deposits an item into a new domain pool' do
|
|
144
|
+
result = engine.deposit_to_pool(domain: 'ideas', item: 'refactor this')
|
|
145
|
+
expect(result).to be(true)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it 'auto-creates the pool if not present' do
|
|
149
|
+
engine.deposit_to_pool(domain: 'fresh_domain', item: 'test idea')
|
|
150
|
+
expect(engine.pools.size).to eq(1)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
it 'deposits into existing pool for domain' do
|
|
154
|
+
engine.deposit_to_pool(domain: 'ideas', item: 'idea 1')
|
|
155
|
+
engine.deposit_to_pool(domain: 'ideas', item: 'idea 2')
|
|
156
|
+
pool = engine.pools.first
|
|
157
|
+
expect(pool.size).to eq(2)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
it 'returns false when MAX_POOLS is reached and domain is new' do
|
|
161
|
+
max = Legion::Extensions::CognitiveTide::Helpers::Constants::MAX_POOLS
|
|
162
|
+
max.times { |i| engine.create_pool(domain: "domain_#{i}") }
|
|
163
|
+
result = engine.deposit_to_pool(domain: 'overflow', item: 'idea')
|
|
164
|
+
expect(result).to be(false)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
describe '#harvest_pools' do
|
|
169
|
+
before do
|
|
170
|
+
add_primary
|
|
171
|
+
engine.deposit_to_pool(domain: 'ideas', item: 'alpha')
|
|
172
|
+
engine.deposit_to_pool(domain: 'ideas', item: 'beta')
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
it 'returns empty hash when tide is not rising' do
|
|
176
|
+
# Make the engine report non-rising phase
|
|
177
|
+
allow(engine).to receive(:rising?).and_return(false)
|
|
178
|
+
expect(engine.harvest_pools).to eq({})
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
it 'returns harvested items by domain when rising' do
|
|
182
|
+
allow(engine).to receive(:rising?).and_return(true)
|
|
183
|
+
result = engine.harvest_pools
|
|
184
|
+
expect(result).to have_key('ideas')
|
|
185
|
+
expect(result['ideas'].size).to eq(2)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
it 'clears pools after harvest' do
|
|
189
|
+
allow(engine).to receive(:rising?).and_return(true)
|
|
190
|
+
engine.harvest_pools
|
|
191
|
+
expect(engine.pools.first.size).to eq(0)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
it 'respects min_depth threshold' do
|
|
195
|
+
allow(engine).to receive(:rising?).and_return(true)
|
|
196
|
+
# Pool has 2 items with capacity 20 → depth = 0.1; skip pools below 0.5
|
|
197
|
+
result = engine.harvest_pools(min_depth: 0.5)
|
|
198
|
+
expect(result).not_to have_key('ideas')
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
it 'skips empty pools' do
|
|
202
|
+
allow(engine).to receive(:rising?).and_return(true)
|
|
203
|
+
engine.create_pool(domain: 'empty_pool')
|
|
204
|
+
result = engine.harvest_pools
|
|
205
|
+
expect(result).not_to have_key('empty_pool')
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
describe '#evaporate_all!' do
|
|
210
|
+
it 'returns total items removed across all pools' do
|
|
211
|
+
20.times { |i| engine.deposit_to_pool(domain: 'ideas', item: "idea #{i}") }
|
|
212
|
+
removed = engine.evaporate_all!(0.1)
|
|
213
|
+
expect(removed).to be > 0
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
it 'evaporates all pools' do
|
|
217
|
+
engine.deposit_to_pool(domain: 'alpha', item: 'idea a')
|
|
218
|
+
engine.deposit_to_pool(domain: 'beta', item: 'idea b')
|
|
219
|
+
removed = engine.evaporate_all!(1.0)
|
|
220
|
+
expect(removed).to eq(2)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
it 'returns 0 when no pools' do
|
|
224
|
+
expect(engine.evaporate_all!).to eq(0)
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
describe '#tide_forecast' do
|
|
229
|
+
it 'returns empty array with no oscillators' do
|
|
230
|
+
expect(engine.tide_forecast(3600)).to eq([])
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
it 'returns forecast entries for duration' do
|
|
234
|
+
add_primary
|
|
235
|
+
forecast = engine.tide_forecast(3600)
|
|
236
|
+
expect(forecast).not_to be_empty
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
it 'each entry has time, level, and label' do
|
|
240
|
+
add_primary
|
|
241
|
+
entry = engine.tide_forecast(3600).first
|
|
242
|
+
expect(entry[:time]).to be_a(Time)
|
|
243
|
+
expect(entry[:level]).to be_between(0.0, 1.0)
|
|
244
|
+
expect(entry[:label]).to be_a(String)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
it 'number of steps is ceil(duration / FORECAST_RESOLUTION)' do
|
|
248
|
+
resolution = Legion::Extensions::CognitiveTide::Helpers::Constants::FORECAST_RESOLUTION
|
|
249
|
+
add_primary
|
|
250
|
+
steps = engine.tide_forecast(resolution * 4).size
|
|
251
|
+
expect(steps).to eq(4)
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
describe '#high_tide?' do
|
|
256
|
+
it 'returns true when level >= 0.65' do
|
|
257
|
+
t = Time.utc(2026, 1, 1)
|
|
258
|
+
allow(Time).to receive(:now).and_return(t)
|
|
259
|
+
engine.add_oscillator(oscillator_type: :primary, period: 86_400, amplitude: 1.0,
|
|
260
|
+
phase_offset: Math::PI / 2.0)
|
|
261
|
+
expect(engine.high_tide?).to be(true)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
it 'returns false when level < 0.65' do
|
|
265
|
+
t = Time.utc(2026, 1, 1)
|
|
266
|
+
allow(Time).to receive(:now).and_return(t)
|
|
267
|
+
engine.add_oscillator(oscillator_type: :primary, period: 86_400, amplitude: 1.0,
|
|
268
|
+
phase_offset: -Math::PI / 2.0)
|
|
269
|
+
expect(engine.high_tide?).to be(false)
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
describe '#low_tide?' do
|
|
274
|
+
it 'returns true when level <= 0.35' do
|
|
275
|
+
t = Time.utc(2026, 1, 1)
|
|
276
|
+
allow(Time).to receive(:now).and_return(t)
|
|
277
|
+
engine.add_oscillator(oscillator_type: :primary, period: 86_400, amplitude: 1.0,
|
|
278
|
+
phase_offset: -Math::PI / 2.0)
|
|
279
|
+
expect(engine.low_tide?).to be(true)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
it 'returns false when level > 0.35' do
|
|
283
|
+
t = Time.utc(2026, 1, 1)
|
|
284
|
+
allow(Time).to receive(:now).and_return(t)
|
|
285
|
+
engine.add_oscillator(oscillator_type: :primary, period: 86_400, amplitude: 1.0,
|
|
286
|
+
phase_offset: Math::PI / 2.0)
|
|
287
|
+
expect(engine.low_tide?).to be(false)
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
describe '#tide_report' do
|
|
292
|
+
it 'returns a hash with expected keys' do
|
|
293
|
+
add_primary
|
|
294
|
+
report = engine.tide_report
|
|
295
|
+
expect(report.keys).to include(:level, :phase, :label, :oscillator_count, :pool_count,
|
|
296
|
+
:high_tide, :low_tide, :pools, :oscillators)
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
it 'oscillator_count matches registered oscillators' do
|
|
300
|
+
add_primary
|
|
301
|
+
add_secondary
|
|
302
|
+
expect(engine.tide_report[:oscillator_count]).to eq(2)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
it 'pool_count matches created pools' do
|
|
306
|
+
engine.create_pool(domain: 'alpha')
|
|
307
|
+
engine.create_pool(domain: 'beta')
|
|
308
|
+
expect(engine.tide_report[:pool_count]).to eq(2)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
it 'pools is an array of pool hashes' do
|
|
312
|
+
engine.create_pool(domain: 'phi')
|
|
313
|
+
pools = engine.tide_report[:pools]
|
|
314
|
+
expect(pools).to be_an(Array)
|
|
315
|
+
expect(pools.first).to be_a(Hash)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
it 'oscillators is an array of oscillator hashes' do
|
|
319
|
+
add_primary
|
|
320
|
+
osc_list = engine.tide_report[:oscillators]
|
|
321
|
+
expect(osc_list).to be_an(Array)
|
|
322
|
+
expect(osc_list.first).to be_a(Hash)
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
end
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::CognitiveTide::Runners::CognitiveTide do
|
|
4
|
+
let(:engine) { Legion::Extensions::CognitiveTide::Helpers::TideEngine.new }
|
|
5
|
+
|
|
6
|
+
# Use extend self — call module methods directly
|
|
7
|
+
subject(:runner) { described_class }
|
|
8
|
+
|
|
9
|
+
describe '.add_oscillator' do
|
|
10
|
+
it 'returns success: true with oscillator hash' do
|
|
11
|
+
result = runner.add_oscillator(oscillator_type: :primary, period: 86_400, engine: engine)
|
|
12
|
+
expect(result[:success]).to be(true)
|
|
13
|
+
expect(result[:oscillator]).to be_a(Hash)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'adds the oscillator to the engine' do
|
|
17
|
+
runner.add_oscillator(oscillator_type: :primary, period: 3600, engine: engine)
|
|
18
|
+
expect(engine.oscillators.size).to eq(1)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'accepts all oscillator_types' do
|
|
22
|
+
%i[primary secondary lunar].each do |type|
|
|
23
|
+
result = runner.add_oscillator(oscillator_type: type, period: 3600, engine: engine)
|
|
24
|
+
expect(result[:success]).to be(true)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'returns success: false for unknown oscillator_type' do
|
|
29
|
+
result = runner.add_oscillator(oscillator_type: :invalid, period: 3600, engine: engine)
|
|
30
|
+
expect(result[:success]).to be(false)
|
|
31
|
+
expect(result[:error]).to be_a(String)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'returns success: false for non-positive period' do
|
|
35
|
+
result = runner.add_oscillator(oscillator_type: :primary, period: 0, engine: engine)
|
|
36
|
+
expect(result[:success]).to be(false)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'accepts amplitude and phase_offset kwargs' do
|
|
40
|
+
result = runner.add_oscillator(oscillator_type: :secondary, period: 43_200,
|
|
41
|
+
amplitude: 0.5, phase_offset: 1.5, engine: engine)
|
|
42
|
+
expect(result[:success]).to be(true)
|
|
43
|
+
expect(result[:oscillator][:amplitude]).to eq(0.5)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it 'accepts extra kwargs via ** splat without error' do
|
|
47
|
+
result = runner.add_oscillator(oscillator_type: :primary, period: 3600,
|
|
48
|
+
extra_key: 'ignored', engine: engine)
|
|
49
|
+
expect(result[:success]).to be(true)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
describe '.check_tide' do
|
|
54
|
+
before { runner.add_oscillator(oscillator_type: :primary, period: 86_400, engine: engine) }
|
|
55
|
+
|
|
56
|
+
it 'returns success: true' do
|
|
57
|
+
expect(runner.check_tide(engine: engine)[:success]).to be(true)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'returns level in [0, 1]' do
|
|
61
|
+
level = runner.check_tide(engine: engine)[:level]
|
|
62
|
+
expect(level).to be >= 0.0
|
|
63
|
+
expect(level).to be <= 1.0
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'returns a valid phase symbol' do
|
|
67
|
+
phase = runner.check_tide(engine: engine)[:phase]
|
|
68
|
+
expect(Legion::Extensions::CognitiveTide::Helpers::Constants::TIDE_PHASES).to include(phase)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it 'returns a string label' do
|
|
72
|
+
expect(runner.check_tide(engine: engine)[:label]).to be_a(String)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it 'works with no oscillators (returns 0.0 level)' do
|
|
76
|
+
empty_engine = Legion::Extensions::CognitiveTide::Helpers::TideEngine.new
|
|
77
|
+
result = runner.check_tide(engine: empty_engine)
|
|
78
|
+
expect(result[:success]).to be(true)
|
|
79
|
+
expect(result[:level]).to eq(0.0)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
describe '.deposit_idea' do
|
|
84
|
+
it 'deposits an idea and returns success: true' do
|
|
85
|
+
result = runner.deposit_idea(domain: 'philosophy', idea: 'consciousness is emergent',
|
|
86
|
+
engine: engine)
|
|
87
|
+
expect(result[:success]).to be(true)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it 'returns domain in response' do
|
|
91
|
+
result = runner.deposit_idea(domain: 'test', idea: 'x', engine: engine)
|
|
92
|
+
expect(result[:domain]).to eq('test')
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it 'skips deposit when tide is above threshold' do
|
|
96
|
+
t = Time.utc(2026, 1, 1)
|
|
97
|
+
allow(Time).to receive(:now).and_return(t)
|
|
98
|
+
engine.add_oscillator(oscillator_type: :primary, period: 86_400, amplitude: 1.0,
|
|
99
|
+
phase_offset: Math::PI / 2.0)
|
|
100
|
+
result = runner.deposit_idea(domain: 'ideas', idea: 'blocked', tide_threshold: 0.1, engine: engine)
|
|
101
|
+
expect(result[:success]).to be(false)
|
|
102
|
+
expect(result[:reason]).to eq(:tide_too_high)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it 'deposits when tide is below threshold' do
|
|
106
|
+
t = Time.utc(2026, 1, 1)
|
|
107
|
+
allow(Time).to receive(:now).and_return(t)
|
|
108
|
+
engine.add_oscillator(oscillator_type: :primary, period: 86_400, amplitude: 1.0,
|
|
109
|
+
phase_offset: -Math::PI / 2.0)
|
|
110
|
+
result = runner.deposit_idea(domain: 'ideas', idea: 'allowed', tide_threshold: 0.5, engine: engine)
|
|
111
|
+
expect(result[:success]).to be(true)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it 'skips threshold check when tide_threshold is nil' do
|
|
115
|
+
result = runner.deposit_idea(domain: 'ideas', idea: 'any tide', tide_threshold: nil, engine: engine)
|
|
116
|
+
expect(result[:success]).to be(true)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it 'accepts ** splat kwargs' do
|
|
120
|
+
result = runner.deposit_idea(domain: 'ideas', idea: 'test', extra: 'ignored', engine: engine)
|
|
121
|
+
expect(result[:success]).to be(true)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
describe '.harvest' do
|
|
126
|
+
before do
|
|
127
|
+
engine.add_oscillator(oscillator_type: :primary, period: 86_400, amplitude: 1.0)
|
|
128
|
+
runner.deposit_idea(domain: 'ideas', idea: 'harvest me', engine: engine)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it 'returns success: true' do
|
|
132
|
+
result = runner.harvest(engine: engine)
|
|
133
|
+
expect(result[:success]).to be(true)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
it 'returns harvested hash and total_items count' do
|
|
137
|
+
allow(engine).to receive(:rising?).and_return(true)
|
|
138
|
+
result = runner.harvest(engine: engine)
|
|
139
|
+
expect(result[:harvested]).to be_a(Hash)
|
|
140
|
+
expect(result[:total_items]).to be_a(Integer)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it 'returns total_items: 0 when not rising' do
|
|
144
|
+
allow(engine).to receive(:rising?).and_return(false)
|
|
145
|
+
result = runner.harvest(engine: engine)
|
|
146
|
+
expect(result[:total_items]).to eq(0)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
it 'accepts min_depth kwarg' do
|
|
150
|
+
allow(engine).to receive(:rising?).and_return(true)
|
|
151
|
+
result = runner.harvest(min_depth: 0.99, engine: engine)
|
|
152
|
+
expect(result[:success]).to be(true)
|
|
153
|
+
# pool depth is well below 0.99, so nothing harvested
|
|
154
|
+
expect(result[:total_items]).to eq(0)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
it 'accepts ** splat kwargs' do
|
|
158
|
+
result = runner.harvest(extra: 'ignored', engine: engine)
|
|
159
|
+
expect(result[:success]).to be(true)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
describe '.tide_forecast' do
|
|
164
|
+
before { engine.add_oscillator(oscillator_type: :primary, period: 86_400) }
|
|
165
|
+
|
|
166
|
+
it 'returns success: true' do
|
|
167
|
+
result = runner.tide_forecast(duration: 3600, engine: engine)
|
|
168
|
+
expect(result[:success]).to be(true)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
it 'returns forecast array' do
|
|
172
|
+
result = runner.tide_forecast(duration: 3600, engine: engine)
|
|
173
|
+
expect(result[:forecast]).to be_an(Array)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
it 'returns correct duration in response' do
|
|
177
|
+
result = runner.tide_forecast(duration: 7200, engine: engine)
|
|
178
|
+
expect(result[:duration]).to eq(7200)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
it 'returns empty forecast for engine with no oscillators' do
|
|
182
|
+
empty_engine = Legion::Extensions::CognitiveTide::Helpers::TideEngine.new
|
|
183
|
+
result = runner.tide_forecast(duration: 3600, engine: empty_engine)
|
|
184
|
+
expect(result[:forecast]).to eq([])
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
it 'accepts ** splat kwargs' do
|
|
188
|
+
result = runner.tide_forecast(duration: 3600, extra: 'ignored', engine: engine)
|
|
189
|
+
expect(result[:success]).to be(true)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
describe '.tide_status' do
|
|
194
|
+
before { engine.add_oscillator(oscillator_type: :primary, period: 86_400) }
|
|
195
|
+
|
|
196
|
+
it 'returns success: true' do
|
|
197
|
+
expect(runner.tide_status(engine: engine)[:success]).to be(true)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
it 'includes tide report fields' do
|
|
201
|
+
result = runner.tide_status(engine: engine)
|
|
202
|
+
expect(result.keys).to include(:level, :phase, :label, :oscillator_count, :pool_count)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
it 'returns oscillator_count matching registered oscillators' do
|
|
206
|
+
expect(runner.tide_status(engine: engine)[:oscillator_count]).to eq(1)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
it 'accepts ** splat kwargs' do
|
|
210
|
+
result = runner.tide_status(extra: 'ignored', engine: engine)
|
|
211
|
+
expect(result[:success]).to be(true)
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
describe '.tide_maintenance' do
|
|
216
|
+
before { engine.add_oscillator(oscillator_type: :primary, period: 86_400) }
|
|
217
|
+
|
|
218
|
+
it 'returns pools_maintained, current_phase, and tide_level' do
|
|
219
|
+
result = runner.tide_maintenance(engine: engine)
|
|
220
|
+
expect(result).to include(:pools_maintained, :current_phase, :tide_level)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
it 'returns pools_maintained as an integer' do
|
|
224
|
+
expect(runner.tide_maintenance(engine: engine)[:pools_maintained]).to be_a(Integer)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
it 'returns a valid phase symbol' do
|
|
228
|
+
phase = runner.tide_maintenance(engine: engine)[:current_phase]
|
|
229
|
+
expect(Legion::Extensions::CognitiveTide::Helpers::Constants::TIDE_PHASES).to include(phase)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
it 'returns tide_level in [0, 1]' do
|
|
233
|
+
level = runner.tide_maintenance(engine: engine)[:tide_level]
|
|
234
|
+
expect(level).to be >= 0.0
|
|
235
|
+
expect(level).to be <= 1.0
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
it 'reflects deposited pools in pools_maintained' do
|
|
239
|
+
engine.deposit_to_pool(domain: 'ideas', item: 'test idea')
|
|
240
|
+
result = runner.tide_maintenance(engine: engine)
|
|
241
|
+
expect(result[:pools_maintained]).to eq(1)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
it 'accepts ** splat kwargs' do
|
|
245
|
+
result = runner.tide_maintenance(extra: 'ignored', engine: engine)
|
|
246
|
+
expect(result).to include(:pools_maintained)
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
|
|
13
|
+
module Extensions
|
|
14
|
+
module Helpers
|
|
15
|
+
module Lex; end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
require 'legion/extensions/cognitive_tide'
|
|
21
|
+
require 'legion/extensions/cognitive_tide/client'
|
|
22
|
+
|
|
23
|
+
RSpec.configure do |config|
|
|
24
|
+
config.example_status_persistence_file_path = '.rspec_status'
|
|
25
|
+
config.disable_monkey_patching!
|
|
26
|
+
config.expect_with(:rspec) { |c| c.syntax = :expect }
|
|
27
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: lex-cognitive-tide
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.1
|
|
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: Circadian-like cognitive rhythm engine for brain-modeled agentic AI —
|
|
27
|
+
tidal oscillators, composite tide levels, and tidal pool idea accumulation
|
|
28
|
+
email:
|
|
29
|
+
- matthewdiverson@gmail.com
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- Gemfile
|
|
35
|
+
- LICENSE
|
|
36
|
+
- README.md
|
|
37
|
+
- lex-cognitive-tide.gemspec
|
|
38
|
+
- lib/legion/extensions/cognitive_tide.rb
|
|
39
|
+
- lib/legion/extensions/cognitive_tide/actors/tide_cycle.rb
|
|
40
|
+
- lib/legion/extensions/cognitive_tide/client.rb
|
|
41
|
+
- lib/legion/extensions/cognitive_tide/helpers/constants.rb
|
|
42
|
+
- lib/legion/extensions/cognitive_tide/helpers/oscillator.rb
|
|
43
|
+
- lib/legion/extensions/cognitive_tide/helpers/tidal_pool.rb
|
|
44
|
+
- lib/legion/extensions/cognitive_tide/helpers/tide_engine.rb
|
|
45
|
+
- lib/legion/extensions/cognitive_tide/runners/cognitive_tide.rb
|
|
46
|
+
- lib/legion/extensions/cognitive_tide/version.rb
|
|
47
|
+
- spec/legion/extensions/cognitive_tide/actors/tide_cycle_spec.rb
|
|
48
|
+
- spec/legion/extensions/cognitive_tide/client_spec.rb
|
|
49
|
+
- spec/legion/extensions/cognitive_tide/helpers/constants_spec.rb
|
|
50
|
+
- spec/legion/extensions/cognitive_tide/helpers/oscillator_spec.rb
|
|
51
|
+
- spec/legion/extensions/cognitive_tide/helpers/tidal_pool_spec.rb
|
|
52
|
+
- spec/legion/extensions/cognitive_tide/helpers/tide_engine_spec.rb
|
|
53
|
+
- spec/legion/extensions/cognitive_tide/runners/cognitive_tide_spec.rb
|
|
54
|
+
- spec/spec_helper.rb
|
|
55
|
+
homepage: https://github.com/LegionIO/lex-cognitive-tide
|
|
56
|
+
licenses:
|
|
57
|
+
- MIT
|
|
58
|
+
metadata:
|
|
59
|
+
homepage_uri: https://github.com/LegionIO/lex-cognitive-tide
|
|
60
|
+
source_code_uri: https://github.com/LegionIO/lex-cognitive-tide
|
|
61
|
+
documentation_uri: https://github.com/LegionIO/lex-cognitive-tide
|
|
62
|
+
changelog_uri: https://github.com/LegionIO/lex-cognitive-tide
|
|
63
|
+
bug_tracker_uri: https://github.com/LegionIO/lex-cognitive-tide/issues
|
|
64
|
+
rubygems_mfa_required: 'true'
|
|
65
|
+
rdoc_options: []
|
|
66
|
+
require_paths:
|
|
67
|
+
- lib
|
|
68
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
69
|
+
requirements:
|
|
70
|
+
- - ">="
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: '3.4'
|
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
74
|
+
requirements:
|
|
75
|
+
- - ">="
|
|
76
|
+
- !ruby/object:Gem::Version
|
|
77
|
+
version: '0'
|
|
78
|
+
requirements: []
|
|
79
|
+
rubygems_version: 3.6.9
|
|
80
|
+
specification_version: 4
|
|
81
|
+
summary: LEX CognitiveTide
|
|
82
|
+
test_files: []
|