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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 97eb0a7dffaaf474e5b7cd62f7b8bcf50f4a720074bd9b98e1477d6a1337d261
|
|
4
|
+
data.tar.gz: 98016f8241ef14e555df861dff8d2bedc9dfa51df4321b6aa57a679ec072ae22
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: bd43de2527c14052222500d002861c2b666708ae7ca7b97551f3e3b279bccf21580d15e4a323b6e71512d5d2434d841f7c4176c280b2c8fdba05fdcf4148b5a9
|
|
7
|
+
data.tar.gz: 425160a6fb2ad24418ce89d8ed908c4ae28344fa1f433bc99d280c4907fd74e1585e6ac8ef0d3e42dc791a34f7e213e0f8b0e1cef2667c1de77d8c7cb151bc9b
|
data/Gemfile
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
source 'https://rubygems.org'
|
|
4
|
+
|
|
5
|
+
gemspec
|
|
6
|
+
|
|
7
|
+
group :test do
|
|
8
|
+
gem 'rspec', '~> 3.13'
|
|
9
|
+
gem 'rubocop', '~> 1.75', require: false
|
|
10
|
+
gem 'rubocop-rspec', require: false
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
gem 'legion-gaia', path: '../../legion-gaia'
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020 Esity
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# lex-cognitive-tide
|
|
2
|
+
|
|
3
|
+
Circadian-like cognitive rhythm engine for LegionIO. Models cognition as tidal forces: multiple oscillators create composite tide levels, tidal pools accumulate ideas during low tide, and high tide marks peak cognitive performance.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add to your Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem 'lex-cognitive-tide'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
client = Legion::Extensions::CognitiveTide::Client.new
|
|
17
|
+
|
|
18
|
+
# Add oscillators
|
|
19
|
+
client.add_oscillator(oscillator_type: :primary, period: 86_400, amplitude: 1.0, phase_offset: 0.0)
|
|
20
|
+
client.add_oscillator(oscillator_type: :secondary, period: 43_200, amplitude: 0.5, phase_offset: 1.5)
|
|
21
|
+
|
|
22
|
+
# Check current tide
|
|
23
|
+
status = client.tide_status
|
|
24
|
+
# => { level: 0.72, phase: :high_tide, label: "peak", oscillator_count: 2, pool_count: 0 }
|
|
25
|
+
|
|
26
|
+
# Deposit an idea during low tide
|
|
27
|
+
client.deposit_idea(domain: 'architecture', idea: 'refactor the auth layer', tide_threshold: 0.5)
|
|
28
|
+
|
|
29
|
+
# Harvest ideas when tide rises
|
|
30
|
+
client.harvest(min_depth: 0.1)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Actors
|
|
34
|
+
|
|
35
|
+
| Actor | Interval | What It Does |
|
|
36
|
+
|-------|----------|--------------|
|
|
37
|
+
| `TideCycle` | Every 60s | Advances oscillators via `tick!` and evaporates all tidal pools at `POOL_EVAPORATION_RATE` |
|
|
38
|
+
|
|
39
|
+
## License
|
|
40
|
+
|
|
41
|
+
MIT
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/legion/extensions/cognitive_tide/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'lex-cognitive-tide'
|
|
7
|
+
spec.version = Legion::Extensions::CognitiveTide::VERSION
|
|
8
|
+
spec.authors = ['Esity']
|
|
9
|
+
spec.email = ['matthewdiverson@gmail.com']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'LEX CognitiveTide'
|
|
12
|
+
spec.description = 'Circadian-like cognitive rhythm engine for brain-modeled agentic AI — ' \
|
|
13
|
+
'tidal oscillators, composite tide levels, and tidal pool idea accumulation'
|
|
14
|
+
spec.homepage = 'https://github.com/LegionIO/lex-cognitive-tide'
|
|
15
|
+
spec.license = 'MIT'
|
|
16
|
+
spec.required_ruby_version = '>= 3.4'
|
|
17
|
+
|
|
18
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
|
19
|
+
spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-cognitive-tide'
|
|
20
|
+
spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-cognitive-tide'
|
|
21
|
+
spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-cognitive-tide'
|
|
22
|
+
spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-cognitive-tide/issues'
|
|
23
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
24
|
+
|
|
25
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
26
|
+
Dir.glob('{lib,spec}/**/*') + %w[lex-cognitive-tide.gemspec Gemfile LICENSE README.md]
|
|
27
|
+
end
|
|
28
|
+
spec.require_paths = ['lib']
|
|
29
|
+
spec.add_development_dependency 'legion-gaia'
|
|
30
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/actors/every'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module CognitiveTide
|
|
8
|
+
module Actor
|
|
9
|
+
class TideCycle < Legion::Extensions::Actors::Every
|
|
10
|
+
def runner_class
|
|
11
|
+
Legion::Extensions::CognitiveTide::Runners::CognitiveTide
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def runner_function
|
|
15
|
+
'tide_maintenance'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def time
|
|
19
|
+
60
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def run_now?
|
|
23
|
+
false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def use_runner?
|
|
27
|
+
false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def check_subtask?
|
|
31
|
+
false
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def generate_task?
|
|
35
|
+
false
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/cognitive_tide/helpers/constants'
|
|
4
|
+
require 'legion/extensions/cognitive_tide/helpers/oscillator'
|
|
5
|
+
require 'legion/extensions/cognitive_tide/helpers/tidal_pool'
|
|
6
|
+
require 'legion/extensions/cognitive_tide/helpers/tide_engine'
|
|
7
|
+
require 'legion/extensions/cognitive_tide/runners/cognitive_tide'
|
|
8
|
+
|
|
9
|
+
module Legion
|
|
10
|
+
module Extensions
|
|
11
|
+
module CognitiveTide
|
|
12
|
+
class Client
|
|
13
|
+
include Runners::CognitiveTide
|
|
14
|
+
|
|
15
|
+
def initialize(**)
|
|
16
|
+
@tide_engine = Helpers::TideEngine.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
attr_reader :tide_engine
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module CognitiveTide
|
|
6
|
+
module Helpers
|
|
7
|
+
module Constants
|
|
8
|
+
TIDE_PHASES = %i[rising high_tide falling low_tide].freeze
|
|
9
|
+
OSCILLATOR_TYPES = %i[primary secondary lunar].freeze
|
|
10
|
+
|
|
11
|
+
MAX_POOLS = 50
|
|
12
|
+
POOL_EVAPORATION_RATE = 0.01
|
|
13
|
+
|
|
14
|
+
# Range-based tide label lookup — ranges are ordered from highest to lowest
|
|
15
|
+
TIDE_LABELS = [
|
|
16
|
+
{ range: (0.85..1.0), label: 'peak' },
|
|
17
|
+
{ range: (0.65..0.85), label: 'high' },
|
|
18
|
+
{ range: (0.45..0.65), label: 'moderate' },
|
|
19
|
+
{ range: (0.25..0.45), label: 'low' },
|
|
20
|
+
{ range: (0.0..0.25), label: 'ebb' }
|
|
21
|
+
].freeze
|
|
22
|
+
|
|
23
|
+
# Spring tide threshold: two oscillators are considered in phase when their values
|
|
24
|
+
# differ by less than this proportion of their combined amplitude
|
|
25
|
+
SPRING_TIDE_PHASE_TOLERANCE = 0.15
|
|
26
|
+
|
|
27
|
+
# Minimum tide level above which harvest is permitted
|
|
28
|
+
HARVEST_RISING_THRESHOLD = 0.3
|
|
29
|
+
|
|
30
|
+
# Forecast resolution: seconds between each forecast sample
|
|
31
|
+
FORECAST_RESOLUTION = 300
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module CognitiveTide
|
|
6
|
+
module Helpers
|
|
7
|
+
class Oscillator
|
|
8
|
+
attr_reader :oscillator_type, :period, :amplitude, :phase_offset, :id
|
|
9
|
+
|
|
10
|
+
def initialize(oscillator_type:, period:, amplitude: 1.0, phase_offset: 0.0)
|
|
11
|
+
unless Constants::OSCILLATOR_TYPES.include?(oscillator_type)
|
|
12
|
+
raise ArgumentError, "unknown oscillator_type: #{oscillator_type.inspect}; " \
|
|
13
|
+
"must be one of #{Constants::OSCILLATOR_TYPES.inspect}"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
raise ArgumentError, 'period must be positive' unless period.positive?
|
|
17
|
+
|
|
18
|
+
@id = SecureRandom.uuid
|
|
19
|
+
@oscillator_type = oscillator_type
|
|
20
|
+
@period = period.to_f
|
|
21
|
+
@amplitude = amplitude.clamp(0.0, 1.0)
|
|
22
|
+
@phase_offset = phase_offset.to_f
|
|
23
|
+
@last_ticked_at = nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Advance the oscillator by computing its current sinusoidal value
|
|
27
|
+
def tick!
|
|
28
|
+
@last_ticked_at = Time.now.utc
|
|
29
|
+
current_value
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Sinusoidal value at a given time, normalized to [0, 1]
|
|
33
|
+
def value_at(time)
|
|
34
|
+
t = time.to_f
|
|
35
|
+
radians = ((2.0 * Math::PI * t) / @period) + @phase_offset
|
|
36
|
+
# sin ranges [-1, 1] — shift to [0, 1]
|
|
37
|
+
((Math.sin(radians) + 1.0) / 2.0 * @amplitude).round(10)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Current value based on Time.now
|
|
41
|
+
def current_value
|
|
42
|
+
value_at(Time.now.utc)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Two oscillators are in phase when their normalized values are within tolerance
|
|
46
|
+
def in_phase_with?(other)
|
|
47
|
+
combined_amplitude = [@amplitude, other.amplitude].sum
|
|
48
|
+
return false if combined_amplitude.zero?
|
|
49
|
+
|
|
50
|
+
tolerance = Constants::SPRING_TIDE_PHASE_TOLERANCE * combined_amplitude
|
|
51
|
+
(current_value - other.current_value).abs <= tolerance
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def to_h
|
|
55
|
+
{
|
|
56
|
+
id: @id,
|
|
57
|
+
oscillator_type: @oscillator_type,
|
|
58
|
+
period: @period,
|
|
59
|
+
amplitude: @amplitude,
|
|
60
|
+
phase_offset: @phase_offset,
|
|
61
|
+
current_value: current_value,
|
|
62
|
+
last_ticked_at: @last_ticked_at
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module CognitiveTide
|
|
6
|
+
module Helpers
|
|
7
|
+
class TidalPool
|
|
8
|
+
attr_reader :id, :domain, :capacity, :evaporation_count
|
|
9
|
+
|
|
10
|
+
def initialize(domain:, capacity: 20)
|
|
11
|
+
raise ArgumentError, 'capacity must be positive' unless capacity.positive?
|
|
12
|
+
|
|
13
|
+
@id = SecureRandom.uuid
|
|
14
|
+
@domain = domain.to_s
|
|
15
|
+
@capacity = capacity
|
|
16
|
+
@items = []
|
|
17
|
+
@evaporation_count = 0
|
|
18
|
+
@created_at = Time.now.utc
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Deposit an idea item into the pool; silently drops if full
|
|
22
|
+
def deposit(item)
|
|
23
|
+
return false if full?
|
|
24
|
+
|
|
25
|
+
@items << { content: item, deposited_at: Time.now.utc, id: SecureRandom.uuid }
|
|
26
|
+
true
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Harvest all items from the pool, clearing it; returns the harvested items
|
|
30
|
+
def harvest!
|
|
31
|
+
harvested = @items.dup
|
|
32
|
+
@items.clear
|
|
33
|
+
harvested
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Apply evaporation: remove a proportion of items (oldest first)
|
|
37
|
+
def evaporate!(rate = Constants::POOL_EVAPORATION_RATE)
|
|
38
|
+
clamped_rate = rate.clamp(0.0, 1.0)
|
|
39
|
+
count_to_remove = (@items.size * clamped_rate).ceil
|
|
40
|
+
removed = @items.shift(count_to_remove)
|
|
41
|
+
@evaporation_count += removed.size
|
|
42
|
+
removed.size
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def empty?
|
|
46
|
+
@items.empty?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def full?
|
|
50
|
+
@items.size >= @capacity
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Depth as a fraction of capacity: item_count / capacity
|
|
54
|
+
def depth
|
|
55
|
+
return 0.0 if @capacity.zero?
|
|
56
|
+
|
|
57
|
+
(@items.size.to_f / @capacity).round(10)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def items
|
|
61
|
+
@items.dup
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def size
|
|
65
|
+
@items.size
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def to_h
|
|
69
|
+
{
|
|
70
|
+
id: @id,
|
|
71
|
+
domain: @domain,
|
|
72
|
+
capacity: @capacity,
|
|
73
|
+
size: @items.size,
|
|
74
|
+
depth: depth,
|
|
75
|
+
evaporation_count: @evaporation_count,
|
|
76
|
+
created_at: @created_at
|
|
77
|
+
}
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module CognitiveTide
|
|
6
|
+
module Helpers
|
|
7
|
+
class TideEngine
|
|
8
|
+
attr_reader :oscillators, :pools
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@oscillators = []
|
|
12
|
+
@pools = []
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Add an oscillator; returns the new Oscillator instance
|
|
16
|
+
def add_oscillator(oscillator_type:, period:, amplitude: 1.0, phase_offset: 0.0)
|
|
17
|
+
osc = Oscillator.new(
|
|
18
|
+
oscillator_type: oscillator_type,
|
|
19
|
+
period: period,
|
|
20
|
+
amplitude: amplitude,
|
|
21
|
+
phase_offset: phase_offset
|
|
22
|
+
)
|
|
23
|
+
@oscillators << osc
|
|
24
|
+
osc
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Composite tide level: clamped sum of all oscillator current values, normalized to [0, 1]
|
|
28
|
+
def composite_tide_level
|
|
29
|
+
return 0.0 if @oscillators.empty?
|
|
30
|
+
|
|
31
|
+
raw = @oscillators.sum(&:current_value)
|
|
32
|
+
max = @oscillators.sum(&:amplitude)
|
|
33
|
+
return 0.0 if max.zero?
|
|
34
|
+
|
|
35
|
+
(raw / max).clamp(0.0, 1.0).round(10)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Determine phase based on comparison of current and recent tide level
|
|
39
|
+
def current_phase
|
|
40
|
+
level = composite_tide_level
|
|
41
|
+
previous = previous_level
|
|
42
|
+
|
|
43
|
+
if level >= 0.65
|
|
44
|
+
:high_tide
|
|
45
|
+
elsif level <= 0.35
|
|
46
|
+
:low_tide
|
|
47
|
+
elsif level > previous
|
|
48
|
+
:rising
|
|
49
|
+
else
|
|
50
|
+
:falling
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Create a new tidal pool for a domain; respects MAX_POOLS limit
|
|
55
|
+
def create_pool(domain:, capacity: 20)
|
|
56
|
+
return nil if @pools.size >= Constants::MAX_POOLS
|
|
57
|
+
|
|
58
|
+
existing = find_pool(domain)
|
|
59
|
+
return existing if existing
|
|
60
|
+
|
|
61
|
+
pool = TidalPool.new(domain: domain, capacity: capacity)
|
|
62
|
+
@pools << pool
|
|
63
|
+
pool
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Deposit an item into a domain pool (creates the pool if needed)
|
|
67
|
+
def deposit_to_pool(domain:, item:, capacity: 20)
|
|
68
|
+
pool = find_pool(domain) || create_pool(domain: domain, capacity: capacity)
|
|
69
|
+
return false unless pool
|
|
70
|
+
|
|
71
|
+
pool.deposit(item)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Harvest pools only when tide is rising; returns hash of domain => items
|
|
75
|
+
def harvest_pools(min_depth: 0.0)
|
|
76
|
+
return {} unless rising?
|
|
77
|
+
|
|
78
|
+
result = {}
|
|
79
|
+
@pools.each do |pool|
|
|
80
|
+
next if pool.depth < min_depth
|
|
81
|
+
next if pool.empty?
|
|
82
|
+
|
|
83
|
+
result[pool.domain] = pool.harvest!
|
|
84
|
+
end
|
|
85
|
+
result
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Apply evaporation to all pools
|
|
89
|
+
def evaporate_all!(rate = Constants::POOL_EVAPORATION_RATE)
|
|
90
|
+
@pools.sum { |pool| pool.evaporate!(rate) }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Forecast tide level at regular intervals over a duration (seconds)
|
|
94
|
+
def tide_forecast(duration)
|
|
95
|
+
return [] if @oscillators.empty?
|
|
96
|
+
|
|
97
|
+
steps = (duration.to_f / Constants::FORECAST_RESOLUTION).ceil
|
|
98
|
+
now = Time.now.utc
|
|
99
|
+
|
|
100
|
+
(0...steps).map do |step|
|
|
101
|
+
t = now + (step * Constants::FORECAST_RESOLUTION)
|
|
102
|
+
level = forecast_level_at(t)
|
|
103
|
+
{ time: t, level: level, label: tide_label(level) }
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def high_tide?
|
|
108
|
+
composite_tide_level >= 0.65
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def low_tide?
|
|
112
|
+
composite_tide_level <= 0.35
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def rising?
|
|
116
|
+
current_phase == :rising
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def tide_report
|
|
120
|
+
level = composite_tide_level
|
|
121
|
+
{
|
|
122
|
+
level: level,
|
|
123
|
+
phase: current_phase,
|
|
124
|
+
label: tide_label(level),
|
|
125
|
+
oscillator_count: @oscillators.size,
|
|
126
|
+
pool_count: @pools.size,
|
|
127
|
+
high_tide: high_tide?,
|
|
128
|
+
low_tide: low_tide?,
|
|
129
|
+
pools: @pools.map(&:to_h),
|
|
130
|
+
oscillators: @oscillators.map(&:to_h)
|
|
131
|
+
}
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
private
|
|
135
|
+
|
|
136
|
+
def find_pool(domain)
|
|
137
|
+
@pools.find { |p| p.domain == domain.to_s }
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def previous_level
|
|
141
|
+
return 0.0 if @oscillators.empty?
|
|
142
|
+
|
|
143
|
+
t = Time.now.utc - Constants::FORECAST_RESOLUTION
|
|
144
|
+
raw = @oscillators.sum { |osc| osc.value_at(t) }
|
|
145
|
+
max = @oscillators.sum(&:amplitude)
|
|
146
|
+
return 0.0 if max.zero?
|
|
147
|
+
|
|
148
|
+
(raw / max).clamp(0.0, 1.0)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def forecast_level_at(time)
|
|
152
|
+
raw = @oscillators.sum { |osc| osc.value_at(time) }
|
|
153
|
+
max = @oscillators.sum(&:amplitude)
|
|
154
|
+
return 0.0 if max.zero?
|
|
155
|
+
|
|
156
|
+
(raw / max).clamp(0.0, 1.0).round(10)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def tide_label(level)
|
|
160
|
+
entry = Constants::TIDE_LABELS.find { |tl| tl[:range].cover?(level) }
|
|
161
|
+
entry ? entry[:label] : 'ebb'
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module CognitiveTide
|
|
6
|
+
module Runners
|
|
7
|
+
module CognitiveTide
|
|
8
|
+
extend self
|
|
9
|
+
|
|
10
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
|
|
11
|
+
Legion::Extensions::Helpers.const_defined?(:Lex)
|
|
12
|
+
|
|
13
|
+
def add_oscillator(oscillator_type: :primary, period: 86_400, amplitude: 1.0,
|
|
14
|
+
phase_offset: 0.0, engine: nil, **)
|
|
15
|
+
eng = engine || tide_engine
|
|
16
|
+
osc = eng.add_oscillator(
|
|
17
|
+
oscillator_type: oscillator_type.to_sym,
|
|
18
|
+
period: period,
|
|
19
|
+
amplitude: amplitude,
|
|
20
|
+
phase_offset: phase_offset
|
|
21
|
+
)
|
|
22
|
+
Legion::Logging.debug "[cognitive_tide] oscillator added: type=#{oscillator_type} " \
|
|
23
|
+
"period=#{period} amplitude=#{amplitude}"
|
|
24
|
+
{ success: true, oscillator: osc.to_h }
|
|
25
|
+
rescue ArgumentError => e
|
|
26
|
+
Legion::Logging.error "[cognitive_tide] add_oscillator failed: #{e.message}"
|
|
27
|
+
{ success: false, error: e.message }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def check_tide(engine: nil, **)
|
|
31
|
+
eng = engine || tide_engine
|
|
32
|
+
level = eng.composite_tide_level
|
|
33
|
+
phase = eng.current_phase
|
|
34
|
+
label = Helpers::Constants::TIDE_LABELS.find { |tl| tl[:range].cover?(level) }&.fetch(:label, 'ebb')
|
|
35
|
+
Legion::Logging.debug "[cognitive_tide] check_tide: level=#{level.round(3)} phase=#{phase} label=#{label}"
|
|
36
|
+
{
|
|
37
|
+
success: true,
|
|
38
|
+
level: level,
|
|
39
|
+
phase: phase,
|
|
40
|
+
label: label
|
|
41
|
+
}
|
|
42
|
+
rescue ArgumentError => e
|
|
43
|
+
Legion::Logging.error "[cognitive_tide] check_tide failed: #{e.message}"
|
|
44
|
+
{ success: false, error: e.message }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def deposit_idea(domain:, idea:, capacity: 20, tide_threshold: nil, engine: nil, **)
|
|
48
|
+
eng = engine || tide_engine
|
|
49
|
+
|
|
50
|
+
if tide_threshold
|
|
51
|
+
level = eng.composite_tide_level
|
|
52
|
+
if level > tide_threshold
|
|
53
|
+
Legion::Logging.debug "[cognitive_tide] deposit_idea skipped: tide=#{level.round(3)} above threshold=#{tide_threshold}"
|
|
54
|
+
return { success: false, reason: :tide_too_high, level: level }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
deposited = eng.deposit_to_pool(domain: domain, item: idea, capacity: capacity)
|
|
59
|
+
Legion::Logging.debug "[cognitive_tide] deposit_idea: domain=#{domain} deposited=#{deposited}"
|
|
60
|
+
{ success: deposited, domain: domain }
|
|
61
|
+
rescue ArgumentError => e
|
|
62
|
+
Legion::Logging.error "[cognitive_tide] deposit_idea failed: #{e.message}"
|
|
63
|
+
{ success: false, error: e.message }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def harvest(min_depth: 0.0, engine: nil, **)
|
|
67
|
+
eng = engine || tide_engine
|
|
68
|
+
result = eng.harvest_pools(min_depth: min_depth.to_f)
|
|
69
|
+
total = result.values.sum(&:size)
|
|
70
|
+
Legion::Logging.debug "[cognitive_tide] harvest: domains=#{result.keys.size} total_items=#{total}"
|
|
71
|
+
{ success: true, harvested: result, total_items: total }
|
|
72
|
+
rescue ArgumentError => e
|
|
73
|
+
Legion::Logging.error "[cognitive_tide] harvest failed: #{e.message}"
|
|
74
|
+
{ success: false, error: e.message }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def tide_forecast(duration: 86_400, engine: nil, **)
|
|
78
|
+
eng = engine || tide_engine
|
|
79
|
+
forecast = eng.tide_forecast(duration)
|
|
80
|
+
Legion::Logging.debug "[cognitive_tide] tide_forecast: duration=#{duration} steps=#{forecast.size}"
|
|
81
|
+
{ success: true, forecast: forecast, duration: duration }
|
|
82
|
+
rescue ArgumentError => e
|
|
83
|
+
Legion::Logging.error "[cognitive_tide] tide_forecast failed: #{e.message}"
|
|
84
|
+
{ success: false, error: e.message }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def tide_status(engine: nil, **)
|
|
88
|
+
eng = engine || tide_engine
|
|
89
|
+
report = eng.tide_report
|
|
90
|
+
Legion::Logging.debug "[cognitive_tide] tide_status: level=#{report[:level].round(3)} " \
|
|
91
|
+
"phase=#{report[:phase]} pools=#{report[:pool_count]}"
|
|
92
|
+
report.merge(success: true)
|
|
93
|
+
rescue ArgumentError => e
|
|
94
|
+
Legion::Logging.error "[cognitive_tide] tide_status failed: #{e.message}"
|
|
95
|
+
{ success: false, error: e.message }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def tide_maintenance(engine: nil, **)
|
|
99
|
+
eng = engine || tide_engine
|
|
100
|
+
eng.evaporate_all!
|
|
101
|
+
eng.oscillators.each(&:tick!)
|
|
102
|
+
pools_maintained = eng.pools.size
|
|
103
|
+
phase = eng.current_phase
|
|
104
|
+
level = eng.composite_tide_level
|
|
105
|
+
Legion::Logging.debug "[tide] maintenance: pools=#{pools_maintained} phase=#{phase} level=#{level}"
|
|
106
|
+
{ pools_maintained: pools_maintained, current_phase: phase, tide_level: level }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
def tide_engine
|
|
112
|
+
@tide_engine ||= Helpers::TideEngine.new
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
require_relative 'cognitive_tide/version'
|
|
5
|
+
require_relative 'cognitive_tide/helpers/constants'
|
|
6
|
+
require_relative 'cognitive_tide/helpers/oscillator'
|
|
7
|
+
require_relative 'cognitive_tide/helpers/tidal_pool'
|
|
8
|
+
require_relative 'cognitive_tide/helpers/tide_engine'
|
|
9
|
+
require_relative 'cognitive_tide/runners/cognitive_tide'
|
|
10
|
+
|
|
11
|
+
module Legion
|
|
12
|
+
module Extensions
|
|
13
|
+
module CognitiveTide
|
|
14
|
+
extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|