lex-emotion 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: 25a14df7629126e71f7e0cb1eaa60da4237f055c074bfc1709093f7601c009c5
4
+ data.tar.gz: c3198212e5b853f06c04add9a5d3959caf90bb38acdab7d1f595bad9dd6f933e
5
+ SHA512:
6
+ metadata.gz: 07db1bffdc454beec694dd1c75bebfe411a8b7ea122a3a7cd75e67e9760a2691da49ab32eb730190e9379fa16302787e4f10ef5ef1a8e48a073f9111171ea687
7
+ data.tar.gz: ccf9b2503bee116f27af9d0aa64a420c7a89991d4aedeac576663c3747e187ff6d516030f9cfc6d04d70d8b2da5d8aef6c389560675003c0093fc0a89cab0b02
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+ gemspec
5
+
6
+ group :test do
7
+ gem 'rake'
8
+ gem 'rspec'
9
+ gem 'rspec_junit_formatter'
10
+ gem 'rubocop', require: false
11
+ gem 'rubocop-rspec', require: false
12
+ gem 'simplecov'
13
+ end
14
+
15
+ gem 'legion-gaia', path: '../../legion-gaia'
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Matthew Iverson
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 all
13
+ 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 THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # lex-emotion
2
+
3
+ Multi-dimensional emotional valence system for brain-modeled agentic AI. Models emotional state across four dimensions, computes arousal and gut instinct signals, and modulates attention.
4
+
5
+ ## Overview
6
+
7
+ `lex-emotion` implements the agent's affective layer. Emotional state is not a single scalar — it is a four-dimensional valence vector. Valence influences memory decay rates, attention allocation, and gut instinct signals. Momentum (exponential moving average) smooths emotional state over time.
8
+
9
+ ## Valence Dimensions
10
+
11
+ | Dimension | Description |
12
+ |-----------|-------------|
13
+ | `urgency` | Time pressure and immediacy |
14
+ | `importance` | Impact scope and outcome severity |
15
+ | `novelty` | Degree of unexpectedness |
16
+ | `familiarity` | How well-known the domain/source is |
17
+
18
+ All dimensions are clamped to `[0.0, 1.0]`.
19
+
20
+ ## Source Urgency Defaults
21
+
22
+ | Source Type | Urgency Weight |
23
+ |-------------|---------------|
24
+ | `firmware_violation` | 1.0 |
25
+ | `human_direct` | 0.9 |
26
+ | `mesh_priority` | 0.7 |
27
+ | `scheduled` | 0.4 |
28
+ | `ambient` | 0.1 |
29
+
30
+ ## Installation
31
+
32
+ Add to your Gemfile:
33
+
34
+ ```ruby
35
+ gem 'lex-emotion'
36
+ ```
37
+
38
+ ## Usage
39
+
40
+ ### Evaluating Valence
41
+
42
+ ```ruby
43
+ require 'legion/extensions/emotion'
44
+
45
+ # Evaluate a signal's emotional valence
46
+ result = Legion::Extensions::Emotion::Runners::Valence.evaluate_valence(
47
+ signal: { urgency_hint: 0.8, domain_weight: 0.9, impact_scope: 0.6,
48
+ reversibility: 0.2, outcome_severity: 0.8 },
49
+ source_type: :human_direct,
50
+ deadline: Time.now.utc + 3600
51
+ )
52
+
53
+ result[:valence] # => { urgency: 0.72, importance: 0.68, novelty: 0.5, familiarity: 0.0 }
54
+ result[:magnitude] # => 1.09 (Euclidean norm)
55
+ result[:dominant_dimension] # => :urgency
56
+ ```
57
+
58
+ ### Aggregating Multiple Valences
59
+
60
+ ```ruby
61
+ valences = [
62
+ { urgency: 0.8, importance: 0.7, novelty: 0.4, familiarity: 0.3 },
63
+ { urgency: 0.5, importance: 0.9, novelty: 0.2, familiarity: 0.6 }
64
+ ]
65
+
66
+ Legion::Extensions::Emotion::Runners::Valence.aggregate_valences(valences: valences)
67
+ # => { aggregate: {...}, arousal: 0.65, dominant: :importance, count: 2 }
68
+ ```
69
+
70
+ ### Gut Instinct
71
+
72
+ ```ruby
73
+ # Compressed parallel query — returns a signal classification
74
+ result = Legion::Extensions::Emotion::Runners::Gut.gut_instinct(
75
+ valences: valences,
76
+ memory_signals: [1, 2, 3],
77
+ confidence_threshold: 0.5
78
+ )
79
+
80
+ result[:signal] # => :alarm | :heightened | :explore | :attend | :calm | :neutral
81
+ result[:confidence] # => 0.0..1.0
82
+ result[:reliable] # => true/false
83
+ ```
84
+
85
+ ### Current Emotional State
86
+
87
+ ```ruby
88
+ Legion::Extensions::Emotion::Runners::Gut.emotional_state
89
+ # => { momentum: { valence_ema: {...}, arousal_ema: 0.4, stability: 0.9, history_size: 5 },
90
+ # baseline: { urgency: {...}, ... } }
91
+ ```
92
+
93
+ ### Attention Modulation
94
+
95
+ ```ruby
96
+ Legion::Extensions::Emotion::Runners::Valence.modulate_attention(
97
+ base_salience: 0.5,
98
+ valence: { urgency: 0.8, importance: 0.7, novelty: 0.3, familiarity: 0.5 }
99
+ )
100
+ # => { original: 0.5, modulated: 0.72, boost: 0.22 }
101
+ ```
102
+
103
+ ## Momentum
104
+
105
+ Emotional momentum is an exponential moving average (alpha = 0.3) over valence and arousal. It tracks `valence_ema`, `arousal_ema`, `stability`, and last 100 data points.
106
+
107
+ ## Actors
108
+
109
+ | Actor | Interval | Description |
110
+ |-------|----------|-------------|
111
+ | `MomentumDecay` | Every 60s | Periodically drifts emotional momentum toward neutral valence via exponential decay, preventing permanent extreme states |
112
+
113
+ ## Development
114
+
115
+ ```bash
116
+ bundle install
117
+ bundle exec rspec
118
+ bundle exec rubocop
119
+ ```
120
+
121
+ ## License
122
+
123
+ MIT
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/legion/extensions/emotion/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'lex-emotion'
7
+ spec.version = Legion::Extensions::Emotion::VERSION
8
+ spec.authors = ['Esity']
9
+ spec.email = ['matthewdiverson@gmail.com']
10
+
11
+ spec.summary = 'LEX Emotion'
12
+ spec.description = 'Multi-dimensional emotional valence for brain-modeled agentic AI'
13
+ spec.homepage = 'https://github.com/LegionIO/lex-emotion'
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-emotion'
19
+ spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-emotion'
20
+ spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-emotion'
21
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-emotion/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-emotion.gemspec Gemfile LICENSE README.md]
26
+ end
27
+ spec.require_paths = ['lib']
28
+ spec.add_development_dependency 'legion-gaia'
29
+ 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 Emotion
8
+ module Actor
9
+ class MomentumDecay < Legion::Extensions::Actors::Every
10
+ def runner_class
11
+ Legion::Extensions::Emotion::Runners::Gut
12
+ end
13
+
14
+ def runner_function
15
+ 'decay_momentum'
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,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/emotion/helpers/valence'
4
+ require 'legion/extensions/emotion/helpers/baseline'
5
+ require 'legion/extensions/emotion/helpers/momentum'
6
+ require 'legion/extensions/emotion/runners/valence'
7
+ require 'legion/extensions/emotion/runners/gut'
8
+
9
+ module Legion
10
+ module Extensions
11
+ module Emotion
12
+ class Client
13
+ include Runners::Valence
14
+ include Runners::Gut
15
+
16
+ def initialize(**)
17
+ @emotion_baseline = Helpers::Baseline.new
18
+ @emotion_momentum = Helpers::Momentum.new
19
+ @domain_counts = Hash.new(0)
20
+ end
21
+
22
+ def track_domain(domain)
23
+ @domain_counts[domain] += 1
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :emotion_baseline, :emotion_momentum
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Emotion
6
+ module Helpers
7
+ class Baseline
8
+ ALPHA = 0.05 # slow adaptation to prevent adversarial manipulation
9
+ MIN_STDDEV = 0.1 # prevents division issues when baseline is stable
10
+ INITIAL_MEAN = 0.5
11
+ INITIAL_STDDEV = 0.25
12
+
13
+ attr_reader :dimensions
14
+
15
+ def initialize
16
+ @dimensions = Valence::DIMENSIONS.to_h do |dim|
17
+ [dim, { mean: INITIAL_MEAN, stddev: INITIAL_STDDEV, count: 0 }]
18
+ end
19
+ end
20
+
21
+ def normalize(raw_score, dimension)
22
+ baseline = @dimensions[dimension]
23
+ return Valence.clamp(raw_score) unless baseline
24
+
25
+ normalized = (raw_score - baseline[:mean]) / [baseline[:stddev], MIN_STDDEV].max
26
+ Valence.clamp(normalized)
27
+ end
28
+
29
+ def update(dimension, raw_score)
30
+ baseline = @dimensions[dimension]
31
+ return unless baseline
32
+
33
+ baseline[:count] += 1
34
+ old_mean = baseline[:mean]
35
+ baseline[:mean] = (ALPHA * raw_score) + ((1.0 - ALPHA) * old_mean)
36
+ # Online stddev update (Welford-like with EMA)
37
+ deviation = (raw_score - baseline[:mean]).abs
38
+ baseline[:stddev] = (ALPHA * deviation) + ((1.0 - ALPHA) * baseline[:stddev])
39
+ end
40
+
41
+ def get(dimension)
42
+ @dimensions[dimension]
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Emotion
6
+ module Helpers
7
+ class Momentum
8
+ attr_reader :valence_ema, :arousal_ema, :stability, :history
9
+
10
+ def initialize
11
+ @valence_ema = Valence.new_valence
12
+ @arousal_ema = 0.0
13
+ @stability = 1.0
14
+ @history = []
15
+ end
16
+
17
+ def update(current_valence, current_arousal)
18
+ alpha = Valence::MOMENTUM_ALPHA
19
+
20
+ previous_aggregate = Valence.magnitude(@valence_ema)
21
+ current_aggregate = Valence.magnitude(current_valence)
22
+
23
+ @valence_ema = Valence::DIMENSIONS.to_h do |dim|
24
+ [dim, (alpha * current_valence[dim]) + ((1.0 - alpha) * @valence_ema[dim])]
25
+ end
26
+
27
+ @arousal_ema = (alpha * current_arousal) + ((1.0 - alpha) * @arousal_ema)
28
+ @stability = Valence.clamp(1.0 - (current_aggregate - previous_aggregate).abs)
29
+
30
+ @history << { valence: current_valence, arousal: current_arousal, timestamp: Time.now.utc }
31
+ @history.shift while @history.size > 100
32
+
33
+ { valence_ema: @valence_ema, arousal_ema: @arousal_ema, stability: @stability }
34
+ end
35
+
36
+ def emotional_state
37
+ {
38
+ valence_ema: @valence_ema,
39
+ arousal_ema: @arousal_ema,
40
+ stability: @stability,
41
+ history_size: @history.size
42
+ }
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Emotion
6
+ module Helpers
7
+ module Valence
8
+ DIMENSIONS = %i[urgency importance novelty familiarity].freeze
9
+ MAX_MAGNITUDE = Math.sqrt(4.0) # all 4 dimensions at 1.0
10
+
11
+ # Attention modulation weights (from spec Section 6.1)
12
+ URGENCY_ATTENTION_WEIGHT = 0.4
13
+ IMPORTANCE_ATTENTION_WEIGHT = 0.35
14
+ NOVELTY_ATTENTION_WEIGHT = 0.25
15
+ ATTENTION_MULTIPLIER = 0.3
16
+
17
+ # Momentum
18
+ MOMENTUM_ALPHA = 0.3
19
+
20
+ # Source urgency map (spec Section 3.2)
21
+ SOURCE_URGENCY = {
22
+ firmware_violation: 1.0,
23
+ human_direct: 0.9,
24
+ mesh_priority: 0.7,
25
+ scheduled: 0.4,
26
+ ambient: 0.1
27
+ }.freeze
28
+
29
+ # Familiarity saturation (spec Section 3.5)
30
+ FAMILIARITY_SATURATION = 100
31
+
32
+ module_function
33
+
34
+ def new_valence(urgency: 0.0, importance: 0.0, novelty: 0.0, familiarity: 0.0)
35
+ {
36
+ urgency: clamp(urgency),
37
+ importance: clamp(importance),
38
+ novelty: clamp(novelty),
39
+ familiarity: clamp(familiarity)
40
+ }
41
+ end
42
+
43
+ def magnitude(valence)
44
+ Math.sqrt(
45
+ (valence[:urgency]**2) +
46
+ (valence[:importance]**2) +
47
+ (valence[:novelty]**2) +
48
+ (valence[:familiarity]**2)
49
+ )
50
+ end
51
+
52
+ def dominant_dimension(valence)
53
+ DIMENSIONS.max_by { |d| valence[d] }
54
+ end
55
+
56
+ def aggregate(valences)
57
+ return new_valence if valences.empty?
58
+
59
+ sums = Hash.new(0.0)
60
+ valences.each do |v|
61
+ DIMENSIONS.each { |d| sums[d] += v[d] }
62
+ end
63
+ n = valences.size.to_f
64
+ new_valence(**DIMENSIONS.to_h { |d| [d, sums[d] / n] })
65
+ end
66
+
67
+ def compute_arousal(valences)
68
+ return 0.0 if valences.empty?
69
+
70
+ total = valences.sum { |v| magnitude(v) }
71
+ clamp(total / (valences.size * MAX_MAGNITUDE))
72
+ end
73
+
74
+ def modulate_salience(base_salience, valence)
75
+ boost = ((valence[:urgency] * URGENCY_ATTENTION_WEIGHT) +
76
+ (valence[:importance] * IMPORTANCE_ATTENTION_WEIGHT) +
77
+ (valence[:novelty] * NOVELTY_ATTENTION_WEIGHT)) * ATTENTION_MULTIPLIER
78
+ clamp(base_salience + boost)
79
+ end
80
+
81
+ def clamp(value, min = 0.0, max = 1.0)
82
+ value.clamp(min, max)
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Emotion
6
+ module Runners
7
+ module Gut
8
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
9
+ Legion::Extensions::Helpers.const_defined?(:Lex)
10
+
11
+ def gut_instinct(valences:, memory_signals: [], confidence_threshold: 0.5, **)
12
+ return { signal: :neutral, confidence: 0.0, basis: :insufficient_data } if valences.empty?
13
+
14
+ arousal = Helpers::Valence.compute_arousal(valences)
15
+ aggregate = Helpers::Valence.aggregate(valences)
16
+ dominant = Helpers::Valence.dominant_dimension(aggregate)
17
+
18
+ signal = determine_signal(aggregate, arousal)
19
+ confidence = compute_confidence(valences, memory_signals)
20
+
21
+ Legion::Logging.debug "[emotion] gut instinct: signal=#{signal} confidence=#{confidence.round(2)} " \
22
+ "arousal=#{arousal.round(2)} dominant=#{dominant} reliable=#{confidence >= confidence_threshold}"
23
+
24
+ result = {
25
+ signal: signal,
26
+ confidence: confidence,
27
+ arousal: arousal,
28
+ dominant: dominant,
29
+ aggregate: aggregate,
30
+ reliable: confidence >= confidence_threshold
31
+ }
32
+
33
+ # Update momentum if available
34
+ emotion_momentum.update(aggregate, arousal) if respond_to?(:emotion_momentum, true)
35
+
36
+ result
37
+ end
38
+
39
+ def decay_momentum(**)
40
+ neutral = { urgency: 0.5, importance: 0.5, novelty: 0.5, familiarity: 0.5 }
41
+ momentum = emotion_momentum
42
+ momentum.update(neutral, 0.5)
43
+ stability = momentum.stability
44
+
45
+ Legion::Logging.debug "[emotion] momentum decay: stability=#{stability.round(2)}"
46
+
47
+ { decayed: true, stability: stability }
48
+ end
49
+
50
+ def emotional_state(**)
51
+ momentum = emotion_momentum
52
+ state = momentum.emotional_state
53
+ Legion::Logging.debug "[emotion] state query: stability=#{state[:stability]&.round(2)}"
54
+ {
55
+ momentum: state,
56
+ baseline: emotion_baseline.dimensions
57
+ }
58
+ end
59
+
60
+ private
61
+
62
+ def emotion_momentum
63
+ @emotion_momentum ||= Helpers::Momentum.new
64
+ end
65
+
66
+ def determine_signal(aggregate, arousal)
67
+ if aggregate[:urgency] > 0.7 && aggregate[:importance] > 0.7
68
+ :alarm
69
+ elsif arousal > 0.7
70
+ :heightened
71
+ elsif aggregate[:novelty] > 0.7 && aggregate[:familiarity] < 0.3
72
+ :explore
73
+ elsif aggregate[:importance] > 0.6
74
+ :attend
75
+ elsif arousal < 0.2
76
+ :calm
77
+ else
78
+ :neutral
79
+ end
80
+ end
81
+
82
+ def compute_confidence(valences, memory_signals)
83
+ return 0.0 if valences.empty?
84
+
85
+ magnitudes = valences.map { |v| Helpers::Valence.magnitude(v) }
86
+ mean_mag = magnitudes.sum / magnitudes.size
87
+ variance = magnitudes.sum { |m| (m - mean_mag)**2 } / magnitudes.size
88
+
89
+ consensus = Helpers::Valence.clamp(1.0 - Math.sqrt(variance))
90
+ evidence = Helpers::Valence.clamp(memory_signals.size / 10.0)
91
+
92
+ (consensus * 0.6) + (evidence * 0.4)
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Emotion
6
+ module Runners
7
+ module Valence
8
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
9
+ Legion::Extensions::Helpers.const_defined?(:Lex)
10
+
11
+ def evaluate_valence(signal:, source_type: :ambient, deadline: nil, domain: nil, **)
12
+ baseline = emotion_baseline
13
+
14
+ urgency_raw = compute_urgency(signal, source_type, deadline)
15
+ importance_raw = compute_importance(signal, domain)
16
+ novelty_raw = compute_novelty(signal)
17
+ familiarity_raw = compute_familiarity(domain)
18
+
19
+ valence = Helpers::Valence.new_valence(
20
+ urgency: baseline.normalize(urgency_raw, :urgency),
21
+ importance: baseline.normalize(importance_raw, :importance),
22
+ novelty: baseline.normalize(novelty_raw, :novelty),
23
+ familiarity: baseline.normalize(familiarity_raw, :familiarity)
24
+ )
25
+
26
+ # Update baselines with raw scores
27
+ Helpers::Valence::DIMENSIONS.each do |dim|
28
+ raw = { urgency: urgency_raw, importance: importance_raw,
29
+ novelty: novelty_raw, familiarity: familiarity_raw }[dim]
30
+ baseline.update(dim, raw)
31
+ end
32
+
33
+ magnitude = Helpers::Valence.magnitude(valence)
34
+ dominant = Helpers::Valence.dominant_dimension(valence)
35
+ Legion::Logging.debug "[emotion] valence: source=#{source_type} magnitude=#{magnitude.round(2)} dominant=#{dominant} " \
36
+ "u=#{valence[:urgency].round(2)} i=#{valence[:importance].round(2)} " \
37
+ "n=#{valence[:novelty].round(2)} f=#{valence[:familiarity].round(2)}"
38
+
39
+ {
40
+ valence: valence,
41
+ magnitude: magnitude,
42
+ dominant_dimension: dominant
43
+ }
44
+ end
45
+
46
+ def aggregate_valences(valences:, **)
47
+ aggregated = Helpers::Valence.aggregate(valences)
48
+ arousal = Helpers::Valence.compute_arousal(valences)
49
+ dominant = Helpers::Valence.dominant_dimension(aggregated)
50
+
51
+ Legion::Logging.debug "[emotion] aggregate: count=#{valences.size} arousal=#{arousal.round(2)} dominant=#{dominant}"
52
+ {
53
+ aggregate: aggregated,
54
+ arousal: arousal,
55
+ dominant: dominant,
56
+ count: valences.size
57
+ }
58
+ end
59
+
60
+ def modulate_attention(base_salience:, valence:, **)
61
+ modulated = Helpers::Valence.modulate_salience(base_salience, valence)
62
+ boost = modulated - base_salience
63
+ Legion::Logging.debug "[emotion] attention modulation: base=#{base_salience.round(2)} modulated=#{modulated.round(2)} boost=#{boost.round(2)}"
64
+ { original: base_salience, modulated: modulated, boost: boost }
65
+ end
66
+
67
+ def compute_arousal(valences:, **)
68
+ arousal = Helpers::Valence.compute_arousal(valences)
69
+ Legion::Logging.debug "[emotion] arousal=#{arousal.round(2)} from #{valences.size} valences"
70
+ { arousal: arousal }
71
+ end
72
+
73
+ private
74
+
75
+ def emotion_baseline
76
+ @emotion_baseline ||= Helpers::Baseline.new
77
+ end
78
+
79
+ def compute_urgency(signal, source_type, deadline)
80
+ deadline_urgency = 0.0
81
+ if deadline
82
+ remaining = [(deadline - Time.now.utc).to_f, 0.0].max
83
+ max_window = 86_400.0 # 24 hours
84
+ deadline_urgency = Helpers::Valence.clamp(1.0 - (remaining / max_window))
85
+ end
86
+
87
+ source_urgency = Helpers::Valence::SOURCE_URGENCY.fetch(source_type, 0.1)
88
+
89
+ pattern_urgency = signal.is_a?(Hash) ? (signal[:urgency_hint] || 0.0) : 0.0
90
+
91
+ (deadline_urgency * 0.5) + (source_urgency * 0.3) + (pattern_urgency * 0.2)
92
+ end
93
+
94
+ def compute_importance(signal, _domain)
95
+ domain_weight = signal.is_a?(Hash) ? (signal[:domain_weight] || 0.5) : 0.5
96
+ impact_scope = signal.is_a?(Hash) ? (signal[:impact_scope] || 0.3) : 0.3
97
+ reversibility = signal.is_a?(Hash) ? (signal[:reversibility] || 0.5) : 0.5
98
+ outcome_severity = signal.is_a?(Hash) ? (signal[:outcome_severity] || 0.3) : 0.3
99
+
100
+ (domain_weight * 0.3) + (impact_scope * 0.2) +
101
+ ((1.0 - reversibility) * 0.25) + (outcome_severity * 0.25)
102
+ end
103
+
104
+ def compute_novelty(signal)
105
+ signal.is_a?(Hash) ? (signal[:novelty_score] || 0.5) : 0.5
106
+ end
107
+
108
+ def compute_familiarity(domain)
109
+ signal_count = @domain_counts&.fetch(domain, 0) || 0
110
+ Helpers::Valence.clamp(signal_count.to_f / Helpers::Valence::FAMILIARITY_SATURATION)
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Emotion
6
+ VERSION = '0.1.1'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/emotion/version'
4
+ require 'legion/extensions/emotion/helpers/valence'
5
+ require 'legion/extensions/emotion/helpers/baseline'
6
+ require 'legion/extensions/emotion/helpers/momentum'
7
+ require 'legion/extensions/emotion/runners/valence'
8
+ require 'legion/extensions/emotion/runners/gut'
9
+
10
+ module Legion
11
+ module Extensions
12
+ module Emotion
13
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Stub the base class before loading the actor
4
+ module Legion
5
+ module Extensions
6
+ module Actors
7
+ class Every; end # rubocop:disable Lint/EmptyClass
8
+ end
9
+ end
10
+ end
11
+
12
+ $LOADED_FEATURES << 'legion/extensions/actors/every'
13
+
14
+ require_relative '../../../../../lib/legion/extensions/emotion/actors/momentum_decay'
15
+
16
+ RSpec.describe Legion::Extensions::Emotion::Actor::MomentumDecay do
17
+ subject(:actor) { described_class.new }
18
+
19
+ describe '#runner_class' do
20
+ it { expect(actor.runner_class).to eq Legion::Extensions::Emotion::Runners::Gut }
21
+ end
22
+
23
+ describe '#runner_function' do
24
+ it { expect(actor.runner_function).to eq 'decay_momentum' }
25
+ end
26
+
27
+ describe '#time' do
28
+ it { expect(actor.time).to eq 60 }
29
+ end
30
+
31
+ describe '#run_now?' do
32
+ it { expect(actor.run_now?).to be false }
33
+ end
34
+
35
+ describe '#use_runner?' do
36
+ it { expect(actor.use_runner?).to be false }
37
+ end
38
+
39
+ describe '#check_subtask?' do
40
+ it { expect(actor.check_subtask?).to be false }
41
+ end
42
+
43
+ describe '#generate_task?' do
44
+ it { expect(actor.generate_task?).to be false }
45
+ end
46
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/emotion/client'
4
+
5
+ RSpec.describe Legion::Extensions::Emotion::Client do
6
+ let(:client) { described_class.new }
7
+
8
+ it 'responds to valence runner methods' do
9
+ expect(client).to respond_to(:evaluate_valence)
10
+ expect(client).to respond_to(:aggregate_valences)
11
+ expect(client).to respond_to(:modulate_attention)
12
+ expect(client).to respond_to(:compute_arousal)
13
+ end
14
+
15
+ it 'responds to gut runner methods' do
16
+ expect(client).to respond_to(:gut_instinct)
17
+ expect(client).to respond_to(:emotional_state)
18
+ end
19
+
20
+ it 'tracks domain counts for familiarity' do
21
+ client.track_domain('work')
22
+ client.track_domain('work')
23
+ client.track_domain('personal')
24
+ # Domain tracking improves familiarity scoring
25
+ result = client.evaluate_valence(signal: {}, domain: 'work')
26
+ expect(result[:valence][:familiarity]).to be >= 0.0
27
+ end
28
+
29
+ it 'round-trips a full emotional evaluation cycle' do
30
+ # Evaluate multiple signals
31
+ v1 = client.evaluate_valence(signal: { urgency_hint: 0.8 }, source_type: :human_direct)
32
+ v2 = client.evaluate_valence(signal: { novelty_score: 0.9 }, source_type: :ambient)
33
+
34
+ # Aggregate
35
+ agg = client.aggregate_valences(valences: [v1[:valence], v2[:valence]])
36
+ expect(agg[:count]).to eq(2)
37
+
38
+ # Gut instinct
39
+ gut = client.gut_instinct(valences: [v1[:valence], v2[:valence]])
40
+ expect(gut[:signal]).to be_a(Symbol)
41
+
42
+ # State persists
43
+ state = client.emotional_state
44
+ expect(state[:momentum][:history_size]).to be >= 1
45
+ end
46
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Emotion::Helpers::Baseline do
4
+ let(:baseline) { described_class.new }
5
+
6
+ describe '#initialize' do
7
+ it 'sets initial values for all dimensions' do
8
+ Legion::Extensions::Emotion::Helpers::Valence::DIMENSIONS.each do |dim|
9
+ state = baseline.get(dim)
10
+ expect(state[:mean]).to eq(0.5)
11
+ expect(state[:stddev]).to eq(0.25)
12
+ expect(state[:count]).to eq(0)
13
+ end
14
+ end
15
+ end
16
+
17
+ describe '#normalize' do
18
+ it 'normalizes a raw score against baseline' do
19
+ result = baseline.normalize(0.5, :urgency)
20
+ expect(result).to be_between(0.0, 1.0)
21
+ end
22
+
23
+ it 'returns higher value for scores above mean' do
24
+ low = baseline.normalize(0.3, :urgency)
25
+ high = baseline.normalize(0.9, :urgency)
26
+ expect(high).to be > low
27
+ end
28
+ end
29
+
30
+ describe '#update' do
31
+ it 'shifts mean toward observed values' do
32
+ original_mean = baseline.get(:urgency)[:mean]
33
+ 10.times { baseline.update(:urgency, 0.9) }
34
+ expect(baseline.get(:urgency)[:mean]).to be > original_mean
35
+ end
36
+
37
+ it 'increments count' do
38
+ 3.times { baseline.update(:importance, 0.5) }
39
+ expect(baseline.get(:importance)[:count]).to eq(3)
40
+ end
41
+
42
+ it 'adapts slowly (alpha=0.05)' do
43
+ baseline.update(:urgency, 1.0)
44
+ # After one update, mean should barely move: 0.05*1.0 + 0.95*0.5 = 0.525
45
+ expect(baseline.get(:urgency)[:mean]).to be_within(0.001).of(0.525)
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Emotion::Helpers::Momentum do
4
+ let(:momentum) { described_class.new }
5
+ let(:valence_mod) { Legion::Extensions::Emotion::Helpers::Valence }
6
+
7
+ describe '#initialize' do
8
+ it 'starts with zero state' do
9
+ state = momentum.emotional_state
10
+ expect(state[:arousal_ema]).to eq(0.0)
11
+ expect(state[:stability]).to eq(1.0)
12
+ expect(state[:history_size]).to eq(0)
13
+ end
14
+ end
15
+
16
+ describe '#update' do
17
+ it 'updates EMA toward current values' do
18
+ v = valence_mod.new_valence(urgency: 0.8, importance: 0.7)
19
+ result = momentum.update(v, 0.6)
20
+ expect(result[:arousal_ema]).to be > 0.0
21
+ expect(result[:valence_ema][:urgency]).to be > 0.0
22
+ end
23
+
24
+ it 'tracks history' do
25
+ v = valence_mod.new_valence(urgency: 0.5)
26
+ 3.times { momentum.update(v, 0.5) }
27
+ expect(momentum.emotional_state[:history_size]).to eq(3)
28
+ end
29
+
30
+ it 'caps history at 100' do
31
+ v = valence_mod.new_valence
32
+ 105.times { momentum.update(v, 0.1) }
33
+ expect(momentum.emotional_state[:history_size]).to eq(100)
34
+ end
35
+
36
+ it 'computes stability as inverse of emotional change' do
37
+ v_calm = valence_mod.new_valence(urgency: 0.1)
38
+ v_alarm = valence_mod.new_valence(urgency: 1.0, importance: 1.0, novelty: 1.0, familiarity: 1.0)
39
+
40
+ momentum.update(v_calm, 0.1)
41
+ result = momentum.update(v_alarm, 0.9)
42
+ expect(result[:stability]).to be < 1.0
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Emotion::Helpers::Valence do
4
+ describe '.new_valence' do
5
+ it 'creates a valence with defaults' do
6
+ v = described_class.new_valence
7
+ expect(v[:urgency]).to eq(0.0)
8
+ expect(v[:importance]).to eq(0.0)
9
+ expect(v[:novelty]).to eq(0.0)
10
+ expect(v[:familiarity]).to eq(0.0)
11
+ end
12
+
13
+ it 'creates a valence with custom values' do
14
+ v = described_class.new_valence(urgency: 0.8, importance: 0.6)
15
+ expect(v[:urgency]).to eq(0.8)
16
+ expect(v[:importance]).to eq(0.6)
17
+ end
18
+
19
+ it 'clamps values to [0, 1]' do
20
+ v = described_class.new_valence(urgency: 1.5, novelty: -0.3)
21
+ expect(v[:urgency]).to eq(1.0)
22
+ expect(v[:novelty]).to eq(0.0)
23
+ end
24
+ end
25
+
26
+ describe '.magnitude' do
27
+ it 'computes zero for zero valence' do
28
+ v = described_class.new_valence
29
+ expect(described_class.magnitude(v)).to eq(0.0)
30
+ end
31
+
32
+ it 'computes sqrt(4) for all-ones valence' do
33
+ v = described_class.new_valence(urgency: 1.0, importance: 1.0, novelty: 1.0, familiarity: 1.0)
34
+ expect(described_class.magnitude(v)).to be_within(0.001).of(2.0)
35
+ end
36
+ end
37
+
38
+ describe '.dominant_dimension' do
39
+ it 'returns the highest dimension' do
40
+ v = described_class.new_valence(urgency: 0.2, importance: 0.9, novelty: 0.1, familiarity: 0.3)
41
+ expect(described_class.dominant_dimension(v)).to eq(:importance)
42
+ end
43
+ end
44
+
45
+ describe '.aggregate' do
46
+ it 'returns zero valence for empty array' do
47
+ result = described_class.aggregate([])
48
+ expect(result[:urgency]).to eq(0.0)
49
+ end
50
+
51
+ it 'averages multiple valences' do
52
+ v1 = described_class.new_valence(urgency: 0.8, importance: 0.2)
53
+ v2 = described_class.new_valence(urgency: 0.4, importance: 0.6)
54
+ result = described_class.aggregate([v1, v2])
55
+ expect(result[:urgency]).to be_within(0.001).of(0.6)
56
+ expect(result[:importance]).to be_within(0.001).of(0.4)
57
+ end
58
+ end
59
+
60
+ describe '.compute_arousal' do
61
+ it 'returns 0 for empty valences' do
62
+ expect(described_class.compute_arousal([])).to eq(0.0)
63
+ end
64
+
65
+ it 'returns 1.0 for all-max valences' do
66
+ v = described_class.new_valence(urgency: 1.0, importance: 1.0, novelty: 1.0, familiarity: 1.0)
67
+ expect(described_class.compute_arousal([v])).to be_within(0.001).of(1.0)
68
+ end
69
+
70
+ it 'returns moderate arousal for mixed valences' do
71
+ v = described_class.new_valence(urgency: 0.5, importance: 0.5)
72
+ arousal = described_class.compute_arousal([v])
73
+ expect(arousal).to be > 0.0
74
+ expect(arousal).to be < 1.0
75
+ end
76
+ end
77
+
78
+ describe '.modulate_salience' do
79
+ it 'boosts salience based on valence' do
80
+ v = described_class.new_valence(urgency: 0.8, importance: 0.6, novelty: 0.4)
81
+ modulated = described_class.modulate_salience(0.5, v)
82
+ expect(modulated).to be > 0.5
83
+ end
84
+
85
+ it 'clamps at 1.0' do
86
+ v = described_class.new_valence(urgency: 1.0, importance: 1.0, novelty: 1.0)
87
+ modulated = described_class.modulate_salience(0.9, v)
88
+ expect(modulated).to eq(1.0)
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/emotion/client'
4
+
5
+ RSpec.describe Legion::Extensions::Emotion::Runners::Gut do
6
+ let(:client) { Legion::Extensions::Emotion::Client.new }
7
+ let(:valence_mod) { Legion::Extensions::Emotion::Helpers::Valence }
8
+
9
+ describe '#gut_instinct' do
10
+ it 'returns neutral for empty valences' do
11
+ result = client.gut_instinct(valences: [])
12
+ expect(result[:signal]).to eq(:neutral)
13
+ expect(result[:confidence]).to eq(0.0)
14
+ end
15
+
16
+ it 'returns alarm for high urgency + importance' do
17
+ v = valence_mod.new_valence(urgency: 0.9, importance: 0.9, novelty: 0.5, familiarity: 0.5)
18
+ result = client.gut_instinct(valences: [v])
19
+ expect(result[:signal]).to eq(:alarm)
20
+ end
21
+
22
+ it 'returns explore for high novelty + low familiarity' do
23
+ v = valence_mod.new_valence(urgency: 0.2, importance: 0.2, novelty: 0.9, familiarity: 0.1)
24
+ result = client.gut_instinct(valences: [v])
25
+ expect(result[:signal]).to eq(:explore)
26
+ end
27
+
28
+ it 'returns calm for low arousal' do
29
+ v = valence_mod.new_valence(urgency: 0.05, importance: 0.05, novelty: 0.05, familiarity: 0.05)
30
+ result = client.gut_instinct(valences: [v])
31
+ expect(result[:signal]).to eq(:calm)
32
+ end
33
+
34
+ it 'includes confidence and reliability' do
35
+ v = valence_mod.new_valence(urgency: 0.5)
36
+ result = client.gut_instinct(valences: [v], memory_signals: [1, 2, 3])
37
+ expect(result).to have_key(:confidence)
38
+ expect(result).to have_key(:reliable)
39
+ end
40
+
41
+ it 'increases confidence with more memory evidence' do
42
+ v = valence_mod.new_valence(urgency: 0.5)
43
+ low_evidence = client.gut_instinct(valences: [v], memory_signals: [])
44
+ high_evidence = client.gut_instinct(valences: [v], memory_signals: Array.new(10, 1))
45
+ expect(high_evidence[:confidence]).to be >= low_evidence[:confidence]
46
+ end
47
+ end
48
+
49
+ describe '#emotional_state' do
50
+ it 'returns momentum and baseline state' do
51
+ state = client.emotional_state
52
+ expect(state).to have_key(:momentum)
53
+ expect(state).to have_key(:baseline)
54
+ end
55
+ end
56
+
57
+ describe '#decay_momentum' do
58
+ it 'returns decayed: true' do
59
+ result = client.decay_momentum
60
+ expect(result[:decayed]).to be true
61
+ end
62
+
63
+ it 'returns a Float stability value' do
64
+ result = client.decay_momentum
65
+ expect(result[:stability]).to be_a(Float)
66
+ end
67
+
68
+ it 'returns stability within [0.0, 1.0]' do
69
+ result = client.decay_momentum
70
+ expect(result[:stability]).to be_between(0.0, 1.0)
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/emotion/client'
4
+
5
+ RSpec.describe Legion::Extensions::Emotion::Runners::Valence do
6
+ let(:client) { Legion::Extensions::Emotion::Client.new }
7
+
8
+ describe '#evaluate_valence' do
9
+ it 'returns a valence with 4 dimensions' do
10
+ result = client.evaluate_valence(signal: { urgency_hint: 0.5 })
11
+ expect(result[:valence].keys).to contain_exactly(:urgency, :importance, :novelty, :familiarity)
12
+ end
13
+
14
+ it 'returns magnitude' do
15
+ result = client.evaluate_valence(signal: {})
16
+ expect(result[:magnitude]).to be >= 0.0
17
+ end
18
+
19
+ it 'returns dominant dimension' do
20
+ result = client.evaluate_valence(signal: { domain_weight: 0.9, impact_scope: 0.8, outcome_severity: 0.9 })
21
+ expect(result[:dominant_dimension]).to be_a(Symbol)
22
+ end
23
+
24
+ it 'responds to source type urgency' do
25
+ ambient = client.evaluate_valence(signal: {}, source_type: :ambient)
26
+ human = client.evaluate_valence(signal: {}, source_type: :human_direct)
27
+ expect(human[:valence][:urgency]).to be >= ambient[:valence][:urgency]
28
+ end
29
+
30
+ it 'responds to deadlines' do
31
+ no_deadline = client.evaluate_valence(signal: {})
32
+ with_deadline = client.evaluate_valence(signal: {}, deadline: Time.now.utc + 60)
33
+ expect(with_deadline[:valence][:urgency]).to be >= no_deadline[:valence][:urgency]
34
+ end
35
+ end
36
+
37
+ describe '#aggregate_valences' do
38
+ it 'aggregates multiple valences' do
39
+ v = Legion::Extensions::Emotion::Helpers::Valence
40
+ valences = [
41
+ v.new_valence(urgency: 0.8, importance: 0.2),
42
+ v.new_valence(urgency: 0.4, importance: 0.6)
43
+ ]
44
+ result = client.aggregate_valences(valences: valences)
45
+ expect(result[:aggregate][:urgency]).to be_within(0.01).of(0.6)
46
+ expect(result[:count]).to eq(2)
47
+ end
48
+ end
49
+
50
+ describe '#modulate_attention' do
51
+ it 'boosts salience' do
52
+ v = Legion::Extensions::Emotion::Helpers::Valence.new_valence(urgency: 0.8, importance: 0.7)
53
+ result = client.modulate_attention(base_salience: 0.5, valence: v)
54
+ expect(result[:modulated]).to be > result[:original]
55
+ expect(result[:boost]).to be > 0
56
+ end
57
+ end
58
+
59
+ describe '#compute_arousal' do
60
+ it 'computes arousal from valences' do
61
+ v = Legion::Extensions::Emotion::Helpers::Valence
62
+ valences = [v.new_valence(urgency: 0.9, importance: 0.9)]
63
+ result = client.compute_arousal(valences: valences)
64
+ expect(result[:arousal]).to be > 0.0
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+
5
+ module Legion
6
+ module Logging
7
+ def self.debug(_msg); end
8
+ def self.info(_msg); end
9
+ def self.warn(_msg); end
10
+ def self.error(_msg); end
11
+ end
12
+ end
13
+
14
+ require 'legion/extensions/emotion'
15
+
16
+ RSpec.configure do |config|
17
+ config.example_status_persistence_file_path = '.rspec_status'
18
+ config.disable_monkey_patching!
19
+ config.expect_with(:rspec) { |c| c.syntax = :expect }
20
+ end
metadata ADDED
@@ -0,0 +1,81 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lex-emotion
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Esity
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: legion-gaia
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ description: Multi-dimensional emotional valence for brain-modeled agentic AI
27
+ email:
28
+ - matthewdiverson@gmail.com
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - Gemfile
34
+ - LICENSE
35
+ - README.md
36
+ - lex-emotion.gemspec
37
+ - lib/legion/extensions/emotion.rb
38
+ - lib/legion/extensions/emotion/actors/momentum_decay.rb
39
+ - lib/legion/extensions/emotion/client.rb
40
+ - lib/legion/extensions/emotion/helpers/baseline.rb
41
+ - lib/legion/extensions/emotion/helpers/momentum.rb
42
+ - lib/legion/extensions/emotion/helpers/valence.rb
43
+ - lib/legion/extensions/emotion/runners/gut.rb
44
+ - lib/legion/extensions/emotion/runners/valence.rb
45
+ - lib/legion/extensions/emotion/version.rb
46
+ - spec/legion/extensions/emotion/actors/momentum_decay_spec.rb
47
+ - spec/legion/extensions/emotion/client_spec.rb
48
+ - spec/legion/extensions/emotion/helpers/baseline_spec.rb
49
+ - spec/legion/extensions/emotion/helpers/momentum_spec.rb
50
+ - spec/legion/extensions/emotion/helpers/valence_spec.rb
51
+ - spec/legion/extensions/emotion/runners/gut_spec.rb
52
+ - spec/legion/extensions/emotion/runners/valence_spec.rb
53
+ - spec/spec_helper.rb
54
+ homepage: https://github.com/LegionIO/lex-emotion
55
+ licenses:
56
+ - MIT
57
+ metadata:
58
+ homepage_uri: https://github.com/LegionIO/lex-emotion
59
+ source_code_uri: https://github.com/LegionIO/lex-emotion
60
+ documentation_uri: https://github.com/LegionIO/lex-emotion
61
+ changelog_uri: https://github.com/LegionIO/lex-emotion
62
+ bug_tracker_uri: https://github.com/LegionIO/lex-emotion/issues
63
+ rubygems_mfa_required: 'true'
64
+ rdoc_options: []
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '3.4'
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ requirements: []
78
+ rubygems_version: 3.6.9
79
+ specification_version: 4
80
+ summary: LEX Emotion
81
+ test_files: []