lex-cognitive-magnet 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: 8878597a795cca69e3919ff364db407c2a96b322271ec9929669f9a4ccfbd75b
4
+ data.tar.gz: 8ca1026de6ed94260b07e084e502a0c6a876dbecc893a3c78824c36b288b528b
5
+ SHA512:
6
+ metadata.gz: cedc41cdfaec14b72dc6987cf68c2de9b7c2154483b8d50c59d34db2434348c5032e315dd05005a413c17c2de8f8b2dd0f2fc9921532afa8676aa61fa9ad0e57
7
+ data.tar.gz: 2edad7d627afaec664d81211115566f5bc90ae00f3e2872f961ee8301bc34ef813fe833dce63cdbbbe87c2d1a0e919c87c79f5cc3729b8499422b1d04f812e49
@@ -0,0 +1,16 @@
1
+ name: CI
2
+ on:
3
+ push:
4
+ branches: [main]
5
+ pull_request:
6
+
7
+ jobs:
8
+ ci:
9
+ uses: LegionIO/.github/.github/workflows/ci.yml@main
10
+
11
+ release:
12
+ needs: ci
13
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
14
+ uses: LegionIO/.github/.github/workflows/release.yml@main
15
+ secrets:
16
+ rubygems-api-key: ${{ secrets.RUBYGEMS_API_KEY }}
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ .rspec_status
2
+ Gemfile.lock
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,64 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.4
3
+ NewCops: enable
4
+ SuggestExtensions: false
5
+
6
+ Layout/LineLength:
7
+ Max: 160
8
+
9
+ Layout/SpaceAroundEqualsInParameterDefault:
10
+ EnforcedStyle: space
11
+
12
+ Layout/HashAlignment:
13
+ EnforcedHashRocketStyle: table
14
+ EnforcedColonStyle: table
15
+
16
+ Metrics/MethodLength:
17
+ Max: 25
18
+
19
+ Metrics/ClassLength:
20
+ Max: 150
21
+
22
+ Metrics/ModuleLength:
23
+ Max: 1500
24
+
25
+ Metrics/BlockLength:
26
+ Max: 40
27
+ Exclude:
28
+ - 'spec/**/*'
29
+
30
+ Metrics/AbcSize:
31
+ Max: 25
32
+
33
+ Metrics/ParameterLists:
34
+ Max: 8
35
+ MaxOptionalParameters: 8
36
+
37
+ Metrics/CyclomaticComplexity:
38
+ Max: 15
39
+
40
+ Metrics/PerceivedComplexity:
41
+ Max: 17
42
+
43
+ Style/Documentation:
44
+ Enabled: false
45
+
46
+ Style/SymbolArray:
47
+ Enabled: true
48
+
49
+ Style/FrozenStringLiteralComment:
50
+ Enabled: true
51
+ EnforcedStyle: always
52
+
53
+ Style/OneClassPerFile:
54
+ Exclude:
55
+ - 'spec/spec_helper.rb'
56
+
57
+ Naming/FileName:
58
+ Enabled: false
59
+
60
+ Naming/PredicateMethod:
61
+ Enabled: false
62
+
63
+ Naming/PredicatePrefix:
64
+ Enabled: false
data/CLAUDE.md ADDED
@@ -0,0 +1,116 @@
1
+ # lex-cognitive-magnet
2
+
3
+ **Level 3 Leaf Documentation**
4
+ - **Parent**: `/Users/miverso2/rubymine/legion/extensions-agentic/CLAUDE.md`
5
+
6
+ ## Purpose
7
+
8
+ Magnetic field metaphor for cognitive attraction and repulsion. Poles represent cognitive elements with a polarity (positive, negative, neutral, bipolar) and a material type. When poles interact, attraction strengthens both; repulsion weakens both. Fields group poles and compute alignment scores via pair-wise interaction analysis. Models how ideas, beliefs, or preferences cluster and conflict in cognitive space.
9
+
10
+ ## Gem Info
11
+
12
+ - **Gem name**: `lex-cognitive-magnet`
13
+ - **Module**: `Legion::Extensions::CognitiveMagnet`
14
+ - **Version**: `0.1.0`
15
+ - **Ruby**: `>= 3.4`
16
+ - **License**: MIT
17
+
18
+ ## File Structure
19
+
20
+ ```
21
+ lib/legion/extensions/cognitive_magnet/
22
+ version.rb
23
+ client.rb
24
+ helpers/
25
+ constants.rb
26
+ pole.rb
27
+ field.rb
28
+ magnet_engine.rb
29
+ runners/
30
+ cognitive_magnet.rb
31
+ ```
32
+
33
+ ## Key Constants
34
+
35
+ | Constant | Value | Purpose |
36
+ |---|---|---|
37
+ | `POLARITY_TYPES` | `%i[positive negative neutral bipolar]` | Valid pole polarity values |
38
+ | `MATERIAL_TYPES` | `%i[iron cobalt nickel lodestone ferrite]` | Valid material types |
39
+ | `MAX_POLES` | `500` | Per-engine pole capacity |
40
+ | `MAX_FIELDS` | `50` | Per-engine field capacity |
41
+ | `ATTRACTION_RATE` | `0.08` | Base strength increase per attraction interaction |
42
+ | `REPULSION_RATE` | `0.06` | Base strength decrease per repulsion interaction |
43
+ | `DECAY_RATE` | `0.02` | Default strength reduction per demagnetize cycle |
44
+ | `STRENGTH_LABELS` | range hash | From `:negligible` to `:overwhelming` |
45
+ | `ALIGNMENT_LABELS` | range hash | From `:chaotic` to `:perfect` |
46
+
47
+ ## Helpers
48
+
49
+ ### `Helpers::Pole`
50
+ Individual magnetic pole. Has `id`, `polarity`, `strength` (0.0–1.0), `material_type`, `domain`, `content`, and `created_at`.
51
+
52
+ - `magnetize!(rate)` — increases strength by rate
53
+ - `demagnetize!(rate)` — decreases strength by rate
54
+ - `attracts?(other_pole)` — true if polarities are opposite; always true for `:bipolar`, always false for `:neutral`
55
+ - `repels?(other_pole)` — true if polarities match; always false for `:neutral` or `:bipolar`
56
+ - `saturated?` — strength >= 1.0
57
+ - `weak?` — strength <= 0.1
58
+ - `strength_label`
59
+ - `to_h`
60
+
61
+ ### `Helpers::Field`
62
+ Named container for poles. Has `id`, `name`, `pole_ids`, `alignment`, and `flux_density`.
63
+
64
+ - `add_pole(pole_id)` — adds pole ID (idempotent)
65
+ - `remove_pole(pole_id)` — removes pole ID
66
+ - `calculate_alignment!(poles)` — pair-wise computation: attraction pairs add `(a.strength + b.strength) / 2.0`, repulsion pairs subtract half that weight; result normalized to [0.0, 1.0]
67
+ - `coherent?` — alignment >= 0.6
68
+ - `chaotic?` — alignment < 0.2
69
+ - `alignment_label`
70
+ - `pole_count`
71
+ - `to_h`
72
+
73
+ ### `Helpers::MagnetEngine`
74
+ Top-level store. Enforces `MAX_POLES` and `MAX_FIELDS`.
75
+
76
+ - `create_pole(polarity:, content:, strength:, material_type:, domain:)` → pole or capacity error
77
+ - `create_field(name:)` → field or capacity error
78
+ - `magnetize(pole_id, rate:)` → strength hash
79
+ - `interact(pole_a_id, pole_b_id)` — mutual magnetize/demagnetize based on attraction/repulsion; logs interaction event
80
+ - `demagnetize_all!(rate:)` → count hash
81
+ - `most_aligned_fields(limit:)` → top N fields by alignment (recalculates before sort)
82
+ - `strongest_poles(limit:)` → top N poles by strength (excludes weak poles)
83
+ - `field_report` → aggregate stats including coherent/chaotic field counts
84
+ - `add_pole_to_field(field_id:, pole_id:)` → status hash
85
+ - `remove_pole_from_field(field_id:, pole_id:)` → status hash
86
+
87
+ ## Runners
88
+
89
+ Module: `Runners::CognitiveMagnet`
90
+
91
+ | Runner Method | Description |
92
+ |---|---|
93
+ | `create_pole(polarity:, content:, strength:, material_type:, domain:)` | Register a new pole |
94
+ | `create_field(name:)` | Create a field |
95
+ | `magnetize(pole_id:, rate:)` | Strengthen a pole |
96
+ | `interact(pole_a_id:, pole_b_id:)` | Trigger mutual interaction |
97
+ | `list_poles(limit:)` | Strongest poles |
98
+ | `magnetic_status` | Full field report |
99
+
100
+ All runners return `{success: true/false, ...}` hashes.
101
+
102
+ ## Integration Points
103
+
104
+ - No direct dependencies on other agentic LEX gems
105
+ - Fields with high coherence represent stable belief clusters; can feed `lex-emotion` valence
106
+ - Repulsion events between poles can register as `lex-conflict` conflicts
107
+ - `interact` drives organic reinforcement and weakening as ideas encounter each other in `lex-tick` cycles
108
+ - Chaotic fields (low alignment) can trigger `lex-prediction` uncertainty signals
109
+
110
+ ## Development Notes
111
+
112
+ - `Client` instantiates `@magnet_engine = Helpers::MagnetEngine.new`
113
+ - `attract?` / `repels?` are asymmetric: `:bipolar` always attracts any polarity, `:neutral` never attracts or repels
114
+ - `interact` logs to `@interaction_log` (unbounded array — callers should periodically prune for long-running sessions)
115
+ - `most_aligned_fields` calls `recalculate_all_fields` before sorting — this is O(fields * poles^2/2) pairs
116
+ - Alignment formula maps raw weighted average to [0.0, 1.0] via `(raw + 1.0) / 2.0`
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'
data/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # lex-cognitive-magnet
2
+
3
+ Magnetic field metaphor for cognitive attraction and repulsion in LegionIO agents. Poles represent ideas or beliefs with polarities; interacting poles strengthen or weaken each other; fields group poles and measure their collective alignment.
4
+
5
+ ## What It Does
6
+
7
+ - Four polarity types: `positive`, `negative`, `neutral`, `bipolar`
8
+ - Five material types: `iron`, `cobalt`, `nickel`, `lodestone`, `ferrite`
9
+ - Attraction: opposite polarities strengthen each other on interaction
10
+ - Repulsion: matching polarities weaken each other on interaction
11
+ - `bipolar` attracts everything; `neutral` never attracts or repels
12
+ - Fields group poles and compute alignment via pair-wise strength-weighted analysis
13
+ - Coherent fields (alignment >= 0.6) and chaotic fields (< 0.2) are classified
14
+ - `demagnetize_all` provides a global decay cycle
15
+
16
+ ## Usage
17
+
18
+ ```ruby
19
+ # Create poles
20
+ a = runner.create_pole(polarity: :positive, content: 'hypothesis_A',
21
+ strength: 0.6, material_type: :iron, domain: :reasoning)
22
+ b = runner.create_pole(polarity: :negative, content: 'counter_hypothesis',
23
+ strength: 0.5, material_type: :iron, domain: :reasoning)
24
+
25
+ # Interact — opposite polarities attract, both strengthen
26
+ runner.interact(pole_a_id: a[:pole][:id], pole_b_id: b[:pole][:id])
27
+ # => { type: :attraction, force: 0.3, ... }
28
+
29
+ # Create a field and add poles
30
+ field = runner.create_field(name: 'reasoning_cluster')
31
+ # (add poles to field via engine directly or via future runner method)
32
+
33
+ # Status
34
+ runner.magnetic_status
35
+ # => { success: true, total_poles: 2, coherent_fields: 0, chaotic_fields: 0, ... }
36
+
37
+ # Strongest poles
38
+ runner.list_poles(limit: 5)
39
+ ```
40
+
41
+ ## Development
42
+
43
+ ```bash
44
+ bundle install
45
+ bundle exec rspec
46
+ bundle exec rubocop
47
+ ```
48
+
49
+ ## License
50
+
51
+ MIT
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/legion/extensions/cognitive_magnet/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'lex-cognitive-magnet'
7
+ spec.version = Legion::Extensions::CognitiveMagnet::VERSION
8
+ spec.authors = ['Esity']
9
+ spec.email = ['matthewdiverson@gmail.com']
10
+
11
+ spec.summary = 'LEX Cognitive Magnet'
12
+ spec.description = 'Magnetic attraction and repulsion between ideas — polarity-driven cognitive clustering for LegionIO agentic AI'
13
+ spec.homepage = 'https://github.com/LegionIO/lex-cognitive-magnet'
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-magnet'
19
+ spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-cognitive-magnet'
20
+ spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-cognitive-magnet'
21
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-cognitive-magnet/issues'
22
+ spec.metadata['rubygems_mfa_required'] = 'true'
23
+
24
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.start_with?('spec/') }
25
+ spec.require_paths = ['lib']
26
+ spec.add_development_dependency 'legion-gaia'
27
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/cognitive_magnet/helpers/constants'
4
+ require 'legion/extensions/cognitive_magnet/helpers/pole'
5
+ require 'legion/extensions/cognitive_magnet/helpers/field'
6
+ require 'legion/extensions/cognitive_magnet/helpers/magnet_engine'
7
+ require 'legion/extensions/cognitive_magnet/runners/cognitive_magnet'
8
+
9
+ module Legion
10
+ module Extensions
11
+ module CognitiveMagnet
12
+ class Client
13
+ include Runners::CognitiveMagnet
14
+
15
+ def initialize(**)
16
+ @magnet_engine = Helpers::MagnetEngine.new
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :magnet_engine
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveMagnet
6
+ module Helpers
7
+ module Constants
8
+ POLARITY_TYPES = %i[positive negative neutral bipolar].freeze
9
+ MATERIAL_TYPES = %i[iron cobalt nickel lodestone ferrite].freeze
10
+ MAX_POLES = 500
11
+ MAX_FIELDS = 50
12
+ ATTRACTION_RATE = 0.08
13
+ REPULSION_RATE = 0.06
14
+ DECAY_RATE = 0.02
15
+
16
+ STRENGTH_LABELS = {
17
+ (0.0..0.1) => :negligible,
18
+ (0.1..0.3) => :faint,
19
+ (0.3..0.5) => :moderate,
20
+ (0.5..0.7) => :strong,
21
+ (0.7..0.9) => :intense,
22
+ (0.9..1.0) => :overwhelming
23
+ }.freeze
24
+
25
+ ALIGNMENT_LABELS = {
26
+ (0.0..0.2) => :chaotic,
27
+ (0.2..0.4) => :discordant,
28
+ (0.4..0.6) => :neutral,
29
+ (0.6..0.8) => :coherent,
30
+ (0.8..1.0) => :perfect
31
+ }.freeze
32
+
33
+ module_function
34
+
35
+ def label_for(table, value)
36
+ table.each do |range, label|
37
+ return label if range.cover?(value)
38
+ end
39
+ :unknown
40
+ end
41
+
42
+ def valid_polarity?(polarity)
43
+ POLARITY_TYPES.include?(polarity)
44
+ end
45
+
46
+ def valid_material?(material)
47
+ MATERIAL_TYPES.include?(material)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module CognitiveMagnet
8
+ module Helpers
9
+ class Field
10
+ attr_reader :id, :name, :pole_ids, :alignment, :flux_density, :created_at
11
+
12
+ def initialize(name:)
13
+ @id = SecureRandom.uuid
14
+ @name = name
15
+ @pole_ids = []
16
+ @alignment = 0.0
17
+ @flux_density = 0.0
18
+ @created_at = Time.now.utc
19
+ end
20
+
21
+ def add_pole(pole_id)
22
+ return false if @pole_ids.include?(pole_id)
23
+
24
+ @pole_ids << pole_id
25
+ true
26
+ end
27
+
28
+ def remove_pole(pole_id)
29
+ removed = @pole_ids.delete(pole_id)
30
+ !removed.nil?
31
+ end
32
+
33
+ def calculate_alignment!(poles)
34
+ field_poles = poles.values.select { |p| @pole_ids.include?(p.id) }
35
+ return self if field_poles.empty?
36
+
37
+ @flux_density = field_poles.sum(&:strength) / field_poles.size.to_f
38
+ @alignment = compute_alignment(field_poles).round(10)
39
+ self
40
+ end
41
+
42
+ def coherent?
43
+ @alignment >= 0.6
44
+ end
45
+
46
+ def chaotic?
47
+ @alignment < 0.2
48
+ end
49
+
50
+ def alignment_label
51
+ Constants.label_for(Constants::ALIGNMENT_LABELS, @alignment)
52
+ end
53
+
54
+ def pole_count
55
+ @pole_ids.size
56
+ end
57
+
58
+ def to_h
59
+ {
60
+ id: @id,
61
+ name: @name,
62
+ pole_ids: @pole_ids.dup,
63
+ alignment: @alignment,
64
+ flux_density: @flux_density,
65
+ pole_count: pole_count,
66
+ coherent: coherent?,
67
+ chaotic: chaotic?,
68
+ alignment_label: alignment_label,
69
+ created_at: @created_at
70
+ }
71
+ end
72
+
73
+ private
74
+
75
+ def compute_alignment(field_poles)
76
+ return 0.0 if field_poles.size < 2
77
+
78
+ total_pairs = 0
79
+ aligned_weight = 0.0
80
+
81
+ field_poles.combination(2) do |a, b|
82
+ total_pairs += 1
83
+ pair_weight = (a.strength + b.strength) / 2.0
84
+
85
+ if a.attracts?(b)
86
+ aligned_weight += pair_weight
87
+ elsif a.repels?(b)
88
+ aligned_weight -= pair_weight * 0.5
89
+ end
90
+ end
91
+
92
+ return 0.5 if total_pairs.zero?
93
+
94
+ raw = aligned_weight / total_pairs.to_f
95
+ ((raw + 1.0) / 2.0).clamp(0.0, 1.0)
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveMagnet
6
+ module Helpers
7
+ class MagnetEngine
8
+ attr_reader :poles, :fields, :interaction_log
9
+
10
+ def initialize
11
+ @poles = {}
12
+ @fields = {}
13
+ @interaction_log = []
14
+ end
15
+
16
+ def create_pole(polarity:, content:, strength: 0.5, material_type: :iron, domain: :general)
17
+ return { error: :capacity_exceeded, max: Constants::MAX_POLES } if at_pole_capacity?
18
+
19
+ pole = Pole.new(
20
+ polarity: polarity,
21
+ content: content,
22
+ strength: strength,
23
+ material_type: material_type,
24
+ domain: domain
25
+ )
26
+ @poles[pole.id] = pole
27
+ pole
28
+ end
29
+
30
+ def create_field(name:)
31
+ return { error: :capacity_exceeded, max: Constants::MAX_FIELDS } if at_field_capacity?
32
+
33
+ field = Field.new(name: name)
34
+ @fields[field.id] = field
35
+ field
36
+ end
37
+
38
+ def magnetize(pole_id, rate: Constants::ATTRACTION_RATE)
39
+ pole = @poles[pole_id]
40
+ return { error: :not_found } unless pole
41
+
42
+ pole.magnetize!(rate)
43
+ { magnetized: true, id: pole_id, strength: pole.strength }
44
+ end
45
+
46
+ def interact(pole_a_id, pole_b_id)
47
+ pole_a = @poles[pole_a_id]
48
+ pole_b = @poles[pole_b_id]
49
+ return { error: :pole_a_not_found } unless pole_a
50
+ return { error: :pole_b_not_found } unless pole_b
51
+ return { error: :same_pole } if pole_a_id == pole_b_id
52
+
53
+ force = compute_force(pole_a, pole_b)
54
+ type = determine_interaction_type(pole_a, pole_b)
55
+
56
+ apply_interaction(pole_a, pole_b, type)
57
+
58
+ event = build_interaction_event(pole_a, pole_b, type, force)
59
+ @interaction_log << event
60
+ event
61
+ end
62
+
63
+ def demagnetize_all!(rate: Constants::DECAY_RATE)
64
+ count = 0
65
+ @poles.each_value do |pole|
66
+ pole.demagnetize!(rate)
67
+ count += 1
68
+ end
69
+ { demagnetized: count, rate: rate }
70
+ end
71
+
72
+ def most_aligned_fields(limit: 5)
73
+ recalculate_all_fields
74
+ @fields.values
75
+ .sort_by { |f| -f.alignment }
76
+ .first(limit)
77
+ end
78
+
79
+ def strongest_poles(limit: 5)
80
+ @poles.values
81
+ .reject(&:weak?)
82
+ .sort_by { |p| -p.strength }
83
+ .first(limit)
84
+ end
85
+
86
+ def field_report
87
+ recalculate_all_fields
88
+ coherent = @fields.values.select(&:coherent?)
89
+ chaotic = @fields.values.select(&:chaotic?)
90
+ saturated = @poles.values.select(&:saturated?)
91
+ weak = @poles.values.select(&:weak?)
92
+
93
+ {
94
+ total_poles: @poles.size,
95
+ total_fields: @fields.size,
96
+ coherent_fields: coherent.size,
97
+ chaotic_fields: chaotic.size,
98
+ saturated_poles: saturated.size,
99
+ weak_poles: weak.size,
100
+ total_interactions: @interaction_log.size,
101
+ strongest: strongest_poles(limit: 3).map(&:to_h),
102
+ top_fields: most_aligned_fields(limit: 3).map(&:to_h)
103
+ }
104
+ end
105
+
106
+ def add_pole_to_field(field_id:, pole_id:)
107
+ field = @fields[field_id]
108
+ return { error: :field_not_found } unless field
109
+ return { error: :pole_not_found } unless @poles[pole_id]
110
+
111
+ added = field.add_pole(pole_id)
112
+ { added: added, field_id: field_id, pole_id: pole_id }
113
+ end
114
+
115
+ def remove_pole_from_field(field_id:, pole_id:)
116
+ field = @fields[field_id]
117
+ return { error: :field_not_found } unless field
118
+
119
+ removed = field.remove_pole(pole_id)
120
+ { removed: removed, field_id: field_id, pole_id: pole_id }
121
+ end
122
+
123
+ private
124
+
125
+ def at_pole_capacity?
126
+ @poles.size >= Constants::MAX_POLES
127
+ end
128
+
129
+ def at_field_capacity?
130
+ @fields.size >= Constants::MAX_FIELDS
131
+ end
132
+
133
+ def compute_force(pole_a, pole_b)
134
+ (pole_a.strength * pole_b.strength).round(10)
135
+ end
136
+
137
+ def determine_interaction_type(pole_a, pole_b)
138
+ if pole_a.attracts?(pole_b)
139
+ :attraction
140
+ elsif pole_a.repels?(pole_b)
141
+ :repulsion
142
+ else
143
+ :neutral
144
+ end
145
+ end
146
+
147
+ def apply_interaction(pole_a, pole_b, type)
148
+ case type
149
+ when :attraction
150
+ pole_a.magnetize!(Constants::ATTRACTION_RATE * pole_b.strength)
151
+ pole_b.magnetize!(Constants::ATTRACTION_RATE * pole_a.strength)
152
+ when :repulsion
153
+ pole_a.demagnetize!(Constants::REPULSION_RATE * pole_b.strength)
154
+ pole_b.demagnetize!(Constants::REPULSION_RATE * pole_a.strength)
155
+ end
156
+ end
157
+
158
+ def build_interaction_event(pole_a, pole_b, type, force)
159
+ {
160
+ type: type,
161
+ pole_a_id: pole_a.id,
162
+ pole_b_id: pole_b.id,
163
+ force: force,
164
+ at: Time.now.utc
165
+ }
166
+ end
167
+
168
+ def recalculate_all_fields
169
+ @fields.each_value { |f| f.calculate_alignment!(@poles) }
170
+ end
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module CognitiveMagnet
8
+ module Helpers
9
+ class Pole
10
+ attr_reader :id, :polarity, :strength, :material_type, :domain, :content, :created_at
11
+
12
+ def initialize(polarity:, content:, strength: 0.5, material_type: :iron, domain: :general)
13
+ @id = SecureRandom.uuid
14
+ @polarity = polarity
15
+ @content = content
16
+ @strength = strength.to_f.clamp(0.0, 1.0).round(10)
17
+ @material_type = material_type
18
+ @domain = domain
19
+ @created_at = Time.now.utc
20
+ end
21
+
22
+ def magnetize!(rate = Constants::ATTRACTION_RATE)
23
+ @strength = (@strength + rate.to_f).clamp(0.0, 1.0).round(10)
24
+ self
25
+ end
26
+
27
+ def demagnetize!(rate = Constants::DECAY_RATE)
28
+ @strength = (@strength - rate.to_f).clamp(0.0, 1.0).round(10)
29
+ self
30
+ end
31
+
32
+ def attracts?(other_pole)
33
+ return false if @polarity == :neutral || other_pole.polarity == :neutral
34
+ return true if @polarity == :bipolar || other_pole.polarity == :bipolar
35
+
36
+ @polarity != other_pole.polarity
37
+ end
38
+
39
+ def repels?(other_pole)
40
+ return false if @polarity == :neutral || other_pole.polarity == :neutral
41
+ return false if @polarity == :bipolar || other_pole.polarity == :bipolar
42
+
43
+ @polarity == other_pole.polarity
44
+ end
45
+
46
+ def saturated?
47
+ @strength >= 1.0
48
+ end
49
+
50
+ def weak?
51
+ @strength <= 0.1
52
+ end
53
+
54
+ def strength_label
55
+ Constants.label_for(Constants::STRENGTH_LABELS, @strength)
56
+ end
57
+
58
+ def to_h
59
+ {
60
+ id: @id,
61
+ polarity: @polarity,
62
+ strength: @strength,
63
+ material_type: @material_type,
64
+ domain: @domain,
65
+ content: @content,
66
+ saturated: saturated?,
67
+ weak: weak?,
68
+ strength_label: strength_label,
69
+ created_at: @created_at
70
+ }
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveMagnet
6
+ module Runners
7
+ module CognitiveMagnet
8
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
9
+ Legion::Extensions::Helpers.const_defined?(:Lex)
10
+
11
+ extend self
12
+
13
+ def create_pole(polarity:, content:, strength: 0.5, material_type: :iron,
14
+ domain: :general, engine: nil, **)
15
+ unless Helpers::Constants.valid_polarity?(polarity)
16
+ return { success: false, error: :invalid_polarity,
17
+ valid_polarities: Helpers::Constants::POLARITY_TYPES }
18
+ end
19
+
20
+ unless Helpers::Constants.valid_material?(material_type)
21
+ return { success: false, error: :invalid_material,
22
+ valid_materials: Helpers::Constants::MATERIAL_TYPES }
23
+ end
24
+
25
+ eng = engine || magnet_engine
26
+ result = eng.create_pole(
27
+ polarity: polarity,
28
+ content: content,
29
+ strength: strength,
30
+ material_type: material_type,
31
+ domain: domain
32
+ )
33
+
34
+ if result.is_a?(Hash) && result[:error]
35
+ Legion::Logging.warn "[cognitive_magnet] create_pole failed: #{result[:error]}"
36
+ return { success: false, **result }
37
+ end
38
+
39
+ Legion::Logging.debug "[cognitive_magnet] pole created id=#{result.id[0..7]} " \
40
+ "polarity=#{polarity} strength=#{strength}"
41
+ { success: true, pole: result.to_h }
42
+ rescue ArgumentError => e
43
+ { success: false, error: :argument_error, message: e.message }
44
+ end
45
+
46
+ def create_field(name:, engine: nil, **)
47
+ eng = engine || magnet_engine
48
+ result = eng.create_field(name: name)
49
+
50
+ if result.is_a?(Hash) && result[:error]
51
+ Legion::Logging.warn "[cognitive_magnet] create_field failed: #{result[:error]}"
52
+ return { success: false, **result }
53
+ end
54
+
55
+ Legion::Logging.debug "[cognitive_magnet] field created id=#{result.id[0..7]} name=#{name}"
56
+ { success: true, field: result.to_h }
57
+ rescue ArgumentError => e
58
+ { success: false, error: :argument_error, message: e.message }
59
+ end
60
+
61
+ def magnetize(pole_id:, rate: Helpers::Constants::ATTRACTION_RATE, engine: nil, **)
62
+ eng = engine || magnet_engine
63
+ result = eng.magnetize(pole_id, rate: rate)
64
+
65
+ if result[:error]
66
+ Legion::Logging.warn "[cognitive_magnet] magnetize failed: #{result[:error]}"
67
+ return { success: false, **result }
68
+ end
69
+
70
+ Legion::Logging.debug "[cognitive_magnet] magnetized id=#{pole_id[0..7]} strength=#{result[:strength]}"
71
+ { success: true, **result }
72
+ rescue ArgumentError => e
73
+ { success: false, error: :argument_error, message: e.message }
74
+ end
75
+
76
+ def interact(pole_a_id:, pole_b_id:, engine: nil, **)
77
+ eng = engine || magnet_engine
78
+ result = eng.interact(pole_a_id, pole_b_id)
79
+
80
+ if result[:error]
81
+ Legion::Logging.warn "[cognitive_magnet] interact failed: #{result[:error]}"
82
+ return { success: false, **result }
83
+ end
84
+
85
+ Legion::Logging.debug "[cognitive_magnet] interaction type=#{result[:type]} " \
86
+ "force=#{result[:force].round(4)}"
87
+ { success: true, **result }
88
+ rescue ArgumentError => e
89
+ { success: false, error: :argument_error, message: e.message }
90
+ end
91
+
92
+ def list_poles(engine: nil, limit: 50, **)
93
+ eng = engine || magnet_engine
94
+ poles = eng.poles.values.first(limit).map(&:to_h)
95
+ Legion::Logging.debug "[cognitive_magnet] list_poles count=#{poles.size}"
96
+ { success: true, poles: poles, count: poles.size }
97
+ rescue ArgumentError => e
98
+ { success: false, error: :argument_error, message: e.message }
99
+ end
100
+
101
+ def magnetic_status(engine: nil, **)
102
+ eng = engine || magnet_engine
103
+ report = eng.field_report
104
+ Legion::Logging.debug "[cognitive_magnet] status: poles=#{report[:total_poles]} " \
105
+ "fields=#{report[:total_fields]} interactions=#{report[:total_interactions]}"
106
+ { success: true, report: report }
107
+ rescue ArgumentError => e
108
+ { success: false, error: :argument_error, message: e.message }
109
+ end
110
+
111
+ private
112
+
113
+ def magnet_engine
114
+ @magnet_engine ||= Helpers::MagnetEngine.new
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveMagnet
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+ require 'legion/extensions/cognitive_magnet/version'
5
+ require 'legion/extensions/cognitive_magnet/helpers/constants'
6
+ require 'legion/extensions/cognitive_magnet/helpers/pole'
7
+ require 'legion/extensions/cognitive_magnet/helpers/field'
8
+ require 'legion/extensions/cognitive_magnet/helpers/magnet_engine'
9
+ require 'legion/extensions/cognitive_magnet/runners/cognitive_magnet'
10
+ require 'legion/extensions/cognitive_magnet/client'
11
+
12
+ module Legion
13
+ module Extensions
14
+ module CognitiveMagnet
15
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
16
+ end
17
+ end
18
+ end
metadata ADDED
@@ -0,0 +1,77 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lex-cognitive-magnet
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
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: Magnetic attraction and repulsion between ideas — polarity-driven cognitive
27
+ clustering for LegionIO agentic AI
28
+ email:
29
+ - matthewdiverson@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - ".github/workflows/ci.yml"
35
+ - ".gitignore"
36
+ - ".rspec"
37
+ - ".rubocop.yml"
38
+ - CLAUDE.md
39
+ - Gemfile
40
+ - README.md
41
+ - lex-cognitive-magnet.gemspec
42
+ - lib/legion/extensions/cognitive_magnet.rb
43
+ - lib/legion/extensions/cognitive_magnet/client.rb
44
+ - lib/legion/extensions/cognitive_magnet/helpers/constants.rb
45
+ - lib/legion/extensions/cognitive_magnet/helpers/field.rb
46
+ - lib/legion/extensions/cognitive_magnet/helpers/magnet_engine.rb
47
+ - lib/legion/extensions/cognitive_magnet/helpers/pole.rb
48
+ - lib/legion/extensions/cognitive_magnet/runners/cognitive_magnet.rb
49
+ - lib/legion/extensions/cognitive_magnet/version.rb
50
+ homepage: https://github.com/LegionIO/lex-cognitive-magnet
51
+ licenses:
52
+ - MIT
53
+ metadata:
54
+ homepage_uri: https://github.com/LegionIO/lex-cognitive-magnet
55
+ source_code_uri: https://github.com/LegionIO/lex-cognitive-magnet
56
+ documentation_uri: https://github.com/LegionIO/lex-cognitive-magnet
57
+ changelog_uri: https://github.com/LegionIO/lex-cognitive-magnet
58
+ bug_tracker_uri: https://github.com/LegionIO/lex-cognitive-magnet/issues
59
+ rubygems_mfa_required: 'true'
60
+ rdoc_options: []
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '3.4'
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ requirements: []
74
+ rubygems_version: 3.6.9
75
+ specification_version: 4
76
+ summary: LEX Cognitive Magnet
77
+ test_files: []