lex-cognitive-pendulum 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f99df9563513ea075e00bd072f2e4c2157e0bcebb7ccd533c824e45bf26ac574
4
+ data.tar.gz: c63527d035a0a23b9d51e0be50316fcf006713c2f1cff1227451a189009430c8
5
+ SHA512:
6
+ metadata.gz: c97ffd26c6de16c060383d895d7377d383f1457d153006ffba08b021986558b38c954227e88e1a71993aa0cbc3676be90bda5f5640aa89ab4bc14955f3d855b4
7
+ data.tar.gz: e27db8176a304f1ec1de57cc1c25bd91db0e769929a8dfd4aa618a17b300e2978ccfcd6e9ed3548f97ab18a732081af9e5293fb225b34fa5c57b1124d549fd11
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ gem 'rspec', '~> 3.13'
8
+ gem 'rubocop', '~> 1.75', require: false
9
+ gem 'rubocop-rspec', require: false
10
+
11
+ gem 'legion-gaia', path: '../../legion-gaia'
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/legion/extensions/cognitive_pendulum/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'lex-cognitive-pendulum'
7
+ spec.version = Legion::Extensions::CognitivePendulum::VERSION
8
+ spec.authors = ['Esity']
9
+ spec.email = ['matthewdiverson@gmail.com']
10
+
11
+ spec.summary = 'LEX Cognitive Pendulum'
12
+ spec.description = 'Models oscillation between cognitive poles with amplitude, period, damping, and resonance detection'
13
+ spec.homepage = 'https://github.com/LegionIO/lex-cognitive-pendulum'
14
+ spec.license = 'MIT'
15
+ spec.required_ruby_version = '>= 3.4'
16
+
17
+ spec.metadata['homepage_uri'] = spec.homepage
18
+ spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-cognitive-pendulum'
19
+ spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-cognitive-pendulum'
20
+ spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-cognitive-pendulum'
21
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-cognitive-pendulum/issues'
22
+ spec.metadata['rubygems_mfa_required'] = 'true'
23
+
24
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
25
+ Dir.glob('{lib,spec}/**/*') + %w[lex-cognitive-pendulum.gemspec Gemfile]
26
+ end
27
+ spec.require_paths = ['lib']
28
+ spec.add_development_dependency 'legion-gaia'
29
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/cognitive_pendulum/helpers/constants'
4
+ require 'legion/extensions/cognitive_pendulum/helpers/pendulum'
5
+ require 'legion/extensions/cognitive_pendulum/helpers/pendulum_engine'
6
+ require 'legion/extensions/cognitive_pendulum/runners/cognitive_pendulum'
7
+
8
+ module Legion
9
+ module Extensions
10
+ module CognitivePendulum
11
+ class Client
12
+ include Runners::CognitivePendulum
13
+
14
+ def initialize(**)
15
+ @pendulum_engine = Helpers::PendulumEngine.new
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :pendulum_engine
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitivePendulum
6
+ module Helpers
7
+ module Constants
8
+ POLE_PAIRS = {
9
+ certainty_doubt: %i[certainty doubt],
10
+ focus_diffusion: %i[focus diffusion],
11
+ analysis_intuition: %i[analysis intuition],
12
+ approach_avoidance: %i[approach avoidance],
13
+ convergent_divergent: %i[convergent divergent]
14
+ }.freeze
15
+
16
+ DAMPING_RATE = 0.01
17
+
18
+ MAX_PENDULUMS = 100
19
+
20
+ AMPLITUDE_LABELS = {
21
+ (0.0..0.2) => :minimal,
22
+ (0.2..0.4) => :low,
23
+ (0.4..0.6) => :moderate,
24
+ (0.6..0.8) => :high,
25
+ (0.8..1.0) => :maximal
26
+ }.freeze
27
+
28
+ module_function
29
+
30
+ def valid_pole_pair?(pole_pair)
31
+ POLE_PAIRS.key?(pole_pair)
32
+ end
33
+
34
+ def amplitude_label(amplitude)
35
+ clamped = amplitude.clamp(0.0, 1.0)
36
+ AMPLITUDE_LABELS.each do |range, label|
37
+ return label if range.cover?(clamped)
38
+ end
39
+ :maximal
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitivePendulum
6
+ module Helpers
7
+ class Pendulum
8
+ attr_reader :id, :pole_pair, :amplitude, :period, :damping, :current_position, :created_at, :swings
9
+
10
+ def initialize(pole_pair:, amplitude: 0.5, period: 10.0, damping: Constants::DAMPING_RATE)
11
+ raise ArgumentError, "unknown pole_pair: #{pole_pair}" unless Constants.valid_pole_pair?(pole_pair)
12
+ raise ArgumentError, 'amplitude must be 0.0..1.0' unless amplitude.between?(0.0, 1.0)
13
+ raise ArgumentError, 'period must be positive' unless period.positive?
14
+ raise ArgumentError, 'damping must be >= 0' unless damping >= 0.0
15
+
16
+ @id = SecureRandom.uuid
17
+ @pole_pair = pole_pair
18
+ @amplitude = amplitude.clamp(0.0, 1.0)
19
+ @period = period.to_f
20
+ @damping = damping.to_f
21
+ @current_position = 0.0
22
+ @created_at = Time.now.utc
23
+ @swings = 0
24
+ end
25
+
26
+ def swing!(force: 0.0)
27
+ force_clamped = force.to_f.clamp(-1.0, 1.0)
28
+ @current_position = (@current_position + force_clamped).clamp(-1.0, 1.0)
29
+ @swings += 1
30
+ @current_position
31
+ end
32
+
33
+ def damp!
34
+ @amplitude = (@amplitude * (1.0 - @damping)).round(10).clamp(0.0, 1.0)
35
+ @current_position = (@current_position * (1.0 - @damping)).round(10).clamp(-1.0, 1.0)
36
+ @amplitude
37
+ end
38
+
39
+ def position_at(time)
40
+ elapsed = time.to_f
41
+ angular_frequency = (2.0 * Math::PI) / @period
42
+ decay = Math.exp(-@damping * elapsed)
43
+ (@amplitude * decay * Math.cos(angular_frequency * elapsed)).round(10).clamp(-1.0, 1.0)
44
+ end
45
+
46
+ def at_pole_a?
47
+ @current_position <= -0.5
48
+ end
49
+
50
+ def at_pole_b?
51
+ @current_position >= 0.5
52
+ end
53
+
54
+ def amplitude_label
55
+ Constants.amplitude_label(@amplitude)
56
+ end
57
+
58
+ def resonant_with?(frequency)
59
+ return false unless frequency.positive?
60
+
61
+ natural_frequency = 1.0 / @period
62
+ ratio = frequency / natural_frequency
63
+ (ratio - 1.0).abs <= 0.05
64
+ end
65
+
66
+ def dominant_pole
67
+ poles = Constants::POLE_PAIRS.fetch(@pole_pair)
68
+ return :neutral if @current_position.abs < 0.1
69
+
70
+ @current_position.negative? ? poles[0] : poles[1]
71
+ end
72
+
73
+ def to_h
74
+ poles = Constants::POLE_PAIRS.fetch(@pole_pair)
75
+ {
76
+ id: @id,
77
+ pole_pair: @pole_pair,
78
+ pole_a: poles[0],
79
+ pole_b: poles[1],
80
+ amplitude: @amplitude.round(10),
81
+ amplitude_label: amplitude_label,
82
+ period: @period,
83
+ damping: @damping,
84
+ current_position: @current_position.round(10),
85
+ dominant_pole: dominant_pole,
86
+ at_pole_a: at_pole_a?,
87
+ at_pole_b: at_pole_b?,
88
+ swings: @swings,
89
+ created_at: @created_at
90
+ }
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitivePendulum
6
+ module Helpers
7
+ class PendulumEngine
8
+ attr_reader :pendulums
9
+
10
+ def initialize
11
+ @pendulums = {}
12
+ end
13
+
14
+ def create_pendulum(pole_pair:, amplitude: 0.5, period: 10.0, damping: Constants::DAMPING_RATE)
15
+ raise ArgumentError, "max pendulums (#{Constants::MAX_PENDULUMS}) reached" if @pendulums.size >= Constants::MAX_PENDULUMS
16
+
17
+ pendulum = Pendulum.new(
18
+ pole_pair: pole_pair,
19
+ amplitude: amplitude,
20
+ period: period,
21
+ damping: damping
22
+ )
23
+ @pendulums[pendulum.id] = pendulum
24
+ pendulum
25
+ end
26
+
27
+ def swing(pendulum_id, force: 0.0)
28
+ p = @pendulums[pendulum_id]
29
+ return nil unless p
30
+
31
+ p.swing!(force: force)
32
+ p
33
+ end
34
+
35
+ def damp_all!
36
+ @pendulums.each_value(&:damp!)
37
+ end
38
+
39
+ def check_resonance(frequency)
40
+ return [] unless frequency.positive?
41
+
42
+ @pendulums.values.select { |p| p.resonant_with?(frequency) }.map(&:id)
43
+ end
44
+
45
+ def dominant_pole(pendulum_id)
46
+ p = @pendulums[pendulum_id]
47
+ return nil unless p
48
+
49
+ p.dominant_pole
50
+ end
51
+
52
+ def most_active(limit: 5)
53
+ @pendulums.values
54
+ .sort_by { |p| -p.amplitude }
55
+ .first(limit)
56
+ end
57
+
58
+ def most_damped(limit: 5)
59
+ @pendulums.values
60
+ .sort_by(&:amplitude)
61
+ .first(limit)
62
+ end
63
+
64
+ def pendulum_report
65
+ {
66
+ total: @pendulums.size,
67
+ max: Constants::MAX_PENDULUMS,
68
+ pole_pairs: @pendulums.values.group_by(&:pole_pair).transform_values(&:count),
69
+ most_active: most_active(limit: 3).map(&:to_h),
70
+ most_damped: most_damped(limit: 3).map(&:to_h)
71
+ }
72
+ end
73
+
74
+ def get(pendulum_id)
75
+ @pendulums[pendulum_id]
76
+ end
77
+
78
+ def count
79
+ @pendulums.size
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitivePendulum
6
+ module Runners
7
+ module CognitivePendulum
8
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
9
+ Legion::Extensions::Helpers.const_defined?(:Lex)
10
+
11
+ def create_pendulum(pole_pair:, amplitude: 0.5, period: 10.0, damping: Helpers::Constants::DAMPING_RATE, **)
12
+ unless Helpers::Constants.valid_pole_pair?(pole_pair)
13
+ return { success: false, error: :invalid_pole_pair, valid_pairs: Helpers::Constants::POLE_PAIRS.keys }
14
+ end
15
+
16
+ raise ArgumentError, 'amplitude must be 0.0..1.0' unless amplitude.between?(0.0, 1.0)
17
+ raise ArgumentError, 'period must be positive' unless period.positive?
18
+
19
+ if pendulum_engine.count >= Helpers::Constants::MAX_PENDULUMS
20
+ return { success: false, error: :max_pendulums_reached, max: Helpers::Constants::MAX_PENDULUMS }
21
+ end
22
+
23
+ pendulum = pendulum_engine.create_pendulum(
24
+ pole_pair: pole_pair,
25
+ amplitude: amplitude,
26
+ period: period,
27
+ damping: damping
28
+ )
29
+
30
+ Legion::Logging.debug "[cognitive_pendulum] created pole_pair=#{pole_pair} id=#{pendulum.id[0..7]}"
31
+ { success: true, pendulum_id: pendulum.id, pole_pair: pole_pair, amplitude: pendulum.amplitude }
32
+ rescue ArgumentError => e
33
+ { success: false, error: :argument_error, message: e.message }
34
+ end
35
+
36
+ def swing(pendulum_id:, force: 0.0, **)
37
+ result = pendulum_engine.swing(pendulum_id, force: force)
38
+ unless result
39
+ Legion::Logging.debug "[cognitive_pendulum] swing failed: #{pendulum_id[0..7]} not found"
40
+ return { success: false, error: :not_found }
41
+ end
42
+
43
+ Legion::Logging.debug "[cognitive_pendulum] swing id=#{pendulum_id[0..7]} position=#{result.current_position.round(4)}"
44
+ { success: true, pendulum_id: pendulum_id, current_position: result.current_position, dominant_pole: result.dominant_pole }
45
+ rescue ArgumentError => e
46
+ { success: false, error: :argument_error, message: e.message }
47
+ end
48
+
49
+ def damp_all(**)
50
+ pendulum_engine.damp_all!
51
+ count = pendulum_engine.count
52
+ Legion::Logging.debug "[cognitive_pendulum] damped all (#{count} pendulums)"
53
+ { success: true, damped: count }
54
+ rescue ArgumentError => e
55
+ { success: false, error: :argument_error, message: e.message }
56
+ end
57
+
58
+ def check_resonance(frequency:, **)
59
+ return { success: false, error: :invalid_frequency, message: 'frequency must be positive' } unless frequency.to_f.positive?
60
+
61
+ resonant_ids = pendulum_engine.check_resonance(frequency.to_f)
62
+ Legion::Logging.debug "[cognitive_pendulum] resonance check frequency=#{frequency} matches=#{resonant_ids.size}"
63
+ { success: true, frequency: frequency, resonant_pendulum_ids: resonant_ids, count: resonant_ids.size }
64
+ rescue ArgumentError => e
65
+ { success: false, error: :argument_error, message: e.message }
66
+ end
67
+
68
+ def get_dominant_pole(pendulum_id:, **)
69
+ pole = pendulum_engine.dominant_pole(pendulum_id)
70
+ if pole.nil?
71
+ Legion::Logging.debug "[cognitive_pendulum] dominant_pole failed: #{pendulum_id[0..7]} not found"
72
+ return { success: false, error: :not_found }
73
+ end
74
+
75
+ Legion::Logging.debug "[cognitive_pendulum] dominant_pole id=#{pendulum_id[0..7]} pole=#{pole}"
76
+ { success: true, pendulum_id: pendulum_id, dominant_pole: pole }
77
+ rescue ArgumentError => e
78
+ { success: false, error: :argument_error, message: e.message }
79
+ end
80
+
81
+ def most_active(limit: 5, **)
82
+ pendulums = pendulum_engine.most_active(limit: limit)
83
+ Legion::Logging.debug "[cognitive_pendulum] most_active limit=#{limit} found=#{pendulums.size}"
84
+ { success: true, pendulums: pendulums.map(&:to_h), count: pendulums.size }
85
+ rescue ArgumentError => e
86
+ { success: false, error: :argument_error, message: e.message }
87
+ end
88
+
89
+ def most_damped(limit: 5, **)
90
+ pendulums = pendulum_engine.most_damped(limit: limit)
91
+ Legion::Logging.debug "[cognitive_pendulum] most_damped limit=#{limit} found=#{pendulums.size}"
92
+ { success: true, pendulums: pendulums.map(&:to_h), count: pendulums.size }
93
+ rescue ArgumentError => e
94
+ { success: false, error: :argument_error, message: e.message }
95
+ end
96
+
97
+ def pendulum_report(**)
98
+ report = pendulum_engine.pendulum_report
99
+ Legion::Logging.debug "[cognitive_pendulum] report total=#{report[:total]}"
100
+ { success: true, report: report }
101
+ rescue ArgumentError => e
102
+ { success: false, error: :argument_error, message: e.message }
103
+ end
104
+
105
+ def get_pendulum(pendulum_id:, **)
106
+ p = pendulum_engine.get(pendulum_id)
107
+ p ? { success: true, pendulum: p.to_h } : { success: false, error: :not_found }
108
+ rescue ArgumentError => e
109
+ { success: false, error: :argument_error, message: e.message }
110
+ end
111
+
112
+ private
113
+
114
+ def pendulum_engine
115
+ @pendulum_engine ||= Helpers::PendulumEngine.new
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitivePendulum
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+ require_relative 'cognitive_pendulum/version'
5
+ require_relative 'cognitive_pendulum/helpers/constants'
6
+ require_relative 'cognitive_pendulum/helpers/pendulum'
7
+ require_relative 'cognitive_pendulum/helpers/pendulum_engine'
8
+ require_relative 'cognitive_pendulum/runners/cognitive_pendulum'
9
+ require_relative 'cognitive_pendulum/client'
10
+
11
+ module Legion
12
+ module Extensions
13
+ module CognitivePendulum
14
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/cognitive_pendulum/client'
4
+
5
+ RSpec.describe Legion::Extensions::CognitivePendulum::Client do
6
+ it 'responds to runner methods' do
7
+ client = described_class.new
8
+ expect(client).to respond_to(:create_pendulum)
9
+ expect(client).to respond_to(:swing)
10
+ expect(client).to respond_to(:damp_all)
11
+ expect(client).to respond_to(:check_resonance)
12
+ expect(client).to respond_to(:get_dominant_pole)
13
+ expect(client).to respond_to(:most_active)
14
+ expect(client).to respond_to(:most_damped)
15
+ expect(client).to respond_to(:pendulum_report)
16
+ expect(client).to respond_to(:get_pendulum)
17
+ end
18
+
19
+ it 'creates a fresh engine per instance' do
20
+ c1 = described_class.new
21
+ c2 = described_class.new
22
+ c1.create_pendulum(pole_pair: :certainty_doubt)
23
+ expect(c2.pendulum_report[:report][:total]).to eq(0)
24
+ end
25
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::CognitivePendulum::Helpers::Constants do
4
+ describe 'POLE_PAIRS' do
5
+ it 'defines five pole pairs' do
6
+ expect(described_class::POLE_PAIRS.size).to eq(5)
7
+ end
8
+
9
+ it 'includes certainty_doubt' do
10
+ expect(described_class::POLE_PAIRS[:certainty_doubt]).to eq(%i[certainty doubt])
11
+ end
12
+
13
+ it 'includes focus_diffusion' do
14
+ expect(described_class::POLE_PAIRS[:focus_diffusion]).to eq(%i[focus diffusion])
15
+ end
16
+
17
+ it 'includes analysis_intuition' do
18
+ expect(described_class::POLE_PAIRS[:analysis_intuition]).to eq(%i[analysis intuition])
19
+ end
20
+
21
+ it 'includes approach_avoidance' do
22
+ expect(described_class::POLE_PAIRS[:approach_avoidance]).to eq(%i[approach avoidance])
23
+ end
24
+
25
+ it 'includes convergent_divergent' do
26
+ expect(described_class::POLE_PAIRS[:convergent_divergent]).to eq(%i[convergent divergent])
27
+ end
28
+
29
+ it 'is frozen' do
30
+ expect(described_class::POLE_PAIRS).to be_frozen
31
+ end
32
+ end
33
+
34
+ describe 'DAMPING_RATE' do
35
+ it 'is 0.01' do
36
+ expect(described_class::DAMPING_RATE).to eq(0.01)
37
+ end
38
+ end
39
+
40
+ describe 'MAX_PENDULUMS' do
41
+ it 'is 100' do
42
+ expect(described_class::MAX_PENDULUMS).to eq(100)
43
+ end
44
+ end
45
+
46
+ describe 'AMPLITUDE_LABELS' do
47
+ it 'has 5 ranges' do
48
+ expect(described_class::AMPLITUDE_LABELS.size).to eq(5)
49
+ end
50
+
51
+ it 'is frozen' do
52
+ expect(described_class::AMPLITUDE_LABELS).to be_frozen
53
+ end
54
+ end
55
+
56
+ describe '.valid_pole_pair?' do
57
+ it 'returns true for known pole pairs' do
58
+ expect(described_class.valid_pole_pair?(:certainty_doubt)).to be true
59
+ end
60
+
61
+ it 'returns false for unknown pole pairs' do
62
+ expect(described_class.valid_pole_pair?(:unknown)).to be false
63
+ end
64
+
65
+ it 'returns false for nil' do
66
+ expect(described_class.valid_pole_pair?(nil)).to be false
67
+ end
68
+ end
69
+
70
+ describe '.amplitude_label' do
71
+ it 'returns :minimal for 0.0' do
72
+ expect(described_class.amplitude_label(0.0)).to eq(:minimal)
73
+ end
74
+
75
+ it 'returns :minimal for 0.1' do
76
+ expect(described_class.amplitude_label(0.1)).to eq(:minimal)
77
+ end
78
+
79
+ it 'returns :low for 0.3' do
80
+ expect(described_class.amplitude_label(0.3)).to eq(:low)
81
+ end
82
+
83
+ it 'returns :moderate for 0.5' do
84
+ expect(described_class.amplitude_label(0.5)).to eq(:moderate)
85
+ end
86
+
87
+ it 'returns :high for 0.7' do
88
+ expect(described_class.amplitude_label(0.7)).to eq(:high)
89
+ end
90
+
91
+ it 'returns :maximal for 1.0' do
92
+ expect(described_class.amplitude_label(1.0)).to eq(:maximal)
93
+ end
94
+
95
+ it 'clamps values above 1.0' do
96
+ expect(described_class.amplitude_label(1.5)).to eq(:maximal)
97
+ end
98
+
99
+ it 'clamps values below 0.0' do
100
+ expect(described_class.amplitude_label(-0.5)).to eq(:minimal)
101
+ end
102
+ end
103
+ end