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 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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveTide
6
+ VERSION = '0.1.1'
7
+ end
8
+ end
9
+ 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