lex-conceptual-metaphor 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: fb3da9900840165ae3f63ca71850bda064b046df078cf2aea51ce64e2948adbd
4
+ data.tar.gz: 1ddee2263dcb341d811fb8457fb239f855efd48f51d64d58d91a91651c1a7506
5
+ SHA512:
6
+ metadata.gz: 5622988996a7f7871587dc50d28c395487819ceb4625c7a9f394f84acfc2b038a6f4307ddcab7ecd80d96a9d75517edc5d03bb30641737723081322e37a83c3f
7
+ data.tar.gz: 3e379a94b39799a96f1a31f252494e7fb3194125ed17669fdec7c384a89f92aa0ead653f4321f918e742799c77f149bf3a1f721149fa182c118d9a1240d94ef4
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ group :development, :test do
8
+ gem 'rspec', '~> 3.13'
9
+ gem 'rubocop', '~> 1.75'
10
+ gem 'rubocop-rspec'
11
+ end
12
+
13
+ gem 'legion-gaia', path: '../../legion-gaia'
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/legion/extensions/conceptual_metaphor/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'lex-conceptual-metaphor'
7
+ spec.version = Legion::Extensions::ConceptualMetaphor::VERSION
8
+ spec.authors = ['Esity']
9
+ spec.email = ['matthewdiverson@gmail.com']
10
+
11
+ spec.summary = 'LEX Conceptual Metaphor'
12
+ spec.description = 'Lakoff & Johnson conceptual metaphor theory — understanding abstractions ' \
13
+ 'through embodied metaphors for brain-modeled agentic AI'
14
+ spec.homepage = 'https://github.com/LegionIO/lex-conceptual-metaphor'
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-conceptual-metaphor'
20
+ spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-conceptual-metaphor'
21
+ spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-conceptual-metaphor'
22
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-conceptual-metaphor/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-conceptual-metaphor.gemspec Gemfile]
27
+ end
28
+ spec.require_paths = ['lib']
29
+ spec.add_development_dependency 'legion-gaia'
30
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module ConceptualMetaphor
6
+ class Client
7
+ include Runners::ConceptualMetaphor
8
+
9
+ def initialize(engine: nil)
10
+ @engine = engine || Helpers::MetaphorEngine.new
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module ConceptualMetaphor
6
+ module Helpers
7
+ module Constants
8
+ MAX_METAPHORS = 200
9
+ MAX_MAPPINGS = 500
10
+ MAX_DOMAINS = 100
11
+ MAX_HISTORY = 300
12
+
13
+ DEFAULT_STRENGTH = 0.5
14
+ STRENGTH_FLOOR = 0.0
15
+ STRENGTH_CEILING = 1.0
16
+
17
+ REINFORCEMENT_BOOST = 0.1
18
+ DECAY_RATE = 0.02
19
+ STALE_THRESHOLD = 120
20
+
21
+ CONVENTIONALITY_THRESHOLD = 0.7
22
+ NOVELTY_THRESHOLD = 0.3
23
+
24
+ METAPHOR_TYPES = %i[structural orientational ontological].freeze
25
+
26
+ CONVENTIONALITY_LABELS = {
27
+ (0.8..) => :dead,
28
+ (0.6...0.8) => :conventional,
29
+ (0.4...0.6) => :familiar,
30
+ (0.2...0.4) => :novel,
31
+ (..0.2) => :creative
32
+ }.freeze
33
+
34
+ STRENGTH_LABELS = {
35
+ (0.8..) => :dominant,
36
+ (0.6...0.8) => :strong,
37
+ (0.4...0.6) => :moderate,
38
+ (0.2...0.4) => :weak,
39
+ (..0.2) => :fading
40
+ }.freeze
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module ConceptualMetaphor
8
+ module Helpers
9
+ class Metaphor
10
+ include Constants
11
+
12
+ attr_reader :id, :source_domain, :target_domain, :metaphor_type,
13
+ :mappings, :entailments, :strength, :conventionality,
14
+ :use_count, :created_at, :last_used_at
15
+
16
+ def initialize(source_domain:, target_domain:, metaphor_type:,
17
+ mappings:, strength: nil, conventionality: nil)
18
+ @id = SecureRandom.uuid
19
+ @source_domain = source_domain
20
+ @target_domain = target_domain
21
+ @metaphor_type = metaphor_type
22
+ @mappings = mappings
23
+ @entailments = []
24
+ @strength = (strength || DEFAULT_STRENGTH).to_f.clamp(STRENGTH_FLOOR, STRENGTH_CEILING)
25
+ @conventionality = (conventionality || DEFAULT_STRENGTH).to_f.clamp(STRENGTH_FLOOR, STRENGTH_CEILING)
26
+ @use_count = 0
27
+ @created_at = Time.now.utc
28
+ @last_used_at = @created_at
29
+ end
30
+
31
+ def use!
32
+ @use_count += 1
33
+ @last_used_at = Time.now.utc
34
+ @strength = (@strength + REINFORCEMENT_BOOST).clamp(STRENGTH_FLOOR, STRENGTH_CEILING)
35
+ increase_conventionality
36
+ end
37
+
38
+ def add_entailment(entailment)
39
+ @entailments << entailment
40
+ end
41
+
42
+ def map_concept(source_concept)
43
+ @mappings[source_concept]
44
+ end
45
+
46
+ def coverage
47
+ return 0.0 if @mappings.empty?
48
+
49
+ @mappings.values.compact.size.to_f / @mappings.size
50
+ end
51
+
52
+ def conventional?
53
+ @conventionality >= CONVENTIONALITY_THRESHOLD
54
+ end
55
+
56
+ def novel?
57
+ @conventionality <= NOVELTY_THRESHOLD
58
+ end
59
+
60
+ def conventionality_label
61
+ CONVENTIONALITY_LABELS.find { |range, _| range.cover?(@conventionality) }&.last || :unknown
62
+ end
63
+
64
+ def strength_label
65
+ STRENGTH_LABELS.find { |range, _| range.cover?(@strength) }&.last || :unknown
66
+ end
67
+
68
+ def decay!
69
+ @strength = (@strength - DECAY_RATE).clamp(STRENGTH_FLOOR, STRENGTH_CEILING)
70
+ end
71
+
72
+ def stale?
73
+ (Time.now.utc - @last_used_at) > STALE_THRESHOLD
74
+ end
75
+
76
+ def to_h
77
+ {
78
+ id: @id,
79
+ source_domain: @source_domain,
80
+ target_domain: @target_domain,
81
+ metaphor_type: @metaphor_type,
82
+ mappings: @mappings,
83
+ entailments: @entailments,
84
+ strength: @strength,
85
+ conventionality: @conventionality,
86
+ conventionality_label: conventionality_label,
87
+ strength_label: strength_label,
88
+ use_count: @use_count,
89
+ coverage: coverage,
90
+ created_at: @created_at,
91
+ last_used_at: @last_used_at
92
+ }
93
+ end
94
+
95
+ private
96
+
97
+ def increase_conventionality
98
+ increment = REINFORCEMENT_BOOST * 0.5
99
+ @conventionality = (@conventionality + increment).clamp(STRENGTH_FLOOR, STRENGTH_CEILING)
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module ConceptualMetaphor
6
+ module Helpers
7
+ class MetaphorEngine
8
+ include Constants
9
+
10
+ attr_reader :history
11
+
12
+ def initialize
13
+ @metaphors = {}
14
+ @domains = Set.new
15
+ @history = []
16
+ end
17
+
18
+ def create_metaphor(source_domain:, target_domain:, metaphor_type:,
19
+ mappings:, strength: nil, conventionality: nil)
20
+ evict_oldest if @metaphors.size >= MAX_METAPHORS
21
+
22
+ return { success: false, reason: :invalid_metaphor_type } unless METAPHOR_TYPES.include?(metaphor_type)
23
+
24
+ metaphor = Metaphor.new(
25
+ source_domain: source_domain,
26
+ target_domain: target_domain,
27
+ metaphor_type: metaphor_type,
28
+ mappings: mappings,
29
+ strength: strength,
30
+ conventionality: conventionality
31
+ )
32
+
33
+ @metaphors[metaphor.id] = metaphor
34
+ register_domain(source_domain)
35
+ register_domain(target_domain)
36
+ record_history(:created, metaphor.id)
37
+ metaphor
38
+ end
39
+
40
+ def apply_metaphor(metaphor_id:, source_concept:)
41
+ metaphor = @metaphors[metaphor_id]
42
+ return { found: false } unless metaphor
43
+
44
+ target = metaphor.map_concept(source_concept)
45
+ return { found: true, mapped: false } unless target
46
+
47
+ metaphor.use!
48
+ record_history(:applied, metaphor_id)
49
+ build_apply_result(metaphor, source_concept, target)
50
+ end
51
+
52
+ def add_entailment(metaphor_id:, entailment:)
53
+ metaphor = @metaphors[metaphor_id]
54
+ return { success: false, reason: :not_found } unless metaphor
55
+
56
+ metaphor.add_entailment(entailment)
57
+ record_history(:entailment_added, metaphor_id)
58
+ { success: true, metaphor_id: metaphor_id, entailment_count: metaphor.entailments.size }
59
+ end
60
+
61
+ def find_by_target(target_domain:)
62
+ @metaphors.values.select { |m| m.target_domain == target_domain }
63
+ end
64
+
65
+ def find_by_source(source_domain:)
66
+ @metaphors.values.select { |m| m.source_domain == source_domain }
67
+ end
68
+
69
+ def find_by_domain(domain:)
70
+ @metaphors.values.select do |m|
71
+ m.source_domain == domain || m.target_domain == domain
72
+ end
73
+ end
74
+
75
+ def conventional_metaphors
76
+ @metaphors.values.select(&:conventional?)
77
+ end
78
+
79
+ def novel_metaphors
80
+ @metaphors.values.select(&:novel?)
81
+ end
82
+
83
+ def strongest(limit: 5)
84
+ @metaphors.values.sort_by { |m| -m.strength }.first(limit)
85
+ end
86
+
87
+ def by_type(metaphor_type:)
88
+ @metaphors.values.select { |m| m.metaphor_type == metaphor_type }
89
+ end
90
+
91
+ def decay_all
92
+ @metaphors.each_value(&:decay!)
93
+ end
94
+
95
+ def prune_weak
96
+ weak_ids = @metaphors.select { |_id, m| m.strength <= 0.05 }.keys
97
+ weak_ids.each { |id| @metaphors.delete(id) }
98
+ weak_ids.size
99
+ end
100
+
101
+ def to_h
102
+ {
103
+ total_metaphors: @metaphors.size,
104
+ total_domains: @domains.size,
105
+ conventional_count: conventional_metaphors.size,
106
+ novel_count: novel_metaphors.size,
107
+ history_count: @history.size,
108
+ domains: @domains.to_a,
109
+ type_counts: type_counts
110
+ }
111
+ end
112
+
113
+ private
114
+
115
+ def build_apply_result(metaphor, source_concept, target)
116
+ {
117
+ found: true,
118
+ mapped: true,
119
+ source_concept: source_concept,
120
+ target_concept: target,
121
+ strength: metaphor.strength,
122
+ metaphor_id: metaphor.id
123
+ }
124
+ end
125
+
126
+ def register_domain(domain)
127
+ @domains.add(domain)
128
+ @domains.delete(@domains.first) if @domains.size > MAX_DOMAINS
129
+ end
130
+
131
+ def evict_oldest
132
+ oldest_id = @metaphors.min_by { |_id, m| m.last_used_at }&.first
133
+ @metaphors.delete(oldest_id) if oldest_id
134
+ end
135
+
136
+ def record_history(event, metaphor_id)
137
+ @history << { event: event, metaphor_id: metaphor_id, at: Time.now.utc }
138
+ @history.shift while @history.size > MAX_HISTORY
139
+ end
140
+
141
+ def type_counts
142
+ @metaphors.values.each_with_object(Hash.new(0)) do |m, counts|
143
+ counts[m.metaphor_type] += 1
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module ConceptualMetaphor
6
+ module Runners
7
+ module ConceptualMetaphor
8
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
9
+ Legion::Extensions::Helpers.const_defined?(:Lex)
10
+
11
+ def create_metaphor(source_domain:, target_domain:, metaphor_type:,
12
+ mappings:, strength: nil, conventionality: nil, **)
13
+ unless Helpers::Constants::METAPHOR_TYPES.include?(metaphor_type)
14
+ return { success: false, error: :invalid_metaphor_type,
15
+ valid_types: Helpers::Constants::METAPHOR_TYPES }
16
+ end
17
+
18
+ result = engine.create_metaphor(
19
+ source_domain: source_domain,
20
+ target_domain: target_domain,
21
+ metaphor_type: metaphor_type,
22
+ mappings: mappings,
23
+ strength: strength,
24
+ conventionality: conventionality
25
+ )
26
+
27
+ return result unless result.is_a?(Helpers::Metaphor)
28
+
29
+ Legion::Logging.debug "[conceptual_metaphor] created #{source_domain}->#{target_domain} " \
30
+ "type=#{metaphor_type} id=#{result.id[0..7]}"
31
+ { success: true, metaphor_id: result.id, source_domain: source_domain,
32
+ target_domain: target_domain, metaphor_type: metaphor_type,
33
+ strength: result.strength, conventionality: result.conventionality }
34
+ end
35
+
36
+ def apply_metaphor(metaphor_id:, source_concept:, **)
37
+ result = engine.apply_metaphor(metaphor_id: metaphor_id, source_concept: source_concept)
38
+ Legion::Logging.debug "[conceptual_metaphor] apply id=#{metaphor_id[0..7]} " \
39
+ "concept=#{source_concept} mapped=#{result[:mapped]}"
40
+ { success: true }.merge(result)
41
+ end
42
+
43
+ def add_metaphor_entailment(metaphor_id:, entailment:, **)
44
+ result = engine.add_entailment(metaphor_id: metaphor_id, entailment: entailment)
45
+ Legion::Logging.debug "[conceptual_metaphor] entailment id=#{metaphor_id[0..7]} " \
46
+ "success=#{result[:success]}"
47
+ result
48
+ end
49
+
50
+ def find_metaphors_for(domain:, **)
51
+ metaphors = engine.find_by_domain(domain: domain)
52
+ Legion::Logging.debug "[conceptual_metaphor] find domain=#{domain} count=#{metaphors.size}"
53
+ { success: true, domain: domain, metaphors: metaphors.map(&:to_h), count: metaphors.size }
54
+ end
55
+
56
+ def conventional_metaphors(**)
57
+ metaphors = engine.conventional_metaphors
58
+ Legion::Logging.debug "[conceptual_metaphor] conventional count=#{metaphors.size}"
59
+ { success: true, metaphors: metaphors.map(&:to_h), count: metaphors.size }
60
+ end
61
+
62
+ def novel_metaphors(**)
63
+ metaphors = engine.novel_metaphors
64
+ Legion::Logging.debug "[conceptual_metaphor] novel count=#{metaphors.size}"
65
+ { success: true, metaphors: metaphors.map(&:to_h), count: metaphors.size }
66
+ end
67
+
68
+ def strongest_metaphors(limit: 5, **)
69
+ metaphors = engine.strongest(limit: limit)
70
+ Legion::Logging.debug "[conceptual_metaphor] strongest limit=#{limit} count=#{metaphors.size}"
71
+ { success: true, metaphors: metaphors.map(&:to_h), count: metaphors.size }
72
+ end
73
+
74
+ def metaphors_by_type(metaphor_type:, **)
75
+ metaphors = engine.by_type(metaphor_type: metaphor_type)
76
+ Legion::Logging.debug "[conceptual_metaphor] by_type=#{metaphor_type} count=#{metaphors.size}"
77
+ { success: true, metaphor_type: metaphor_type, metaphors: metaphors.map(&:to_h),
78
+ count: metaphors.size }
79
+ end
80
+
81
+ def update_conceptual_metaphor(**)
82
+ engine.decay_all
83
+ pruned = engine.prune_weak
84
+ Legion::Logging.debug "[conceptual_metaphor] decay+prune pruned=#{pruned}"
85
+ { success: true, pruned: pruned }
86
+ end
87
+
88
+ def conceptual_metaphor_stats(**)
89
+ stats = engine.to_h
90
+ Legion::Logging.debug "[conceptual_metaphor] stats total=#{stats[:total_metaphors]}"
91
+ { success: true }.merge(stats)
92
+ end
93
+
94
+ private
95
+
96
+ def engine
97
+ @engine ||= Helpers::MetaphorEngine.new
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module ConceptualMetaphor
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/conceptual_metaphor/version'
4
+ require 'legion/extensions/conceptual_metaphor/helpers/constants'
5
+ require 'legion/extensions/conceptual_metaphor/helpers/metaphor'
6
+ require 'legion/extensions/conceptual_metaphor/helpers/metaphor_engine'
7
+ require 'legion/extensions/conceptual_metaphor/runners/conceptual_metaphor'
8
+ require 'legion/extensions/conceptual_metaphor/client'
9
+
10
+ module Legion
11
+ module Extensions
12
+ module ConceptualMetaphor
13
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::ConceptualMetaphor::Client do
4
+ subject(:client) { described_class.new }
5
+
6
+ it 'creates a metaphor' do
7
+ result = client.create_metaphor(
8
+ source_domain: :money, target_domain: :time,
9
+ metaphor_type: :structural, mappings: { spend: :waste }
10
+ )
11
+ expect(result[:success]).to be true
12
+ end
13
+
14
+ it 'applies a metaphor' do
15
+ created = client.create_metaphor(
16
+ source_domain: :money, target_domain: :time,
17
+ metaphor_type: :structural, mappings: { spend: :waste }
18
+ )
19
+ result = client.apply_metaphor(
20
+ metaphor_id: created[:metaphor_id], source_concept: :spend
21
+ )
22
+ expect(result[:target_concept]).to eq(:waste)
23
+ end
24
+
25
+ it 'returns stats' do
26
+ result = client.conceptual_metaphor_stats
27
+ expect(result[:success]).to be true
28
+ end
29
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::ConceptualMetaphor::Helpers::MetaphorEngine do
4
+ subject(:engine) { described_class.new }
5
+
6
+ let(:metaphor) do
7
+ engine.create_metaphor(
8
+ source_domain: :money,
9
+ target_domain: :time,
10
+ metaphor_type: :structural,
11
+ mappings: { spend: :waste, save: :conserve }
12
+ )
13
+ end
14
+
15
+ describe '#create_metaphor' do
16
+ it 'creates and stores a metaphor' do
17
+ result = metaphor
18
+ expect(result).to be_a(Legion::Extensions::ConceptualMetaphor::Helpers::Metaphor)
19
+ expect(result.source_domain).to eq(:money)
20
+ end
21
+
22
+ it 'rejects invalid metaphor types' do
23
+ result = engine.create_metaphor(
24
+ source_domain: :a, target_domain: :b,
25
+ metaphor_type: :invalid, mappings: {}
26
+ )
27
+ expect(result[:success]).to be false
28
+ end
29
+
30
+ it 'records history' do
31
+ metaphor
32
+ expect(engine.history.size).to eq(1)
33
+ end
34
+ end
35
+
36
+ describe '#apply_metaphor' do
37
+ it 'maps a source concept to target' do
38
+ result = engine.apply_metaphor(metaphor_id: metaphor.id, source_concept: :spend)
39
+ expect(result[:mapped]).to be true
40
+ expect(result[:target_concept]).to eq(:waste)
41
+ end
42
+
43
+ it 'returns found: false for unknown metaphor' do
44
+ result = engine.apply_metaphor(metaphor_id: 'nonexistent', source_concept: :spend)
45
+ expect(result[:found]).to be false
46
+ end
47
+
48
+ it 'returns mapped: false for unmapped concept' do
49
+ result = engine.apply_metaphor(metaphor_id: metaphor.id, source_concept: :borrow)
50
+ expect(result[:mapped]).to be false
51
+ end
52
+ end
53
+
54
+ describe '#add_entailment' do
55
+ it 'adds entailment to metaphor' do
56
+ result = engine.add_entailment(metaphor_id: metaphor.id, entailment: 'time is valuable')
57
+ expect(result[:success]).to be true
58
+ expect(result[:entailment_count]).to eq(1)
59
+ end
60
+
61
+ it 'returns error for unknown metaphor' do
62
+ result = engine.add_entailment(metaphor_id: 'bad', entailment: 'test')
63
+ expect(result[:success]).to be false
64
+ end
65
+ end
66
+
67
+ describe '#find_by_domain' do
68
+ it 'finds metaphors involving the domain' do
69
+ metaphor
70
+ results = engine.find_by_domain(domain: :time)
71
+ expect(results.size).to eq(1)
72
+ end
73
+ end
74
+
75
+ describe '#find_by_source' do
76
+ it 'finds metaphors by source domain' do
77
+ metaphor
78
+ results = engine.find_by_source(source_domain: :money)
79
+ expect(results.size).to eq(1)
80
+ end
81
+ end
82
+
83
+ describe '#find_by_target' do
84
+ it 'finds metaphors by target domain' do
85
+ metaphor
86
+ results = engine.find_by_target(target_domain: :time)
87
+ expect(results.size).to eq(1)
88
+ end
89
+ end
90
+
91
+ describe '#conventional_metaphors' do
92
+ it 'returns metaphors with high conventionality' do
93
+ engine.create_metaphor(
94
+ source_domain: :war, target_domain: :argument,
95
+ metaphor_type: :structural, mappings: { attack: :criticize },
96
+ conventionality: 0.9
97
+ )
98
+ expect(engine.conventional_metaphors.size).to eq(1)
99
+ end
100
+ end
101
+
102
+ describe '#novel_metaphors' do
103
+ it 'returns metaphors with low conventionality' do
104
+ engine.create_metaphor(
105
+ source_domain: :ocean, target_domain: :emotion,
106
+ metaphor_type: :ontological, mappings: { depth: :intensity },
107
+ conventionality: 0.1
108
+ )
109
+ expect(engine.novel_metaphors.size).to eq(1)
110
+ end
111
+ end
112
+
113
+ describe '#strongest' do
114
+ it 'returns metaphors sorted by strength' do
115
+ metaphor
116
+ engine.create_metaphor(
117
+ source_domain: :war, target_domain: :argument,
118
+ metaphor_type: :structural, mappings: {}, strength: 0.9
119
+ )
120
+ results = engine.strongest(limit: 2)
121
+ expect(results.first.strength).to be >= results.last.strength
122
+ end
123
+ end
124
+
125
+ describe '#by_type' do
126
+ it 'filters by metaphor type' do
127
+ metaphor
128
+ engine.create_metaphor(
129
+ source_domain: :container, target_domain: :mind,
130
+ metaphor_type: :ontological, mappings: { full: :knowledgeable }
131
+ )
132
+ structural = engine.by_type(metaphor_type: :structural)
133
+ expect(structural.size).to eq(1)
134
+ end
135
+ end
136
+
137
+ describe '#decay_all' do
138
+ it 'reduces strength of all metaphors' do
139
+ original = metaphor.strength
140
+ engine.decay_all
141
+ expect(metaphor.strength).to be < original
142
+ end
143
+ end
144
+
145
+ describe '#prune_weak' do
146
+ it 'removes very weak metaphors' do
147
+ weak = engine.create_metaphor(
148
+ source_domain: :a, target_domain: :b,
149
+ metaphor_type: :structural, mappings: {}, strength: 0.03
150
+ )
151
+ 30.times { weak.decay! }
152
+ pruned = engine.prune_weak
153
+ expect(pruned).to be >= 1
154
+ end
155
+ end
156
+
157
+ describe '#to_h' do
158
+ it 'returns summary stats' do
159
+ metaphor
160
+ stats = engine.to_h
161
+ expect(stats[:total_metaphors]).to eq(1)
162
+ expect(stats[:total_domains]).to eq(2)
163
+ expect(stats).to include(:type_counts, :conventional_count, :novel_count)
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::ConceptualMetaphor::Helpers::Metaphor do
4
+ subject(:metaphor) do
5
+ described_class.new(
6
+ source_domain: :money,
7
+ target_domain: :time,
8
+ metaphor_type: :structural,
9
+ mappings: { spend: :waste, save: :conserve, invest: :dedicate },
10
+ strength: 0.6,
11
+ conventionality: 0.8
12
+ )
13
+ end
14
+
15
+ describe '#initialize' do
16
+ it 'assigns a UUID id' do
17
+ expect(metaphor.id).to match(/\A[0-9a-f-]{36}\z/)
18
+ end
19
+
20
+ it 'stores source and target domains' do
21
+ expect(metaphor.source_domain).to eq(:money)
22
+ expect(metaphor.target_domain).to eq(:time)
23
+ end
24
+
25
+ it 'stores metaphor type' do
26
+ expect(metaphor.metaphor_type).to eq(:structural)
27
+ end
28
+
29
+ it 'stores mappings' do
30
+ expect(metaphor.mappings).to eq({ spend: :waste, save: :conserve, invest: :dedicate })
31
+ end
32
+
33
+ it 'clamps strength within bounds' do
34
+ over = described_class.new(source_domain: :a, target_domain: :b, metaphor_type: :structural,
35
+ mappings: {}, strength: 2.0)
36
+ expect(over.strength).to eq(1.0)
37
+ end
38
+ end
39
+
40
+ describe '#use!' do
41
+ it 'increments use count' do
42
+ expect { metaphor.use! }.to change(metaphor, :use_count).by(1)
43
+ end
44
+
45
+ it 'boosts strength' do
46
+ original = metaphor.strength
47
+ metaphor.use!
48
+ expect(metaphor.strength).to be > original
49
+ end
50
+
51
+ it 'increases conventionality' do
52
+ original = metaphor.conventionality
53
+ metaphor.use!
54
+ expect(metaphor.conventionality).to be >= original
55
+ end
56
+ end
57
+
58
+ describe '#map_concept' do
59
+ it 'returns mapped target concept' do
60
+ expect(metaphor.map_concept(:spend)).to eq(:waste)
61
+ end
62
+
63
+ it 'returns nil for unmapped concept' do
64
+ expect(metaphor.map_concept(:borrow)).to be_nil
65
+ end
66
+ end
67
+
68
+ describe '#coverage' do
69
+ it 'returns ratio of mapped concepts' do
70
+ expect(metaphor.coverage).to eq(1.0)
71
+ end
72
+
73
+ it 'returns 0.0 for empty mappings' do
74
+ empty = described_class.new(source_domain: :a, target_domain: :b,
75
+ metaphor_type: :structural, mappings: {})
76
+ expect(empty.coverage).to eq(0.0)
77
+ end
78
+ end
79
+
80
+ describe '#conventional?' do
81
+ it 'returns true when conventionality is high' do
82
+ expect(metaphor).to be_conventional
83
+ end
84
+ end
85
+
86
+ describe '#novel?' do
87
+ it 'returns false when conventionality is high' do
88
+ expect(metaphor).not_to be_novel
89
+ end
90
+
91
+ it 'returns true when conventionality is low' do
92
+ fresh = described_class.new(source_domain: :a, target_domain: :b,
93
+ metaphor_type: :structural, mappings: {},
94
+ conventionality: 0.2)
95
+ expect(fresh).to be_novel
96
+ end
97
+ end
98
+
99
+ describe '#conventionality_label' do
100
+ it 'returns a label symbol' do
101
+ expect(metaphor.conventionality_label).to eq(:dead)
102
+ end
103
+ end
104
+
105
+ describe '#strength_label' do
106
+ it 'returns a label symbol' do
107
+ expect(metaphor.strength_label).to be_a(Symbol)
108
+ end
109
+ end
110
+
111
+ describe '#decay!' do
112
+ it 'reduces strength' do
113
+ original = metaphor.strength
114
+ metaphor.decay!
115
+ expect(metaphor.strength).to be < original
116
+ end
117
+ end
118
+
119
+ describe '#add_entailment' do
120
+ it 'adds entailment to the list' do
121
+ metaphor.add_entailment('wasting time is losing money')
122
+ expect(metaphor.entailments).to include('wasting time is losing money')
123
+ end
124
+ end
125
+
126
+ describe '#to_h' do
127
+ it 'returns a hash representation' do
128
+ hash = metaphor.to_h
129
+ expect(hash).to include(:id, :source_domain, :target_domain, :mappings,
130
+ :strength, :conventionality, :coverage)
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::ConceptualMetaphor::Runners::ConceptualMetaphor do
4
+ let(:runner_host) do
5
+ obj = Object.new
6
+ obj.extend(described_class)
7
+ obj
8
+ end
9
+
10
+ describe '#create_metaphor' do
11
+ it 'creates a metaphor successfully' do
12
+ result = runner_host.create_metaphor(
13
+ source_domain: :money, target_domain: :time,
14
+ metaphor_type: :structural, mappings: { spend: :waste }
15
+ )
16
+ expect(result[:success]).to be true
17
+ expect(result[:metaphor_id]).to be_a(String)
18
+ end
19
+
20
+ it 'rejects invalid metaphor type' do
21
+ result = runner_host.create_metaphor(
22
+ source_domain: :a, target_domain: :b,
23
+ metaphor_type: :invalid, mappings: {}
24
+ )
25
+ expect(result[:success]).to be false
26
+ end
27
+ end
28
+
29
+ describe '#apply_metaphor' do
30
+ it 'maps a concept through a metaphor' do
31
+ created = runner_host.create_metaphor(
32
+ source_domain: :money, target_domain: :time,
33
+ metaphor_type: :structural, mappings: { spend: :waste }
34
+ )
35
+ result = runner_host.apply_metaphor(
36
+ metaphor_id: created[:metaphor_id], source_concept: :spend
37
+ )
38
+ expect(result[:success]).to be true
39
+ expect(result[:target_concept]).to eq(:waste)
40
+ end
41
+ end
42
+
43
+ describe '#add_metaphor_entailment' do
44
+ it 'adds an entailment' do
45
+ created = runner_host.create_metaphor(
46
+ source_domain: :money, target_domain: :time,
47
+ metaphor_type: :structural, mappings: {}
48
+ )
49
+ result = runner_host.add_metaphor_entailment(
50
+ metaphor_id: created[:metaphor_id],
51
+ entailment: 'wasting time is losing money'
52
+ )
53
+ expect(result[:success]).to be true
54
+ end
55
+ end
56
+
57
+ describe '#find_metaphors_for' do
58
+ it 'finds metaphors for a domain' do
59
+ runner_host.create_metaphor(
60
+ source_domain: :money, target_domain: :time,
61
+ metaphor_type: :structural, mappings: {}
62
+ )
63
+ result = runner_host.find_metaphors_for(domain: :time)
64
+ expect(result[:success]).to be true
65
+ expect(result[:count]).to eq(1)
66
+ end
67
+ end
68
+
69
+ describe '#conventional_metaphors' do
70
+ it 'returns conventional metaphors' do
71
+ runner_host.create_metaphor(
72
+ source_domain: :war, target_domain: :argument,
73
+ metaphor_type: :structural, mappings: {},
74
+ conventionality: 0.9
75
+ )
76
+ result = runner_host.conventional_metaphors
77
+ expect(result[:success]).to be true
78
+ expect(result[:count]).to eq(1)
79
+ end
80
+ end
81
+
82
+ describe '#novel_metaphors' do
83
+ it 'returns novel metaphors' do
84
+ runner_host.create_metaphor(
85
+ source_domain: :ocean, target_domain: :emotion,
86
+ metaphor_type: :ontological, mappings: {},
87
+ conventionality: 0.1
88
+ )
89
+ result = runner_host.novel_metaphors
90
+ expect(result[:success]).to be true
91
+ expect(result[:count]).to eq(1)
92
+ end
93
+ end
94
+
95
+ describe '#strongest_metaphors' do
96
+ it 'returns strongest metaphors' do
97
+ runner_host.create_metaphor(
98
+ source_domain: :a, target_domain: :b,
99
+ metaphor_type: :structural, mappings: {}, strength: 0.9
100
+ )
101
+ result = runner_host.strongest_metaphors(limit: 3)
102
+ expect(result[:success]).to be true
103
+ end
104
+ end
105
+
106
+ describe '#metaphors_by_type' do
107
+ it 'filters by type' do
108
+ runner_host.create_metaphor(
109
+ source_domain: :a, target_domain: :b,
110
+ metaphor_type: :orientational, mappings: {}
111
+ )
112
+ result = runner_host.metaphors_by_type(metaphor_type: :orientational)
113
+ expect(result[:success]).to be true
114
+ expect(result[:count]).to eq(1)
115
+ end
116
+ end
117
+
118
+ describe '#update_conceptual_metaphor' do
119
+ it 'runs decay and prune cycle' do
120
+ result = runner_host.update_conceptual_metaphor
121
+ expect(result[:success]).to be true
122
+ expect(result).to include(:pruned)
123
+ end
124
+ end
125
+
126
+ describe '#conceptual_metaphor_stats' do
127
+ it 'returns stats' do
128
+ result = runner_host.conceptual_metaphor_stats
129
+ expect(result[:success]).to be true
130
+ expect(result).to include(:total_metaphors, :total_domains)
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Helpers
6
+ module Lex; end
7
+ end
8
+ end
9
+
10
+ module Logging
11
+ def self.debug(*); end
12
+
13
+ def self.info(*); end
14
+
15
+ def self.warn(*); end
16
+
17
+ def self.error(*); end
18
+ end
19
+ end
20
+
21
+ require 'legion/extensions/conceptual_metaphor'
22
+
23
+ RSpec.configure do |config|
24
+ config.expect_with :rspec do |expectations|
25
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
26
+ end
27
+
28
+ config.mock_with :rspec do |mocks|
29
+ mocks.verify_partial_doubles = true
30
+ end
31
+
32
+ config.filter_run_when_matching :focus
33
+ config.order = :random
34
+ end
metadata ADDED
@@ -0,0 +1,75 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lex-conceptual-metaphor
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: Lakoff & Johnson conceptual metaphor theory — understanding abstractions
27
+ through embodied metaphors for brain-modeled agentic AI
28
+ email:
29
+ - matthewdiverson@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - Gemfile
35
+ - lex-conceptual-metaphor.gemspec
36
+ - lib/legion/extensions/conceptual_metaphor.rb
37
+ - lib/legion/extensions/conceptual_metaphor/client.rb
38
+ - lib/legion/extensions/conceptual_metaphor/helpers/constants.rb
39
+ - lib/legion/extensions/conceptual_metaphor/helpers/metaphor.rb
40
+ - lib/legion/extensions/conceptual_metaphor/helpers/metaphor_engine.rb
41
+ - lib/legion/extensions/conceptual_metaphor/runners/conceptual_metaphor.rb
42
+ - lib/legion/extensions/conceptual_metaphor/version.rb
43
+ - spec/legion/extensions/conceptual_metaphor/client_spec.rb
44
+ - spec/legion/extensions/conceptual_metaphor/helpers/metaphor_engine_spec.rb
45
+ - spec/legion/extensions/conceptual_metaphor/helpers/metaphor_spec.rb
46
+ - spec/legion/extensions/conceptual_metaphor/runners/conceptual_metaphor_spec.rb
47
+ - spec/spec_helper.rb
48
+ homepage: https://github.com/LegionIO/lex-conceptual-metaphor
49
+ licenses:
50
+ - MIT
51
+ metadata:
52
+ homepage_uri: https://github.com/LegionIO/lex-conceptual-metaphor
53
+ source_code_uri: https://github.com/LegionIO/lex-conceptual-metaphor
54
+ documentation_uri: https://github.com/LegionIO/lex-conceptual-metaphor
55
+ changelog_uri: https://github.com/LegionIO/lex-conceptual-metaphor
56
+ bug_tracker_uri: https://github.com/LegionIO/lex-conceptual-metaphor/issues
57
+ rubygems_mfa_required: 'true'
58
+ rdoc_options: []
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: '3.4'
66
+ required_rubygems_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ requirements: []
72
+ rubygems_version: 3.6.9
73
+ specification_version: 4
74
+ summary: LEX Conceptual Metaphor
75
+ test_files: []